Polarized observations

This tutorial covers working with polarized instrument and maps, and recovering polarized maps from observations.

We start with a normal instrument, and create two orthogonally polarized copies of each detector by setting polarized: True in the Array config:

[1]:
import maria
from maria.instrument import Band

f090 = Band(
    center=90e9,  # in Hz
    width=20e9,  # in Hz
    NET_RJ=40e-6,  # in K sqrt(s)
    knee=1e0,    # in Hz
    gain_error=5e-2)

f150 = Band(
    center=150e9,
    width=30e9,
    NET_RJ=60e-6,
    knee=1e0,
    gain_error=5e-2)

array = {"field_of_view": 0.5,
         "shape": "circle",
         "beam_spacing": 1.5,
         "primary_size": 10,
         "polarized": True,
         "bands": [f090, f150]}

instrument = maria.get_instrument(array=array)

print(instrument.arrays)
          n     FOV baseline        bands polarized
array1  652  0.478°      0 m  [f090,f150]      True

We can see the resulting polarization footprint in the instrument plot:

[2]:
print(instrument)
instrument.plot()
Instrument(1 array)
├ arrays:
│            n     FOV baseline        bands polarized
│  array1  652  0.478°      0 m  [f090,f150]      True
│
└ bands:
      name   center   width    η         NEP      NET_RJ         NET_CMB     FWHM
   0  f090   90 GHz  20 GHz  0.5  5.445 aW√s  40 uK_RJ√s  49.13 uK_CMB√s   1.458’
   1  f150  150 GHz  30 GHz  0.5  12.25 aW√s  60 uK_RJ√s    104 uK_CMB√s  0.8748’
../_images/tutorials_polarized-observations_4_1.png

Let’s observe the use the Einstein map, which has a faint polarization signature underneath the unpolarized signal of Einstein’s face. Remember that all maps are five dimensional (stokes, frequency, time, y, x); this map has four channels in the stokes dimensions (the I, Q, U, and V Stokes parameters). We can plot all the channels by giving plot a shaped set of stokes parameters.

[3]:
from maria.io import fetch

input_map = maria.map.load(fetch("maps/einstein.h5"))
input_map.plot(stokes=[["I", "Q"],
                       ["U", "V"]])
2025-10-07 18:40:33.267 INFO: Fetching https://github.com/thomaswmorris/maria-data/raw/master/maps/einstein.h5
Downloading: 100%|████████████████| 931k/931k [00:00<00:00, 32.1MB/s]
../_images/tutorials_polarized-observations_6_1.png
[4]:
from maria import Planner

planner = Planner(target=input_map, site="llano_de_chajnantor", constraints={"el": (60, 90)})
plans = planner.generate_plans(total_duration=900,  # in seconds
                               sample_rate=50)  # in Hz

plans[0].plot()
print(plans)
PlanList(2 plans, 900 s):
                           start_time duration   target(ra,dec)     center(az,el)
chunk
0      2025-10-08 01:14:21.672 +00:00    600 s  (360°, -22.99°)  (95.88°, 61.23°)
1      2025-10-08 01:24:59.172 +00:00    300 s     (360°, -23°)  (95.45°, 63.12°)
../_images/tutorials_polarized-observations_7_1.png
[5]:
sim = maria.Simulation(
    instrument,
    plans=plans,
    site="llano_de_chajnantor",
    atmosphere="2d",
    atmosphere_kwargs={"weather": {"pwv": 0.5}},
    map=input_map
)

print(sim)
Simulation
├ Instrument(1 array)
│ ├ arrays:
│ │            n     FOV baseline        bands polarized
│ │  array1  652  0.478°      0 m  [f090,f150]      True
│ │
│ └ bands:
│       name   center   width    η         NEP      NET_RJ         NET_CMB     FWHM
│    0  f090   90 GHz  20 GHz  0.5  5.445 aW√s  40 uK_RJ√s  49.13 uK_CMB√s   1.458’
│    1  f150  150 GHz  30 GHz  0.5  12.25 aW√s  60 uK_RJ√s    104 uK_CMB√s  0.8748’
├ Site:
│   region: chajnantor
│   timezone: America/Santiago
│   location:
│     longitude: 67°45’17.28” W
│     latitude:  23°01’45.84” S
│     altitude: 5064 m
│   seasonal: True
│   diurnal: True
├ PlanList(2 plans, 900 s):
│                            start_time duration   target(ra,dec)     center(az,el)
│ chunk
│ 0      2025-10-08 01:14:21.672 +00:00    600 s  (360°, -22.99°)  (95.88°, 61.23°)
│ 1      2025-10-08 01:24:59.172 +00:00    300 s     (360°, -23°)  (95.45°, 63.12°)
├ '2d'
└ ProjectedMap:
    shape(stokes, nu, y, x): (4, 1, 685, 685)
    stokes: IQUV
    nu: [90.] GHz
    t: naive
    z: naive
    quantity: rayleigh_jeans_temperature
    units: K_RJ
      min: -1.000e-02
      max: 2.540e-01
    center:
      ra:  00ʰ00ᵐ0.00ˢ
      dec: -23°00’0.00”
    size(y, x): (1°, 1°)
    resolution(y, x): (5.255”, 5.255”)
    beam(maj, min, rot): [0. 0. 0.] rad
    memory: 30.03 MB
[6]:
tods = sim.run()

