In this lesson we will use the TOAST Operator OpSimPySM
to create timestreams for an instrument given a sky model.
# 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
import toast
import healpy as hp
import numpy as np
env = toast.Environment.get()
env.set_log_level("DEBUG")
Before being able to scan a map into a timestream we need to define a scanning strategy and get pointing information for each channel.
We use the same satellite scanning used in lesson 2 about scanning strategies,
see the 02_Simulated_Scan_Strategies/simscan_satellite.ipynb
for more details.
focal_plane = fake_focalplane()
focal_plane.keys()
focal_plane["0A"]["fwhm_arcmin"]
# Scan parameters
alpha = 50.0 # precession opening angle, degrees
beta = 45.0 # spin opening angle, degrees
p_alpha = 25.0 # precession period, minutes
p_beta = 1.25 # spin period, minutes
samplerate = 0.5 # sample rate, Hz
hwprpm = 5.0 # HWP rotation in RPM
nside = 64 # Healpix NSIDE
# We will use one observation per day, with no gaps in between, and
# run for one year.
obs_samples = int(24 * 3600.0 * samplerate) - 1
nobs = 366
# Slew the precession axis so that it completes one circle
deg_per_day = 360.0 / nobs
from toast.todmap import TODSatellite, slew_precession_axis
detquat = {ch: focal_plane[ch]["quat"] for ch in focal_plane}
# Create distributed data
comm = toast.Comm()
data = toast.Data(comm)
# Append observations
for ob in range(nobs):
obsname = "{:03d}".format(ob)
obsfirst = ob * (obs_samples + 1)
obsstart = 24 * 3600.0
tod = TODSatellite(
comm.comm_group,
detquat,
obs_samples,
firstsamp=obsfirst,
firsttime=obsstart,
rate=samplerate,
spinperiod=p_beta,
spinangle=beta,
precperiod=p_alpha,
precangle=alpha,
coord="E",
hwprpm=hwprpm
)
qprec = np.empty(4 * tod.local_samples[1], dtype=np.float64).reshape((-1, 4))
slew_precession_axis(
qprec,
firstsamp=obsfirst,
samplerate=samplerate,
degday=deg_per_day,
)
tod.set_prec_axis(qprec=qprec)
obs = dict()
obs["tod"] = tod
data.obs.append(obs)
from toast.todmap import (
OpPointingHpix,
OpAccumDiag
)
from toast.map import (
DistPixels
)
# Make a simple pointing matrix
pointing = OpPointingHpix(nside=nside, nest=True, mode="IQU")
pointing.exec(data)
# Construct a distributed map to store the hit map
npix = 12 * nside**2
hits = DistPixels(
data,
nnz=1,
dtype=np.int64,
)
hits.data.fill(0)
# Accumulate the hit map locally
build_hits = OpAccumDiag(hits=hits)
build_hits.exec(data)
# Reduce the map across processes (a No-op in this case)
hits.allreduce()
%matplotlib inline
hp.mollview(hits.data.flatten(), nest=True)
Then we define the sky model parameters, choosing the desired set of PySM
models and then we specify the band center and the bandwidth for a top-hat bandpass.
Currently top-hat bandpasses are the only type supported by the operator, in the future we will implement arbitrary bandpasses.
Then bandpass parameters can be added directly to the focal_plane
dictionary:
for ch in focal_plane:
focal_plane[ch]["bandcenter_ghz"] = 70
focal_plane[ch]["bandwidth_ghz"] = 10
focal_plane[ch]["fwhm"] = 60*2
pysm_sky_config = ["s1", "f1", "a1", "d1"]
The OpSimPySM
operator:
PySM
PySMSky
object just with 1 channel at a timePySMSky
to evaluate the sky models and bandpass-integratePySM
to perform distributed smoothing with libsharp
libsharp
)DistMap
object to communicate to each process the part of the sky they observe OpSimScan
to rescan the map to a timelinefrom toast.todmap import OpSimPySM
OpSimPySM?
opsim_pysm = OpSimPySM(
data,
comm=None,
pysm_model=pysm_sky_config,
apply_beam=True,
debug=True,
focalplanes=[focal_plane],
)
opsim_pysm.exec(data)
%matplotlib inline
import matplotlib.pyplot as plt
tod = data.obs[0]['tod']
pix = tod.cache.reference("pixels_0A")
import toast.qarray as qa
theta, phi, pa = qa.to_angles(tod.read_pntg(detector="0A"))
pix
num = 10000
plt.figure(figsize=(7, 5))
plt.plot(np.degrees(theta[:num]), tod.cache.reference("signal_0A")[:num], ".")
plt.xlabel("$Colatitude [deg]$")
plt.ylabel("$Signal [ \mu K_{RJ} ]$");
from numba import njit
@njit
def just_make_me_a_map(output_map, signals):
"""Temperature only binner
Bins a list of (pix, signal) tuples into an output map,
it does not support polarization, so it just averages it out.
Parameters
----------
output_map : np.array
already zeroed output map
signals : numba.typed.List of (np.array[int64] pix, np.array[np.double] signal)
Returns
-------
hits : np.array[np.int64]
hitmap
"""
hits = np.zeros(len(output_map), dtype=np.int64)
for pix, signal in signals:
for p,s in zip(pix, signal):
output_map[p] += s
hits[p] += 1
output_map[hits != 0] /= hits[hits != 0]
return hits
from numba.typed import List
signals = List()
for obs in data.obs:
for ch in focal_plane:
signals.append((obs["tod"].cache.reference("pixels_%s" % ch), obs["tod"].cache.reference("signal_%s" % ch)))
output_map = np.zeros(npix, dtype=np.double)
h = just_make_me_a_map(output_map, signals)
hp.mollview(h, title="hitmap", nest=True)
hp.mollview(output_map, nest=True, min=0, max=1e-3, cmap="coolwarm")
hp.gnomview(output_map, rot=(0,0), xsize=5000, ysize=2000, cmap="coolwarm", nest=True, min=0, max=1e-2)
pysm_component_objects
: pass custom PySM component objects, see for example the WebSkyCIB model in the so_pysm_models repository, it provides a Cosmic Infrared Background computed from