Ⓒ Lincoln Greenhill and Danny Price, 2015
Sythesis imaging is a method to combine the outputs of multiple radio antennas into a single image, boosting angular resolution and overall sensitivity. It depends upon the outputs of antennas being interfered together in pairs, measuring the amplitude and phase of the interfered signal. The output of each interfered pair gives information about the sky, which can be aggregated to form an image of the sky.
This notebook gives a quick interactive introduction to radio interferometers and synthesis imaging.
Before jumping into the subject, we need to load some boilerplate libraries we'll use in the code examples below.
%matplotlib inline from IPython.display import Image from IPython.html.widgets import interact from numpy import pi, cos, sin import numpy as np import pylab as plt import seaborn as sns sns.set()
A radio interferometer computes pair-wise correlations between antennas. For an array of \(N_ant\) antennas, the number of possible pairs of antennas is given by
\( N{pairs} = \frac{(N{ant})(N_{ant} - 1)}{2}.\)
To give a simple example, imagine an array of three simple isotropic antennas (i.e. uniform sensitivity), arranged in a line with equal spacing \(b\) between antennas:
Fig 1: Geometry of an elementary three-element interfometer. Three antennas (elements) are arranged in a line with a spacing \(b\) between them.
If we connect the voltage outputs of the antennas to a device that cross-multiplies pairs of voltage streams \(V_aV_b^\), then we have a simple three-element interferometer, as shown in Fig 1. here, \ denotes complex conjugation. Such a device is known as a correlator.
Now, consider a single source at a sufficient distance to that the incident wavefront can be considered a plane wave. For now, let's consider the sources radiation to be given by
\(e(t) = sin(2\pi\nu t)\)
where \(\nu\) represents frequency and \(t\) represents time. The radiation will reach the right-hand antenna before the left-hand antenna by a time equal to
\(\tau_g = \frac{2b sin\theta}{c},\)
where \(c\) is the speed of light, and \(\theta\) is the angle of incidence. Given this, the output of the middle cross-multiplier in the diagram is:
\(F = sin(2\pi\nu t)sin2\pi\nu(t - \tau_g),\)
for which time average of which can be shown to be \(
The "fringe function" \(F\) is given by Eq. 2.2 in your textbook:
\(F = cos2\pi\nu\tau_g=cos(\frac{2\pi b l}{\pi})\)
Where \(l=sin(\theta)\). Here, \(\theta\) is the "pointing angle" of the telescope.
As the point source transits across the sky, the value of \(\theta\) changes and the fringe function will respond accordingly:
def plot_fringe(bl_length, wavelength): """ Plot the fringe function for a baseline (see Fig 1) bl_length: distance between antennas, in m wavelength: wavelength, in m """ theta = np.linspace(-np.pi, np.pi, 401) l = sin(theta) F = cos(2 * pi * bl_length * l / wavelength) F2 = cos(2 * pi * 2 * bl_length * l / wavelength) plt.plot(l, F, c='#cc0000', label="Baseline 1-2") plt.plot(l, F2, c='#0000cc', label="Baseline 1-3") plt.xlabel("$sin(\\theta)$") plt.ylabel("Fringe amplitude") plt.ylim(-2, 2) plt.legend() f = interact(plot_fringe, bl_length=(1, 100), wavelength=(1, 100))
We see that as baseline increases, the fringe function oscillates more rapidly.
Things get more complicated when there is more than a single point source in the sky. Image that our point source has an companion a distance of \(\theta_{spc}\) away. What would then output of the correlator look like?
def plot_fringe(bl_length, wavelength, source_spacing): """ Plot the fringe function for a baseline with 2 sources (see Fig 1) bl_length: distance between antennas, in m wavelength: wavelength, in m source_spacing: distance between sources, in degrees """ theta = np.linspace(-np.pi, np.pi, 401) l = sin(theta) l_spc = sin(source_spacing / 57.3) # first baseline F_src1_bl1 = cos(2 * pi * bl_length * l / wavelength) F_src2_bl1 = cos(2 * pi * bl_length * (l - l_spc) / wavelength) F_bl1 = F_src1_bl1 + F_src2_bl1 # second baseline F_src1_bl2 = cos(2 * pi * 2 * bl_length * l / wavelength) F_src2_bl2 = cos(2 * pi * 2* bl_length * (l - l_spc) / wavelength) F_bl2 = F_src1_bl2 + F_src2_bl2 plt.plot(l, F_bl1, c='#cc0000', label="Baseline 1-2") plt.plot(l, F_bl2, c='#0000cc', label="Baseline 1-3") plt.xlabel("$sin(\\theta)$") plt.ylabel("Fringe amplitude") plt.ylim(-2, 2) plt.legend() f = interact(plot_fringe, bl_length=(1, 100), wavelength=(1, 100), source_spacing=(0, 90))
Have a play around with the source spacing and you'll see that some source spacings result in absolutely no response (i.e. fringe amplitude is uniformly zero, no matter what the position \(\theta\) of the first source is).
By now I hope you're convinced that the output of the correlator is telling you a lot of information about the composition of the sky. But how do we turn these data into images? To answer that will require a bit more reading of your textbook and a bunch of math. So you'll have to wait until the next notebook!
NB: This is a good time to copy, paste & hack the examples to make some discoveries
Cross-correlation of every sample at Nyquist rate for every antenna pair can be very computationally expensive. Pairwise correlation of all antennas is an \(O(N^2)\) operation, meaning that as the number of antennas \(N\) increases, the number of computations required increases in proportion to \(N^2\).
More precisely, the number of possible pairs of antennas (counting pairing with themselves) is given by
\( N{pairs} = \frac{(N{ant})(N_{ant} + 1)}{2}.\)
The number of computation operations required per second (measured in FLOP/s) also depends upon the bandwidth of the digitized signals. Radio arrays tend to have signal bandwidths \(\Delta\nu\) in the range 10 MHz - 10 GHz; digitization requires sampling at a rate \(2\Delta\nu\).
The widget below computes the number of FLOP/s a correlator would need for a given bandwidth and number of antennas. A rough estimate of how many graphics processing units (GPUs) would be required if they are used to do the computation is also given, along with how much they'd cost to purchase (you would need host systems for them as well!)
tflops_per_gpu = 1.5 # TFLOP/s achievable on a single GPU, NVIDIA GTX960 cost_per_gpu = 250.0 # Cost per GPU def n_ops(bandwidth, n_ant): n_pol = 2 n_antpol = n_ant * n_pol n_baselines = (n_antpol) * (n_antpol + 1) / 2 # Dual-pol + autocorrelations mult_per_sec = n_baselines * bandwidth tflops_per_sec = mult_per_sec * 5 / 1e6 n_gpu = np.ceil(tflops_per_sec / tflops_per_gpu) to_print = "Bandwidth: %s MHz\n" % bandwidth to_print += "Number of antennas: %i\n" % n_ant to_print += "Number of baselines (dualpol): %i\n" % n_baselines to_print += "Correlator computations: %2.3f TFLOP/s\n" % tflops_per_sec to_print += "Number of GPUs required: %i\n" % n_gpu to_print += "Cost to purchase: $%i" % (n_gpu * cost_per_gpu) print to_print f = interact(n_ops, bandwidth=(1, 1e3), n_ant=(2, 1024))