Skip to content

Overview

This tutorial will walk through the basic data containers used in TOAST and how they can be constructed and accessed. The goal is to provide an overview

In [1]:
# TOAST interactive startup
import toast.interactive
%load_ext toast.interactive
In [2]:
%toast -p 1 -a
TOAST INFO: Using 1 processes with 1 threads each.
In [3]:
# Built-in modules
import sys
import os
import datetime

# External modules
import numpy as np
import matplotlib.pyplot as plt
import astropy.units as u

# TOAST
import toast
from toast.tests import helpers
from toast.observation import default_values as defaults

# Display inline plots
%matplotlib inline

Runtime Environment

The toast module can be influenced by a few environment variables, which must be set before importing toast:

In [4]:
help(toast)
Help on package toast:

NAME
    toast

DESCRIPTION
    Time Ordered Astrophysics Scalable Tools (TOAST) is a software package
    designed to allow the processing of data from telescopes that acquire
    data as timestreams (rather than images).

    Runtime behavior of this package can be controlled by setting some
    environment variables (before importing the package):

    TOAST_LOGLEVEL=<value>
        * Possible values are "VERBOSE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL".
          Default is "INFO".
        * Controls logging of both C++ and Python code.

    TOAST_FUNCTIME=<value>
        * Values "1", "true", or "yes" will enable python function timers in many parts of the code.

    TOAST_GPU_OPENMP=<value>
        * Values "1", "true", or "yes" will enable runtime-support for OpenMP
          target offload.
        * Requires compile-time support for OpenMP 5.x features.

    TOAST_GPU_MEM_GB=<value>
        * Value in GB of memory to use for OpenMP on each GPU.
        * Conservative default is 2GB.

    TOAST_GPU_JAX=<value>
        * Values "1", "true", or "yes" will enable runtime support for jax.
        * Requires jax to be available / importable.

    TOAST_GPU_HYBRID_PIPELINES=<value>
        * Values "0", "false", or "no" will disable runtime support for hybrid GPU pipelines.
        * Requires TOAST_GPU_OPENMP or TOAST_GPU_JAX to be enabled.

    OMP_NUM_THREADS=<integer>
        * Toast uses OpenMP threading in several places and the concurrency is set by the
          usual environment variable.

    OMP_TARGET_OFFLOAD=[MANDATORY | DISABLED | DEFAULT]
        * If the TOAST_GPU_OPENMP environment variable is set, this standard OpenMP
          environment variable controls the offload behavior.

    MPI_DISABLE=<value>
        * Any non-empty value will disable a try block that looks for mpi4py.  Needed on
          some systems where mpi4py is available but does not work.
        * The same variable also controls the `pshmem` package used by toast.

    CUDA_MEMPOOL_FRACTION=<float>
        * If compiled with CUDA support (-DUSE_CUDA), create a memory pool that
          pre-allocates this fraction of the device memory allocated to each process.

PACKAGE CONTENTS
    _aux (package)
    _libtoast
    _version
    accelerator (package)
    atm
    config (package)
    coordinates
    covariance
    data
    dipole
    dist
    fft
    footprint
    healpix
    hwp_utils
    instrument
    instrument_coords
    instrument_sim
    interactive (package)
    intervals
    io (package)
    jax (package)
    job
    mpi
    noise
    noise_sim
    observation
    observation_data
    observation_dist
    observation_view
    ops (package)
    pixels
    pixels_io_healpix
    pixels_io_utils
    pixels_io_wcs
    pointing_utils
    qarray
    rng
    schedule
    schedule_sim_ground
    schedule_sim_satellite
    scripts (package)
    spt3g (package)
    templates (package)
    tests (package)
    timing
    trait_utils
    traits
    utils
    vis
    weather
    widgets

