Skip to content

Commit 918d42a

Browse files
committed
Add self cal algorithm and example script
1 parent e183746 commit 918d42a

File tree

3 files changed

+184
-36
lines changed

3 files changed

+184
-36
lines changed

lofarimaging/lofarimaging.py

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
"""Functions for working with LOFAR single station data"""
22

3+
import logging
34
from typing import Dict, List
4-
import numpy as np
5-
from numpy.linalg import norm, lstsq
6-
import numexpr as ne
5+
76
import numba
7+
import numexpr as ne
8+
import numpy as np
9+
import scipy.optimize
810
from astropy.coordinates import SkyCoord, SkyOffsetFrame, CartesianRepresentation
9-
11+
from numpy.linalg import norm, lstsq
1012

1113
__all__ = ["nearfield_imager", "sky_imager", "ground_imager", "skycoord_to_lmn", "calibrate", "simulate_sky_source",
1214
"subtract_sources"]
@@ -170,6 +172,70 @@ def calibrate(vis, modelvis, maxiter=30, amplitudeonly=True):
170172
return residual, gains
171173

172174

175+
def compute_calibrated_model(vis, model_vis, maxiter=30):
176+
n_ant = vis.shape[0]
177+
gain_phase = np.zeros((n_ant), dtype=complex)
178+
179+
def gains_difference(gain_phase, vis, model_vis):
180+
gains = np.exp(1.j * gain_phase)
181+
gains_mat = np.diag(gains)
182+
gains_mat_star = np.diag(np.conj(gains))
183+
predicted = gains_mat_star @ model_vis @ gains_mat
184+
residual = np.sum(np.abs(vis - predicted))
185+
return residual
186+
187+
result = scipy.optimize.minimize(gains_difference, gain_phase, args=(vis, model_vis), options={'maxiter': maxiter})
188+
gain_phase = result.x
189+
gains_mat = np.diag(np.exp(1.j * gain_phase))
190+
191+
calibrated = np.conj(gains_mat) @ model_vis @ gains_mat
192+
193+
return calibrated, gains_difference(gain_phase, vis, model_vis)
194+
195+
196+
def compute_pointing_matrix(sources_positions, baselines, frequency):
197+
n_baselines = baselines.shape[0]
198+
n_sources = sources_positions.shape[0]
199+
200+
pointing_matrix = np.zeros(shape=(n_baselines, n_baselines, n_sources), dtype=complex)
201+
202+
for q in range(n_sources):
203+
pointing_matrix[:, :, q] = simulate_sky_source(sources_positions[q, :], baselines, frequency)
204+
return pointing_matrix
205+
206+
207+
def estimate_sources_flux(visibilities, pointing_matrix):
208+
def residual_flux(signal, vis, point_matrix):
209+
residual = vis - point_matrix @ signal
210+
residual = np.sum(np.abs(residual))
211+
return residual
212+
213+
n_antennas = visibilities.shape[0]
214+
n_sources = pointing_matrix.shape[2]
215+
lin_visibilities = visibilities.reshape((n_antennas * n_antennas))
216+
lin_pointing_matrix = pointing_matrix.reshape((n_antennas * n_antennas, n_sources))
217+
fluxes, *_ = np.linalg.lstsq(lin_pointing_matrix, lin_visibilities)
218+
219+
result = scipy.optimize.minimize(residual_flux, fluxes, args=(visibilities, pointing_matrix))
220+
return result.x
221+
222+
223+
def estimate_model_visibilities(sources_positions, visibilities, baselines, frequency):
224+
pointing_matrix = compute_pointing_matrix(sources_positions, baselines, frequency)
225+
fluxes = estimate_sources_flux(visibilities, pointing_matrix)
226+
model = pointing_matrix @ fluxes
227+
228+
return model
229+
230+
231+
def self_cal(visibilities, expected_sources, baselines, frequency, iterations=10):
232+
model = estimate_model_visibilities(expected_sources, visibilities, baselines, frequency)
233+
for i in range(iterations):
234+
model, residual = compute_calibrated_model(visibilities, model, maxiter=50)
235+
logging.debug('residual after iteration %d: %s', i, residual)
236+
return model
237+
238+
173239
def simulate_sky_source(lmn_coord: np.array, baselines: np.array, freq: float):
174240
"""
175241
Simulate visibilities for a sky source

lofarimaging/singlestationutil.py

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,32 @@
11
"""Functions for working with LOFAR single station data"""
22

3-
import os
43
import datetime
4+
import os
55
from typing import List, Dict, Tuple, Union
66

7-
import numpy as np
8-
from packaging import version
9-
import tqdm
7+
import astropy.units as u
108
import h5py
11-
12-
import matplotlib.pyplot as plt
9+
import lofarantpos
10+
import lofargeotiff
1311
import matplotlib.animation
14-
from matplotlib.ticker import FormatStrFormatter
15-
from matplotlib import cm
16-
from matplotlib.figure import Figure
17-
from matplotlib.colors import ListedColormap, Normalize
18-
from matplotlib.patches import Circle
1912
import matplotlib.axes as maxes
20-
from mpl_toolkits.axes_grid1 import make_axes_locatable
21-
13+
import matplotlib.pyplot as plt
14+
import numpy as np
15+
import tqdm
2216
from astropy.coordinates import SkyCoord, GCRS, EarthLocation, AltAz, get_sun, get_moon
23-
import astropy.units as u
2417
from astropy.time import Time
25-
26-
import lofargeotiff
2718
from lofarantpos.db import LofarAntennaDatabase
28-
import lofarantpos
19+
from matplotlib import cm
20+
from matplotlib.colors import ListedColormap, Normalize
21+
from matplotlib.figure import Figure
22+
from matplotlib.patches import Circle
23+
from matplotlib.ticker import FormatStrFormatter
24+
from mpl_toolkits.axes_grid1 import make_axes_locatable
25+
from packaging import version
2926

30-
from .maputil import get_map, make_leaflet_map
31-
from .lofarimaging import nearfield_imager, sky_imager, skycoord_to_lmn, subtract_sources
3227
from .hdf5util import write_hdf5
33-
28+
from .lofarimaging import nearfield_imager, sky_imager, skycoord_to_lmn, subtract_sources
29+
from .maputil import get_map, make_leaflet_map
3430

3531
__all__ = ["sb_from_freq", "freq_from_sb", "find_caltable", "read_caltable",
3632
"rcus_in_station", "read_acm_cube", "get_station_pqr", "get_station_type",
@@ -331,7 +327,7 @@ def get_station_pqr(station_name: str, rcu_mode: Union[str, int], db):
331327
'intl': GENERIC_INT_201512, 'remote': GENERIC_REMOTE_201512, 'core': GENERIC_CORE_201512
332328
}
333329
selected_dipoles = selected_dipole_config[station_type] + \
334-
np.arange(len(selected_dipole_config[station_type])) * 16
330+
np.arange(len(selected_dipole_config[station_type])) * 16
335331
station_pqr = db.hba_dipole_pqr(full_station_name)[selected_dipoles]
336332
else:
337333
raise RuntimeError("Station name did not contain LBA or HBA, could not load antenna positions")
@@ -340,7 +336,7 @@ def get_station_pqr(station_name: str, rcu_mode: Union[str, int], db):
340336

341337

342338
def make_ground_plot(image: np.ndarray, background_map: np.ndarray, extent: List[float], title: str = "Ground plot",
343-
subtitle: str = "", opacity: float = 0.6, fig: Figure = None, draw_contours: bool = True, **kwargs) \
339+
subtitle: str = "", opacity: float = 0.6, fig: Figure = None, draw_contours: bool = True, **kwargs) \
344340
-> Tuple[Figure, np.ndarray]:
345341
"""
346342
Make a ground plot of an array with data
@@ -585,7 +581,6 @@ def make_xst_plots(xst_data: np.ndarray,
585581
opacity: Opacity for map overlay. Defaults to 0.6.
586582
hdf5_filename: Filename where hdf5 results can be written. Defaults to outputpath + '/results.h5'
587583
outputpath: Directory where results can be saved. Defaults to 'results'
588-
subtract: List of sources to subtract. Defaults to None
589584
590585
591586
Returns:
@@ -662,14 +657,14 @@ def make_xst_plots(xst_data: np.ndarray,
662657
marked_bodies = {
663658
'Cas A': SkyCoord(ra=350.85 * u.deg, dec=58.815 * u.deg),
664659
'Cyg A': SkyCoord(ra=299.86815191 * u.deg, dec=40.73391574 * u.deg),
665-
'Per A': SkyCoord(ra=49.95066567*u.deg, dec=41.51169838 * u.deg),
666-
'Her A': SkyCoord(ra=252.78343333*u.deg, dec=4.99303056*u.deg),
667-
'Cen A': SkyCoord(ra=201.36506288*u.deg, dec=-43.01911267*u.deg),
668-
'Vir A': SkyCoord(ra=187.70593076*u.deg, dec=12.39112329*u.deg),
669-
'3C295': SkyCoord(ra=212.83527917*u.deg, dec=52.20264444*u.deg),
660+
'Per A': SkyCoord(ra=49.95066567 * u.deg, dec=41.51169838 * u.deg),
661+
'Her A': SkyCoord(ra=252.78343333 * u.deg, dec=4.99303056 * u.deg),
662+
'Cen A': SkyCoord(ra=201.36506288 * u.deg, dec=-43.01911267 * u.deg),
663+
'Vir A': SkyCoord(ra=187.70593076 * u.deg, dec=12.39112329 * u.deg),
664+
'3C295': SkyCoord(ra=212.83527917 * u.deg, dec=52.20264444 * u.deg),
670665
'Moon': get_moon(obstime_astropy, location=station_earthlocation).transform_to(GCRS),
671666
'Sun': get_sun(obstime_astropy),
672-
'3C196': SkyCoord(ra=123.40023371*u.deg, dec=48.21739888*u.deg)
667+
'3C196': SkyCoord(ra=123.40023371 * u.deg, dec=48.21739888 * u.deg)
673668
}
674669

675670
marked_bodies_lmn = {}
@@ -752,7 +747,7 @@ def make_xst_plots(xst_data: np.ndarray,
752747
"pixels_per_metre": pixels_per_metre}
753748
tags.update(calibration_info)
754749
lon_min, lon_max, lat_min, lat_max = extent_lonlat
755-
lofargeotiff.write_geotiff(ground_img[::-1,:], os.path.join(outputpath, f"{fname}_nearfield_calibrated.tiff"),
750+
lofargeotiff.write_geotiff(ground_img[::-1, :], os.path.join(outputpath, f"{fname}_nearfield_calibrated.tiff"),
756751
(lon_min, lat_max), (lon_max, lat_min), as_pqr=False,
757752
stationname=station_name, obsdate=obstime, tags=tags)
758753

@@ -769,7 +764,7 @@ def make_sky_movie(moviefilename: str, h5file: h5py.File, obsnums: List[str], vm
769764
"""
770765
Make movie of a list of observations
771766
"""
772-
fig = plt.figure(figsize=(10,10))
767+
fig = plt.figure(figsize=(10, 10))
773768
for obsnum in tqdm.tqdm(obsnums):
774769
obs_h5 = h5file[obsnum]
775770
skydata_h5 = obs_h5["sky_img"]
@@ -787,7 +782,28 @@ def make_sky_movie(moviefilename: str, h5file: h5py.File, obsnums: List[str], vm
787782

788783
# Thanks to Maaijke Mevius for making this animation work!
789784
ims = fig.get_children()[1:]
790-
ims = [ims[i:i+2] for i in range(0, len(ims), 2)]
785+
ims = [ims[i:i + 2] for i in range(0, len(ims), 2)]
791786
ani = matplotlib.animation.ArtistAnimation(fig, ims, interval=30, blit=False, repeat_delay=1000)
792787
writer = matplotlib.animation.writers['ffmpeg'](fps=5, bitrate=800)
793788
ani.save(moviefilename, writer=writer, dpi=fig.dpi)
789+
790+
791+
def compute_baselines(station_name, rcu_mode):
792+
# Setup the database
793+
db = LofarAntennaDatabase()
794+
795+
station_pqr = get_station_pqr(station_name, rcu_mode, db)
796+
797+
station_name = get_full_station_name(station_name, rcu_mode)
798+
799+
# Rotate station_pqr to a north-oriented xyz frame, where y points North, in a plane through the station.
800+
rotation = db.rotation_from_north(station_name)
801+
802+
pqr_to_xyz = np.array([[np.cos(-rotation), -np.sin(-rotation), 0],
803+
[np.sin(-rotation), np.cos(-rotation), 0],
804+
[0, 0, 1]])
805+
806+
station_xyz = (pqr_to_xyz @ station_pqr.T).T
807+
808+
baselines = station_xyz[:, np.newaxis, :] - station_xyz[np.newaxis, :, :]
809+
return baselines

self_cal_demo.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import h5py
2+
import numpy
3+
import lofarimaging.hdf5util as h5utils
4+
from lofarimaging.singlestationutil import compute_baselines, make_sky_plot
5+
from lofarimaging.lofarimaging import compute_pointing_matrix, compute_calibrated_model, sky_imager,\
6+
self_cal, estimate_model_visibilities
7+
import matplotlib.pyplot as plt
8+
9+
test_data_path = '/home/mmancini/Documents/Projects/SingleStationImager/test_dataset.h5'
10+
test_station = 'CS103'
11+
test_data = h5py.File(test_data_path, 'r')
12+
obs_names = h5utils.get_obsnums(test_data, station_name=test_station)
13+
an_obs = test_data[obs_names[10]]
14+
15+
a_frequency = an_obs.attrs['frequency']
16+
a_source_lmn = an_obs.attrs['source_lmn']
17+
a_source_name = an_obs.attrs['source_names']
18+
a_rcu_mode = an_obs.attrs['rcu_mode']
19+
a_dataset = an_obs['calibrated_data']
20+
21+
a_dataset_xx = a_dataset[::2, ::2]
22+
a_dataset_yy = a_dataset[1::2, 1::2]
23+
24+
a_dataset_i = a_dataset_xx + a_dataset_yy
25+
a_dataset_q = a_dataset_xx - a_dataset_yy
26+
a_dataset_u = 2 * (a_dataset_xx * a_dataset_yy.conj()).real
27+
a_dataset_v = -2 * (a_dataset_xx * a_dataset_yy.conj()).imag
28+
29+
baselines = compute_baselines(test_station, a_rcu_mode)
30+
sky_image = sky_imager(a_dataset_i, baselines, a_frequency, 100, 100)
31+
32+
model = estimate_model_visibilities(a_source_lmn, a_dataset_i, baselines, a_frequency)
33+
34+
model_image = sky_imager(model, baselines, a_frequency, 100, 100)
35+
36+
pointing_matrix = compute_pointing_matrix(a_source_lmn, baselines, a_frequency)
37+
calibrated_model, _ = compute_calibrated_model(a_dataset_i, model, maxiter=50)
38+
self_cal_model = self_cal(a_dataset_i, a_source_lmn, baselines, a_frequency)
39+
40+
sky_without_calibrated_model = sky_imager(a_dataset_i - self_cal_model, baselines, a_frequency, 100, 100)
41+
calibrated_model_image = sky_imager(calibrated_model, baselines, a_frequency, 100, 100)
42+
sky_without_model = sky_imager(a_dataset_i - model, baselines, a_frequency, 100, 100)
43+
44+
45+
f, (ax1, ax2, ax3, ax4) = plt.subplots(1, 4)
46+
min, max = numpy.min(sky_image), numpy.max(sky_image)
47+
48+
ax1.scatter(a_source_lmn[:, 0], a_source_lmn[:, 1], marker='o', color='r', s=100, facecolors='none')
49+
im = ax1.imshow(sky_image[::-1,:], vmin=min, vmax=max, extent=(1, -1, -1, 1))
50+
ax1.set_title('initial sky')
51+
52+
ax2.scatter(a_source_lmn[:, 0], a_source_lmn[:, 1], marker='o', color='r', s=100, facecolors='none')
53+
ax2.imshow(sky_without_model[::-1, :], extent=(1, -1, -1, 1), vmin=min, vmax=max)
54+
ax2.set_title('sky without model')
55+
56+
ax3.scatter(a_source_lmn[:, 0], a_source_lmn[:, 1], marker='o', color='r', s=100, facecolors='none')
57+
ax3.imshow(calibrated_model_image[::-1, :], extent=(1, -1, -1, 1))
58+
ax3.set_title('calibrated model image')
59+
60+
ax4.imshow(sky_without_calibrated_model[::-1, :], extent=(1, -1, -1, 1), vmin=min, vmax=max)
61+
to_plot = {name: pos for pos, name in zip(a_source_lmn, a_source_name)}
62+
ax4.set_title('sky without calibrated model')
63+
64+
make_sky_plot(sky_image, to_plot)
65+
66+
plt.show()

0 commit comments

Comments
 (0)