Bright Sources Contamination

As NenuFAR Mini-Arrays consist of hexagonal tiles of dipole antennas (which are analog-phased), the element spacing causes spatial aliasing effects known as grating lobes. These are basically copies of the primary beam (aimed at the target celestial position), with intensities that can even exceed it for some configurations, while combined with the anntenna radiation pattern (maximal at the local zenith). In order to dampen this effect, the Mini-Arrays are rotated with respect to each other (see Mini-Array distribution), and coherently summed for beamforming observations.

See also

NenuFAR Beam Simulation to better visualize the NenuFAR beam shape.

Nonetheless, bright radio sources could still fall within the NenuFAR grating lobes during the course of an observation. This results in specific artefacts such as time-frequency ‘granularities’ seen in dynamic spectra obtained from beamforming observations or causes visibilities mixing for imaging observations.

Predicting such contamination may be helpful while scheduling NenuFAR observations. Alternatively, a posteriori identification of artefacts origin may ease scientific analysis. The module nenupy.schedule.contamination aims at addressing both issues.

For the following examples illustrated in this page, a few packages should be loaded:

from astropy.time import Time, TimeDelta
import astropy.units as u
import numpy as np

Then, some nenupy objects are required, namely Pointing (see Instrument Pointing) FixedTarget (see Astronomical Targets), BST and BeamLobes:

from nenupy.schedule.contamination import BeamLobes
from nenupy.instru import NenuFAR_Configuration
from nenupy.astro.pointing import Pointing
from nenupy.astro.target import FixedTarget
from nenupy.io.bst import BST

Source contamination prediction

While planning a NenuFAR observation, it may be useful to check whether the data will be contaminated by strong radio sources that are not the main observation target. Then, careful selection of time and frequency ranges could eventually overcome artefact issues and ease scientific analysis.

Contamination in tracking observation

As an example, a 12-hours tracking observation of the source 3C 380 is considered. Finding out when to observe this object at NenuFAR’s location is straightforward. Best observing conditions are when the source is at its highest elevation, at the meridian transit:

src_3c380 = FixedTarget.from_name("3C 380")
src_3c380.next_meridian_transit( Time("2021-06-01 12:00:00") )