DATA
    interval_dtype = dtype([('start', '<f8'), ('stop', '<f8'), ('first', '...

VERSION
    3.0.2

FILE
    /home/runner/conda/envs/docs/lib/python3.14/site-packages/toast/__init__.py


In [5]:
toast?

You can get the current TOAST runtime configuration from the "Environment" class.

In [6]:
env = toast.Environment.get()
print(env)
<toast.Environment
  Source code version = 3.0.2
  Logging level = INFO
  Handling enabled for 0 signals:
  Max threads = 4
>

The logging level can be changed by either setting the TOAST_LOGLEVEL environment variable to one of the supported levels (VERBOSE, DEBUG, INFO, WARNING, ERROR, CRITICAL) or by using the set_log_level() method of the Environment class. The maximum number of threads is controlled by the standard OMP_NUM_THREADS environment variable.

Data Model

The basic data model in a toast workflow consists of a set of Observation instances, each of which is associated with a Focalplane on a Telescope. Note that a Focalplane instance is probably just a sub-set of detectors on the actual physical focalplane. These detectors must be co-sampled and likely have other things in common (for example, they are on the same wafer or are correlated in some other way). For this notebook, we will manually create these objects, but usually these will be loaded / created by some experiment-specific function.

MPI is completely optional in TOAST, although it is required to achieve good parallel performance on systems with many (e.g. 4 or more) cores. Most of the parallelism in TOAST is MPI process-based, not threaded. In this section we show how interactive use of TOAST can be done without any reference to MPI. In a separate notebook in this directory we show how to make use of distributed data and operations in parallel workflows.

In [7]:
# Start by making a fake focalplane

from toast.instrument_sim import (
    fake_hexagon_focalplane,
    plot_focalplane,
)

focalplane_pixels = 7 # (hexagonal, pixel zero at center)
field_of_view = 5.0 * u.degree
sample_rate = 10.0 * u.Hz

focalplane = fake_hexagon_focalplane(
    n_pix=focalplane_pixels,
    width=field_of_view,
    fwhm=1.0 * u.degree,
    sample_rate=sample_rate,
    epsilon=0.0,
)
In [8]:
# Make a plot of this focalplane layout.

detpolcol = {
    x: "red" if x.endswith("A") else "blue" for x in focalplane.detectors
}

plot_focalplane(
    focalplane=focalplane,
    width=1.3 * field_of_view,
    height=1.3 * field_of_view,
    show_labels=True,
    pol_color=detpolcol
)
No description has been provided for this image
Out[8]:
No description has been provided for this image
In [9]:
# Now make a fake telescope

telescope = toast.Telescope(name="fake", focalplane=focalplane, site=toast.SpaceSite(name="L2"))

Now that we have a fake telescope created, we can create an observation:

In [10]:
# Make an empty observation

samples = 10

ob = toast.Observation(
    toast.Comm(),
    telescope, 
    name="2020-07-31_A", 
    n_samples=samples
)

print(ob)
<Observation
  name = '2020-07-31_A'
  uid = '2881371475'  group has 1 processes
  telescope = <Telescope 'fake': uid = 4020063252, site = <SpaceSite 'L2' : uid = 3584191102>, focalplane = <Focalplane: 14 detectors, sample_rate = 10.0 Hz, FOV = 7.700000000000001 deg, detectors = [D0A-150 .. D6B-150]>>
  session = <Session '2020-07-31_A': uid = 2881371475, start = None, end = None>
  10 total samples (10 local)
  shared:  <SharedDataManager>
  detdata:  <DetDataManager 14 local detectors, 10 samples>
  intervals:  <IntervalsManager 1 lists>
>

Here we see our observation simply has the starting information we passed to the constructor. Next we will discuss the 3 types of data objects that can be stored in an Observation: detector data products, shared telescope data, and arbitrary metadata.

Metadata

By default, the observation is empty. You can add arbitrary metadata to the observation- it acts just like a dictionary.

In [11]:
hk = {
    "Temperature 1": np.array([1.0, 2.0, 3.0]),
    "Other Sensor": 1.2345
}

ob["housekeeping"] = hk

print(ob)
<Observation
  name = '2020-07-31_A'
  uid = '2881371475'  group has 1 processes
  telescope = <Telescope 'fake': uid = 4020063252, site = <SpaceSite 'L2' : uid = 3584191102>, focalplane = <Focalplane: 14 detectors, sample_rate = 10.0 Hz, FOV = 7.700000000000001 deg, detectors = [D0A-150 .. D6B-150]>>
  session = <Session '2020-07-31_A': uid = 2881371475, start = None, end = None>
  housekeeping = {'Temperature 1': array([1., 2., 3.]), 'Other Sensor': 1.2345}
  10 total samples (10 local)
  shared:  <SharedDataManager>
  detdata:  <DetDataManager 14 local detectors, 10 samples>
  intervals:  <IntervalsManager 1 lists>
>

Metadata like this is not synchronized in any way between processes. A user or Operator can put any keys here to store small data objects.

Detector Data

Detector data has some unique properties that we often want to leverage in our analyses. Each process has some detectors and some time slice of the observation. In the case of a single process like this example, all the data is local. Before using data we need to create it within the empty Observation. Here we create a "signal" object for the detectors. The detector data is accessed under the detdata attribute of the observation:

FIXME: talk about naming conventions

Here we create and initialize to zero some detector data named "signal". This has one value per sample per detector and each value is a 64bit float.

In [12]:
ob.detdata.create("signal", dtype=np.float64)
In [13]:
print(ob.detdata)
<DetDataManager 14 local detectors, 10 samples
    signal: shape=(14, 10), dtype=float64, units=''>
In [14]:
print(ob.detdata["signal"])
<DetectorData 14 detectors each with shape (10,), type float64, units :
  D0A-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D0B-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D1A-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D1B-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D2A-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D2B-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D3A-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D3B-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D4A-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D4B-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D5A-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D5B-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D6A-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D6B-150 = [ 0.0 0.0 ... 0.0 0.0 ]
>

You can create other types of detector data, and there is some shortcut notation that can be used to create detector data objects from existing arrays. For example:

In [15]:
# This takes an existing N_detector x N_sample array and creates from that

some_data = 3.0 * np.ones(
    (
        len(ob.local_detectors), 
        ob.n_local_samples
    ),
    dtype=np.float32
)

ob.detdata["existing_signal"] = some_data
print(ob.detdata["existing_signal"])
<DetectorData 14 detectors each with shape (10,), type float32, units :
  D0A-150 = [ 3.0 3.0 ... 3.0 3.0 ]
  D0B-150 = [ 3.0 3.0 ... 3.0 3.0 ]
  D1A-150 = [ 3.0 3.0 ... 3.0 3.0 ]
  D1B-150 = [ 3.0 3.0 ... 3.0 3.0 ]
  D2A-150 = [ 3.0 3.0 ... 3.0 3.0 ]
  D2B-150 = [ 3.0 3.0 ... 3.0 3.0 ]
  D3A-150 = [ 3.0 3.0 ... 3.0 3.0 ]
  D3B-150 = [ 3.0 3.0 ... 3.0 3.0 ]
  D4A-150 = [ 3.0 3.0 ... 3.0 3.0 ]
  D4B-150 = [ 3.0 3.0 ... 3.0 3.0 ]
  D5A-150 = [ 3.0 3.0 ... 3.0 3.0 ]
  D5B-150 = [ 3.0 3.0 ... 3.0 3.0 ]
  D6A-150 = [ 3.0 3.0 ... 3.0 3.0 ]
  D6B-150 = [ 3.0 3.0 ... 3.0 3.0 ]
>
In [16]:
# This takes one detectors-worth of data and replicates it to all detectors
# while creating a new data object.

ob.detdata["replicated"] = 5 * np.ones(ob.n_local_samples, dtype=np.int32)
print(ob.detdata["replicated"])
<DetectorData 14 detectors each with shape (10,), type int32, units :
  D0A-150 = [ 5 5 ... 5 5 ]
  D0B-150 = [ 5 5 ... 5 5 ]
  D1A-150 = [ 5 5 ... 5 5 ]
  D1B-150 = [ 5 5 ... 5 5 ]
  D2A-150 = [ 5 5 ... 5 5 ]
  D2B-150 = [ 5 5 ... 5 5 ]
  D3A-150 = [ 5 5 ... 5 5 ]
  D3B-150 = [ 5 5 ... 5 5 ]
  D4A-150 = [ 5 5 ... 5 5 ]
  D4B-150 = [ 5 5 ... 5 5 ]
  D5A-150 = [ 5 5 ... 5 5 ]
  D5B-150 = [ 5 5 ... 5 5 ]
  D6A-150 = [ 5 5 ... 5 5 ]
  D6B-150 = [ 5 5 ... 5 5 ]
>
In [17]:
# You can also create detector data objects from a dictionary
# of single-detector arrays
other = dict()
for i, d in enumerate(ob.local_detectors):
    other[d] = i * np.ones(ob.n_local_samples, dtype=np.int32)

ob.detdata["other_signal"] = other
print(ob.detdata["other_signal"])
<DetectorData 14 detectors each with shape (10,), type int32, units :
  D0A-150 = [ 0 0 ... 0 0 ]
  D0B-150 = [ 1 1 ... 1 1 ]
  D1A-150 = [ 2 2 ... 2 2 ]
  D1B-150 = [ 3 3 ... 3 3 ]
  D2A-150 = [ 4 4 ... 4 4 ]
  D2B-150 = [ 5 5 ... 5 5 ]
  D3A-150 = [ 6 6 ... 6 6 ]
  D3B-150 = [ 7 7 ... 7 7 ]
  D4A-150 = [ 8 8 ... 8 8 ]
  D4B-150 = [ 9 9 ... 9 9 ]
  D5A-150 = [ 10 10 ... 10 10 ]
  D5B-150 = [ 11 11 ... 11 11 ]
  D6A-150 = [ 12 12 ... 12 12 ]
  D6B-150 = [ 13 13 ... 13 13 ]
>

By default you will get detector data with one element per sample and float64 dtype. However, you can specify the shape of each detector sample:

In [18]:
# Example of data with different shape

ob.detdata.create("pointing", sample_shape=(4,), dtype=np.float32)
print(ob.detdata["pointing"])
<DetectorData 14 detectors each with shape (10, 4), type float32, units :
  D0A-150 = [ [0. 0. 0. 0.] [0. 0. 0. 0.] ... [0. 0. 0. 0.] [0. 0. 0. 0.] ]
  D0B-150 = [ [0. 0. 0. 0.] [0. 0. 0. 0.] ... [0. 0. 0. 0.] [0. 0. 0. 0.] ]
  D1A-150 = [ [0. 0. 0. 0.] [0. 0. 0. 0.] ... [0. 0. 0. 0.] [0. 0. 0. 0.] ]
  D1B-150 = [ [0. 0. 0. 0.] [0. 0. 0. 0.] ... [0. 0. 0. 0.] [0. 0. 0. 0.] ]
  D2A-150 = [ [0. 0. 0. 0.] [0. 0. 0. 0.] ... [0. 0. 0. 0.] [0. 0. 0. 0.] ]
  D2B-150 = [ [0. 0. 0. 0.] [0. 0. 0. 0.] ... [0. 0. 0. 0.] [0. 0. 0. 0.] ]
  D3A-150 = [ [0. 0. 0. 0.] [0. 0. 0. 0.] ... [0. 0. 0. 0.] [0. 0. 0. 0.] ]
  D3B-150 = [ [0. 0. 0. 0.] [0. 0. 0. 0.] ... [0. 0. 0. 0.] [0. 0. 0. 0.] ]
  D4A-150 = [ [0. 0. 0. 0.] [0. 0. 0. 0.] ... [0. 0. 0. 0.] [0. 0. 0. 0.] ]
  D4B-150 = [ [0. 0. 0. 0.] [0. 0. 0. 0.] ... [0. 0. 0. 0.] [0. 0. 0. 0.] ]
  D5A-150 = [ [0. 0. 0. 0.] [0. 0. 0. 0.] ... [0. 0. 0. 0.] [0. 0. 0. 0.] ]
  D5B-150 = [ [0. 0. 0. 0.] [0. 0. 0. 0.] ... [0. 0. 0. 0.] [0. 0. 0. 0.] ]
  D6A-150 = [ [0. 0. 0. 0.] [0. 0. 0. 0.] ... [0. 0. 0. 0.] [0. 0. 0. 0.] ]
  D6B-150 = [ [0. 0. 0. 0.] [0. 0. 0. 0.] ... [0. 0. 0. 0.] [0. 0. 0. 0.] ]
>

Details of Detector Data

In the commands above we created named data objects and each one seems to contain an array for each detector. However, this container actually allocates memory in a single block, and you can slice the object both in the detector and sample direction. For example:

In [19]:
# Access one detector by name
ob.detdata["signal"]["D0A-150"] = np.arange(samples, dtype=np.float64)

# Access one detector by index
ob.detdata["signal"][1] = 10.0 * np.arange(samples, dtype=np.float64)

# Slice by both detector and sample
ob.detdata["signal"][["D2A-150", "D2B-150"], 0:2] = 5.0

print(ob.detdata["signal"])
<DetectorData 14 detectors each with shape (10,), type float64, units :
  D0A-150 = [ 0.0 1.0 ... 8.0 9.0 ]
  D0B-150 = [ 0.0 10.0 ... 80.0 90.0 ]
  D1A-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D1B-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D2A-150 = [ 5.0 5.0 ... 0.0 0.0 ]
  D2B-150 = [ 5.0 5.0 ... 0.0 0.0 ]
  D3A-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D3B-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D4A-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D4B-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D5A-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D5B-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D6A-150 = [ 0.0 0.0 ... 0.0 0.0 ]
  D6B-150 = [ 0.0 0.0 ... 0.0 0.0 ]
>
In [20]:
# Access the whole thing as a 2D array
print(ob.detdata["signal"][:])
[array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]), array([ 0., 10., 20., 30., 40., 50., 60., 70., 80., 90.]), array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), array([5., 5., 0., 0., 0., 0., 0., 0., 0., 0.]), array([5., 5., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])]

