Detectors, channels, and instruments

The core of the LiteBIRD instruments is the set of detectors that perform the measurement of the electromagnetic power entering through the telescope. Several simulation modules revolve around the concept of «detector», «frequency channel», and «instrument», and the LiteBIRD Simulation Framework implements a few classes and functions to handle the information associated with them.

Consider this example:

import litebird_sim as lbs

detectors = [
    lbs.DetectorInfo(
        name="Dummy detector #1",
        net_ukrts=100.0,
        bandcenter_ghz=123.4,
        bandwidth_ghz=5.6,
    ),
    lbs.DetectorInfo(
        name="Dummy detector #2",
        net_ukrts=110.0,
        fwhm_arcmin=65.8,
    ),
]

# Now simulate the behavior of the two detectors
# …

Here you see that the DetectorInfo class can be used to store information related to single detectors, and that you can choose which information provide for each detector: the example specifies bandshape information only for the first detector (Dummy detector #1), but not for the other. Missing information is usually initialized with zero or a sensible default. The simulation modules provided by the framework typically expect one or more DetectorInfo objects to be passed as input.

Detectors are grouped according to their nominal central frequency in frequency channels; there are several frequency channels in the three LiteBIRD instruments (LFT, MFT, HFT), and sometimes one does not need to simulate each detector within a frequency channel, but only the channel as a whole. In this case there is the FreqChannelInfo class, which can produce a mock DetectorInfo object by taking out the «average» information of the frequency channel:

import litebird_sim as lbs

chinfo = lbs.FreqChannelInfo(
    bandcenter_ghz=40.0,
    net_channel_ukrts=40.0, # Taken from Sugai et al. 2020 (JLTP)
    number_of_detectors=64, # 32 pairs
)

# Return a "mock" detector that is representative of the
# frequency channel
mock_det = chinfo.get_boresight_detector(name="mydet")

# Look, ma, a detector!
assert isinstance(mock_det, lbs.DetectorInfo)
assert mock_det.name == "mydet"

print("The NET of one detector at 40 GHz is {0:.1f} µK·√s"
      .format(mock_det.net_ukrts))
The NET of one detector at 40 GHz is 320.0 µK·√s

Finally, the Instrument class holds information about one of the three instruments onboard the LiteBIRD spacecraft (LFT, MFT, and HFT).

Reading from the IMO

The way information about detectors and frequency channels is stored in the IMO (see The Instrument Model Database (IMO)) closely match the DetectorInfo and FreqChannelInfo classes. In fact, they can be retrieved easily from the IMO using the static methods DetectorInfo.from_imo() and FreqChannelInfo.from_imo().

The following example uses the PTEP IMO to show how to use the API:

import litebird_sim as lbs

# The location of the PTEP IMO is stored in the constant
# PTEP_IMO_LOCATION
imo = lbs.Imo(flatfile_location=lbs.PTEP_IMO_LOCATION)

det = lbs.DetectorInfo.from_imo(
    imo=imo,
    url="/releases/vPTEP/satellite/LFT/L1-040/"
        "000_000_003_QA_040_T/detector_info",
)

print(f"The bandcenter for {det.name} is {det.bandcenter_ghz} GHz")

freqch = lbs.FreqChannelInfo.from_imo(
    imo=imo,
    url="/releases/vPTEP/satellite/LFT/L1-040/channel_info",
)

print(
    f"The average bandcenter for {freqch.channel} "
    f"is {freqch.bandcenter_ghz} GHz"
)
The bandcenter for 000_000_003_QA_040_T is 40.0 GHz
The average bandcenter for L1-040 is 40.0 GHz

Detectors in parameter files

It is often the case that the list of detectors to simulate must be read from a parameter file. There are several situations that typically need to be accomodated:

  1. In some simulations, you just need to simulate one detector (whose parameters can be taken either from the IMO or from the parameter file itself);

  2. In other cases, you want to simulate all the detectors in one or more frequency channels: in this case, you would like to avoid specifying them one by one!

  3. In other cases, you might want to specify just a subset

  4. Finally, you might base your simulation on the IMO definition of a detector/channel but twiddle a bit with some parameters.

The LiteBIRD simulation framework provides a way to read a set of detectors/channels from a dictionary, which can be used to build a list of DetectorInfo/FreqChannelInfo objects.

In the following examples, we will refer to a «mock» IMO, which contains information about a few fake detectors. It’s included in the source distribution of the framework, under the directory litebird_sim/test/mock_imo, and it defines one frequency channel with four detectors. Here is a summary of its contents:

from pathlib import Path
import litebird_sim as lbs

# This ensures that the mock IMO can be found when generating this
# HTML page
imo = lbs.Imo(flatfile_location=lbs.PTEP_IMO_LOCATION)

# This UUID refers to a 140 GHz "channel_info" object.
ch = imo.query("/data_files/463e9ea9-c1f0-484d-9bfd-05092851d8f4")
metadata = ch.metadata

print("Here are the contents of the mock IMO:")
print(f'Channel: {metadata["channel"]}')

detector_names = metadata["detector_names"]
print(f'There are {len(detector_names)} detectors')
print("Here are the first 5 of them:")
for name, obj in list(zip(metadata["detector_names"],
                          metadata["detector_objs"]))[0:5]:
    det_obj = imo.query(obj)
    det_metadata = det_obj.metadata
    bandcenter_ghz = det_metadata["bandcenter_ghz"]
    print(f'  {name}: band center at {bandcenter_ghz:.1f} GHz')
Here are the contents of the mock IMO:
Channel: M1-140
There are 366 detectors
Here are the first 5 of them:
  001_002_030_00A_140_T: band center at 140.0 GHz
  001_002_030_00A_140_B: band center at 140.0 GHz
  001_002_031_15B_140_T: band center at 140.0 GHz
  001_002_031_00B_140_B: band center at 140.0 GHz
  001_002_022_15A_140_T: band center at 140.0 GHz

Now, let’s turn back to the problem of specifying a set of detectors in a parameter file. The following TOML file shows some of the possibilities granted by the framework. The parameter random_seed is mandatory for the Simulation constructor.

# This is file "det_list1.toml"

# Mandatory parameter
[simulation]
random_seed = 12345

# In this example we list the detectors to use in the simulation
# using one of TOML's features: lists of elements. They are
# indicated using double square brackets, and each occurrence
# appends a new item to the end of the list.

# First element
[[detectors]]
# Take the parameters for the detector from the IMO;
# here we specify the UUID of the object
detector_info_obj = "/data_files/34b46c91-e196-49b9-9d44-d69b4827f751"

# Append two more elements
[[detectors]]
# Take the parameters for the set of detectors from the channel
# information in the IMO. As above, we use an UUID
channel_info_obj = "/data_files/463e9ea9-c1f0-484d-9bfd-05092851d8f4"
# Just generate two detectors (default is to generate all the
# detectors in this frequency channel, which contains *four*
# detectors). This is useful when debugging, because it can
# drastically reduce the amount of data to simulate
num_of_detectors_from_channel = 2

# Append one more element
[[detectors]]
# Unlike in the previous example, here we just want to generate *one*
# mock detector associated with a specified frequency channel. This
# detector will be aligned with the boresight of the focal plane, and
# it will use the «average» detector parameters for this channel
channel_info_obj = "/data_files/463e9ea9-c1f0-484d-9bfd-05092851d8f4"
# This implies num_of_detectors_from_channel == 1
use_only_one_boresight_detector = true
detector_name = "foo_boresight"

# Append one more element
[[detectors]]
# Set up every parameter manually
name = "planck30GHz"
channel = "30 GHz"
fwhm_arcmin = 33.10
fknee_mhz = 113.9
bandwidth_ghz = 9.89
bandcenter_ghz = 28.4
sampling_rate_hz = 32.5

# Add one last element
[[detectors]]
# Take the parameters from the IMO but fix one parameter by hand
detector_info_obj = "/data_files/34b46c91-e196-49b9-9d44-d69b4827f751"
sampling_rate_hz = 1.0   # Fix this parameter

If the TOML file you are using in your simulation follows this structure, you can use the function detector_list_from_parameters(), which parses the parameters and uses an Imo object to build a list of DetectorInfo objects.

The following code will read the TOML file above and produce a list of 6 detectors:

from pathlib import Path
import litebird_sim as lbs

imo = lbs.Imo(flatfile_location=lbs.PTEP_IMO_LOCATION)

# Tell Simulation to use the PTEP IMO and to
# load the TOML file shown above
sim = lbs.Simulation(imo=imo, parameter_file="det_list1.toml")

det_list = lbs.detector_list_from_parameters(
    imo=sim.imo,
    definition_list=sim.parameters["detectors"],
)

for idx, det in enumerate(det_list):
    print(f"{idx + 1}. {det.name}: band center at {det.bandcenter_ghz} GHz")
1. 000_000_003_QA_040_T: band center at 40.0 GHz
2. 001_002_030_00A_140_T: band center at 140.0 GHz
3. 001_002_030_00A_140_B: band center at 140.0 GHz
4. foo_boresight: band center at 140.0 GHz
5. planck30GHz: band center at 28.4 GHz
6. 000_000_003_QA_040_T: band center at 40.0 GHz

You are not forced to use detectors as the name of the parameter in the TOML file, as detector_list_from_parameters() accepts a generic list. As an example, consider the following TOML file. Note again the mandatory parameter random_seed.

# This is file "det_list2.toml"

# Mandatory parameter
[simulation]
random_seed = 12345

[simulation1]

# Detectors to be used for the first simulation
[[simulation1.detectors]]
detector_info_obj = "/data_files/899ba8cc-789c-47bf-9bb1-40f61b25b3a9"

[simulation2]

# Detectors to be used for the second simulation
[[simulation2.detectors]]
detector_info_obj = "/data_files/e4401060-9233-4b14-a642-c00293c99c70"

Its purpose is to provide the parameters for a two-staged simulation, and each of them requires its own list of detectors. For this purpose, it uses the TOML syntax [[simulation1.detectors]] to specify that the detectors list belongs to the section simulation1, and similarly for [[simulation2.detectors]]. Here is a Python script that interprets the TOML file and prints the detectors to be used at each stage of the simulation:

from pathlib import Path
import litebird_sim as lbs

imo = lbs.Imo(flatfile_location=lbs.PTEP_IMO_LOCATION)

# Tell the Simulation object that we want to use the PTEP IMO
sim = lbs.Simulation(imo=imo, parameter_file="det_list2.toml")

det_list1 = lbs.detector_list_from_parameters(
    imo=sim.imo,
    definition_list=sim.parameters["simulation1"]["detectors"],
)

det_list2 = lbs.detector_list_from_parameters(
    imo=sim.imo,
    definition_list=sim.parameters["simulation2"]["detectors"],
)

print("Detectors to be used in the first simulation:")
for det in det_list1:
    print(f"- {det.name}")

print("Detectors to be used in the second simulation:")
for det in det_list2:
    print(f"- {det.name}")
Detectors to be used in the first simulation:
- 000_000_004_QB_040_B
Detectors to be used in the second simulation:
- 000_000_004_QB_040_T

API reference

class litebird_sim.detectors.DetectorInfo(name: str = '', wafer: str | None = None, pixel: int | None = None, pixtype: str | None = None, channel: str | None = None, sampling_rate_hz: float = 0.0, fwhm_arcmin: float = 0.0, ellipticity: float = 0.0, bandcenter_ghz: float = 0.0, bandwidth_ghz: float = 0.0, band_freqs_ghz: None | ndarray = None, band_weights: None | ndarray = None, net_ukrts: float = 0.0, pol_sensitivity_ukarcmin: float = 0.0, fknee_mhz: float = 0.0, fmin_hz: float = 0.0, alpha: float = 0.0, pol: str | None = None, orient: str | None = None, quat: Any = (0.0, 0.0, 0.0, 1.0))

Bases: object

A class wrapping the basic information about a detector.

This is a data class that encodes the basic properties of a LiteBIRD detector. It can be initialized in three ways:

  • Through the default constructor; all its parameters are optional, but you probably want to specify at least name and sampling_rate_hz:

    det = DetectorInfo(name="dummy", sampling_rate_hz=10.0)
    
  • Through the class method from_dict(), which takes a dictionary as input.

  • Through the class method from_imo(), which reads the definition of a detector from the LiteBIRD Instrument Model (see the Imo class).

Parameters:
  • name (-) – the name of the detector; the default is the empty string

  • wafer (-) – The name of the wafer hosting the detector, e.g. H00, L07, etc. The default is None

  • pixel (-) – The number of the pixel within the wafer. The default is None

  • pixtype (-) – The type of the pixel, e.g., HP1, LP3, etc. The default is None

  • channel (-) – The channel. The default is None

  • sampling_rate_hz (-) – The sampling rate of the ADC associated with this detector. The default is 0.0

  • fwhm_arcmin (-) – (float): The Full Width Half Maximum of the radiation pattern associated with the detector, in arcminutes. The default is 0.0

  • ellipticity (-) – The ellipticity of the radiation pattern associated with the detector. The default is 0.0

  • net_ukrts (-) – The noise equivalent temperature of the signal produced by the detector in nominal conditions, expressed in μK/√s. This is the noise per sample to be used when adding noise to the timelines. The default is 0.0

  • pol_sensitivity_ukarcmin (-) – The detector sensitivity in microK_arcmin. This value considers the effect of cosmic ray loss, repointing maneuvers, etc., and other issues that cause loss of integration time. Therefore, it should not be used with the functions that add noise to the timelines. The default is 0.0

  • bandcenter_ghz (-) – The center frequency of the detector, in GHz. The default is 0.0

  • bandwidth_ghz (-) – The bandwidth of the detector, in GHz. The default is 0.0

  • band_freqs_ghz (-) – band sampled frequencies, in GHz. The default is None

  • band_weights (-) – band profile. The default is None

  • fknee_mhz (-) – The knee frequency between the 1/f and the white noise components in nominal conditions, in mHz. The default is 0.0

  • fmin_hz (-) – The minimum frequency of the noise when producing synthetic noise, in Hz. The default is 0.0

  • alpha (-) – The slope of the 1/f component of the noise in nominal conditions. The default is 0.0

  • pol (-) – The polarization of the detector (T/B). The default is None

  • orient (-) – The orientation of the detector (Q/U). The default is None

  • quat (-) – The quaternion expressing the rotation from the detector reference frame to the boresight reference frame. The default is no rotation at all, i.e., the detector is aligned with the boresight direction.

alpha: float = 0.0
band_freqs_ghz: None | ndarray = None
band_weights: None | ndarray = None
bandcenter_ghz: float = 0.0
bandwidth_ghz: float = 0.0
channel: str | None = None
ellipticity: float = 0.0
fknee_mhz: float = 0.0
fmin_hz: float = 0.0
static from_dict(dictionary: Dict[str, Any])

Create a detector from the contents of a dictionary

The parameter dictionary must contain one key for each of the fields in this dataclass:

  • name

  • wafer

  • pixel

  • pixtype

  • channel

  • bandcenter_ghz

  • bandwidth_ghz

  • band_freqs_ghz

  • band_weights

  • sampling_rate_hz

  • fwhm_arcmin

  • ellipticity

  • net_ukrts

  • pol_sensitivity_ukarcmin

  • fknee_mhz

  • fmin_hz

  • alpha

  • pol

  • orient

  • quat

static from_imo(imo: Imo, url: UUID | str)

Create a DetectorInfo object from a definition in the IMO

The url must either specify a UUID or a full URL to the object.

Example:

import litebird_sim as lbs
imo = Imo()
det = DetectorInfo.from_imo(
    imo=imo,
    url="/releases/v1.0/satellite/LFT/L1-040/L00_008_QA_040T/detector_info",
)
fwhm_arcmin: float = 0.0
name: str = ''
net_ukrts: float = 0.0
orient: str | None = None
pixel: int | None = None
pixtype: str | None = None
pol: str | None = None
pol_sensitivity_ukarcmin: float = 0.0
quat: Any = (0.0, 0.0, 0.0, 1.0)
sampling_rate_hz: float = 0.0
wafer: str | None = None
class litebird_sim.detectors.FreqChannelInfo(bandcenter_ghz: float, channel: Optional[str] = None, bandwidth_ghz: float = 0.0, band_freqs_ghz: Optional[numpy.ndarray] = None, band_weights: Optional[numpy.ndarray] = None, net_detector_ukrts: float = 0.0, net_channel_ukrts: float = 0.0, pol_sensitivity_channel_ukarcmin: float = 0.0, sampling_rate_hz: float = 0.0, fwhm_arcmin: float = 0.0, fknee_mhz: float = 0.0, fmin_hz: float = 1e-05, alpha: float = 1.0, number_of_detectors: int = 0, detector_names: List[str] = <factory>, detector_objs: List[uuid.UUID] = <factory>)

Bases: object

alpha: float = 1.0
band_freqs_ghz: None | ndarray = None
band_weights: None | ndarray = None
bandcenter_ghz: float
bandwidth_ghz: float = 0.0
channel: str | None = None
detector_names: List[str]
detector_objs: List[UUID]
fknee_mhz: float = 0.0
fmin_hz: float = 1e-05
static from_dict(dictionary: Dict[str, Any])
static from_imo(imo: Imo, url: UUID | str)
fwhm_arcmin: float = 0.0
get_boresight_detector(name='mock') DetectorInfo
net_channel_ukrts: float = 0.0
net_detector_ukrts: float = 0.0
number_of_detectors: int = 0
pol_sensitivity_channel_ukarcmin: float = 0.0
sampling_rate_hz: float = 0.0
class litebird_sim.detectors.InstrumentInfo(name: str = '', boresight_rotangle_rad: float = 0.0, spin_boresight_angle_rad: float = 0.0, spin_rotangle_rad: float = 0.0, hwp_rpm: float = 0.0, number_of_channels: int = 0, channel_names: List[str] = <factory>, channel_objs: List[uuid.UUID] = <factory>, wafer_names: List[str] = <factory>, wafer_space_cm: float = 0.0)

Bases: object

bore2spin_quat = array([0., 0., 0., 1.])
boresight_rotangle_rad: float = 0.0
channel_names: List[str]
channel_objs: List[UUID]
static from_dict(dictionary: Dict[str, Any])
static from_imo(imo: Imo, url: UUID | str)
hwp_rpm: float = 0.0
name: str = ''
number_of_channels: int = 0
spin_boresight_angle_rad: float = 0.0
spin_rotangle_rad: float = 0.0
wafer_names: List[str]
wafer_space_cm: float = 0.0
litebird_sim.detectors.detector_list_from_parameters(imo: Imo, definition_list: List[Any]) List[DetectorInfo]
litebird_sim.detectors.url_to_uuid(url: str) UUID