Time Ordered Simulations¶
The LiteBIRD simulation framework intends to offer the capability to
simulate many effects in detector timestreams. This page will detail
these simulated effects as they become available. The timestreams are
stored in the tod
field of Observation
objects. These
arrays have shape (n_detectors, n_samples)
, where detectors are
indexed in the same order as the Observation.detector_global_info
array. All the time streams inside the framework use Kelvin as the
default unit.
Filling TOD with signal¶
The framework provides scan_map()
, a routine which scans an
input map accordingly to the scanning strategy and fills the detector
timestreams. You can fill with signal an existing TOD by using the
function scan_map_in_observations()
, as the following example
shows:
import litebird_sim as lbs
import numpy as np
hwp_radpsec = np.pi / 8
start_time_s = 0
time_span_s = 1
nside = 256
npix = 12 * nside * nside
# Create a simulation
sim = lbs.Simulation(
base_path="./output",
start_time=start_time_s,
duration_s=time_span_s,
random_seed=12345,
)
# Define the scanning strategy
sim.set_scanning_strategy(
lbs.SpinningScanningStrategy(
spin_sun_angle_rad=0.785_398_163_397_448_3,
precession_rate_hz=8.664_850_513_998_931e-05,
spin_rate_hz=0.000_833_333_333_333_333_4,
start_time=start_time_s,
),
delta_time_s=7200,
)
sim.set_instrument(
lbs.InstrumentInfo(
boresight_rotangle_rad=0.0,
spin_boresight_angle_rad=0.872_664_625_997_164_8,
spin_rotangle_rad=3.141_592_653_589_793,
),
)
# Create a detector object
det = lbs.DetectorInfo(
name="Detector",
sampling_rate_hz=10,
quat=[0.0, 0.0, 0.0, 1.0],
)
# Initialize the observation
(obs,) = sim.create_observations(detectors=[det])
# Compute the pointing information
sim.compute_pointings()
# Create a map to scan (in realistic simulations,
# use the MBS module provided by litebird_sim)
maps = np.ones((3, npix))
in_map = {"Detector": maps, "Coordinates": lbs.CoordinateSystem.Ecliptic}
# Here scan the map and fill tod
lbs.scan_map_in_observations(
obs,
in_map,
input_map_in_galactic = False,
)
for i in range(obs.n_samples):
# + 0. removes leading minus from negative zero
value = np.round(obs.tod[0][i], 5) + 0.
print(f"{value:.5f}")
0.00000
-0.00075
-0.00151
-0.00226
-0.00301
-0.00376
-0.00451
-0.00526
-0.00601
-0.00676
The input maps to scan can be either included in a dictionary with the name of the channel or the name of the dectector as keyword (the routines described in Synthetic sky maps already provied the inputs in the correct format), or a numpy array with shape (3, n_pixels).
The pointing information can be included in the observation or passed through pointings. If both obs and pointings are provided, they must be coherent, so either a single Observation and a single numpy array, or same lenght list of Observations and numpy arrays. If the input map is ecliptic coordinates set input_map_in_galactic to False. The effect of a possible HWP is included in the pointing information, see Scanning strategy.
The routine provides an on-the-fly interpolation of the input maps. This option is available through the argument interpolation which specifies the type of TOD interpolation (”” for no interpolation, “linear” for linear interpolation). Default: no interpolation.
Adding Noise¶
The ability to add noise to your detector timestreams is supported through the
function add_noise_to_observations()
and the low-level versions
add_noise()
, add_white_noise()
, and
add_one_over_f_noise()
.
Here is a short example that shows how to add noise:
import litebird_sim as lbs
import numpy as np
# Create a simulation lasting 100 seconds
sim = lbs.Simulation(
base_path='./output',
start_time=0,
duration_s=100,
random_seed=12345,
)
# Create a detector object
det = lbs.DetectorInfo(
net_ukrts=100,
sampling_rate_hz=10
)
obs = sim.create_observations(detectors=[det])
# Here we add white noise using the detector
# noise parameters from the `det` object.
# We use the random number generator provided
# by `sim`, which is initialized with the
# seed we passed to the Simulation constructor
# to ensure repeatability.
lbs.noise.add_noise_to_observations(obs, 'white', random=sim.random)
for i in range(10):
print(f"{obs[0].tod[0][i]:.5e}")
5.65263e-04
-1.54522e-04
3.42276e-04
-1.42274e-04
-1.71110e-04
8.72188e-05
-1.23400e-04
-6.99311e-05
6.58389e-05
5.51306e-04
Note that we pass sim.random
as the number generator to use.
This is a member variable that is initialized by the constructor
of the class Simulation
, and it is safe to be used with
multiple MPI processes as it ensures that each process has its
own random number generator with a different seed. You can also
pass another random number generator, as long as it has the
normal
method. More information on the generation of random
numbers can be found in Random numbers in litebird_sim.
To add white noise using a custom white noise sigma, in µK, we can call the low level function directly:
import litebird_sim as lbs
sim = lbs.Simulation(
base_path='./output',
start_time=0,
duration_s=100,
random_seed=12345,
)
det = lbs.DetectorInfo(
net_ukrts=100,
sampling_rate_hz=10,
)
obs = sim.create_observations(detectors=[det])
custom_sigma_uk = 1234
lbs.noise.add_white_noise(obs[0].tod[0], custom_sigma_uk, random=sim.random)
We can also add 1/f noise using a very similar call to the above:
import litebird_sim as lbs
sim = lbs.Simulation(
base_path='./output',
start_time=0,
duration_s=100,
random_seed=12345,
)
det = lbs.DetectorInfo(
net_ukrts=100,
sampling_rate_hz=10,
alpha=1,
fknee_mhz=10
)
obs = sim.create_observations(detectors=[det])
# Here we add 1/f noise using the detector noise
# parameters from the detector object
lbs.noise.add_noise_to_observations(obs, 'one_over_f', random=sim.random)
Again, to generate noise with custom parameters, we can either use the low-level function or edit the Observation
object to contain the desired noise parameters.
import litebird_sim as lbs
import numpy as np
sim = lbs.Simulation(
base_path='./output',
start_time=0,
duration_s=100,
random_seed=12345,
)
det = lbs.DetectorInfo(
net_ukrts=100,
sampling_rate_hz=10,
alpha=1,
fknee_mhz=10,
fmin_hz=0.001,
)
obs = sim.create_observations(detectors=[det])
custom_sigma_uk = 1234
custom_fknee_mhz = 12.34
custom_alpha = 1.234
custom_fmin_hz = 0.0123
# Option 1: we call the low-level function directly
lbs.noise.add_one_over_f_noise(
obs[0].tod[0],
custom_fknee_mhz,
custom_fmin_hz,
custom_alpha,
custom_sigma_uk,
obs[0].sampling_rate_hz,
sim.random,
)
# Option 2: we change the values in `obs`
obs[0].fknee_mhz[0] = custom_fknee_mhz
obs[0].fmin_hz[0] = custom_fmin_hz
obs[0].alpha[0] = custom_alpha
obs[0].net_ukrts[0] = (
custom_sigma_uk / np.sqrt(obs[0].sampling_rate_hz)
)
lbs.noise.add_noise_to_observations(obs, 'one_over_f', random=sim.random)
Warning
Be sure you understand the difference between the noise level in a timestream and the noise level in a map. Although of course the latter depends on the former, the conversion depends on several factors.
A common mistake is use the mission time divided by the number of pixels in
the map in a call to add_white_noise()
. This is wrong, as the noise
level per pixel depends on the overall integration time, which is always
less than the mission time because of cosmic ray loss, repointing maneuvers, etc.
These effects reduce the number of samples in the timeline that can be used to
estimate the map, but they do not affect the noise of the timeline.
Methods of the Simulation class¶
The class Simulation
provides two simple functions that fill
with sky signal and noise all the observations of a given simulation.
The function Simulation.fill_tods()
takes a map and scans it, while
the function Simulation.add_noise()
adds noise to the timelines.
Thanks to these functions the generation of a simulation becomes quite
transparent:
import litebird_sim as lbs
from astropy.time import Time
import numpy as np
start_time = 0
time_span_s = 1000.0
sampling_hz = 10.0
nside = 128
sim = lbs.Simulation(
start_time=start_time,
duration_s=time_span_s,
random_seed=12345,
)
# We pick a simple scanning strategy where the spin axis is aligned
# with the Sun-Earth axis, and the spacecraft spins once every minute
sim.set_scanning_strategy(
lbs.SpinningScanningStrategy(
spin_sun_angle_rad=np.deg2rad(0),
precession_rate_hz=0,
spin_rate_hz=1 / 60,
start_time=start_time,
),
delta_time_s=5.0,
)
# We simulate an instrument whose boresight is perpendicular to
# the spin axis.
sim.set_instrument(
lbs.InstrumentInfo(
boresight_rotangle_rad=0.0,
spin_boresight_angle_rad=np.deg2rad(90),
spin_rotangle_rad=np.deg2rad(75),
)
)
# A simple detector looking along the boresight direction
det = lbs.DetectorInfo(
name="Boresight_detector",
sampling_rate_hz=sampling_hz,
bandcenter_ghz=100.0,
net_ukrts=50.0,
)
sim.create_observations(detectors=det)
sim.compute_pointings()
sky_signal = np.ones((3,12*nside*nside))*1e-4
sim.fill_tods(sky_signal)
sim.add_noise(noise_type='white', random=sim.random)
for i in range(5):
print(f"{sim.observations[0].tod[0][i]:.5e}")
4.14241e-04
5.46700e-05
3.03378e-04
6.13975e-05
4.72613e-05
API reference¶
- litebird_sim.scan_map.compute_signal_for_one_sample(T, Q, U, co, si)¶
Bolometric equation
- litebird_sim.scan_map.scan_map(tod, pointings, pol_angle, maps: Dict[str, ndarray], input_names, input_map_in_galactic: bool = True, interpolation: str | None = '')¶
Scan a map filling time-ordered data
This function modifies the values in tod by adding the contribution of the bolometric equation given a list of TQU maps maps. The pointings argument must be a DxNx2 matrix containing the pointing information, where D is the number of detector for the current observation and N is the size of the tod array. pol_angle is the array of size DxN containing the polarization angle in radiants. input_names is an array containing the keywords that allow to select the proper input in maps for each detector in the TOD. If input_map_in_galactic is set to False the input map is assumed in ecliptic coordinates, default galactic. The interpolation argument specifies the type of TOD interpolation (”” for no interpolation, “linear” for linear interpolation)
- litebird_sim.scan_map.scan_map_for_one_detector(tod_det, input_T, input_Q, input_U, pol_angle_det)¶
- litebird_sim.scan_map.scan_map_in_observations(obs: Observation | List[Observation], maps: Dict[str, ndarray], pointings: ndarray | List[ndarray] | None = None, input_map_in_galactic: bool = True, component: str = 'tod', interpolation: str | None = '')¶
Scan a map filling time-ordered data
This is a wrapper around the
scan_map()
function that applies to the TOD stored in obs and the pointings stored in pointings. The two types can either bed aObservation
instance and a NumPy matrix, or a list of observations and a list of NumPy matrices; in the latter case, they must have the same number of elements.The field maps must either be a dictionary associating the name of each detector with a
(3, NPIX)
array containing the three I/Q/U maps or a plain(3, NPIX)
array. In the latter case, the I/Q/U maps will be used for all the detectors.The coordinate system is usually specified using the key Coordinates in the dictionary passed to the maps argument, and it must be an instance of the class
CoordinateSystem
. If you are using a plain NumPy array instead of a dictionary for maps, you should specify whether to use Ecliptic or Galactic coordinates through the parameter input_map_in_galactic. Ifmaps["Coordinates"]
is present, it must be consistent with the value for input_map_in_galactic; if not, the code prints a warning and uses the former.By default, the signal is added to
Observation.tod
. If you want to add it to some other field of theObservation
class, use component:for cur_obs in sim.observations: # Allocate a new TOD for the sky signal alone cur_obs.sky_tod = np.zeros_like(cur_obs.tod) # Ask `add_noise_to_observations` to store the noise # in `obs.sky_tod` scan_map_in_observations(sim.observations, …, component="sky_tod")
- litebird_sim.noise.add_noise(tod, noise_type: str, sampling_rate_hz: float, net_ukrts, fknee_mhz, fmin_hz, alpha, random, scale=1.0)¶
Add noise (white or 1/f) to a 2D array of floating-point values
This function sums an array of random number following a white noise model with an optional 1/f component to data, which is assumed to be a D×N array containing the TOD for D detectors, each containing N samples.
The parameter noisetype must either be
white
orone_over_f
; in the latter case, the noise will contain a 1/f part and a white noise part.Be sure not to include cosmic ray loss, repointing maneuvers, etc., in the value passed as net_ukrts, as these affect the integration time but not the white noise per sample.
The parameter scale can be used to introduce measurement unit conversions when appropriate. Default units: [K].
The parameter random must be specified and must be a random number generator that implements the
normal
method. You should typically use the random field of aSimulation
object for this.The parameters net_ukrts, fknee_mhz, fmin_hz, alpha, and scale can either be scalars or arrays; in the latter case, their size must be the same as
tod.shape[0]
, which is the number of detectors in the TOD.
- litebird_sim.noise.add_noise_to_observations(obs: Observation | List[Observation], noise_type: str, random: Generator, scale: float = 1.0, component: str = 'tod')¶
Add noise of the defined type to the observations in obs
This class provides an interface to the low-level function
add_noise()
. The parameter obs can either be oneObservation
instance or a list of observations, which are typically taken from the field observations of aSimulation
object. Unlikeadd_noise()
, it is not needed to pass the noise parameters here, as they are taken from the characteristics of the detectors saved in obs. The parameter random must be specified and must be a random number generator that implements thenormal
method. You should typically use the random field of aSimulation
object for this.By default, the noise is added to
Observation.tod
. If you want to add it to some other field of theObservation
class, use component:- for cur_obs in sim.observations:
# Allocate a new TOD for the noise alone cur_obs.noise_tod = np.zeros_like(cur_obs.tod)
# Ask add_noise_to_observations to store the noise # in obs.noise_tod add_noise_to_observations(sim.observations, …, component=”noise_tod”)
See
add_noise()
for more information.
- litebird_sim.noise.add_one_over_f_noise(data, fknee_mhz: float, fmin_hz: float, alpha: float, sigma: float, sampling_rate_hz: float, random)¶
Adds a 1/f noise timestream with the given f knee and alpha to data To be called from add_noise_to_observations
- Parameters:
data – 1-D numpy array
fknee_mhz – knee frequency in mHz
fmin_hz – kmin frequency for high pass in Hz
alpha – low frequency spectral tilt
sigma – the white noise level per sample. Be sure not to include cosmic ray loss, repointing maneuvers, etc., as these affect the integration time but not the white noise per sample.
sampling_rate_hz – the sampling frequency of the data
random – a random number generator that implements the
normal
method. You should typically use the random field of aSimulation
object for this. It must be specified
- litebird_sim.noise.add_white_noise(data, sigma: float, random)¶
Adds white noise with the given sigma to the array data.
To be called from add_noise_to_observations.
- Parameters:
data – 1-D numpy array
sigma – the white noise level per sample. Be sure not to include cosmic ray loss, repointing maneuvers, etc., as these affect the integration time but not the white noise per sample.
random – a random number generator that implements the
normal
method. You should typically use the random field of aSimulation
object for this. It must be specified
- litebird_sim.noise.build_one_over_f_model(ft, freqs, fknee_mhz, fmin_hz, alpha, sigma)¶
- litebird_sim.noise.nearest_pow2(data)¶
returns the next largest power of 2 that will encompass the full data set
data: 1-D numpy array
- litebird_sim.noise.rescale_noise(net_ukrts: float, sampling_rate_hz: float, scale: float)¶