Shared Data

Many types of data are common to multiple detectors. Some examples would be telescope pointing, timestamps, other sensor data, etc. When running in parallel we want to have just one copy of this data per node in order to save memory. The shared data is accessed under the "shared" attribute of the observation. For this serial notebook, you will not need to worry about the details of communicators, but when running in parallel it becomes important.
For this serial notebook, the shared attribute will look very much like a dictionary of numpy arrays. See the "parallel" intro notebook for more examples of using shared data when each observation is distributed across a grid of processes.

In [21]:
# Create some time stamps by assigning from an existing array on one process.
# When running with multiple processes, this syntax has extra communication.
ob.shared["times"] = np.arange(ob.n_local_samples, dtype=np.float64)
print(ob.shared["times"])

# Create and initialize to zero some boresight quaternions
ob.shared.create_column(
    "boresight_radec", shape=(ob.n_local_samples, 4), dtype=np.float64
)
print(ob.shared["boresight_radec"])
<MPIShared
  replicated on 1 nodes, each with 1 processes (1 total)
  shape = (10,), dtype = float64
  [ 0.0 1.0 ... 8.0 9.0 ]
>
<MPIShared
  replicated on 1 nodes, each with 1 processes (1 total)
  shape = (10, 4), dtype = float64
  [ [0. 0. 0. 0.] [0. 0. 0. 0.] ... [0. 0. 0. 0.] [0. 0. 0. 0.] ]
>

You can see that the data objects are a special "MPIShared" object from the pshmem package. Shared data objects can be read with slicing notation just like normal numpy arrays:

In [22]:
print(ob.shared["boresight_radec"][:])
[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]

However, they are intended to be "write once", "read many" objects. You cannot simply assign data to them. The reason is that the data is replicated across nodes and so setting array values must be a collective operation.

In [23]:
nullquat = np.array([0.0, 0.0, 0.0, 1.0])
full_data = np.tile(nullquat, ob.n_local_samples).reshape((-1, 4))

# In the serial case, simple assignment works just like array assignment
#ob.shared["boresight_radec"][:] = full_data

# When running with MPI, the set() method avoids some communication
ob.shared["boresight_radec"].set(full_data, fromrank=0)

print(ob.shared["boresight_radec"])
<MPIShared
  replicated on 1 nodes, each with 1 processes (1 total)
  shape = (10, 4), dtype = float64
  [ [0. 0. 0. 1.] [0. 0. 0. 1.] ... [0. 0. 0. 1.] [0. 0. 0. 1.] ]
>

Intervals

Each Observation may contain one or more "interval lists" which act as a global (within the observation) list of time / sample ranges where some feature of the data is constant. Interval lists support sample-wise inversion, intersection and union operations using the standard python bitwise operators (^, &, and |).

Intervals are not intended to act as individual sample quality flags. Per-sample flags should be created either as a shared timestream (for flags common to all detectors) or as a detector data object (for per-detector flags). Intervals can be used to represent things changing less frequently, for example: left or right moving telescope scans, satellite repointing maneuvers, calibration measurements, etc.

A single Interval consists of a time and a (local) sample range:

In [24]:
? toast.Interval
Object ` toast.Interval` not found.
In [25]:
# The observation starts with no lists of intervals

ob.intervals
Out[25]:
<IntervalsManager 1 lists>

To add a new interval list, use the create() method. Remember, in this notebook we have only one process, so do not have to worry about which process this information is coming from:

In [26]:
help(ob.intervals.create)
Help on method create in module toast.observation_data:

create(name, global_timespans, local_times, fromrank=0) method of toast.observation_data.IntervalsManager instance
    Create local interval lists from global time ranges on one process.

    In some situations, a single process has loaded data from the disk, queried a
    database, etc and has information about some time spans that are global across
    the observation.  This function automatically creates the named local interval
    list consisting of the intersection of the local sample range with these global
    intervals.

    Args:
        name (str):  The key to use in the local intervals dictionary.
        global_timespans (list):  List of start, stop tuples containing time ranges
            within the observation.
        local_times (array):  The local timestamps on this process.
        fromrank (int):  Get the list from this process rank of the observation
            communicator.  Input arguments on other processes are ignored.

Here we create one list of intervals. We specify the time ranges and the local array of timestamp values. Inside the code, the timestamps are used to convert these input time ranges into Interval objects.

In [27]:
ob.intervals.create("good", [(1.5, 3.5), (4.5, 6.), (7., 8.5)], ob.shared["times"])
In [28]:
# Now there is one interval list in the observation

print(ob.intervals)
<IntervalsManager 2 lists
  good: 3 intervals>
In [29]:
# The create method converted the time ranges into actual Interval instances:

print(ob.intervals["good"])
<IntervalList [
           1.500 -           3.500 (        2 -         4),
           4.500 -           6.000 (        5 -         6),
           7.000 -           8.500 (        7 -        10),
]>

Now create another list of intervals:

In [30]:
ob.intervals.create("stable", [(0.5, 2.5), (3.5, 5.), (6., 7.5)], ob.shared["times"])
print(ob.intervals)
<IntervalsManager 3 lists
  good: 3 intervals
  stable: 3 intervals>

As mentioned before, we can combine these in different ways:

In [31]:
ob.intervals["stable-and-not-good"] = ob.intervals["stable"] & ~ob.intervals["good"]

print(ob.intervals)
print(ob.intervals["stable-and-not-good"])
<IntervalsManager 4 lists
  good: 3 intervals
  stable: 3 intervals
  stable-and-not-good: 1 intervals>
<IntervalList [
           0.500 -           1.500 (        1 -         2),
]>
In [32]:
ob.intervals["not-stable-or-not-good"] = ~ob.intervals["stable"] | ~ob.intervals["good"]

print(ob.intervals)
print(ob.intervals["not-stable-or-not-good"])
<IntervalsManager 5 lists
  good: 3 intervals
  stable: 3 intervals
  stable-and-not-good: 1 intervals
  not-stable-or-not-good: 2 intervals>
<IntervalList [
           0.000 -           1.500 (        0 -         2),
           7.500 -           9.000 (        8 -        10),
]>

Views

Typically when defining data intervals in the last section it is because you want to do something with only the data falling in those sample ranges. Each observation has the ability to provide a "view" into the detector and shared data given by a previously defined interval list. Views are created on the fly on first access and are deleted automatically if the underlying interval is deleted. First, examine a view of the "good" interval list we defined in the previous section:

In [33]:
print(ob.view["good"])
[slice(np.int64(2), np.int64(4), 1), slice(np.int64(5), np.int64(6), 1), slice(np.int64(7), np.int64(10), 1)]

The string represention of a view is just a list of sample slices. However, the real power is that we can get a view of any of the observation detdata or shared objects. For example, we could get a view of the detector signal data. Recall that the full data for this is:

In [34]:
ob.detdata["signal"][:]
Out[34]:
[array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]),
 array([ 0., 10., 20., 30., 40., 50., 60., 70., 80., 90.]),
 array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]),
 array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]),
 array([5., 5., 0., 0., 0., 0., 0., 0., 0., 0.]),
 array([5., 5., 0., 0., 0., 0., 0., 0., 0., 0.]),
 array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]),
 array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]),
 array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]),
 array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]),
 array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]),
 array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]),
 array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]),
 array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])]

A view of the signal data falling in the "good" intervals is:

In [35]:
ob.view["good"].detdata["signal"][:]
Out[35]:
[<DetectorData (view) 14 detectors each with shape (2,), type float64, units :
   D0A-150 = [ 2.0 3.0 ]
   D0B-150 = [ 20.0 30.0 ]
   D1A-150 = [ 0.0 0.0 ]
   D1B-150 = [ 0.0 0.0 ]
   D2A-150 = [ 0.0 0.0 ]
   D2B-150 = [ 0.0 0.0 ]
   D3A-150 = [ 0.0 0.0 ]
   D3B-150 = [ 0.0 0.0 ]
   D4A-150 = [ 0.0 0.0 ]
   D4B-150 = [ 0.0 0.0 ]
   D5A-150 = [ 0.0 0.0 ]
   D5B-150 = [ 0.0 0.0 ]
   D6A-150 = [ 0.0 0.0 ]
   D6B-150 = [ 0.0 0.0 ]
 >,
 <DetectorData (view) 14 detectors each with shape (1,), type float64, units :
   D0A-150 = [ 5.0 ]
   D0B-150 = [ 50.0 ]
   D1A-150 = [ 0.0 ]
   D1B-150 = [ 0.0 ]
   D2A-150 = [ 0.0 ]
   D2B-150 = [ 0.0 ]
   D3A-150 = [ 0.0 ]
   D3B-150 = [ 0.0 ]
   D4A-150 = [ 0.0 ]
   D4B-150 = [ 0.0 ]
   D5A-150 = [ 0.0 ]
   D5B-150 = [ 0.0 ]
   D6A-150 = [ 0.0 ]
   D6B-150 = [ 0.0 ]
 >,
 <DetectorData (view) 14 detectors each with shape (3,), type float64, units :
   D0A-150 = [ 7.0 8.0 9.0 ]
   D0B-150 = [ 70.0 80.0 90.0 ]
   D1A-150 = [ 0.0 0.0 0.0 ]
   D1B-150 = [ 0.0 0.0 0.0 ]
   D2A-150 = [ 0.0 0.0 0.0 ]
   D2B-150 = [ 0.0 0.0 0.0 ]
   D3A-150 = [ 0.0 0.0 0.0 ]
   D3B-150 = [ 0.0 0.0 0.0 ]
   D4A-150 = [ 0.0 0.0 0.0 ]
   D4B-150 = [ 0.0 0.0 0.0 ]
   D5A-150 = [ 0.0 0.0 0.0 ]
   D5B-150 = [ 0.0 0.0 0.0 ]
   D6A-150 = [ 0.0 0.0 0.0 ]
   D6B-150 = [ 0.0 0.0 0.0 ]
 >]

This view is a list of arrays which have sliced the data in the time direction. These are not copies- they provide read/write access to underlying buffer. If you are doing many operations with a view it is easier to name it something else:

In [36]:
sng = ob.view["stable-and-not-good"]
sng.detdata["signal"]
Out[36]:
[<DetectorData (view) 14 detectors each with shape (1,), type float64, units :
   D0A-150 = [ 1.0 ]
   D0B-150 = [ 10.0 ]
   D1A-150 = [ 0.0 ]
   D1B-150 = [ 0.0 ]
   D2A-150 = [ 5.0 ]
   D2B-150 = [ 5.0 ]
   D3A-150 = [ 0.0 ]
   D3B-150 = [ 0.0 ]
   D4A-150 = [ 0.0 ]
   D4B-150 = [ 0.0 ]
   D5A-150 = [ 0.0 ]
   D5B-150 = [ 0.0 ]
   D6A-150 = [ 0.0 ]
   D6B-150 = [ 0.0 ]
 >]

Again, we can use a view to assign data to a subset of the full samples:

In [37]:
sng.detdata["signal"] = 7.0

print(ob.detdata["signal"][:])
[array([0., 7., 2., 3., 4., 5., 6., 7., 8., 9.]), array([ 0.,  7., 20., 30., 40., 50., 60., 70., 80., 90.]), array([0., 7., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 7., 0., 0., 0., 0., 0., 0., 0., 0.]), array([5., 7., 0., 0., 0., 0., 0., 0., 0., 0.]), array([5., 7., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 7., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 7., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 7., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 7., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 7., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 7., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 7., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 7., 0., 0., 0., 0., 0., 0., 0., 0.])]

We can access shared data as well with this view, but it is read-only from the view (the set() method of the shared objects or a collective assignment must be used to modify shared data):

In [38]:
ob.view["good"].shared["boresight_radec"]
Out[38]:
[array([[0., 0., 0., 1.],
        [0., 0., 0., 1.]]),
 array([[0., 0., 0., 1.]]),
 array([[0., 0., 0., 1.],
        [0., 0., 0., 1.],
        [0., 0., 0., 1.]])]
In [39]:
sng.shared["boresight_radec"]
Out[39]:
[array([[0., 0., 0., 1.]])]

Data Container

The Observation instances discussed previously are usually stored as a list inside a top-level container class called Data. This class also stores the TOAST MPI communicator information. For this serial example you can just instantiate an empty Data class and add things to the observation list:

In [40]:
data = toast.Data()

print(data)

print(data.obs)
<Data with 0 Observations:
Metadata:
{}
>
[]

Obviously this Data object has no observations yet. We'll fix that in the next section!

Processing Model

The TOAST processing model consists of Operator class instances running in a sequence on a subset of data. These sequences could be nested within other sequences (see the Pipeline operator below).

The Operator base class defines the interfaces for operators working on data. Operators are configured by defining class traits (attributes) which can be set during construction. An operator has an exec() method that works with Data objects (potentially just a subset of the data). Operators also have a finalize() method which is designed to do any final calculations after all passes through the timestream data are done. We will start by looking at the SimSatellite operator to simulate fake telescope scan strategies for a generic satellite. We can always see the options and default values by using the standard help function or the '?' command:

In [41]:
from toast import ops
from toast.schedule_sim_satellite import create_satellite_schedule

?ops.SimSatellite

You can instantiate a class directly by overriding some defaults:

In [42]:
simsat = ops.SimSatellite(
    num_observations=2, 
    observation_time=5 * u.minute,
)

print(simsat)
<SimSatellite
  API = 0 # Internal interface version for this operator
  boresight = boresight_radec # Observation shared key for boresight
  coord = C # Coordinate system to use for pointing. One of ('C', 'E', 'G')
  det_data = signal # Observation detdata key to initialize
  det_data_units = K # Output units if creating detector data
  det_flags = flags # Observation detdata key for flags to initialize
  detset_key = None # If specified, use this column of the focalplane detector_data to group detectors
  distribute_time = False # Distribute observation data along the time axis rather than detector axis
  enabled = True # If True, this class instance is marked as enabled
  hwp_angle = None # Observation shared key for HWP angle
  hwp_rpm = None # The rate (in RPM) of the HWP rotation
  hwp_step = None # For stepped HWP, the angle of each step
  hwp_step_time = None # For stepped HWP, the time between steps
  kernel_implementation = 0 # Which kernel implementation to use (DEFAULT, COMPILED, NUMPY, JAX).
  name = SimSatellite # The 'name' of this class instance
  position = position # Observation shared key for position
  prec_angle = 65.0 deg # The opening angle of the spin axis from the precession axis
  schedule = None # Instance of a SatelliteSchedule
  schedule_file = None # satellite-based observing schedule file
  shared_flags = flags # Observation shared key for common flags
  spin_angle = 30.0 deg # The opening angle of the boresight from the spin axis
  telescope = None # This must be an instance of a Telescope
  telescope_file = None # Path to HDF5 file containing an instrument group.
  times = times # Observation shared key for timestamps
  timing = False # If True, print timing of exec() and finalize()
  velocity = velocity # Observation shared key for velocity
>

If you are using multi instances of an operator in your pipeline with different configurations, then you should also pass a unique "name" to the constructor. This allows keeping the operators distinct when using config files (see more below):

