lofarimaging/lofarimaging/lofarimaging.py

665 lines
27 KiB
Python
Raw Normal View History

"""Functions for working with LOFAR single station data"""
__all__ = ["sb_from_freq", "freq_from_sb", "find_caltable", "read_caltable",
2020-02-26 05:30:47 -07:00
"rcus_in_station", "read_acm_cube", "get_background_image", "nearfield_imager",
"sky_imager", "ground_imager", "get_station_pqr", "skycoord_to_lmn"]
__version__ = "1.5.0"
# Configurations for HBA observations with a single dipole activated per tile.
GENERIC_INT_201512 = [0, 5, 3, 1, 8, 3, 12, 15, 10, 13, 11, 5, 12, 12, 5, 2, 10, 8, 0, 3, 5, 1, 4, 0, 11, 6, 2, 4, 9, 14, 15, 3, 7, 5, 13, 15, 5, 6, 5, 12, 15, 7, 1, 1, 14, 9, 4, 9, 3, 9, 3,
13, 7, 14, 7, 14, 2, 8, 8, 0, 1, 4, 2, 2, 12, 15, 5, 7, 6, 10, 12, 3, 3, 12, 7, 4, 6, 0, 5, 9, 1, 10, 10, 11, 5, 11, 7, 9, 7, 6, 4, 4, 15, 4, 1, 15]
GENERIC_CORE_201512 = [0, 10, 4, 3, 14, 0, 5, 5, 3, 13, 10, 3, 12, 2, 7, 15, 6, 14, 7, 5, 7, 9, 0, 15, 0, 10, 4, 3, 14, 0, 5, 5, 3, 13, 10, 3, 12, 2, 7, 15, 6, 14, 7, 5, 7, 9, 0, 15];
GENERIC_REMOTE_201512 = [0, 13, 12, 4, 11, 11, 7, 8, 2, 7, 11, 2, 10, 2, 6, 3, 8, 3, 1, 7, 1, 15, 13, 1, 11, 1, 12, 7, 10, 15, 8, 2, 12, 13, 9, 13, 4, 5, 5, 12, 5, 5, 9, 11, 15, 12, 2, 15];
import numpy as np
import numexpr as ne
import os
from matplotlib.pyplot import imread
import folium
import datetime
import lofargeotiff
from scipy import ndimage
from lofarantpos.db import LofarAntennaDatabase
import matplotlib.pyplot as plt
from matplotlib.ticker import FormatStrFormatter
from matplotlib import cm
from matplotlib.colors import ListedColormap
import warnings
from mpl_toolkits.axes_grid1 import make_axes_locatable
import matplotlib.axes as maxes
from astropy.coordinates import SkyCoord, GCRS, EarthLocation, AltAz, SkyOffsetFrame, CartesianRepresentation, get_sun, get_moon
import astropy.units as u
from astropy.time import Time
import lofarantpos
from packaging import version
assert(version.parse(lofarantpos.__version__) >= version.parse("0.4.0"))
def sb_from_freq(freq: float, rcu_mode='1'):
"""
Convert subband number to central frequency
2020-02-26 05:32:08 -07:00
Args:
rcu_mode: rcu mode
freq: frequency in Hz
clock: clock speed in Hz
Returns:
int: subband number
"""
clock = 200e6
if rcu_mode == '6':
clock = 160e6
sb_bandwidth = 0.5 * clock / 512.
freq_offset = 0
if rcu_mode == '5':
freq_offset = 100e6
elif rcu_mode == '6':
freq_offset = 160e6
elif rcu_mode == '7':
freq_offset = 200e6
sb = round((freq - freq_offset) / sb_bandwidth)
return int(sb)
def freq_from_sb(sb: int, rcu_mode='1'):
"""
Convert central frequency to subband number
Args:
rcu_mode: rcu mode
sb: subband number
clock: clock speed in Hz
Returns:
float: frequency in Hz
"""
clock = 200e6
if rcu_mode == '6':
clock = 160e6
freq_offset = 0
if rcu_mode == '5':
freq_offset = 100e6
elif rcu_mode == '6':
freq_offset = 160e6
elif rcu_mode == '7':
freq_offset = 200e6
sb_bandwidth = 0.5 * clock / 512.
freq = (sb * sb_bandwidth) + freq_offset
return freq
def skycoord_to_lmn(pos: SkyCoord, phasecentre: SkyCoord):
"""
Convert astropy sky coordinates into the l,m,n coordinate system
relative to a phase centre.
The l,m,n is a RHS coordinate system with
* its origin on the sky sphere
* m,n and the celestial north on the same plane
* l,m a tangential plane of the sky sphere
Note that this means that l increases east-wards
"""
# Determine relative sky position
todc = pos.transform_to(SkyOffsetFrame(origin=phasecentre))
dc = todc.represent_as(CartesianRepresentation)
dc /= dc.norm()
# Do coordinate transformation - astropy's relative coordinates do
# not quite follow imaging conventions
return dc.y.value, dc.z.value, dc.x.value - 1
def find_caltable(field_name: str, rcu_mode: str, config_dir='caltables'):
"""
Find the file of a caltable.
Args:
field_name: Name of the antenna field, e.g. 'DE602LBA'
rcu_mode: Receiver mode for which the calibration table is requested.
Probably should be 'inner' or 'outer'
config_dir: Root directory under which station information is stored in
subdirectories DE602C/etc/, RS106/etc/, ...
Returns:
str: filename if it exists, None if nothing found
"""
station, field = field_name[0:5].upper(), field_name[5:].upper()
station_number = station[2:5]
# Map to the correct file depending on the RCU mode
if rcu_mode == 'outer' and 'LBA' in field_name:
filename = os.path.join(config_dir, f"CalTable-{station_number}-LBA_OUTER-10_90.dat")
elif rcu_mode == 'inner' and 'LBA' in field_name:
filename = os.path.join(config_dir, f"CalTable-{station_number}-LBA_INNER-10_90.dat")
else:
filename = os.path.join(config_dir, f"CalTable_{station_number}_mode{rcu_mode}.dat")
if os.path.exists(filename):
return filename
# If the original folder structure is kept
if rcu_mode == 'outer' and 'LBA' in field_name:
filename = os.path.join(config_dir, f"{station}/CalTable-{station_number}-LBA_OUTER-10_90.dat")
elif rcu_mode == 'inner' and 'LBA' in field_name:
filename = os.path.join(config_dir, f"{station}/CalTable-{station_number}-LBA_INNER-10_90.dat")
else:
filename = os.path.join(config_dir, f"{station}/CalTable_{station_number}_mode{rcu_mode}.dat")
if os.path.exists(filename):
return filename
else:
return None
def read_caltable(filename: str, num_subbands=512):
"""
Read a station's calibration table.
Args:
filename: Filename with the caltable
num_subbands: Number of subbands
Returns:
Tuple(Dict[str, str], np.array): A tuple containing a dict with
the header lines, and a 2D numpy.array of complex numbers
representing the station gain coefficients.
"""
infile = open(filename, 'rb')
header_lines = []
try:
while True:
header_lines.append(infile.readline().decode('utf8').strip())
if 'HeaderStop' in header_lines[-1]:
break
except UnicodeDecodeError:
# No header; close and open again
infile.close()
infile = open(filename, 'rb')
caldata = np.fromfile(infile, dtype=np.complex128)
num_rcus = len(caldata) // num_subbands
infile.close()
header_dict = {key: val for key, val in [line.split(" = ")
for line in header_lines[1:-1]]}
return header_dict, caldata.reshape((num_subbands, num_rcus))
def rcus_in_station(station_type: str):
"""
Give the number of RCUs in a station, given its type.
Args:
station_type: Kind of station that produced the correlation. One of
'core', 'remote', 'intl'.
"""
return {'core': 96, 'remote': 96, 'intl': 192}[station_type]
def read_acm_cube(filename: str, station_type: str):
"""
Read an ACM binary data cube (function from Michiel)
Args:
filename: File containing the array correlation matrix.
station_type: Kind of station that produced the correlation. One of
'core', 'remote', 'intl'.
Returns:
np.array: 3D cube of complex numbers, with indices [time slots, rcu, rcu].
Examples:
>>> cube = read_acm_cube('20170720_095816_xst.dat', 'intl')
>>> cube.shape
(29, 192, 192)
"""
num_rcu = rcus_in_station(station_type)
data = np.fromfile(filename, dtype=np.complex128)
time_slots = int(len(data) / num_rcu / num_rcu)
return data.reshape((time_slots, num_rcu, num_rcu))
def get_background_image(lon_min, lon_max, lat_min, lat_max, zoom=19):
"""
Get an ESRI World Imagery map of the selected region
Args:
lon_min: Minimum longitude (degrees)
lon_max: Maximum longitude (degrees)
lat_min: Minimum latitude (degrees)
lat_max: Maximum latitude (degrees)
zoom: Zoom level
Returns:
np.array: Numpy array which can be plotted with plt.imshow
"""
from owslib.wmts import WebMapTileService
import mercantile
wmts = WebMapTileService("http://server.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/WMTS/1.0.0/WMTSCapabilities.xml")
upperleft_tile = mercantile.tile(lon_min, lat_max, zoom)
xmin, ymin = upperleft_tile.x, upperleft_tile.y
lowerright_tile = mercantile.tile(lon_max, lat_min, zoom)
xmax, ymax = lowerright_tile.x, lowerright_tile.y
total_image = np.zeros([256 * (ymax - ymin + 1), 256 * (xmax - xmin + 1), 3], dtype='uint8')
tile_min = mercantile.tile(lon_min, lat_min, zoom)
tile_max = mercantile.tile(lon_max, lat_max, zoom)
for x in range(tile_min.x, tile_max.x + 1):
for y in range(tile_max.y, tile_min.y + 1):
tile = wmts.gettile(layer="World_Imagery", tilematrix=str(zoom), row=y, column=x)
out = open("tmp.jpg", "wb")
out.write(tile.read())
out.close()
tile_image = imread("tmp.jpg")
total_image[(y - ymin) * 256: (y - ymin + 1) * 256,
(x - xmin) * 256: (x - xmin + 1) * 256] = tile_image
total_lonlatmin = {'lon': mercantile.bounds(xmin, ymax, zoom).west, 'lat': mercantile.bounds(xmin, ymax, zoom).south}
total_lonlatmax = {'lon': mercantile.bounds(xmax, ymin, zoom).east, 'lat': mercantile.bounds(xmax, ymin, zoom).north}
pix_xmin = int(round(np.interp(lon_min, [total_lonlatmin['lon'], total_lonlatmax['lon']], [0, total_image.shape[1]])))
pix_ymin = int(round(np.interp(lat_min, [total_lonlatmin['lat'], total_lonlatmax['lat']], [0, total_image.shape[0]])))
pix_xmax = int(round(np.interp(lon_max, [total_lonlatmin['lon'], total_lonlatmax['lon']], [0, total_image.shape[1]])))
pix_ymax = int(round(np.interp(lat_max, [total_lonlatmin['lat'], total_lonlatmax['lat']], [0, total_image.shape[0]])))
return total_image[total_image.shape[0]-pix_ymax: total_image.shape[0]-pix_ymin, pix_xmin: pix_xmax]
SPEED_OF_LIGHT = 299792458.0
def get_station_pqr(station_name: str, station_type: str, array_type: str, db):
if 'LBA' in station_name:
# Get the PQR positions for an individual station
station_pqr = db.antenna_pqr(station_name)
# Exception: for Dutch stations (sparse not yet accommodated)
if (station_type == 'core' or station_type == 'remote') and array_type == 'inner':
station_pqr = station_pqr[0:48, :]
elif (station_type == 'core' or station_type == 'remote') and array_type == 'outer':
station_pqr = station_pqr[48:, :]
elif 'HBA' in station_name:
selected_dipole_config = {
'intl': GENERIC_INT_201512, 'remote': GENERIC_REMOTE_201512, 'core': GENERIC_CORE_201512
}
selected_dipoles = selected_dipole_config[station_type] + np.arange(len(selected_dipole_config[station_type])) * 16
station_pqr = db.hba_dipole_pqr(station_name)[selected_dipoles]
else:
raise RuntimeError("Station name did not contain LBA or HBA, could not load antenna positions")
return station_pqr.astype('float32')
def sky_imager(visibilities, baselines, freq, npix_l, npix_m):
"""Do a Fourier transform for sky imaging"""
img = np.zeros([npix_m, npix_l], dtype=np.float32)
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message="Casting complex values to real discards the imaginary part")
for m_ix, m in enumerate(np.linspace(-1, 1, npix_l)):
for l_ix, l in enumerate(np.linspace(1, -1, npix_m)):
img[m_ix, l_ix] = np.mean(visibilities *
np.exp(-2j * np.pi * freq *
(baselines[:, :, 0] * l + baselines[:, :, 1] * m) / SPEED_OF_LIGHT))
return img
def ground_imager(visibilities, baselines, freq, npix_p, npix_q, dims, station_pqr, height=1.5):
"""Do a Fourier transform for ground imaging"""
img = np.zeros([npix_q, npix_p], dtype=np.complex)
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message="Casting complex values to real discards the imaginary part")
for q_ix, q in enumerate(np.linspace(dims[2], dims[3], npix_q)):
for p_ix, p in enumerate(np.linspace(dims[0], dims[1], npix_p)):
r = height
pqr = np.array([p, q, r], dtype=np.float32)
antdist = np.linalg.norm(station_pqr - pqr[np.newaxis, :], axis=1)
groundbase = antdist[:, np.newaxis] - antdist[np.newaxis, :]
# Note: this is RFI integration second - normal second, to take out interference
img[q_ix, p_ix] = np.mean(visibilities * np.exp(-2j * np.pi * freq * (-groundbase) / SPEED_OF_LIGHT))
2020-02-26 05:30:47 -07:00
return np.abs(img)
def nearfield_imager(visibilities, baselines_indices, freqs, npix_p, npix_q, dims, station_pqr, height=1.5):
2020-02-26 05:32:08 -07:00
ant_pos=station_pqr
z=height
x = np.linspace(dims[0], dims[1], npix_p)
y = np.linspace(dims[2], dims[3], npix_q)
posx, posy = np.meshgrid(x, y)
dd = (ant_pos[:, 0][:, None, None] - posx[None]) ** 2 \
+ (ant_pos[:, 1][:, None, None] - posy[None]) ** 2 \
+ (ant_pos[:, 2][:, None, None] - z) ** 2
delay = np.sqrt(dd)
ddelay = np.array([delay[i] - delay[j] for i, j in baselines_indices])
2020-02-26 05:32:08 -07:00
vis = visibilities
img = 0
j2pi = 1j * 2 * np.pi
d = ddelay
for ifreq, freq in enumerate(freqs):
v = vis[:, ifreq][:, None, None]
lamb = SPEED_OF_LIGHT / freq
#h = ne.evaluate("v * exp(j2pi * d / lamb)") #v[:,np.newaxis,np.newaxis]*np.exp(-2j*np.pi*freq/c*groundbase_pixels[:,:,:]/c) groundbase_pixels=nvis x npix x npix
img = ne.evaluate("img + v * exp(j2pi * d / lamb) ")
2020-02-26 05:30:47 -07:00
img = np.mean(img / len(freqs))
2020-02-26 05:32:08 -07:00
return img
2020-02-26 05:32:08 -07:00
def make_ground_image(xst_filename,
station_name,
caltable_dir,
extent=None,
pixels_per_metre=0.5,
sky_vmin=None,
sky_vmax=None,
ground_vmin=None,
ground_vmax=None,
height=1.5,
map_zoom=19,
sky_only=False,
opacity=0.6):
"""Make a ground image"""
cubename = os.path.basename(xst_filename)
if extent is None:
extent = [-150, 150, -150, 150]
if station_name[0] == "C":
station_type = "core"
elif station_name[0] == "R" or station_name[:5] == "PL611":
station_type = "remote"
else:
station_type = "intl"
try:
os.mkdir('results')
except FileExistsError:
pass
# Distill metadata from filename
obsdatestr, obstimestr, _, rcu_mode, _, subbandname = cubename.rstrip(".dat").split("_")
subband = int(subbandname[2:])
# Needed for NL stations: inner (rcu_mode 3/4), outer (rcu_mode 1/2), (sparse tbd)
# Should be set to 'inner' if station type = 'intl'
array_type = None
if rcu_mode in ('1', '2'):
array_type = 'outer'
elif rcu_mode in ('3', '4'):
array_type = 'inner'
elif rcu_mode in ('5', '6', '7'):
array_type = rcu_mode
else:
raise Exception("Unexpected rcu_mode: ", rcu_mode)
# Get the data
fname = f"{obsdatestr}_{obstimestr}_{station_name}_SB{subband}"
npix_l, npix_m = 131, 131
freq = freq_from_sb(subband, rcu_mode=rcu_mode)
# Which slice in time to visualise
timestep = 0
# For ground imaging
ground_resolution = pixels_per_metre # pixels per metre for ground_imaging, default is 0.5 pixel/metre
obstime = datetime.datetime.strptime(obsdatestr + ":" + obstimestr, '%Y%m%d:%H%M%S')
cube = read_acm_cube(xst_filename, station_type)
# Apply calibration
caltable_filename = find_caltable(station_name, rcu_mode=array_type,
config_dir=caltable_dir)
if caltable_filename is None:
print('No calibration table found... cube remains uncalibrated!')
else:
cal_header, cal_data = read_caltable(caltable_filename)
rcu_gains = cal_data[subband, :]
rcu_gains = np.array(rcu_gains, dtype=np.complex64)
gain_matrix = rcu_gains[np.newaxis, :] * np.conj(rcu_gains[:, np.newaxis])
cube = cube / gain_matrix
# Split into the XX and YY polarisations (RCUs)
# This needs to be modified in future for LBA sparse
cube_xx = cube[:, 0::2, 0::2]
cube_yy = cube[:, 1::2, 1::2]
visibilities_all = cube_xx + cube_yy
# Stokes I for specified timestep
visibilities = visibilities_all[timestep]
# Setup the database
db = LofarAntennaDatabase()
station_pqr = get_station_pqr(station_name, station_type, array_type, db)
baselines = station_pqr[:, np.newaxis, :] - station_pqr[np.newaxis, :, :]
#baselines = [(i,j) for i in range(len(station_pqr)) for j in range(len(station_pqr))]
2020-02-26 05:32:08 -07:00
rotation = np.rad2deg(db.rotation_from_north(station_name))
# Make a sky image, by numerically Fourier-transforming from visibilities to image plane
from matplotlib.patches import Circle
# Fourier transform, and account for the rotation (rotation is positive in this space)
# visibilities = cube_xx[2,:,:]
img = sky_imager(visibilities, baselines, freq, npix_l, npix_m)
img = ndimage.interpolation.rotate(img, -rotation, reshape=False, mode='nearest')
obstime_astropy = Time(obstime)
# Determine positions of Cas A and Cyg A
station_earthlocation = EarthLocation.from_geocentric(*(db.phase_centres[station_name] * u.m))
zenith = AltAz(az=0 * u.deg, alt=90 * u.deg, obstime=obstime_astropy,
location=station_earthlocation).transform_to(GCRS)
marked_bodies = {
'Cas A': SkyCoord(ra=350.85*u.deg, dec=58.815*u.deg),
'Cyg A': SkyCoord(ra=299.868*u.deg, dec=40.734*u.deg),
# 'Per A': SkyCoord.from_name("Perseus A"),
# 'Her A': SkyCoord.from_name("Hercules A"),
# 'Cen A': SkyCoord.from_name("Centaurus A"),
# '?': SkyCoord.from_name("J101415.9+105106"),
# '3C295': SkyCoord.from_name("3C295"),
# 'Moon': get_moon(obstime_astropy, location=station_earthlocation).transform_to(GCRS),
'Sun': get_sun(obstime_astropy)
# '3C196': SkyCoord.from_name("3C196")
}
marked_bodies_lmn = {}
for body_name, body_coord in marked_bodies.items():
#print(body_name, body_coord.separation(zenith), body_coord.separation(zenith))
if body_coord.transform_to(AltAz(location=station_earthlocation, obstime=obstime_astropy)).alt > 0:
marked_bodies_lmn[body_name] = skycoord_to_lmn(marked_bodies[body_name], zenith)
# Plot the resulting sky image
fig, ax = plt.subplots(1)
circle1 = Circle((0, 0), 1.0, edgecolor='k', fill=False, facecolor='none', alpha=0.3)
ax.add_artist(circle1)
cimg = ax.imshow(img, origin='lower', cmap=cm.Spectral_r, extent=(-1, 1, -1, 1),
clip_path=circle1, clip_on=True, vmin=sky_vmin, vmax=sky_vmax)
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.2, axes_class=maxes.Axes)
fig.colorbar(cimg, cax=cax, orientation="vertical", format="%.1e")
ax.set_xticks(np.arange(-1, 1.1, 0.5))
ax.xaxis.set_major_formatter(FormatStrFormatter('%.1f'))
ax.set_yticks(np.arange(-1, 1.1, 0.5))
ax.yaxis.set_major_formatter(FormatStrFormatter('%.1f'))
for body, lmn in marked_bodies_lmn.items():
ax.plot([-lmn[0]], [lmn[1]], marker='x', color='white', mew=0.5)
# Labels
ax.set_xlabel('$â„“$', fontsize=14)
ax.set_ylabel('$m$', fontsize=14)
for body_name, lmn in marked_bodies_lmn.items():
ax.annotate(body_name, (-lmn[0], lmn[1]))
ax.set_title(f"Sky image for {station_name}\nSB {subband} ({freq / 1e6:.1f} MHz), {str(obstime)[:16]}", fontsize=16)
# Plot the compass directions
ax.text(0.9, 0, 'W', horizontalalignment='center', verticalalignment='center', color='w', fontsize=17)
ax.text(-0.9, 0, 'E', horizontalalignment='center', verticalalignment='center', color='w', fontsize=17)
ax.text(0, 0.9, 'N', horizontalalignment='center', verticalalignment='center', color='w', fontsize=17)
ax.text(0, -0.9, 'S', horizontalalignment='center', verticalalignment='center', color='w', fontsize=17)
plt.savefig(f'results/{fname}_sky_calibrated.png', bbox_inches='tight', dpi=200)
plt.close(fig)
if sky_only:
return img
from shapely import affinity, geometry
def extent_from_shapely(minx, miny, maxx, maxy):
return minx, maxx, miny, maxy
def extent_to_shapely(minx, maxx, miny, maxy):
return minx, miny, maxx, maxy
to_plot_xyz = affinity.rotate(
affinity.rotate(
geometry.box(*extent_to_shapely(*extent)),
rotation, origin=(0, 0)).envelope,
-rotation, origin=(0, 0))
to_plot_pqr = db.pqr_to_localnorth(station_name)[:2, :2].T @ (np.asarray(to_plot_xyz.exterior.coords[:4])).T
extent_pqr = (np.min(to_plot_pqr[0, :]), np.max(to_plot_pqr[0, :]), np.min(to_plot_pqr[1, :]), np.max(to_plot_pqr[1, :]))
npix_p, npix_q = int(ground_resolution * (extent[1] - extent[0])), int(ground_resolution * (extent[3] - extent[2]))
img = ground_imager(visibilities, baselines, freq, npix_p, npix_q, extent_pqr, station_pqr, height=height)
img_rotated = ndimage.interpolation.rotate(img, rotation, mode='constant', cval=np.nan)
outer_extent_xyz = extent_from_shapely(*(to_plot_xyz.envelope.bounds)) # Extent in xyz coordinates of the rotated image.
# Convert bottom left and upper right to PQR just for lofargeo
pmin, qmin, _ = db.pqr_to_localnorth(station_name).T @ (np.array([extent[0], extent[2], 0]))
pmax, qmax, _ = db.pqr_to_localnorth(station_name).T @ (np.array([extent[1], extent[3], 0]))
lon_center, lat_center, _ = lofargeotiff.pqr_to_longlatheight([0, 0, 0], station_name)
lon_min, lat_min, _ = lofargeotiff.pqr_to_longlatheight([pmin, qmin, 0], station_name)
lon_max, lat_max, _ = lofargeotiff.pqr_to_longlatheight([pmax, qmax, 0], station_name)
# Convert bottom left and upper right to PQR just for lofargeo
outer_pmin, outer_qmin, _ = db.pqr_to_localnorth(station_name).T @ (np.array([outer_extent_xyz[0], outer_extent_xyz[2], 0]))
outer_pmax, outer_qmax, _ = db.pqr_to_localnorth(station_name).T @ (np.array([outer_extent_xyz[1], outer_extent_xyz[3], 0]))
outer_lon_min, outer_lat_min, _ = lofargeotiff.pqr_to_longlatheight([outer_pmin, outer_qmin, 0], station_name)
outer_lon_max, outer_lat_max, _ = lofargeotiff.pqr_to_longlatheight([outer_pmax, outer_qmax, 0], station_name)
background_image = get_background_image(lon_min, lon_max, lat_min, lat_max, zoom=map_zoom)
# Make colors semi-transparent in the lower 3/4 of the scale
cmap = cm.Spectral_r
cmap_with_alpha = cmap(np.arange(cmap.N))
cmap_with_alpha[:, -1] = np.clip(np.linspace(0, 1.5, cmap.N), 0., 1.)
cmap_with_alpha = ListedColormap(cmap_with_alpha)
# Plot the resulting image
fig = plt.figure(figsize=(10, 10), constrained_layout=True)
ax = fig.add_subplot(111, ymargin=-0.4)
ax.imshow(background_image, extent=extent)
cimg = ax.imshow(img_rotated, origin='lower', cmap=cmap_with_alpha, extent=outer_extent_xyz,
alpha=0.7, vmin=ground_vmin, vmax=ground_vmax)
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.2, axes_class=maxes.Axes)
cbar = fig.colorbar(cimg, cax=cax, orientation="vertical", format="%.1e")
cbar.set_alpha(1.0)
cbar.draw_all()
# cbar.set_ticks([])
ax.set_xlabel('$W-E$ (metres)', fontsize=14)
ax.set_ylabel('$S-N$ (metres)', fontsize=14)
ax.set_title(f"Near field image for {station_name}\nSB {subband} ({freq / 1e6:.1f} MHz), {str(obstime)[:16]}", fontsize=16)
# Change limits to match the original specified extent in the localnorth frame
ax.set_xlim(extent[0], extent[1])
ax.set_ylim(extent[2], extent[3])
ax.tick_params(axis='both', which='both', length=0)
# Place the NSEW coordinate directions
ax.text(0.95, 0.5, 'E', color='w', fontsize=18, transform=ax.transAxes, horizontalalignment='center', verticalalignment='center')
ax.text(0.05, 0.5, 'W', color='w', fontsize=18, transform=ax.transAxes, horizontalalignment='center', verticalalignment='center')
ax.text(0.5, 0.95, 'N', color='w', fontsize=18, transform=ax.transAxes, horizontalalignment='center', verticalalignment='center')
ax.text(0.5, 0.05, 'S', color='w', fontsize=18, transform=ax.transAxes, horizontalalignment='center', verticalalignment='center')
ground_vmin_img, ground_vmax_img = cimg.get_clim()
ax.contour(img_rotated, np.linspace(ground_vmin_img, ground_vmax_img, 15), origin='lower', cmap=cm.Greys,
extent=outer_extent_xyz, linewidths=0.5, alpha=opacity)
ax.grid(True, alpha=0.3)
plt.savefig(f"results/{fname}_nearfield_calibrated.png", bbox_inches='tight', dpi=200)
plt.close(fig)
plt.imsave(f"results/tmp.png", img_rotated,
cmap=cmap_with_alpha, origin='lower', vmin=ground_vmin, vmax=ground_vmax)
obstime = datetime.datetime.strptime(obsdatestr + ":" + obstimestr, '%Y%m%d:%H%M%S')
tags = {"datafile": xst_filename,
"generated_with": f"lofarimaging v{__version__}",
"caltable": caltable_filename,
"subband": subband,
"frequency": freq,
"extent_xyz": extent,
"height": height,
"station": station_name,
"pixels_per_metre": pixels_per_metre,
"outer_extent_xyz": list(outer_extent_xyz)}
if "CalTableHeader.Observation.Date" in cal_header:
tags["calibration_obsdate"] = cal_header["CalTableHeader.Observation.Date"]
if "CalTableHeader.Calibration.Date" in cal_header:
tags["calibration_date"] = cal_header["CalTableHeader.Calibration.Date"]
if "CalTableHeader.Comment" in cal_header:
tags["calibration_comment"] = cal_header["CalTableHeader.Comment"]
lofargeotiff.write_geotiff(img_rotated, f"results/{fname}_nearfield_calibrated.tiff",
(outer_pmin, outer_qmin), (outer_pmax, outer_qmax), stationname=station_name,
obsdate=obstime, tags=tags)
m = folium.Map(location=[lat_center, lon_center], zoom_start=19,
tiles='http://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/MapServer/tile/{z}/{y}/{x}',
attr='ESRI')
folium.TileLayer(tiles="OpenStreetMap").add_to(m)
folium.raster_layers.ImageOverlay(
name='Near field image',
image=f"results/tmp.png",
bounds=[[outer_lat_min, outer_lon_min], [outer_lat_max, outer_lon_max]],
opacity=opacity,
interactive=True,
cross_origin=False,
zindex=1
).add_to(m)
folium.LayerControl().add_to(m)
return m