def test_wavelength(): wvl = wavelength(30 * u.MHz) assert isinstance(wvl, u.Quantity) assert wvl.to(u.m).value == pytest.approx(9.993, 1e-3) wvl_array = wavelength([10, 20, 30] * u.MHz) assert wvl_array.shape == (3, )
def array_factor(self, phase_center, coords, antpos): """ """ if not (isinstance(phase_center, AltAz) or hasattr(phase_center, 'altaz')): raise TypeError('phase_center should be an AltAz instance') if not (isinstance(coords, AltAz) or hasattr(coords, 'altaz')): raise TypeError('coords should be an AltAz instance') if not isinstance(antpos, np.ndarray): raise TypeError('antpos should be an np.ndarray instance') if antpos.shape[1] != 3: raise IndexError('antpos should have 2nd dimension = 3 (x, y, z)') def get_phi(az, el, antpos): """ az, el in radians """ xyz_proj = np.array( [np.cos(az) * np.cos(el), np.sin(az) * np.cos(el), np.sin(el)]) antennas = np.array(antpos) phi = np.dot(antennas, xyz_proj) return phi phi0 = get_phi(az=[phase_center.az.rad], el=[phase_center.alt.rad], antpos=antpos) phi_grid = get_phi(az=coords.az.rad, el=coords.alt.rad, antpos=antpos) delay = phi_grid - phi0 coeff = 2j * np.pi / wavelength(self.freq).value af = np.sum(np.exp(coeff * delay), axis=0) return np.real(af * af.conjugate())
def freq_range(self, f): if f == [None, None]: self._conditions['freq'] = '' self._freq_range = f return if not isinstance(f, list): raise TypeError('freq_range must be a list') if not len(f) == 2: raise ValueError('freq_range must be of length 2') if not all([isinstance(fi, u.Quantity) for fi in f]): f = [fi * u.MHz for fi in f] lmax = wavelength(f[0]).to(u.m).value lmin = wavelength(f[1]).to(u.m).value self._conditions['freq'] = f'(em_min >= {lmin} AND '\ f'em_max <= {lmax})' log.info(f'freq_range set to {f}.') self._freq_range = f return
def test_wavelength(): # Float input wl = wavelength(30) assert isinstance(wl, u.Quantity) assert wl.unit == 'm' assert wl.to(u.m).value == pytest.approx(10., 1e-2) # Nnumpy ndarray input freqs = np.array([10, 20, 30, 40]) wavel = np.array([30, 15, 10, 7.5]) wl = wavelength(freqs) assert isinstance(wl, u.Quantity) assert wl.unit == 'm' assert wl.to(u.m).value == pytest.approx(wavel, 1e-2) # Astropy Quantity input freqs = freqs * 1e6 * u.Hz wl = wavelength(freqs) assert isinstance(wl, u.Quantity) assert wl.unit == 'm' assert wl.to(u.m).value == pytest.approx(wavel, 1e-2)
def uvw_wave(self): """ UVW in lambdas. :getter: (times, freqs, baselines, UVW) :type: :class:`~numpy.ndarray` """ if not hasattr(self, '_uvw'): raise Exception('Run .compute() first.') if self.freqs is None: raise ValueError('No frequency input, fill self.freqs.') lamb = wavelength(self.freqs).value na = np.newaxis return self._uvw[:, na, :, :] / lamb[na, :, na, na]
def get_beamform(self, pointing: Pointing, frequency_selection: str = None, time_selection: str = None, mini_arrays: np.ndarray = np.array([0, 1]), polarization: str = "NW", calibration: str = "default" ): """ :Example: from nenupy.io.bst import BST, XST bst = BST("20191129_141900_BST.fits") xst = XST("20191129_141900_XST.fits") bf_cal = xst.get_beamform( pointing = Pointing.from_bst(bst, beam=0, analog=False), mini_arrays=bst.mini_arrays, calibration="default" ) """ frequency_mask = self._get_freq_mask(frequency_selection) time_mask = self._get_time_mask(time_selection) # Select the mini-arrays cross correlations nenufar = NenuFAR()#[self.mini_arrays] bf_nenufar = NenuFAR()[mini_arrays] ma_real_indices = np.array([nenufar_miniarrays[name]["id"] for name in bf_nenufar.antenna_names]) if np.any( ~np.isin(ma_real_indices, self.mini_arrays) ): raise IndexError( f"Selected Mini-Arrays {mini_arrays} are outside possible values: {self.mini_arrays}." ) ma_indices = np.arange(self.mini_arrays.size, dtype="int")[np.isin(self.mini_arrays, ma_real_indices)] ma1, ma2 = np.tril_indices(self.mini_arrays.size, 0) mask = np.isin(ma1, ma_indices) & np.isin(ma2, ma_indices) # Calibration table if calibration.lower() == "none": # No calibration cal = np.ones( (self.frequencies[frequency_mask].size, ma_indices.size) ) else: pol_idx = {"NW": [0], "NE": [1]} cal = read_cal_table( calibration_file=calibration ) cal = cal[np.ix_( freq2sb(self.frequencies[frequency_mask]), ma_real_indices, pol_idx[polarization] )].squeeze(axis=2) # Load and filter the data vis = self.get( frequency_selection=frequency_selection, time_selection=time_selection, polarization= "XX" if polarization.upper() == "NW" else "YY", )[:, :, mask] # Insert the data in a matrix tri_x, tri_y = np.tril_indices(ma_indices.size, 0) vis_matrix = np.zeros( ( self.time[time_mask].size, self.frequencies[frequency_mask].size, ma_indices.size, ma_indices.size ), dtype=np.complex ) vis_matrix[:, :, tri_x, tri_y] = vis vis_matrix[:, :, tri_y, tri_x] = vis_matrix[:, :, tri_x, tri_y].conj() # Calibrate the Xcorr with the caltable for fi in range(vis_matrix.shape[1]): cal_i = np.expand_dims(cal[fi], axis=1) cal_i_h = np.expand_dims(cal[fi].T.conj(), axis=0) mul = np.dot(cal_i, cal_i_h) vis_matrix[:, fi, :, :] *= mul[np.newaxis, :, :] # Phase the visibilities towards the phase center phase = np.ones( ( self.time[time_mask].size, self.frequencies[frequency_mask].size, ma_indices.size, ma_indices.size ), dtype=np.complex ) altaz_pointing = pointing.horizontal_coordinates if altaz_pointing.size == 1: # Transit pass else: # Multiple pointings, get the correct value for all times altaz_pointing = pointing[self.time[time_mask]].horizontal_coordinates az = altaz_pointing.az.rad el = altaz_pointing.alt.rad ground_projection = np.array([ np.cos(el) * np.cos(az), np.cos(el) * np.sin(az), np.sin(el) ]) rot = np.radians(-90) rotation = np.array( [ [ np.cos(rot), np.sin(rot), 0], [-np.sin(rot), np.cos(rot), 0], [ 0, 0, 1] ] ) ma1_pos = np.dot( nenufar.antenna_positions[ma1[mask]], rotation ) ma2_pos = np.dot( nenufar.antenna_positions[ma2[mask]], rotation ) dphi = np.dot( ma1_pos - ma2_pos, ground_projection ).T wvl = wavelength(self.frequencies[frequency_mask]).to(u.m).value phase[:, :, tri_x, tri_y] = np.exp( -2.j*np.pi/wvl[None, :, None] * dphi[:, None, :] ) phase[:, :, tri_y, tri_x] = phase[:, :, tri_x, tri_y].conj().copy() data = np.sum((vis_matrix * phase).real, axis=(2, 3)) return BST_Slice( time=self.time[time_mask], frequency=self.frequencies[frequency_mask], value=data.squeeze() )
def make_nearfield(self, radius: u.Quantity = 400*u.m, npix: int = 64, sources: list = [] ): r""" Computes the Near-field image from the cross-correlation statistics data :math:`\mathcal{V}`. The distances between each Mini-Array :math:`{\rm MA}_i` and the ground positions :math:`Delta` is: .. math:: d_{\rm{MA}_i} (x, y) = \sqrt{ ({\rm MA}_{i, x} - \Delta_x)^2 + ({\rm MA}_{i, y} - \Delta_y)^2 + \left( {\rm MA}_{i, z} - \sum_j \frac{{\rm MA}_{j, z}}{n_{\rm MA}} - 1 \right)^2 } Then, the near-field image :math:`n_f` can be retrieved as follows (:math:`k` and :math:`l` being two distinct Mini-Arrays): .. math:: n_f (x, y) = \sum_{k, l} \left| \sum_{\nu} \langle \mathcal{V}_{\nu, k, l}(t) \rangle_t e^{2 \pi i \left( d_{{\rm MA}_k} - d_{{\rm MA}_l} \right) (x, y) \frac{\nu}{c}} \right| .. note:: To simulate astrophysical source of brightness :math:`\mathcal{B}` footprint on the near-field, its visibility per baseline of Mini-Arrays :math:`k` and :math:`l` are computed as: .. math:: \mathcal{V}_{{\rm simu}, k, l} = \mathcal{B} e^{2 \pi i \left( \mathbf{r}_k - \mathbf{r}_l \right) \cdot \mathbf{u} \frac{\nu}{c}} with :math:`\mathbf{r}` the ENU position of the Mini-Arrays, :math:`\mathbf{u} = \left( \cos(\theta) \sin(\phi), \cos(\theta) \cos(\phi), sin(\theta) \right)` the ground projection vector (in East-North-Up coordinates), (:math:`\phi` and :math:`\theta` are the source horizontal coordinates azimuth and elevation respectively). :param radius: Radius of the ground image. Default is ``400m``. :type radius: :class:`~astropy.units.Quantity` :param npix: Number of pixels of the image size. Default is ``64``. :type npix: `int` :param sources: List of source names for which their near-field footprint may be computed. Only sources above 10 deg elevation will be considered. :type sources: `list` :returns: Tuple of near-field image and a dictionnary containing all source footprints. :rtype: `tuple`(:class:`~numpy.ndarray`, `dict`) :Example: from nenupy.io.xst import XST xst = XST("xst_file.fits") nearfield, src_dict = xst.make_nearfield(sources=["Cas A", "Sun"]) .. versionadded:: 1.1.0 """ def compute_nearfield_imprint(visibilities, phase): # Phase and average in frequency nearfield = np.mean( visibilities[..., None, None] * phase, axis=0 ) # Average in baselines nearfield = np.nanmean(np.abs(nearfield), axis=0) with ProgressBar() if log.getEffectiveLevel() <= logging.INFO else DummyCtMgr(): return nearfield.compute() # Mini-Array positions in ENU coordinates nenufar = NenuFAR()[self.mini_arrays] ma_etrs = l93_to_etrs(nenufar.antenna_positions) ma_enu = etrs_to_enu(ma_etrs) # Treat baselines ma1, ma2 = np.tril_indices(self.mini_arrays.size, 0) cross_mask = ma1 != ma2 # Mean time of observation obs_time = self.time[0] + (self.time[-1] - self.time[0])/2. # Delays at the ground radius_m = radius.to(u.m).value ground_granularity = np.linspace(-radius_m, radius_m, npix) posx, posy = np.meshgrid(ground_granularity, ground_granularity) posz = np.ones_like(posx) * (np.average(ma_enu[:, 2]) + 1) ground_grid = np.stack((posx, posy, posz), axis=2) ground_distances = np.sqrt( np.sum( (ma_enu[:, None, None, :] - ground_grid[None])**2, axis=-1 ) ) grid_delays = ground_distances[ma1] - ground_distances[ma2] # (nvis, npix, npix) n_bsl = ma1[cross_mask].size grid_delays = da.from_array( grid_delays[cross_mask], chunks=(np.floor(n_bsl/os.cpu_count()), npix, npix) ) # Mean in time the visibilities vis = np.mean( self.value, axis=0 )[..., cross_mask] # (nfreqs, nvis) vis = da.from_array( vis, chunks=(1, np.floor(n_bsl/os.cpu_count()))#(self.frequency.size, np.floor(n_bsl/os.cpu_count())) ) # Make the nearfield image log.info( f"Computing nearfield (time: {self.time.size}, frequency: {self.frequency.size}, baselines: {vis.shape[1]}, pixels: {posx.size})... " ) wvl = wavelength(self.frequency).to(u.m).value phase = np.exp(2.j * np.pi * (grid_delays[None, ...]/wvl[:, None, None, None])) log.debug("Computing the phase term...") with ProgressBar() if log.getEffectiveLevel() <= logging.INFO else DummyCtMgr(): phase = phase.compute() log.debug("Computing the nearf-field...") nearfield = compute_nearfield_imprint(vis, phase) # Compute nearfield imprints for other sources simu_sources = {} for src_name in sources: # Check that the source is visible if src_name.lower() in ["sun", "moon", "venus", "mars", "jupiter", "saturn", "uranus", "neptune"]: src = SolarSystemTarget.from_name(name=src_name, time=obs_time) else: src = FixedTarget.from_name(name=src_name, time=obs_time) altaz = src.horizontal_coordinates#[0] if altaz.alt.deg <= 10: log.debug(f"{src_name}'s elevation {altaz[0].alt.deg}<=10deg, not considered for nearfield imprint.") continue # Projection from AltAz to ENU vector az_rad = altaz.az.rad el_rad = altaz.alt.rad cos_az = np.cos(az_rad) sin_az = np.sin(az_rad) cos_el = np.cos(el_rad) sin_el = np.sin(el_rad) to_enu = np.array( [cos_el*sin_az, cos_el*cos_az, sin_el] ) # src_delays = np.matmul( # ma_enu[ma1] - ma_enu[ma2], # to_enu # ) # src_delays = da.from_array( # src_delays[cross_mask, :], # chunks=((np.floor(n_bsl/os.cpu_count()), npix, npix), 1) # ) ma1_enu = da.from_array( ma_enu[ma1[cross_mask]], chunks=np.floor(n_bsl/os.cpu_count()) ) ma2_enu = da.from_array( ma_enu[ma2[cross_mask]], chunks=np.floor(n_bsl/os.cpu_count()) ) src_delays = np.matmul( ma1_enu - ma2_enu, to_enu ) # Simulate visibilities src_vis = np.exp(2.j * np.pi * (src_delays/wvl)) src_vis = np.swapaxes(src_vis, 1, 0) log.debug(f"Computing the nearf-field imprint of {src_name}...") simu_sources[src_name] = compute_nearfield_imprint(src_vis, phase) return nearfield, simu_sources
def make_image(self, resolution: u.Quantity = 1*u.deg, fov_radius: u.Quantity = 25*u.deg, phase_center: SkyCoord = None, stokes: str = "I" ): """ :Example: xst = XST("XST.fits") data = xst.get_stokes("I") sky = data.make_image( resolution=0.5*u.deg, fov_radius=27*u.deg, phase_center=SkyCoord(277.382, 48.746, unit="deg") ) sky[0, 0, 0].plot( center=SkyCoord(277.382, 48.746, unit="deg"), radius=24.5*u.deg ) """ exposure = self.time[-1] - self.time[0] # Compute XST UVW coordinates (zenith phased) uvw = compute_uvw( interferometer=NenuFAR()[self.mini_arrays], phase_center=None, # will be zenith time=self.time, ) # Prepare visibilities rephasing rephase_matrix, uvw = self.rephase_visibilities( phase_center=phase_center, uvw=uvw ) # Mask auto-correlations ma1, ma2 = np.tril_indices(self.mini_arrays.size, 0) cross_mask = ma1 != ma2 uvw = uvw[:, cross_mask, :] # Transform to lambda units wvl = wavelength(self.frequency).to(u.m).value uvw = uvw[:, None, :, :]/wvl[None, :, None, None] # (t, f, bsl, 3) # Mean in time uvw = np.mean(uvw, axis=0) # Prepare the sky sky = HpxSky( resolution=resolution, time=self.time[0] + exposure/2, frequency=np.mean(self.frequency), polarization=np.array([stokes]), value=np.nan ) # Compute LMN coordinates image_mask = sky.visible_mask[0, 0, 0] image_mask *= sky.coordinates.separation(phase_center) <= fov_radius l, m, n = sky.compute_lmn( phase_center=phase_center, coordinate_mask=image_mask ) lmn = np.array([l, m, (n - 1)], dtype=np.float32).T n_pix = l.size lmn = da.from_array( lmn, chunks=(np.floor(n_pix/os.cpu_count()), 3) ) # Transform to Dask array n_bsl = uvw.shape[1] n_freq = self.frequency.size n_pix = l.size uvw = da.from_array( uvw.astype(np.float32), chunks=(n_freq, np.floor(n_bsl/os.cpu_count()), 3) ) # Compute the phase uvwlmn = np.sum(uvw[:, :, None, :] * lmn[None, None, :, :], axis=-1) phase = np.exp( -2j * np.pi * uvwlmn ) # (f, bsl, npix) # Rephase and average visibilites vis = np.mean( # Mean in time self.value * rephase_matrix, axis=0 )[..., cross_mask] # (nfreqs, nvis) # Make dirty image dirty = np.nanmean( # mean in baselines np.real( np.mean( # mean in freq vis[:, :, None] * phase, axis=0 ) ), axis=0 ) # Insert dirty image in Sky object log.info( f"Computing image (time: {self.time.size}, frequency: {self.frequency.size}, baselines: {vis.shape[1]}, pixels: {phase.shape[-1]})... " ) with ProgressBar() if log.getEffectiveLevel() <= logging.INFO else DummyCtMgr(): sky.value[0, 0, 0, image_mask] = dirty.compute() return sky
def rephase_visibilities(self, phase_center, uvw): """ """ # Compute the zenith original phase center zenith = SkyCoord( np.zeros(self.time.size), np.ones(self.time.size)*90, unit="deg", frame=AltAz( obstime=self.time, location=nenufar_position ) ) zenith_phase_center = altaz_to_radec(zenith) # Define the rotation matrix def rotation_matrix(skycoord): """ """ ra_rad = skycoord.ra.rad dec_rad = skycoord.dec.rad if np.isscalar(ra_rad): ra_rad = np.array([ra_rad]) dec_rad = np.array([dec_rad]) cos_ra = np.cos(ra_rad) sin_ra = np.sin(ra_rad) cos_dec = np.cos(dec_rad) sin_dec = np.sin(dec_rad) return np.array([ [cos_ra, -sin_ra, np.zeros(ra_rad.size)], [-sin_ra*sin_dec, -cos_ra*sin_dec, cos_dec], [sin_ra*cos_dec, cos_ra*cos_dec, sin_dec], ]) # Transformation matrices to_origin = rotation_matrix(zenith_phase_center) # (3, 3, ntimes) to_new_center = rotation_matrix(phase_center) # (3, 3, 1) total_transformation = np.matmul( np.transpose( to_new_center, (2, 0, 1) ), to_origin ) # (3, 3, ntimes) rotUVW = np.matmul( np.expand_dims( (to_origin[2, :] - to_new_center[2, :]).T, axis=1 ), np.transpose( to_origin, (2, 1, 0) ) ) # (ntimes, 1, 3) phase = np.matmul( rotUVW, np.transpose(uvw, (0, 2, 1)) ) # (ntimes, 1, nvis) rotate_visibilities = np.exp( 2.j*np.pi*phase/wavelength(self.frequency).to(u.m).value[None, :, None] ) # (ntimes, nfreqs, nvis) new_uvw = np.matmul( uvw, # (ntimes, nvis, 3) np.transpose(total_transformation, (2, 0, 1)) ) return rotate_visibilities, new_uvw
def array_factor(self, az, el, antpos, freq): r""" Computes the array factor :math:`\mathcal{A}` (i.e. the far-field radiation pattern obtained for an array of :math:`n_{\rm ant}` radiators). .. math:: \mathcal{A} = \left| \sum_{n_{\scriptscriptstyle \rm ant}} e^{2 \pi i \frac{\nu}{c} (\varphi_0 - \varphi)} \right|^2 .. math:: \varphi = \underset{\scriptstyle n_{\scriptscriptstyle \rm ant}\, \times\, 3}{\mathbf{P}_{\rm ant}} \cdot \pmatrix{ \cos(\phi)\cos(\theta)\\ \sin(\phi)\cos(\theta)\\ \sin(\theta) } .. math:: \varphi_0 = \underset{\scriptstyle n_{\scriptscriptstyle \rm ant}\, \times\, 3}{\mathbf{P}_{\rm ant}} \cdot \pmatrix{ \cos(\phi_0)\cos(\theta_0)\\ \sin(\phi_0)\cos(\theta_0)\\ \sin(\theta_0) } :math:`\mathbf{P}_{\rm ant}` is the antenna position matrix, :math:`\phi` and :math:`\theta` are the sky local coordinates (azimuth and elevation respectively) gridded on a HEALPix representation, whereas :math:`\phi_0` and :math:`\theta_0` are the pointing direction in local coordinates. :param az: Pointing azimuth (in degrees if `float`) :type az: `float` or :class:`~astropy.units.Quantity` :param el: Pointing elevation (in degrees if `float`) :type el: `float` or :class:`~astropy.units.Quantity` :param antpos: Antenna positions shaped as (n_ant, 3) :type antpos: :class:`~numpy.ndarray` :param freq: Frequency (in MHz if `float`) :type freq: `float` or :class:`~astropy.units.Quantity` :returns: Array factor :rtype: :class:`~numpy.ndarray` """ def get_phi(az, el, antpos): """ az, el in radians """ xyz_proj = np.array( [np.cos(az) * np.cos(el), np.sin(az) * np.cos(el), np.sin(el)]) antennas = np.array(antpos) phi = np.dot(antennas, xyz_proj) return phi if not isinstance(az, u.Quantity): az *= u.deg if not isinstance(el, u.Quantity): el *= u.deg self.phase_center = to_radec(ho_coord(az=az, alt=el, time=self.time)) phi0 = get_phi(az=[az.to(u.rad).value], el=[el.to(u.rad).value], antpos=antpos) phi_grid = get_phi(az=self.ho_coords.az.rad, el=self.ho_coords.alt.rad, antpos=antpos) nt = ne.set_num_threads(ne._init_num_threads()) delay = ne.evaluate('phi_grid-phi0') coeff = 2j * np.pi / wavelength(freq).value if self.ncpus == 1: # Normal af = ne.evaluate('sum(exp(coeff*delay),axis=0)') # elif self.ncpus == 'numba': # af = perfcompute(coeff * delay) else: # Multiproc af = np.sum(mp_expo(self.ncpus, coeff, delay), axis=0) #return np.abs(af * af.conjugate()) return np.real(af * af.conjugate())
def image(self, resolution=1, fov=50): r""" Converts NenuFAR-TV-like data sets containing visibilities (:math:`V(u,v,\nu , t)`) into images :math:`I(l, m, \nu)` phase-centered at the local zenith while time averaging the visibilities. The Field of View ``fov`` argument defines the diameter angular size (zenith-centered) above which the image is not computed. .. math:: I(l, m, \nu) = \int \langle V(u, v, \nu, t) \rangle_t e^{ 2 \pi i \frac{\nu}{c} \left( \langle u(t) \rangle_t l + \langle v(t) \rangle_t m \right) } \, du \, dv :param resolution: Resoltion (in degrees if a `float` is given) of the HEALPix grid (passed to initialize the :class:`~nenupy.astro.hpxsky.HpxSky` object). :type resolution: `float` or :class:`~astropy.units.Quantity` :param fov: Field of view diameter of the image (in degrees if a `float` is given). :type fov: `float` or :class:`~astropy.units.Quantity` :returns: HEALPix sky object embedding the computed image. :rtype: :class:`~nenupy.astro.hpxsky.HpxSky` :Example: >>> from nenupy.crosslet import TV_Data >>> import astropy.units as u >>> tv = TV_Data('20191204_132113_nenufarTV.dat') >>> im = tv.image( resolution=0.2*u.deg, fov=60*u.deg ) .. seealso:: :class:`~nenupy.astro.hpxsky.HpxSky`, :meth:`~nenupy.astro.hpxsky.HpxSky.lmn`, :meth:`~nenupy.crosslet.uvw.UVW.from_tvdata` .. warning:: This method is intended to be used for NenuFAR-TV data and relatively small XST datasets. It is not suited to long observations for which a MS conversion is required before using imaging dedicated softwares. """ if not isinstance(fov, un.Quantity): fov *= un.deg f_idx = 0 # Frequency index # Sky preparation sky = HpxSky(resolution=resolution) exposure = self.times[-1] - self.times[0] sky.time = self.times[0] + exposure / 2. sky._is_visible = sky._ho_coords.alt >= 90 * un.deg - fov / 2. phase_center = eq_zenith(sky.time) l, m, n = sky.lmn(phase_center=phase_center) # UVW coordinates uvw = UVW.from_tvdata(self) u = np.mean( # Mean in time uvw.uvw[:, :, 0], axis=0)[self.mask_auto] / wavelength( self.freqs[f_idx]).value v = np.mean(uvw.uvw[:, :, 1], axis=0)[self.mask_auto] / wavelength( self.freqs[f_idx]).value w = np.mean( # Mean in time uvw.uvw[:, :, 2], axis=0)[self.mask_auto] / wavelength( self.freqs[f_idx]).value # Mulitply (u, v) by (l, m) and compute FT exp ul = ft_mul(x=np.tile(u, (l.size, 1)).T, y=np.tile(l, (u.size, 1))) vm = ft_mul(x=np.tile(v, (m.size, 1)).T, y=np.tile(m, (v.size, 1))) phase = ft_phase(ul, vm) # Phase visibilities vis = np.mean( # Mean in time self.stokes_i, axis=0)[f_idx, :][self.mask_auto] im = np.zeros(l.size) for i in tqdm(prange(l.size)): im[i] = np.real(ft_sum(vis, phase[:, i])) sky.skymap[sky._is_visible] = im return sky
def beamform(self, az, el, pol='NW', ma=None, calibration='default'): r""" Converts cross correlation statistics data XST, :math:`\mathbf{X}(t, \nu)`, in beamformed data BST, :math:`B(t, \nu)`, where :math:`t` and :math:`\nu` are the time and the frequency respectively. :math:`\mathbf{X}(t, \nu)` is a subset of XST data at the required polarization ``pol``. This is done for a given phasing direction in local sky coordinates :math:`\varphi` (azimuth, ``az``) and :math:`\theta` (elevation, ``el``), with a selection of Mini-Arrays ``ma`` (numbered :math:`a`). .. math:: B (t, \nu) = \operatorname{Re} \left\{ \sum \left[ \underset{\scriptscriptstyle a \times 1}{\mathbf{C}} \cdot \underset{\scriptscriptstyle 1 \times a}{\mathbf{C}^{H}} \right](\nu) \cdot \underset{\scriptscriptstyle a \times a}{\mathbf{X}} (t, \nu) \cdot \left[ \underset{\scriptscriptstyle a \times 1}{\mathbf{P}} \cdot \underset{\scriptscriptstyle 1 \times a}{\mathbf{P}^{H}} \right](\nu) \right\} .. math:: \rm{with} \quad \cases{ \mathbf{C}(\nu ) = e^{2 \pi i \nu \mathbf{ t }} \quad \rm{the~calibration~ file}\\ \mathbf{P} (\nu) = e^{-2 \pi i \frac{\nu}{c} (\mathbf{b} \cdot \mathbf{u})} \quad \rm{phasing}\\ \underset{\scriptscriptstyle a \times 3}{\mathbf{b}} = \mathbf{a}_{1} - \mathbf{a}_{2} \quad \rm{baseline~positions}\\ \underset{\scriptscriptstyle 3 \times 1}{\mathbf{u}} = \left[ \cos(\theta)\cos(\varphi), \cos(\theta)\sin(\varphi), \sin(\theta) \right] } :param az: Azimuth coordinate used for beamforming (default unit is degrees in `float` input). :type az: `float` or :class:`~astropy.units.Quantity` :param el: Elevation coordinate used for beamforming (default unit is degrees in `float` input). :type el: `float` or :class:`~astropy.units.Quantity` :param pol: Polarization (either ``'NW'`` or ``'NE'``. :type pol: `str` :param ma: Subset of Mini-Arrays (minimum 2) used for beamforming. :type ma: `list` or :class:`~numpy.ndarray` :param calibration: Antenna delay calibration file (i.e, :math:`\mathbf{C}`). If ``'none'``, no calibration is applied. If ``'default'``, the standard calibration file is used, otherwise the calibration file name should be given (see also :func:`~nenupy.instru.instru.read_cal_table`). :type calibration: `str` :returns: Beamformed data. :rtype: :class:`~nenupy.beamlet.sdata.SData` :Example: >>> from nenupy.crosslet import XST_Data >>> xst = XST_Data('20191129_141900_XST.fits') >>> bf = xst.beamform( az=180, el=90, pol='NW', ma=[17, 44], calibration='default' ) """ log.info('Beamforming towards az={}, el={}, pol={}'.format( az, el, pol)) # Mini-Array selection if ma is None: ma = self.mas.copy() mas = self._mas_idx[np.isin(self.mas, ma)] # Calibration table if calibration.lower() == 'none': # No calibration cal = np.ones((self.sb_idx.size, mas.size)) else: pol_idx = {'NW': [0], 'NE': [1]} cal = read_cal_table(calfile=calibration) cal = cal[np.ix_(self.sb_idx, mas, pol_idx[pol])].squeeze() # Matrix of BSTs c = np.zeros((self.times.size, self.freqs.size, mas.size, mas.size), dtype=np.complex) # Matrix of phasings p = np.ones((self.freqs.size, mas.size, mas.size), dtype=np.complex) # Pointing direction if isinstance(az, un.Quantity): az = az.to(un.deg).value if isinstance(el, un.Quantity): el = el.to(un.deg).value az = np.radians(az) el = np.radians(el) u = np.array( [np.cos(el) * np.cos(az), np.cos(el) * np.sin(az), np.sin(el)]) # Polarization selection mask = np.isin(self._ant1, mas) & np.isin(self._ant2, mas) log.info('Loading data...') if pol.upper() == 'NW': cpol = self.xx[:, :, mask] else: cpol = self.yy[:, :, mask] log.info('Data of shape {} loaded for beamforming'.format(cpol.shape)) # Put the Xcorr in a matrix trix, triy = np.tril_indices(mas.size, 0) c[:, :, trix, triy] = cpol c[:, :, triy, trix] = c[:, :, trix, triy].conj() # Calibrate the Xcorr with the caltable for fi in prange(c.shape[1]): cal_i = np.expand_dims(cal[fi], axis=1) cal_i_h = np.expand_dims(cal[fi].T.conj(), axis=0) mul = np.dot(cal_i, cal_i_h) c[:, fi, :, :] *= mul[np.newaxis, :, :] # Phase the Xcorr dphi = np.dot(ma_pos[self._ant1[mask]] - ma_pos[self._ant2[mask]], u) wavel = wavelength(self.freqs).value p[:, trix, triy] = np.exp(-2.j * np.pi / wavel[:, None] * dphi) p[:, triy, trix] = p[:, trix, triy].conj() data = np.sum((c * p).real, axis=(2, 3)) log.info('Beamforming complete.') return SData(data=np.expand_dims(data, axis=2), time=self.times, freq=self.freqs, polar=np.array([pol.upper()]))