In [43]:
other_simsat = ops.SimSatellite(
    name="other_simsat",
    num_observations=2, 
    observation_time=5 * u.minute,
)

print(other_simsat)
<SimSatellite
  API = 0 # Internal interface version for this operator
  boresight = boresight_radec # Observation shared key for boresight
  coord = C # Coordinate system to use for pointing. One of ('C', 'E', 'G')
  det_data = signal # Observation detdata key to initialize
  det_data_units = K # Output units if creating detector data
  det_flags = flags # Observation detdata key for flags to initialize
  detset_key = None # If specified, use this column of the focalplane detector_data to group detectors
  distribute_time = False # Distribute observation data along the time axis rather than detector axis
  enabled = True # If True, this class instance is marked as enabled
  hwp_angle = None # Observation shared key for HWP angle
  hwp_rpm = None # The rate (in RPM) of the HWP rotation
  hwp_step = None # For stepped HWP, the angle of each step
  hwp_step_time = None # For stepped HWP, the time between steps
  kernel_implementation = 0 # Which kernel implementation to use (DEFAULT, COMPILED, NUMPY, JAX).
  name = other_simsat # The 'name' of this class instance
  position = position # Observation shared key for position
  prec_angle = 65.0 deg # The opening angle of the spin axis from the precession axis
  schedule = None # Instance of a SatelliteSchedule
  schedule_file = None # satellite-based observing schedule file
  shared_flags = flags # Observation shared key for common flags
  spin_angle = 30.0 deg # The opening angle of the boresight from the spin axis
  telescope = None # This must be an instance of a Telescope
  telescope_file = None # Path to HDF5 file containing an instrument group.
  times = times # Observation shared key for timestamps
  timing = False # If True, print timing of exec() and finalize()
  velocity = velocity # Observation shared key for velocity
>

After the operator is constructed, the parameters can be changed directly. For example:

In [44]:
simsat.telescope = telescope
simsat.num_observations = 3

# Create a schedule for our simulated observing
simsat.schedule = create_satellite_schedule(
    mission_start=datetime.datetime(2023, 2, 23),
    observation_time=24 * u.hour,
    gap_time=0 * u.second,
    num_observations=1,
    prec_period=90 * u.minute,
    spin_period=10 * u.minute,
)

print(simsat)
<SimSatellite
  API = 0 # Internal interface version for this operator
  boresight = boresight_radec # Observation shared key for boresight
  coord = C # Coordinate system to use for pointing. One of ('C', 'E', 'G')
  det_data = signal # Observation detdata key to initialize
  det_data_units = K # Output units if creating detector data
  det_flags = flags # Observation detdata key for flags to initialize
  detset_key = None # If specified, use this column of the focalplane detector_data to group detectors
  distribute_time = False # Distribute observation data along the time axis rather than detector axis
  enabled = True # If True, this class instance is marked as enabled
  hwp_angle = None # Observation shared key for HWP angle
  hwp_rpm = None # The rate (in RPM) of the HWP rotation
  hwp_step = None # For stepped HWP, the angle of each step
  hwp_step_time = None # For stepped HWP, the time between steps
  kernel_implementation = 0 # Which kernel implementation to use (DEFAULT, COMPILED, NUMPY, JAX).
  name = SimSatellite # The 'name' of this class instance
  position = position # Observation shared key for position
  prec_angle = 65.0 deg # The opening angle of the spin axis from the precession axis
  schedule = <SatelliteSchedule site=space telescope=satellite with 1 scans
  <SatelliteScan '000000_2023-02-23T00:00+00:00' at 2023-02-23T00:00:00+00:00 with prec period 90.0 min, spin period 10.0 min>
> # Instance of a SatelliteSchedule
  schedule_file = None # satellite-based observing schedule file
  shared_flags = flags # Observation shared key for common flags
  spin_angle = 30.0 deg # The opening angle of the boresight from the spin axis
  telescope = <Telescope 'fake': uid = 4020063252, site = <SpaceSite 'L2' : uid = 3584191102>, focalplane = <Focalplane: 14 detectors, sample_rate = 10.0 Hz, FOV = 7.700000000000001 deg, detectors = [D0A-150 .. D6B-150]>> # This must be an instance of a Telescope
  telescope_file = None # Path to HDF5 file containing an instrument group.
  times = times # Observation shared key for timestamps
  timing = False # If True, print timing of exec() and finalize()
  velocity = velocity # Observation shared key for velocity
>
TOAST WARNING: Mission start time '2023-02-23 00:00:00' is not timezone-aware.  Assuming UTC.

And now we have an Operator that is ready to use. This particular operator creates observations from scratch with telescope properties generated and stored. We can create an empty Data object and then run this operator on it:

In [45]:
# This is equivalent to single call to "exec()" with all processes,
# and then a call to "finalize()".

simsat.apply(data)
In [46]:
print(data)
<Data with 1 Observations:
<Observation
  name = '000000_2023-02-23T00:00+00:00_1677110400'
  uid = '1190045437'  group has 1 processes
  telescope = <Telescope 'fake': uid = 4020063252, site = <SpaceSite 'L2' : uid = 3584191102>, focalplane = <Focalplane: 14 detectors, sample_rate = 10.0 Hz, FOV = 7.700000000000001 deg, detectors = [D0A-150 .. D6B-150]>>
  session = <Session '000000_2023-02-23T00:00+00:00_1677110400': uid = 214738611, start = 2023-02-23 00:00:00+00:00, end = 2023-02-23 23:59:59.900000+00:00>
  864000 total samples (864000 local)
  shared:  <SharedDataManager
    times (column): shape=(864000,), dtype=float64
    flags (column): shape=(864000,), dtype=uint8
    position (column): shape=(864000, 3), dtype=float64
    velocity (column): shape=(864000, 3), dtype=float64
    boresight_radec (column): shape=(864000, 4), dtype=float64>
  detdata:  <DetDataManager 14 local detectors, 864000 samples
    signal: shape=(14, 864000), dtype=float64, units='K'
    flags: shape=(14, 864000), dtype=uint8, units=''>
  intervals:  <IntervalsManager 1 lists>
>
Metadata:
{}
>

For this trivial case, we use the apply() method of the operator, which simply calls exec() once and then finalize(). When running a more complicated pipeline, the exec() method might be called multiple times on different detector sets (for example) before calling finalize().

Pipelines