3C 380 next meridian transit (after 2021-06-01 12:00:00") occurs at 2021-06-02 01:38:17. Knowing the exposure time (12 hours), and setting a simulation time precision dt of 10 minutes, the number of time steps can be computed:

dt = TimeDelta(10*60, format="sec")
total_duration = TimeDelta(12*3600, format="sec")
time_steps = int(total_duration/dt) + 1

From that, the time and frequency arrays on which the simulation will be computed are set:

times = Time("2021-06-01T20:00:00") + np.arange(time_steps)*dt
frequencies = np.linspace(25, 80, 30)*u.MHz

Warning

The simulation computing time is obviously very dependent on the number of time and frequency steps. As a general rule of thumbs, a time precision of 10 minutes and a frequency precision of 2 MHz are more than enough. Going to smaller values would result in longer computing times without increasing the output precision by a comparable amount.

A tracking Pointing is instantiated, as further described in Instrument Pointing:

pointing = Pointing.target_tracking(
    target=src_3c380,
    time=times,
    duration=dt
)

The simulation is performed at the instantiation of BeamLobes. Besides the time/frequeny/pointing parameters, the miniarray_rotations argument controls which of the six possible Mini-Array rotations to include in the simulation (by default, if None, all rotations are considered).

grating_lobes = BeamLobes(
    time=times,
    frequency=frequencies,
    pointing=pointing,
    miniarray_rotations=None
)

Once the simulation is done, meaning the array factors for the selected Mini-Array rotations, Multi-Order Coverage objects (MOCs) can be computed. The compute_moc() method takes as argument the maximum_ratio as the threshold above which a sky region, HEALPix tesselated, should be included in the ‘grating lobe MOC’. For instance, a value of 0.5 would mean that every HEALPix cell whose value is greater than \({\rm max}(\mathcal{F}_{\rm MA})/2\) is considered as being part of the grating lobe region, where \(\mathcal{F}_{\rm MA}\) is the Mini-Array array factor.

grating_lobes.compute_moc(maximum_ratio=0.4)

The next step consists in cross-matching the computed MOC (over times and frequencies) with a list of bright sources. The method sources_in_lobes() accepts a list of source names, computes another MOC out of their positions (with respect to time if Solar System sources are included) and finds the intersection between the two MOCs. It outputs a SourceInLobes object, that can be visualized using its plot() method:

src_contamination = grating_lobes.sources_in_lobes(["Cas A", "Cyg A", "Vir A", "Sun"])
src_contamination.plot()
../_images/3c380_tracking_contamination.png

Time-frequency distribution of bright sources crossing the NenuFAR grating lobes during the tracking observation of 3C 380.

Note

Instead of selecting an arbitrary list of sources, one could also directly use the Global Sky Model (Oliveira et al., 2008). Applying temperature thresholds result in sky coverages including bright sources as well as diffuse emission. The method gsm_in_lobes() is designed to perfom this task (as a replacement for sources_in_lobes()) and is sometimes way more accurate in the contamination prediction.

Contamination in transit observation

The very same procedure could be applied to any kind of Pointing, especially for transit observations.

Let’s consider a transit observation of the source Virgo A. The closest meridian transit time can be computed (in order to know when to start observing):

vira = FixedTarget.from_name("Vir A")
vira.next_meridian_transit( Time("2021-06-21 00:00:00") )

Then, time and frequency arrays are defined as previously explained. The difference lies in the usage of target_transit() (see Source transit pointing for more details).

dt = TimeDelta(10*60, format="sec")
total_duration = TimeDelta(12*3600, format="sec")
time_steps = int(total_duration/dt)
times = Time("2021-06-21 12:30:00") + np.arange(time_steps)*dt
frequencies = np.linspace(20, 80, 40)*u.MHz
pointing = Pointing.target_transit(
    target=vira,
    t_min=Time("2021-06-21 00:00:00"),
    duration=total_duration,
    dt=dt
)

The array factor is simulated and a MOC is generated for values above 40% of the maximal value.

grating_lobes = BeamLobes(
    time=times,
    frequency=frequencies,
    pointing=pointing,
    miniarray_rotations=None
)
grating_lobes.compute_moc(0.4)

Here, for illustration purposes, Virgo A is also selected as the unique source whose presence in NenuFAR grating lobes is to be checked.

src_contamination = grating_lobes.sources_in_lobes(
    ["Vir A"]
)
src_contamination.plot()
../_images/vira_transit_contamination.png

Time-frequency distribution of Virgo A crossing the NenuFAR grating lobes during its transit observation. The typical ‘Atari’ pattern arises as Virgo A passes through the ‘grating lobes ring’ as well as the primary beam.

To further understand the shape of this graph, one could also plot the simulated NenuFAR array factor on the sky using the plot() method. Below, such a plot is made for a time-frequency point that shows Virgo A as within the grating lobes MOC.

grating_lobes.plot(
    time=Time("2021-06-21 14:30:00"),
    frequency=70*u.MHz,
    decibel=True
)
../_images/vira_sky_transit.png

Simulated array factor. The ‘grating lobes MOC’ is overplotted as a transparent red sky region. For this time and frequency, Virgo A is indeed within this region (right-hand part of the map).

Selecting Mini-Array rotations

Each NenuFAR Mini-Array is rotated at a specific angle with respect to the others. Although this smooths out the grating lobes influence in comparison to the main lobe, the array remains sensitive to some sky regions far from the target direction. If a bright source falls within those grating lobes, its intensity can still be higher than the observation target.

One could prevent this by selecting time windows and/or frequency ranges where strong radio sources stay away from NenuFAR sensitivity pattern. Alternatively, a less conservative (but finer-tuned) approach could be adopted, by removing the Mini-Arrays creating grating lobes at undesired positions.

As an example, a tracking observation is simulated around a meridian transit of Virgo A:

vira = FixedTarget.from_name("Vir A")
vira_transit = vira.next_meridian_transit( Time("2021-12-15 12:00:00") )

exposure = TimeDelta(4*3600, format="sec")
dt = TimeDelta(10*60, format="sec")
time_steps = int(np.ceil(exposure/dt))

start_time = vira_transit - exposure/2

times = start_time + np.arange(time_steps)*dt
frequencies = np.linspace(20, 80, 30)*u.MHz

pointing = Pointing.target_tracking(
    target=vira,
    time=times,
    duration=dt
)

The source contamination is computed such as previously described with th addition of the miniarray_rotations argument. The latter takes a list of rotations (integer values, multiples of 10) to consider. In the following example, all the Mini-Arrays with a rotation values that could reduce to \(0^{\circ}\, {\rm mod}\, 60^{\circ}\) are considered.

Note

One could quickly get the NenuFAR Mini-Arrays that are rotated to \(x^{\circ}\, {\rm mod}\, 60^{\circ}\) using the miniarrays_rotated_like() function:

from nenupy.instru import miniarrays_rotated_like
miniarrays_rotated_like([0])
grating_lobes = BeamLobes(
    time=times,
    frequency=frequencies,
    pointing=pointing,
    miniarray_rotations=[0]
)
grating_lobes.compute_moc(0.4)
src_contamination = grating_lobes.sources_in_lobes(
    ["Cyg A", "Cas A"]
)
src_contamination.plot()
../_images/src_contamination_ma0.png

Time-frequency distribution of bright sources falling with the grating lobes of NenuFAR Mini-Arrays with a rotation of \(0^{\circ}\, {\rm mod}\, 60^{\circ}\) during the tracking observation of Vir A.

Then, the same steps could be reproduced, while modifying the selection on the Mini-Array rotation (this time \(10^{\circ}\, {\rm mod}\, 60^{\circ}\)).

grating_lobes = BeamLobes(
    time=times,
    frequency=frequencies,
    pointing=pointing,
    miniarray_rotations=[10]
)
grating_lobes.compute_moc(0.4)
src_contamination = grating_lobes.sources_in_lobes(
    ["Cyg A", "Cas A"]
)
src_contamination.plot()
../_images/src_contamination_ma10.png

Time-frequency distribution of bright sources falling with the grating lobes of NenuFAR Mini-Arrays with a rotation of \(10^{\circ}\, {\rm mod}\, 60^{\circ}\) during the tracking observation of Vir A.

Doing that for all the possible rotations (i.e., \(\{ 0, 10, 20, 30, 40, 50 \}^{\circ}\, \rm{mod}\, 60^{\circ}\)) gives the observation scheduler the tool to possibly filter out the Mini-Arrays allowing source contamination in some interesting time-frequency zones.

grating_lobes.plot(
    time=Time("2021-12-16 06:10:00"),
    frequency=78*u.MHz
)
../_images/sky_plot_contamination_10.png

Array factor of the Mini-Arrays whose rotation is \(10^{\circ}\, {\rm mod}\, 60^{\circ}\). At 80 MHz, Cyg A is within a grating lobe.

Source contamination identification

BST

bst = BST("20211206_000000_BST.fits", beam=0)
freq_min = bst.frequencies.min()
freq_max = bst.frequencies.max()
time_min = bst.time[0]
time_max = bst.time[-1]

from_bst()

pointing = Pointing.from_bst(bst, beam=0, analog=True)
dt = TimeDelta(10*60, format="sec")
total_duration = time_max - time_min
time_steps = int(np.ceil(total_duration/dt)) + 1
times = time_min + np.arange(time_steps)*dt
frequencies = np.linspace(freq_min, freq_max, 40)
grating_lobes = BeamLobes(
    time=times,
    frequency=frequencies,
    pointing=pointing,
    miniarray_rotations=None
)
grating_lobes.compute_moc(0.4)
src_contamination = grating_lobes.sources_in_lobes(
    ["Cyg A", "Cas A", "Vir A", "Tau A", "Her A", "Hydra A"]
)
src_contamination.plot()
../_images/55cnc_contamination_raw.png

blabla.

data = bst.get(beam=0)
vals = src_contamination.value
vals[vals >= 1] = 1
data.plot(
    hatched_overlay=(src_contamination.time, src_contamination.frequency, vals),
    vmax=88
)
../_images/55cnc_contamination.png

blabla.