print(tods)
tods[0].plot()
2025-10-07 18:40:46.141 INFO: Simulating observation 1 of 2
Constructing atmosphere: 100%|████████████████| 10/10 [00:20<00:00,  2.04s/it]
Generating turbulence: 100%|████████████████| 10/10 [00:00<00:00, 17.70it/s]
Sampling turbulence: 100%|████████████████| 10/10 [00:09<00:00,  1.08it/s]
Computing atmospheric emission: 100%|████████████████| 2/2 [00:02<00:00,  1.17s/it, band=f150]
Sampling map: 100%|████████████████| 2/2 [00:09<00:00,  4.81s/it, band=f150, channel=(0 Hz, inf Hz), stokes=V]
Generating noise: 100%|████████████████| 2/2 [00:00<00:00,  2.12it/s, band=f150]
2025-10-07 18:41:44.696 INFO: Simulated observation 1 of 2 in 58.54 s
2025-10-07 18:41:44.697 INFO: Simulating observation 2 of 2
Constructing atmosphere: 100%|████████████████| 10/10 [00:08<00:00,  1.21it/s]
Generating turbulence: 100%|████████████████| 10/10 [00:00<00:00, 29.95it/s]
Sampling turbulence: 100%|████████████████| 10/10 [00:06<00:00,  1.59it/s]
Computing atmospheric emission: 100%|████████████████| 2/2 [00:01<00:00,  1.27it/s, band=f150]
Sampling map: 100%|████████████████| 2/2 [00:05<00:00,  2.74s/it, band=f150, channel=(0 Hz, inf Hz), stokes=V]
Generating noise: 100%|████████████████| 2/2 [00:00<00:00,  3.34it/s, band=f150]
2025-10-07 18:42:15.302 INFO: Simulated observation 2 of 2 in 30.59 s
[TOD(shape=(652, 30000), fields=['atmosphere', 'map', 'noise'], units='K_RJ', start=2025-10-08 01:24:21.651 +00:00, duration=600.0s, sample_rate=50.0Hz, metadata={'atmosphere': True, 'sim_time': <Arrow [2025-10-07T18:41:31.611362+00:00]>, 'altitude': 5064.0, 'region': 'chajnantor', 'pwv': 0.5, 'base_temperature': 274.172}), TOD(shape=(652, 15000), fields=['atmosphere', 'map', 'noise'], units='K_RJ', start=2025-10-08 01:29:59.151 +00:00, duration=300.0s, sample_rate=50.0Hz, metadata={'atmosphere': True, 'sim_time': <Arrow [2025-10-07T18:42:09.067998+00:00]>, 'altitude': 5064.0, 'region': 'chajnantor', 'pwv': 1.22, 'base_temperature': 274.115})]
../_images/tutorials_polarized-observations_9_2.png
[7]:
tods
[7]:
[TOD(shape=(652, 30000), fields=['atmosphere', 'map', 'noise'], units='K_RJ', start=2025-10-08 01:24:21.651 +00:00, duration=600.0s, sample_rate=50.0Hz, metadata={'atmosphere': True, 'sim_time': <Arrow [2025-10-07T18:41:31.611362+00:00]>, 'altitude': 5064.0, 'region': 'chajnantor', 'pwv': 0.5, 'base_temperature': 274.172}),
 TOD(shape=(652, 15000), fields=['atmosphere', 'map', 'noise'], units='K_RJ', start=2025-10-08 01:29:59.151 +00:00, duration=300.0s, sample_rate=50.0Hz, metadata={'atmosphere': True, 'sim_time': <Arrow [2025-10-07T18:42:09.067998+00:00]>, 'altitude': 5064.0, 'region': 'chajnantor', 'pwv': 1.22, 'base_temperature': 274.115})]
[8]:
from maria.mappers import BinMapper

mapper = BinMapper(
    center=(0, -23),
    stokes="IQU",
    frame="ra/dec",
    width=1.0,
    height=1.0,
    resolution=1.0 / 256,
    tod_preprocessing={
        "window": {"name": "tukey", "kwargs": {"alpha": 0.1}},
        "remove_spline": {"knot_spacing": 60, "remove_el_gradient": True},
        "remove_modes": {"modes_to_remove": [0]},
    },
    map_postprocessing={
        "gaussian_filter": {"sigma": 1},
        # "median_filter": {"size": 1},
    },
    units="mK_RJ",
)

mapper.add_tods(tods)

output_map = mapper.run()

print(output_map)
Preprocessing TODs: |                | 0/0 [00:00<?, ?it/s]
Preprocessing TODs: 100%|████████████████| 2/2 [00:03<00:00,  1.54s/it]
Mapping band f090: 100%|██████████| 2/2 [00:03<00:00,  1.81s/it, stokes=U, tod=2/2]
2025-10-07 18:42:26.523 INFO: Ran mapper for band f090 in 3.629 s.
Mapping band f150: 100%|██████████| 2/2 [00:03<00:00,  1.76s/it, stokes=U, tod=2/2]
2025-10-07 18:42:30.067 INFO: Ran mapper for band f150 in 3.531 s.
ProjectedMap:
  shape(stokes, nu, y, x): (3, 2, 256, 256)
  stokes: IQU
  nu: [ 90. 150.] GHz
  t: naive
  z: naive
  quantity: rayleigh_jeans_temperature
  units: mK_RJ
    min: -2.456e+02
    max: 2.586e+02
  center:
    ra:  00ʰ00ᵐ0.00ˢ
    dec: -23°00’0.00”
  size(y, x): (1°, 1°)
  resolution(y, x): (14.06”, 14.06”)
  beam(maj, min, rot): [[1.45801568 1.45801568 0.        ]
 [0.87480941 0.87480941 0.        ]]’
  memory: 6.291 MB

Note that we can’t see any of the circular polarization, since our instrument isn’t sensitive to it.

[9]:
output_map.plot(stokes=["I", "Q", "U"], nu_index=[[0], [1]])
../_images/tutorials_polarized-observations_13_0.png