TOAST includes a special operator (the Pipeline class), which is designed to run other operators (including other Pipeline instances. The purpose of this operator is to run sequences of other operators over sets of detectors to reduce the memory cost of intermediate products and / or to group together operators that support the use of accelerators to avoid memory copies to the host system.

In [47]:
? ops.Pipeline

As an example, we can create two simple operators and put them in a pipeline:

In [48]:
simsat = ops.SimSatellite(
    num_observations=2, 
    observation_time=5 * u.minute,
    telescope=telescope,
    schedule = create_satellite_schedule(
        mission_start=datetime.datetime(2023, 2, 23),
        observation_time=24 * u.hour,
        gap_time=0 * u.second,
        num_observations=1,
        prec_period=90 * u.minute,
        spin_period=10 * u.minute,
    ),
)

default_noise = ops.DefaultNoiseModel()
TOAST WARNING: Mission start time '2023-02-23 00:00:00' is not timezone-aware.  Assuming UTC.
In [49]:
pipe = ops.Pipeline(
    operators=[simsat, default_noise]
)

Now we can start with an empty Data object and run the pipeline on it:

In [50]:
data = toast.Data()

pipe.apply(data)

print(data)
<Data with 1 Observations:
<Observation
  name = '000000_2023-02-23T00:00+00:00_1677110400'
  uid = '1190045437'  group has 1 processes
  telescope = <Telescope 'fake': uid = 4020063252, site = <SpaceSite 'L2' : uid = 3584191102>, focalplane = <Focalplane: 14 detectors, sample_rate = 10.0 Hz, FOV = 7.700000000000001 deg, detectors = [D0A-150 .. D6B-150]>>
  session = <Session '000000_2023-02-23T00:00+00:00_1677110400': uid = 214738611, start = 2023-02-23 00:00:00+00:00, end = 2023-02-23 23:59:59.900000+00:00>
  noise_model = <AnalyticNoise model with 14 detectors>
  864000 total samples (864000 local)
  shared:  <SharedDataManager
    times (column): shape=(864000,), dtype=float64
    flags (column): shape=(864000,), dtype=uint8
    position (column): shape=(864000, 3), dtype=float64
    velocity (column): shape=(864000, 3), dtype=float64
    boresight_radec (column): shape=(864000, 4), dtype=float64>
  detdata:  <DetDataManager 14 local detectors, 864000 samples
    signal: shape=(14, 864000), dtype=float64, units='K'
    flags: shape=(14, 864000), dtype=uint8, units=''>
  intervals:  <IntervalsManager 1 lists>
>
Metadata:
{}
>

You can see here that the same satellite simulation was run, and then a default noise model (using the focalplane properties in each observation) was created.

Configuration of Operators

Operators are configured through class traits which can be passed as keyword arguments to the constructor. We can also dump information about these traits (name, type, help string) to an intermediate config dictionary and then write that to files in TOML or JSON format. These config dictionaries can also be used to instantiate operators directly.

In [51]:
import toast.config as tc

import tempfile
from pprint import PrettyPrinter

pp = PrettyPrinter(indent=1)

tmpdir = tempfile.mkdtemp()
toml_file = os.path.join(tmpdir, "test.toml")
yaml_file = os.path.join(tmpdir, "test.yaml")

As an example, we can take a previous operator and look at the "round trip" from class or instance, to a config dictionary, to a file, and back into creating a new operator instance from that:

In [52]:
# This gives us the config for an existing instance

conf = other_simsat.get_config()
pp.pprint(conf)
OrderedDict([('operators',
              OrderedDict([('other_simsat',
                            OrderedDict([('class',
                                          'toast.ops.sim_satellite.SimSatellite'),
                                         ('API',
                                          OrderedDict([('value', '0'),
                                                       ('type', 'int'),
                                                       ('help',
                                                        'Internal interface '
                                                        'version for this '
                                                        'operator')])),
                                         ('boresight',
                                          OrderedDict([('value',
                                                        'boresight_radec'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Observation shared '
                                                        'key for boresight')])),
                                         ('coord',
                                          OrderedDict([('value', 'C'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Coordinate system to '
                                                        'use for pointing. One '
                                                        "of ('C', 'E', "
                                                        "'G')")])),
                                         ('det_data',
                                          OrderedDict([('value', 'signal'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Observation detdata '
                                                        'key to initialize')])),
                                         ('det_data_units',
                                          OrderedDict([('value', "Unit('K')"),
                                                       ('type', 'Unit'),
                                                       ('help',
                                                        'Output units if '
                                                        'creating detector '
                                                        'data')])),
                                         ('det_flags',
                                          OrderedDict([('value', 'flags'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Observation detdata '
                                                        'key for flags to '
                                                        'initialize')])),
                                         ('detset_key',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'If specified, use '
                                                        'this column of the '
                                                        'focalplane '
                                                        'detector_data to '
                                                        'group detectors')])),
                                         ('distribute_time',
                                          OrderedDict([('value', 'False'),
                                                       ('type', 'bool'),
                                                       ('help',
                                                        'Distribute '
                                                        'observation data '
                                                        'along the time axis '
                                                        'rather than detector '
                                                        'axis')])),
                                         ('enabled',
                                          OrderedDict([('value', 'True'),
                                                       ('type', 'bool'),
                                                       ('help',
                                                        'If True, this class '
                                                        'instance is marked as '
                                                        'enabled')])),
                                         ('hwp_angle',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Observation shared '
                                                        'key for HWP angle')])),
                                         ('hwp_rpm',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'float'),
                                                       ('help',
                                                        'The rate (in RPM) of '
                                                        'the HWP rotation')])),
                                         ('hwp_step',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'Quantity'),
                                                       ('help',
                                                        'For stepped HWP, the '
                                                        'angle of each '
                                                        'step')])),
                                         ('hwp_step_time',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'Quantity'),
                                                       ('help',
                                                        'For stepped HWP, the '
                                                        'time between '
                                                        'steps')])),
                                         ('kernel_implementation',
                                          OrderedDict([('value', '0'),
                                                       ('type', 'enum'),
                                                       ('help',
                                                        'Which kernel '
                                                        'implementation to use '
                                                        '(DEFAULT, COMPILED, '
                                                        'NUMPY, JAX).')])),
                                         ('name',
                                          OrderedDict([('value',
                                                        'other_simsat'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        "The 'name' of this "
                                                        'class instance')])),
                                         ('position',
                                          OrderedDict([('value', 'position'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Observation shared '
                                                        'key for position')])),
                                         ('prec_angle',
                                          OrderedDict([('value',
                                                        "Quantity('6.50000000000000e+01 "
                                                        "deg')"),
                                                       ('type', 'Quantity'),
                                                       ('help',
                                                        'The opening angle of '
                                                        'the spin axis from '
                                                        'the precession '
                                                        'axis')])),
                                         ('schedule',
                                          OrderedDict([('value', 'None'),
                                                       ('type',
                                                        'toast.schedule.SatelliteSchedule'),
                                                       ('help',
                                                        'Instance of a '
                                                        'SatelliteSchedule')])),
                                         ('schedule_file',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'satellite-based '
                                                        'observing schedule '
                                                        'file')])),
                                         ('shared_flags',
                                          OrderedDict([('value', 'flags'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Observation shared '
                                                        'key for common '
                                                        'flags')])),
                                         ('spin_angle',
                                          OrderedDict([('value',
                                                        "Quantity('3.00000000000000e+01 "
                                                        "deg')"),
                                                       ('type', 'Quantity'),
                                                       ('help',
                                                        'The opening angle of '
                                                        'the boresight from '
                                                        'the spin axis')])),
                                         ('telescope',
                                          OrderedDict([('value', 'None'),
                                                       ('type',
                                                        'toast.instrument.Telescope'),
                                                       ('help',
                                                        'This must be an '
                                                        'instance of a '
                                                        'Telescope')])),
                                         ('telescope_file',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Path to HDF5 file '
                                                        'containing an '
                                                        'instrument group.')])),
                                         ('times',
                                          OrderedDict([('value', 'times'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Observation shared '
                                                        'key for '
                                                        'timestamps')])),
                                         ('timing',
                                          OrderedDict([('value', 'False'),
                                                       ('type', 'bool'),
                                                       ('help',
                                                        'If True, print timing '
                                                        'of exec() and '
                                                        'finalize()')])),
                                         ('velocity',
                                          OrderedDict([('value', 'velocity'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Observation shared '
                                                        'key for '
                                                        'velocity')]))]))]))])
In [53]:
# This gives us the default config values for a class

