TOAST provides an interface, OpSimConviqt
, to the spherical harmonic convolution library, libconviqt
. It was developed by Gary Prezeau and Martin Reinecke and described in
G. Prézeau and M. Reinecke:
Algorithm for the Evaluation of Reduced Wigner Matrices,
APJS 190 (2010) 267
arXiv:1002.1050. This particular implementation of the algorithm is available at https://github.com/hpc4cmb/libconviqt.
# Load common tools for all lessons
import sys
sys.path.insert(0, "..")
from lesson_tools import (
fake_focalplane
)
# Capture C++ output in the jupyter cells
%reload_ext wurlitzer
libconviqt
takes in spherical harmonic expansions of the beam and the sky and then synthesizes TOD samples at sample positions in the proper orientation. For efficiency, the sky is distributed as isolatitude rings and then each process gets the detector samples that fall on their rings. The calculation itself has two steps, first conviqt
builds a 3D interpolator of the beam-convolved sky on a grid of $(\theta, \phi, \psi)$ and then the detector samples are interpolated from the grid. Finally the samples are communited back to the processes that own them.
Typically the interpolation step dominates but if there are few detector samples and the sky and beam expansion orders are high, it is possible that building the interpolator is more expensive.
In this section we create a TOAST data object with simulated signal and noise and process the data into hit maps, pixels noise matrices and signal maps.
import toast
import toast.todmap
import toast.pipeline_tools
from toast.mpi import MPI
import numpy as np
import matplotlib.pyplot as plt
mpiworld, procs, rank = toast.mpi.get_world()
comm = toast.mpi.Comm(mpiworld)
# A pipeline would create the args object with argparse
class args:
sample_rate = 10 # Hz
hwp_rpm = None
hwp_step_deg = None
hwp_step_time_s = None
spin_period_min = 1 # 10
spin_angle_deg = 20 # 30
prec_period_min = 100 # 50
prec_angle_deg = 30 # 65
coord = "E"
nside = 64
nnz = 3
outdir = "maps"
sky_file = "slm.fits"
beam_file = "blm.fits"
# Create a fake focalplane, we could also load one from file.
# The Focalplane class interprets the focalplane dictionary
# created by fake_focalplane() but it can also load the information
# from file.
focalplane = fake_focalplane(samplerate=args.sample_rate, fknee=0.1, alpha=2)
detectors = sorted(focalplane.keys())
detquats = {}
for d in detectors:
detquats[d] = focalplane[d]["quat"]
nsample = 100000
start_sample = 0
start_time = 0
iobs = 0
tod = toast.todmap.TODSatellite(
comm.comm_group,
detquats,
nsample,
coord=args.coord,
firstsamp=start_sample,
firsttime=start_time,
rate=args.sample_rate,
spinperiod=args.spin_period_min,
spinangle=args.spin_angle_deg,
precperiod=args.prec_period_min,
precangle=args.prec_angle_deg,
detranks=comm.group_size,
hwprpm=args.hwp_rpm,
hwpstep=args.hwp_step_deg,
hwpsteptime=args.hwp_step_time_s,
)
# Constantly slewing precession axis
precquat = np.empty(4 * tod.local_samples[1], dtype=np.float64).reshape((-1, 4))
toast.todmap.slew_precession_axis(
precquat,
firstsamp=start_sample + tod.local_samples[0],
samplerate=args.sample_rate,
degday=360.0 / 365.25,
)
tod.set_prec_axis(qprec=precquat)
noise = toast.pipeline_tools.get_analytic_noise(args, comm, focalplane)
obs = {}
obs["name"] = "science_{:05d}".format(iobs)
obs["tod"] = tod
obs["intervals"] = None
obs["baselines"] = None
obs["noise"] = noise
obs["id"] = iobs
# Conviqt requires at least minimal focal plane information to be present in the observation
obs["focalplane"] = toast.pipeline_tools.Focalplane(focalplane)
"""
for det in tod.local_dets:
obs["focalplane"][det] = {
"epsilon" : focalplane[det]["epsilon"],
}
if det.endswith("A"):
obs["focalplane"][det]["psi_pol_deg"] = 0,
elif det.endswith("B"):
obs["focalplane"][det]["psi_pol_deg"] = 90,
"""
data = toast.Data(comm)
data.obs.append(obs)
import healpy as hp
import numpy as np
nside_high = 1024
npix_high = 12 * nside_high ** 2
pointsource_map = np.zeros([3, npix_high])
coords = []
for lon in np.linspace(0, 360, 9, endpoint=False):
for lat in np.linspace(-90, 90, 7):
pix = hp.ang2pix(nside_high, lon, lat, lonlat=True)
# Add a completely unpolarized source and see if beam asymmetries manufacture polarization
pointsource_map[0, pix] = 1
coords.append((lon, lat))
coords = np.vstack(coords).T
hp.mollview(np.zeros(12), title="Input signal", cmap="coolwarm")
hp.projplot(np.pi/2 - np.radians(coords[1]), np.radians(coords[0]), 'o')
lmax_high = nside_high * 2
cl, alm = hp.anafast(pointsource_map, lmax=lmax_high, iter=0, alm=True)
hp.write_map("sim_sources_map.fits", hp.reorder(pointsource_map, r2n=True), nest=True, overwrite=True)
hp.write_alm(args.sky_file, alm, overwrite=True)
beam_map = np.zeros([3, npix_high])
x, y, z = hp.pix2vec(nside_high, np.arange(npix_high))
xvar = .01
yvar = 5 * xvar
beam = np.exp(-(x ** 2 / xvar + y ** 2 / yvar))
beam[z < 0] = 0
hp.mollview(beam, cmap="coolwarm", rot=[0, 90])
beam_map = np.zeros([3, npix_high])
beam_map[0] = beam
beam_map[1] = beam
bl, blm = hp.anafast(beam_map, lmax=lmax_high, iter=0, alm=True)
hp.write_alm(args.beam_file, blm, overwrite=True)
import toast
toast.todmap.OpPointingHpix(nside=args.nside, nest=True, mode="IQU").exec(data)
npix = 12 * args.nside ** 2
hitmap = np.zeros(npix)
tod = data.obs[0]["tod"]
for det in tod.local_dets:
pixels = tod.cache.reference("pixels_{}".format(det))
hitmap[pixels] = 1
hitmap[hitmap == 0] = hp.UNSEEN
hp.mollview(hitmap, nest=True, title="all hit pixels", cbar=False)
hp.graticule(22.5, verbose=False)
name = "signal"
toast.tod.OpCacheClear(name).exec(data)
conviqt = toast.todmap.OpSimConviqt(
comm.comm_rank,
args.sky_file,
args.beam_file,
lmax=512, # Will use maximum from file
beammmax=16, # Will use maximum from file
pol=True,
fwhm=0,
order=13,
calibrate=True,
dxx=True,
out=name,
quat_name=None,
flag_name=None,
flag_mask=255,
common_flag_name=None,
common_flag_mask=255,
apply_flags=False,
remove_monopole=False,
remove_dipole=False,
normalize_beam=True,
verbosity=1,
)
conviqt.exec(data)
Destripe the signal and make a map. We use the nascent TOAST mapmaker because it can be run in serial mode without MPI. The TOAST mapmaker is still significantly slower so production runs should used libMadam
.
mapmaker = toast.todmap.OpMapMaker(
nside=args.nside,
nnz=3,
name=name,
outdir=args.outdir,
outprefix="toast_test_",
baseline_length=10,
# maskfile=self.maskfile_binary,
# weightmapfile=self.maskfile_smooth,
# subharmonic_order=None,
iter_max=100,
use_noise_prior=False,
# precond_width=30,
)
mapmaker.exec(data)
Plot a segment of the timelines
plt.figure(figsize=[12, 8])
hitmap = hp.read_map("maps/toast_test_hits.fits")
hitmap[hitmap == 0] = hp.UNSEEN
hp.mollview(hitmap, sub=[2, 2, 1], title="hits")
binmap = hp.read_map("maps/toast_test_binned.fits")
binmap[binmap == 0] = hp.UNSEEN
hp.mollview(binmap, sub=[2, 2, 2], title="binned map", cmap="coolwarm")
destriped = hp.read_map("maps/toast_test_destriped.fits")
destriped[destriped == 0] = hp.UNSEEN
hp.mollview(destriped, sub=[2, 2, 3], title="destriped map", cmap="coolwarm")
inmap = hp.ud_grade(hp.read_map("sim_sources_map.fits"), args.nside)
inmap[hitmap == hp.UNSEEN] = hp.UNSEEN
hp.mollview(inmap, sub=[2, 2, 4], title="input map", cmap="coolwarm")