Non-linearity injection#
Non-linearity is the effect of a non-ideal TES detector response, where the responsivity \(S\) is not constant as the optical power varies. The LiteBIRD Simulation Framework provides a non-linearity simulation module to simulate the effect of non-linearity on TODs.
The framework provides the simplest case, which is quadratic non-linearity. This case is described in Micheli+2024, where the effect of non-linearity is propagated to the estimation of the tensor-to-scalar ratio.
Considering a first order correction of the usual linear gain, a TOD \(d(t)\) is modified according to:
where \(g_1\) is the detector non-linearity factor in units of \(K^{-1}\).
To simulate a quadratic non-linearity, one can use the method of
Simulation class
Simulation.apply_quadratic_nonlin(), or any of the low-level
functions: apply_quadratic_nonlin_to_observations(),
apply_quadratic_nonlin_for_one_detector(). The following
example shows the typical usage of the method and low-level functions:
import numpy as np
import litebird_sim as lbs
from astropy.time import Time
start_time = Time("2025-02-02T00:00:00")
mission_time_days = 1
sampling_hz = 19
dets = [
lbs.DetectorInfo(
name="det_A", sampling_rate_hz=sampling_hz
),
lbs.DetectorInfo(
name="det_B", sampling_rate_hz=sampling_hz
),
]
sim = lbs.Simulation(
base_path="nonlin_example",
start_time=start_time,
duration_s=mission_time_days * 24 * 3600.0,
random_seed=12345,
)
sim.create_observations(
detectors=dets,
split_list_over_processes=False,
)
# Creating fiducial TODs
sim.observations[0].nl_2_self = np.ones_like(sim.observations[0].tod)
sim.observations[0].nl_2_obs = np.ones_like(sim.observations[0].tod)
sim.observations[0].nl_2_det = np.ones_like(sim.observations[0].tod)
If the non-linearity parameter is not read from the IMo, one has to
specify \(g_1\) using the g_one_over_k argument as in the
following example:
# Define non-linear parameters for the detectors. We choose the same
# value for both detectors in this example, but it is not necessary.
sim.observations[0].g_one_over_k = np.ones(len(dets)) * 1e-3
# Applying non-linearity using the `Simulation` class method
sim.apply_quadratic_nonlin(component = "nl_2_self",)
# Applying non-linearity on the given TOD component of an `Observation`
# object
lbs.non_linearity.apply_quadratic_nonlin_to_observations(
observations=sim.observations,
component="nl_2_obs",
)
# Applying non-linearity on the TOD arrays of the individual detectors.
for idx, tod in enumerate(sim.observations[0].nl_2_det):
lbs.non_linearity.apply_quadratic_nonlin_for_one_detector(
tod_det=tod,
g_one_over_k=sim.observations[0].g_one_over_k[idx],
)
In particular, the effect of detector non-linearity must be included to assess its impact when coupled with other systematic effects. As described in Micheli+2024, a typical case is the coupling with HWP synchronous signal (HWPSS) appearing at twice the rotation frequency of the HWP. This kind of signal can be produced by non-idealities of the HWP, such as its differential transmission and emission.
In that case, the usual TOD \(d(t)\) will contain an additional term, and can be written as:
where \(A_2\) is the amplitude of the HWPSS and \(\omega_{HWP}\) is the rotation speed of the HWP. In presence of detector non-linearity, the 2f signal is up-modulated to 4f, affecting the science band.
The framework provides an independent module to introduce this signal
in the simulation, adding it to the TODs. To simulate the 2f signal
from a rotating, non-ideal HWP, one can use the method of
Simulation class Simulation.add_2f(), or any of the
low-level functions: add_2f_to_observations(),
add_2f_for_one_detector().
If the 2f amplitude is not read from the IMo, one has to specify
\(A_2\) using the amplitude_2f_k argument. The argument
optical_power_k enables the inclusion of the integrated nominal
optical power expected for each channel. See the following example:
import numpy as np
import litebird_sim as lbs
from astropy.time import Time
from litebird_sim.pointings import get_hwp_angle
telescope = "LFT"
channel = "L4-140"
detlist = ["000_001_017_QB_140_T", "000_001_017_QB_140_B"]
imo_version = "vPTEP"
start_time = Time("2025-02-02T00:00:00")
mission_time_days = 1
imo = lbs.Imo(flatfile_location=lbs.PTEP_IMO_LOCATION)
sim = lbs.Simulation(
base_path="nonlin_example",
start_time=start_time,
imo=imo,
duration_s=mission_time_days * 24 * 3600.0,
random_seed=12345,
)
# Load the definition of the instrument
sim.set_instrument(
lbs.InstrumentInfo.from_imo(
imo,
f"/releases/{imo_version}/satellite/{telescope}/instrument_info",
)
)
dets = []
for n_det in detlist:
det = lbs.DetectorInfo.from_imo(
url=f"/releases/{imo_version}/satellite/{telescope}/{channel}/{n_det}/detector_info",
imo=imo,)
det.sampling_rate_hz = 1
dets.append(det)
sim.create_observations(
detectors=dets,
split_list_over_processes=False,
)
sim.set_scanning_strategy(imo_url=f"/releases/{imo_version}/satellite/scanning_parameters/")
sim.set_hwp(
lbs.IdealHWP(sim.instrument.hwp_rpm * 2 * np.pi / 60,),
)
sim.prepare_pointings()
sim.precompute_pointings()
# Creating fiducial TODs
sim.observations[0].tod_2f = np.zeros_like(sim.observations[0].tod)
# Define differential emission parameters for the detectors.
sim.observations[0].amplitude_2f_k = np.array([0.1, 0.1])
sim.observations[0].optical_power_k = np.array([1.0, 1.0])
# Adding 2f signal from HWP differential emission using the `Simulation` class method
sim.add_2f(component="tod_2f")
Refer to the API reference for the full list of non-linearity simulation parameters.
API reference#
- litebird_sim.non_linearity.apply_quadratic_nonlin(tod, g_one_over_k: float)#
Apply a quadratic nonlinearity to some time-ordered data
This functions modifies the values in tod by adding a parabolic function of the TOD itself. The g_one_over_k argument must be a N_dets array containing the amplitude of the nonlinear gain to be applied.
- litebird_sim.non_linearity.apply_quadratic_nonlin_for_one_detector(tod_det, g_one_over_k)#
- litebird_sim.non_linearity.apply_quadratic_nonlin_for_one_sample(data, g_one_over_k)#
- litebird_sim.non_linearity.apply_quadratic_nonlin_to_observations(observations: Observation | List[Observation], component: str = 'tod', g_one_over_k: float | None = None)#
Apply a quadratic nonlinearity to some time-ordered data
This is a wrapper around the
apply_quadratic_nonlin()function that applies to the TOD stored in observations, which can either be oneObservationinstance or a list of observations.By default, the modified TOD is
Observation.tod. If you want to modify some other field of theObservationclass, use component:for cur_obs in sim.observations: # Allocate a new TOD for the nonlinear TOD alone cur_obs.nl_tod = np.zeros_like(cur_obs.tod) # Ask `apply_quadratic_nonlin_to_observations` to store the nonlinear TOD # in `observations.nl_tod` apply_quadratic_nonlin_to_observations(sim.observations, component="nl_tod")