default_conf = ops.SimSatellite.get_class_config()
pp.pprint(default_conf)
OrderedDict([('operators',
              OrderedDict([('SimSatellite',
                            OrderedDict([('class',
                                          'toast.ops.sim_satellite.SimSatellite'),
                                         ('API',
                                          OrderedDict([('value', '0'),
                                                       ('type', 'int'),
                                                       ('help',
                                                        'Internal interface '
                                                        'version for this '
                                                        'operator')])),
                                         ('boresight',
                                          OrderedDict([('value',
                                                        'boresight_radec'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Observation shared '
                                                        'key for boresight')])),
                                         ('coord',
                                          OrderedDict([('value', 'C'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Coordinate system to '
                                                        'use for pointing. One '
                                                        "of ('C', 'E', "
                                                        "'G')")])),
                                         ('det_data',
                                          OrderedDict([('value', 'signal'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Observation detdata '
                                                        'key to initialize')])),
                                         ('det_data_units',
                                          OrderedDict([('value', "Unit('K')"),
                                                       ('type', 'Unit'),
                                                       ('help',
                                                        'Output units if '
                                                        'creating detector '
                                                        'data')])),
                                         ('det_flags',
                                          OrderedDict([('value', 'flags'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Observation detdata '
                                                        'key for flags to '
                                                        'initialize')])),
                                         ('detset_key',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'If specified, use '
                                                        'this column of the '
                                                        'focalplane '
                                                        'detector_data to '
                                                        'group detectors')])),
                                         ('distribute_time',
                                          OrderedDict([('value', 'False'),
                                                       ('type', 'bool'),
                                                       ('help',
                                                        'Distribute '
                                                        'observation data '
                                                        'along the time axis '
                                                        'rather than detector '
                                                        'axis')])),
                                         ('enabled',
                                          OrderedDict([('value', 'True'),
                                                       ('type', 'bool'),
                                                       ('help',
                                                        'If True, this class '
                                                        'instance is marked as '
                                                        'enabled')])),
                                         ('hwp_angle',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Observation shared '
                                                        'key for HWP angle')])),
                                         ('hwp_rpm',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'float'),
                                                       ('help',
                                                        'The rate (in RPM) of '
                                                        'the HWP rotation')])),
                                         ('hwp_step',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'Quantity'),
                                                       ('help',
                                                        'For stepped HWP, the '
                                                        'angle of each '
                                                        'step')])),
                                         ('hwp_step_time',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'Quantity'),
                                                       ('help',
                                                        'For stepped HWP, the '
                                                        'time between '
                                                        'steps')])),
                                         ('kernel_implementation',
                                          OrderedDict([('value', '0'),
                                                       ('type', 'enum'),
                                                       ('help',
                                                        'Which kernel '
                                                        'implementation to use '
                                                        '(DEFAULT, COMPILED, '
                                                        'NUMPY, JAX).')])),
                                         ('name',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        "The 'name' of this "
                                                        'class instance')])),
                                         ('position',
                                          OrderedDict([('value', 'position'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Observation shared '
                                                        'key for position')])),
                                         ('prec_angle',
                                          OrderedDict([('value',
                                                        "Quantity('6.50000000000000e+01 "
                                                        "deg')"),
                                                       ('type', 'Quantity'),
                                                       ('help',
                                                        'The opening angle of '
                                                        'the spin axis from '
                                                        'the precession '
                                                        'axis')])),
                                         ('schedule',
                                          OrderedDict([('value',
                                                        traitlets.Undefined),
                                                       ('type',
                                                        'toast.schedule.SatelliteSchedule'),
                                                       ('help',
                                                        'Instance of a '
                                                        'SatelliteSchedule')])),
                                         ('schedule_file',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'satellite-based '
                                                        'observing schedule '
                                                        'file')])),
                                         ('shared_flags',
                                          OrderedDict([('value', 'flags'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Observation shared '
                                                        'key for common '
                                                        'flags')])),
                                         ('spin_angle',
                                          OrderedDict([('value',
                                                        "Quantity('3.00000000000000e+01 "
                                                        "deg')"),
                                                       ('type', 'Quantity'),
                                                       ('help',
                                                        'The opening angle of '
                                                        'the boresight from '
                                                        'the spin axis')])),
                                         ('telescope',
                                          OrderedDict([('value',
                                                        traitlets.Undefined),
                                                       ('type',
                                                        'toast.instrument.Telescope'),
                                                       ('help',
                                                        'This must be an '
                                                        'instance of a '
                                                        'Telescope')])),
                                         ('telescope_file',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Path to HDF5 file '
                                                        'containing an '
                                                        'instrument group.')])),
                                         ('times',
                                          OrderedDict([('value', 'times'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Observation shared '
                                                        'key for '
                                                        'timestamps')])),
                                         ('timing',
                                          OrderedDict([('value', 'False'),
                                                       ('type', 'bool'),
                                                       ('help',
                                                        'If True, print timing '
                                                        'of exec() and '
                                                        'finalize()')])),
                                         ('velocity',
                                          OrderedDict([('value', 'velocity'),
                                                       ('type', 'str'),
                                                       ('help',
                                                        'Observation shared '
                                                        'key for '
                                                        'velocity')]))]))]))])
In [54]:
tc.dump_toml(toml_file, conf)
tc.dump_yaml(yaml_file, conf)

Now we can see what this config looks like dumped to TOML and YAML:

In [55]:
!cat {toml_file}
# TOAST config
# Generated with version 3.0.2

[operators.other_simsat]
    class = "toast.ops.sim_satellite.SimSatellite"
    API = 0 # Internal interface version for this operator
    boresight = "boresight_radec" # Observation shared key for boresight
    coord = "C" # Coordinate system to use for pointing. One of ('C', 'E', 'G')
    det_data = "signal" # Observation detdata key to initialize
    det_data_units = "Unit('K')" # Output units if creating detector data
    det_flags = "flags" # Observation detdata key for flags to initialize
    detset_key = "None" # If specified, use this column of the focalplane detector_data to group detectors
    distribute_time = false # Distribute observation data along the time axis rather than detector axis
    enabled = true # If True, this class instance is marked as enabled
    hwp_angle = "None" # Observation shared key for HWP angle
    hwp_rpm = "None" # The rate (in RPM) of the HWP rotation
    hwp_step = "None" # For stepped HWP, the angle of each step
    hwp_step_time = "None" # For stepped HWP, the time between steps
    kernel_implementation = 0 # Which kernel implementation to use (DEFAULT, COMPILED, NUMPY, JAX).
    name = "other_simsat" # The 'name' of this class instance
    position = "position" # Observation shared key for position
    prec_angle = "Quantity('6.50000000000000e+01 deg')" # The opening angle of the spin axis from the precession axis
    schedule = "None" # Instance of a SatelliteSchedule
    schedule_file = "None" # satellite-based observing schedule file
    shared_flags = "flags" # Observation shared key for common flags
    spin_angle = "Quantity('3.00000000000000e+01 deg')" # The opening angle of the boresight from the spin axis
    telescope = "None" # This must be an instance of a Telescope
    telescope_file = "None" # Path to HDF5 file containing an instrument group.
    times = "times" # Observation shared key for timestamps
    timing = false # If True, print timing of exec() and finalize()
    velocity = "velocity" # Observation shared key for velocity
In [56]:
!cat {yaml_file}
# TOAST config generated with version 3.0.2
operators:
  other_simsat:
    class: toast.ops.sim_satellite.SimSatellite
    API: 0  # Internal interface version for this operator
    boresight: boresight_radec # Observation shared key for boresight
    coord: C # Coordinate system to use for pointing. One of ('C', 'E', 'G')
    det_data: signal # Observation detdata key to initialize
    det_data_units: Unit('K') # Output units if creating detector data
    det_flags: flags # Observation detdata key for flags to initialize
    detset_key: # If specified, use this column of the focalplane detector_data to group detectors
    distribute_time: false # Distribute observation data along the time axis rather than detector axis
    enabled: true # If True, this class instance is marked as enabled
    hwp_angle: # Observation shared key for HWP angle
    hwp_rpm: # The rate (in RPM) of the HWP rotation
    hwp_step: # For stepped HWP, the angle of each step
    hwp_step_time: # For stepped HWP, the time between steps
    kernel_implementation: 0 # Which kernel implementation to use (DEFAULT, COMPILED, NUMPY, JAX).
    name: other_simsat # The 'name' of this class instance
    position: position # Observation shared key for position
    prec_angle: Quantity('6.50000000000000e+01 deg') # The opening angle of the spin axis from the precession axis
    schedule: # Instance of a SatelliteSchedule
    schedule_file: # satellite-based observing schedule file
    shared_flags: flags # Observation shared key for common flags
    spin_angle: Quantity('3.00000000000000e+01 deg') # The opening angle of the boresight from the spin axis
    telescope: # This must be an instance of a Telescope
    telescope_file: # Path to HDF5 file containing an instrument group.
    times: times # Observation shared key for timestamps
    timing: false # If True, print timing of exec() and finalize()
    velocity: velocity # Observation shared key for velocity

And then we can load the config back in to a dictionary:

