def test_sky_lmn(): sky = Sky(coordinates=SkyCoord([100, 200, 300], [10, 20, 30], unit="deg"), time=Time(["2022-01-01T12:00:00", "2022-01-01T14:00:00"]), frequency=np.linspace(30, 50, 6) * u.MHz, polarization="NE", value=0) l, m, n = sky.compute_lmn(phase_center=SkyCoord(200, 20, unit="deg")) assert l.shape == (1, 3) assert l[0, 1] == 0. assert m[0, 1] == 0. assert n[0, 1] == 1.
def test_sky_get(mock_show): sky = Sky(coordinates=SkyCoord([100, 200, 300], [10, 20, 30], unit="deg"), time=Time(["2022-01-01T12:00:00", "2022-01-01T14:00:00"]), frequency=np.linspace(30, 50, 6) * u.MHz, polarization="NE", value=10) sky_slice = sky[0, 0, 0] assert sky_slice.value.shape == (3, ) assert sky_slice.visible_sky.shape == (3, ) sky_slice.plot(decibel=True, altaz_overlay=True)
def test_sky_init(): sky = Sky(coordinates=SkyCoord([100, 200, 300], [10, 20, 30], unit="deg"), time=Time(["2022-01-01T12:00:00", "2022-01-01T14:00:00"]), frequency=np.linspace(30, 50, 6) * u.MHz, polarization="NE", value=0) assert sky.shape == (2, 6, 1, 3) assert sky.time.size == 2 assert sky.frequency.size == 6 assert sky.polarization.size == 1 assert sky.coordinates.size == 3 assert sky.visible_mask.shape == (2, 6, 1, 3) assert sky.visible_mask[0, 0, 0, 1] assert sky.horizontal_coordinates.shape == (2, 3) assert str( sky ) == "<class 'nenupy.astro.sky.Sky'> instance\nvalue: (2, 6, 1, 3)\n\t* time: (2,)\n\t* frequency: (6,)\n\t* polarization: (1,)\n\t* coordinates: (3,)\n"
def test_beam(self): beam = self.ma.beam(sky=Sky(coordinates=SkyCoord([100, 200, 300], [10, 50, 90], unit="deg"), time=Time("2022-01-01T12:00:00"), frequency=50 * u.MHz, polarization=Polarization.NW), pointing=Pointing.zenith_tracking( time=Time("2022-01-01T11:00:00"), duration=TimeDelta(7200, format="sec")), configuration=NenuFAR_Configuration( beamsquint_correction=True, beamsquint_frequency=50 * u.MHz)) assert beam.value.shape == (1, 1, 1, 3) beam_values = beam[0, 0, 0].value.compute() assert np.ma.is_masked(beam_values[0]) assert beam_values[1] == pytest.approx(29.467, 1e-3) assert beam_values[2] == pytest.approx(301.108, 1e-3)
def test_sky_operations(): sky1 = Sky(coordinates=SkyCoord([100, 200, 300], [10, 20, 30], unit="deg"), time=Time(["2022-01-01T12:00:00", "2022-01-01T14:00:00"]), frequency=np.linspace(30, 50, 2) * u.MHz, polarization="NE", value=6) sky2 = Sky(coordinates=SkyCoord([100, 200, 300], [10, 20, 30], unit="deg"), time=Time(["2022-01-01T12:00:00", "2022-01-01T14:00:00"]), frequency=np.linspace(30, 50, 2) * u.MHz, polarization="NE", value=2) result = sky1 / sky2 assert np.unique(result.value)[0] == 3. sky1 = Sky(coordinates=SkyCoord([100, 200, 300], [10, 20, 30], unit="deg"), time=Time(["2022-01-01T12:00:00", "2022-01-01T14:00:00"]), frequency=np.linspace(30, 50, 2) * u.MHz, polarization="NE", value=6) sky2 = Sky(coordinates=SkyCoord([100, 200, 300], [10, 20, 30], unit="deg"), time=Time(["2022-01-01T12:00:00", "2022-01-01T14:00:00"]), frequency=np.linspace(30, 50, 2) * u.MHz, polarization="NE", value=2) result = sky1 * sky2 assert np.unique(result.value)[0] == 12. sky1 = Sky(coordinates=SkyCoord([100, 200, 300], [10, 20, 30], unit="deg"), time=Time(["2022-01-01T12:00:00", "2022-01-01T14:00:00"]), frequency=np.linspace(30, 50, 2) * u.MHz, polarization="NE", value=6) sky2 = Sky(coordinates=SkyCoord([100, 200, 300], [10, 20, 30], unit="deg"), time=Time(["2022-01-01T12:00:00", "2022-01-01T14:00:00"]), frequency=np.linspace(30, 50, 3) * u.MHz, polarization="NE", value=2) with pytest.raises(ValueError): result = sky1 / sky2
def attenuation_from_zenith(self, coordinates, time: Time = Time.now(), frequency: u.Quantity = 50 * u.MHz, polarization: Polarization = Polarization.NW): """ Returns the attenuation factor evaluated at given ``coordinates`` compared to the zenithal Mini-Array beam gain. :param coordinates: Sky positions equatorial coordinates. :type coordinates: :class:`~astropy.coordinates.SkyCoord` :param time: UTC time at which the attenuation is evaluated. Default is ``now``. :type time: :class:`~astropy.time.Time` :param frequency: Frequency at which the attenuation is evaluated. Default is ``50 MHz``. :type frquency: :class:`~astropy.units.Quantity` :param polarization: NenuFAR antenna polarization. Default is ``Polarization.NW``. :type polarization: :class:`~nenupy.instru.nenufar.Polarization` :returns: Attenuation factor shaped as ``(time, frequency, polarization, coordinates)``. ``NaN`` is returned for any ``coordinates`` that is below the horizon. :rtype: :class:`~numpy.ndarray` :Example: >>> from nenupy.instru.nenufar import MiniArray >>> from astropy.coordinates import SkyCoord >>> ma = MiniArray(index=0) >>> attenuation = ma.attenuation_from_zenith( coordinates=SkyCoord.from_name("Cyg A") ) >>> from nenupy.instru.nenufar import MiniArray >>> from astropy.coordinates import SkyCoord >>> import astropy.units as u >>> ma = MiniArray(index=0) >>> attenuation = ma.attenuation_from_zenith( coordinates=SkyCoord.from_name("Cyg A"), frequency=np.linspace(20, 80, 10)*u.MHz ) .. versionadded:: 2.0.0 """ # Define the pointing towards the zenith pointing = Pointing.zenith_tracking(time=time.reshape((1, )), duration=TimeDelta(10, format="sec")) # Compute the local zenith in equatorial coordinates local_zenith = SkyCoord( 180, 90, unit="deg", frame=AltAz(obstime=time, location=nenufar_position)).transform_to( coordinates.frame) # Find the coordinates below the horizon and compute a mask input_coord_altaz = radec_to_altaz(radec=coordinates, time=time) invisible_mask = input_coord_altaz.alt.deg <= 0 # Concatenate local_zenith and coordinates if coordinates.obstime is None: coordinates.obstime = local_zenith.obstime if coordinates.location is None: coordinates.location = local_zenith.location if coordinates.isscalar: coordinates = coordinates.reshape((1, )) coordinates = coordinates.insert(0, local_zenith) # Prepare a Sky instance for the beam simulation sky = Sky(coordinates=coordinates, frequency=frequency, time=time, polarization=polarization) # Compute the beam beam = self.beam(sky=sky, pointing=pointing) # Compute the attenuation factor relative to the zenith (first member) values = beam.value.compute() output_values = values[..., 1:] / np.expand_dims(values[..., 0], 3) output_values[..., invisible_mask] = np.nan return output_values
def beam( self, sky: Sky, pointing: Pointing, analog_pointing: Pointing = None, configuration: NenuFAR_Configuration = NenuFAR_Configuration() ) -> Sky: r""" Computes the NenuFAR beam over the ``sky`` for a given ``pointing``. .. math:: \mathcal{G}_{\rm NenuFAR}(\nu, \phi, \theta) = \mathcal{F}_{\rm NenuFAR} (\nu, \phi, \theta) \sum_{\rm MA} \mathcal{G}_{\rm MA}(\nu, \phi, \theta) where :math:`\nu` is the frequency, :math:`\phi` is the azimuth, :math:`\theta` is the elevation, :math:`\mathcal{G}_{\rm MA}` is the MiniArray response (see :meth:`~nenupy.instru.nenufar.MiniArray.beam`) and :math:`\mathcal{F}_{\rm NenuFAR}` is the array factor. This method considers the ``sky`` as the desired output (in terms of time, frequency, polarization and sky positions). It evaluates the effective pointing directions for every time step defined in ``sky`` regarding the ``pointing`` input. :param sky: Desired output contained in a :class:`~nenupy.astro.sky.Sky` instance. (:attr:`~nenupy.astro.sky.Sky.time`, :attr:`~nenupy.astro.sky.Sky.frequency`, :attr:`~nenupy.astro.sky.Sky.polarization` and :attr:`~nenupy.astro.sky.Sky.coordinates` are used as inputs for the computation). :type sky: :class:`~nenupy.astro.sky.Sky` :param pointing: Instance of :class:`~nenupy.astro.pointing.Pointing` that defines the targeted **numerical** pointing directions over the time. :type pointing: :class:`~nenupy.astro.pointing.Pointing` :param analog_pointing: Instance of :class:`~nenupy.astro.pointing.Pointing` that defines the **analog** pointing directions over the time. This pointing is subject to beamsquint corrections. :type analog_pointing: :class:`~nenupy.astro.pointing.Pointing` :param configuration: NenuFAR configuration to consider during the beam simulation. The beamsquint correction and its frequency setting are defined here. Default is ``NenuFAR_Configuration(beamsquint_correction=True, beamsquint_frequency=50MHz)``. :type configuration: :class:`~nenupy.instru.nenufar.NenuFAR_Configuration` :returns: The instance of :class:`~nenupy.astro.sky.Sky` given as input is returned, its attribute :attr:`~nenupy.astro.sky.Sky.value` is updated with the result of the beam computation (stored as an :class:`~dask.array.Array`) and shaped as ``(time, frequency, polarization, coordinates)``. :rtype: :class:`~nenupy.astro.sky.Sky` .. seealso:: :meth:`~nenupy.instru.interferometer.Interferometer.array_factor` and :ref:`beam_simulation_doc` """ log.info(f"Computing <class 'NenuFAR'> beam ({self.size} " f"Mini-Arrays, {sky.time.size} time and " f"{sky.frequency.size} frequency slots).") # Sorting out the analog pointing, make it equal to the # numerical pointing if it is not specifically defined. if not analog_pointing: analog_pointing = pointing log.info( "Analog pointing is set according to the numerical pointing.") # Computing the Array Factor of the whole NenuFAR array. array_factor = self.array_factor(sky=sky, pointing=pointing) # Finding the unique Mini-Array rotations and the number # of MAs corresponding to each rotation. rots, indices, counts = np.unique( self.miniarray_rotations.to(u.deg).value % 60, return_counts=True, return_index=True) # Summing all different (due to rotation) Mini-Array beam # patterns, although only executing it at most 6 times # because there could only be 6 different rotations. # Even though antGain updates the same sky instance, the # value attr * count creates new memeory allocations. antenna_gain = np.sum(np.array([ gain(sky=sky, pointing=analog_pointing, configuration=configuration).value * count for gain, count in zip(self.antenna_gains[indices], counts) ]), axis=0) # Updating the sky object value array where the the sky # is above the horizon as the product of the NenuFAR array # factor and the combined Mini-Array gain patterns. sky.value = array_factor * antenna_gain return sky
def beam(self, sky: Sky, pointing: Pointing) -> Sky: r""" Computes the phased-array response :math:`\mathcal{G}` over the ``sky`` for a given ``pointing``. .. math:: \mathcal{G}(\nu, \phi, \theta) = \sum_{\rm ant} \mathcal{F}(\nu, \phi, \theta) \mathcal{G}_{\rm ant} (\nu, \phi, \theta) where :math:`\nu` is the frequency, :math:`\phi` is the azimuth, :math:`\theta` is the elevation, :math:`\mathcal{G}_{\rm ant}` is the individual array element radiation pattern and :math:`\mathcal{F}` is the array factor. This method considers the ``sky`` as the desired output (in terms of time, frequency, polarization and sky positions). It evaluates the effective pointing directions for every time step defined in ``sky`` regarding the ``pointing`` input. :param sky: Desired output contained in a :class:`~nenupy.astro.sky.Sky` instance. :type sky: :class:`~nenupy.astro.sky.Sky` :param pointing: Instance of :class:`~nenupy.astro.pointing.Pointing` that defines the targeted pointing directions over the time. :type pointing: :class:`~nenupy.astro.pointing.Pointing` :return: The instance of :class:`~nenupy.astro.sky.Sky` given as input is returned, its attribute :attr:`~nenupy.astro.sky.Sky.value` is updated with the result of the beam computation (stored as an :class:`~dask.array.Array`) and shaped as ``(time, frequency, polarization, coordinates)``. :rtype: :class:`~nenupy.astro.sky.Sky` .. seealso:: :meth:`~nenupy.instru.interferometer.Interferometer.array_factor` """ # Compute the array factor array_factor = self.array_factor(sky=sky, pointing=pointing) # Compute the total antenna gain, i.e. the sum of all # antenna gains for beamforming. antenna_gain = np.sum(np.array( [gain(sky=sky, pointing=pointing) for gain in self.antenna_gains]), axis=0) # Rechunk the Dask Array before the computation # coord_chunk = array_factor.shape[-1]//cpu_count() # coord_chunk = 1 if coord_chunk == 0 else coord_chunk # array_factor = array_factor.rechunk(array_factor.shape[:-1] + (coord_chunk,)) # Perform the Dask computation of array factor times antenna # gains. Update the sky instance values. sky.value = array_factor * antenna_gain return sky