In [57]:
newconf = tc.load_config(yaml_file)
pp.pprint(newconf)
OrderedDict([('operators',
              OrderedDict([('other_simsat',
                            OrderedDict([('class',
                                          'toast.ops.sim_satellite.SimSatellite'),
                                         ('API',
                                          OrderedDict([('value', '0'),
                                                       ('type', 'int')])),
                                         ('boresight',
                                          OrderedDict([('value',
                                                        'boresight_radec'),
                                                       ('type', 'str')])),
                                         ('coord',
                                          OrderedDict([('value', 'C'),
                                                       ('type', 'str')])),
                                         ('det_data',
                                          OrderedDict([('value', 'signal'),
                                                       ('type', 'str')])),
                                         ('det_data_units',
                                          OrderedDict([('value', "Unit('K')"),
                                                       ('type', 'Unit')])),
                                         ('det_flags',
                                          OrderedDict([('value', 'flags'),
                                                       ('type', 'str')])),
                                         ('detset_key',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'unknown')])),
                                         ('distribute_time',
                                          OrderedDict([('value', 'False'),
                                                       ('type', 'bool')])),
                                         ('enabled',
                                          OrderedDict([('value', 'True'),
                                                       ('type', 'bool')])),
                                         ('hwp_angle',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'unknown')])),
                                         ('hwp_rpm',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'unknown')])),
                                         ('hwp_step',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'unknown')])),
                                         ('hwp_step_time',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'unknown')])),
                                         ('kernel_implementation',
                                          OrderedDict([('value', '0'),
                                                       ('type', 'int')])),
                                         ('name',
                                          OrderedDict([('value',
                                                        'other_simsat'),
                                                       ('type', 'str')])),
                                         ('position',
                                          OrderedDict([('value', 'position'),
                                                       ('type', 'str')])),
                                         ('prec_angle',
                                          OrderedDict([('value',
                                                        "Quantity('6.50000000000000e+01 "
                                                        "deg')"),
                                                       ('type', 'Quantity')])),
                                         ('schedule',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'unknown')])),
                                         ('schedule_file',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'unknown')])),
                                         ('shared_flags',
                                          OrderedDict([('value', 'flags'),
                                                       ('type', 'str')])),
                                         ('spin_angle',
                                          OrderedDict([('value',
                                                        "Quantity('3.00000000000000e+01 "
                                                        "deg')"),
                                                       ('type', 'Quantity')])),
                                         ('telescope',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'unknown')])),
                                         ('telescope_file',
                                          OrderedDict([('value', 'None'),
                                                       ('type', 'unknown')])),
                                         ('times',
                                          OrderedDict([('value', 'times'),
                                                       ('type', 'str')])),
                                         ('timing',
                                          OrderedDict([('value', 'False'),
                                                       ('type', 'bool')])),
                                         ('velocity',
                                          OrderedDict([('value', 'velocity'),
                                                       ('type',
                                                        'str')]))]))]))])

Finally, we can create new instances of operators from this config dictionary:

In [58]:
run = toast.traits.create_from_config(newconf)
print(run)
namespace(operators=namespace(other_simsat=<SimSatellite
  API = 0 # Internal interface version for this operator
  boresight = boresight_radec # Observation shared key for boresight
  coord = C # Coordinate system to use for pointing. One of ('C', 'E', 'G')
  det_data = signal # Observation detdata key to initialize
  det_data_units = K # Output units if creating detector data
  det_flags = flags # Observation detdata key for flags to initialize
  detset_key = None # If specified, use this column of the focalplane detector_data to group detectors
  distribute_time = False # Distribute observation data along the time axis rather than detector axis
  enabled = True # If True, this class instance is marked as enabled
  hwp_angle = None # Observation shared key for HWP angle
  hwp_rpm = None # The rate (in RPM) of the HWP rotation
  hwp_step = None # For stepped HWP, the angle of each step
  hwp_step_time = None # For stepped HWP, the time between steps
  kernel_implementation = 0 # Which kernel implementation to use (DEFAULT, COMPILED, NUMPY, JAX).
  name = other_simsat # The 'name' of this class instance
  position = position # Observation shared key for position
  prec_angle = 65.0 deg # The opening angle of the spin axis from the precession axis
  schedule = None # Instance of a SatelliteSchedule
  schedule_file = None # satellite-based observing schedule file
  shared_flags = flags # Observation shared key for common flags
  spin_angle = 30.0 deg # The opening angle of the boresight from the spin axis
  telescope = None # This must be an instance of a Telescope
  telescope_file = None # Path to HDF5 file containing an instrument group.
  times = times # Observation shared key for timestamps
  timing = False # If True, print timing of exec() and finalize()
  velocity = velocity # Observation shared key for velocity
>))

Now we access our new operator and use it:

In [59]:
new_simsat = run.operators.other_simsat
print(new_simsat)
<SimSatellite
  API = 0 # Internal interface version for this operator
  boresight = boresight_radec # Observation shared key for boresight
  coord = C # Coordinate system to use for pointing. One of ('C', 'E', 'G')
  det_data = signal # Observation detdata key to initialize
  det_data_units = K # Output units if creating detector data
  det_flags = flags # Observation detdata key for flags to initialize
  detset_key = None # If specified, use this column of the focalplane detector_data to group detectors
  distribute_time = False # Distribute observation data along the time axis rather than detector axis
  enabled = True # If True, this class instance is marked as enabled
  hwp_angle = None # Observation shared key for HWP angle
  hwp_rpm = None # The rate (in RPM) of the HWP rotation
  hwp_step = None # For stepped HWP, the angle of each step
  hwp_step_time = None # For stepped HWP, the time between steps
  kernel_implementation = 0 # Which kernel implementation to use (DEFAULT, COMPILED, NUMPY, JAX).
  name = other_simsat # The 'name' of this class instance
  position = position # Observation shared key for position
  prec_angle = 65.0 deg # The opening angle of the spin axis from the precession axis
  schedule = None # Instance of a SatelliteSchedule
  schedule_file = None # satellite-based observing schedule file
  shared_flags = flags # Observation shared key for common flags
  spin_angle = 30.0 deg # The opening angle of the boresight from the spin axis
  telescope = None # This must be an instance of a Telescope
  telescope_file = None # Path to HDF5 file containing an instrument group.
  times = times # Observation shared key for timestamps
  timing = False # If True, print timing of exec() and finalize()
  velocity = velocity # Observation shared key for velocity
>

Running the Test Suite

TOAST includes extensive tests built in to the package. Running all of them takes some time, but you can also run just one test by specifying the name of the file in the toast/tests directory (without the ".py" extension):

In [60]:
import toast.tests

# Run just a couple simple tests in toast/tests/env.py
toast.tests.run("env")
test_comm (toast.tests.env.EnvTest.test_comm) ... 
<toast.Comm
  World MPI communicator = <mpi4py.MPI.Intracomm object at 0x7f7b00794490>
  World MPI size = 1
  World MPI rank = 0
  Group MPI communicator = <mpi4py.MPI.Intracomm object at 0x7f7b00794490>
  Group MPI size = 1
  Group MPI rank = 0
  Rank MPI communicator = None
>
[0]ok 
test_env (toast.tests.env.EnvTest.test_env) ... 
<toast.Environment
  Source code version = 3.0.2
  Logging level = INFO
  Handling enabled for 0 signals:
  Max threads = 4
>
[0]ok 
test_mpi_lock (toast.tests.env.EnvTest.test_mpi_lock)
This is based on the simple tests from the upstream repo: ... 
lock:  rank 0, instance 0 locking shared window
lock:  rank 0, instance 0 list = [0]
lock:  rank 0, instance 0 putting wait number 1
lock:  rank 0, instance 0 unlocking shared window
lock:  rank 0 got the lock
test lock:  process 0 got the lock
unlock:  rank 0, instance 0 locking shared window
unlock:  rank 0, instance 0 list = [1]
unlock:  rank 0, instance 0 reset wait to zero
unlock:  rank 0, instance 0 unlocking shared window
unlock:  rank 0 sent the lock
[0]ok 
test_mpi_shared (toast.tests.env.EnvTest.test_mpi_shared)
This is based on the simple tests from the upstream repo: ... 
proc 0 slice has dims (10,), dtype <f8, C = True
proc 0 slice has dims (10,), dtype <f4, C = True
proc 0 slice has dims (10,), dtype <i8, C = True
proc 0 slice has dims (10,), dtype <i4, C = True
[0]ok 
test_redirect (toast.tests.env.EnvTest.test_redirect) ... 
TOAST INFO: Begin log redirection to /home/runner/work/toast/toast/docs/docs/tutorials/toast_output/env/redirected.txt at Sat May  2 03:28:19 2026
[0]ok 


----------------------------------------------------------------------
Ran 5 tests in 0.119s

[0] OK
TOAST INFO: End log redirection to /home/runner/work/toast/toast/docs/docs/tutorials/toast_output/env/redirected.txt at Sat May  2 03:28:19 2026
Out[60]:
0
In [61]:
# Now run **ALL** the (serial) tests
# toast.tests.run()