Dipole anisotropy¶
The LiteBIRD Simulation Framework provides tools to simulate the signal associated with the relative velocity between the spacecraft’s rest frame with respect to the CMB. The motion of the spacecraft in the rest frame of the CMB is the composition of several components:
The motion of the spacecraft around L2;
The motion of the L2 point in the Ecliptic plane;
The motion of the Solar System around the Galactic Centre;
The motion of the Milky Way.
Components 1 and 2 are simulated by the LiteBIRD Simulation Framework using appropriate models for the motions, while components 3 and 4 are included using the Sun velocity derived by the solar dipole measured by the Planck satellite.
The motion of the spacecraft around L2 is modelled using a Lissajous
orbit similar to what was used for the WMAP experiment
[1], and it is encoded using the
SpacecraftOrbit
class.
Position and velocity of the spacecraft¶
The class SpacecraftOrbit
describes the orbit of the
LiteBIRD spacecraft with respect to the Barycentric Ecliptic Reference
Frame and the motion of Barycentric Ecliptic Reference Frame with respect
to the CMB; this class is necessary because the class
ScanningStrategy
(see the chapter Scanning strategy)
only models the direction each instrument is looking at but knows
nothing about the velocity of the spacecraft itself.
The class SpacecraftOrbit
is a dataclass that is able to
initialize its members to sensible default values, which are taken
from the literature. As the LiteBIRD orbit around L2 is not fixed yet,
the code assumes a WMAP-like Lissajous orbit, [1].
For the Sun velocity it assumes Planck 2018 solar dipole
[6].
To compute the position/velocity of the spacecraft, you call
spacecraft_pos_and_vel()
; it requires either a time span or an
Observation
object, and it returns an instance of the class
SpacecraftPositionAndVelocity
:
import litebird_sim as lbs
from astropy.time import Time
orbit = lbs.SpacecraftOrbit(start_time=Time("2023-01-01"))
posvel = lbs.spacecraft_pos_and_vel(
orbit,
start_time=orbit.start_time,
time_span_s=86_400.0, # One day
delta_time_s=3600.0 # One hour
)
print(posvel)
SpacecraftPositionAndVelocity(start_time=2023-01-01 00:00:00.000, time_span_s=86400.0, nsamples=25)
The output of the script shows that 25 «samples» have been computed;
this means that the posvel
variable holds information about 25
position/velocity pairs evenly spaced between 2023-01-01 and
2023-01-02: one at midnight, one at 1:00, etc., till midnight
2023-01-02. The SpacecraftPositionAndVelocity
class keeps
the table with the positions and the velocities in the fields
positions_km
and velocities_km_s
, respectively, which are
arrays of shape (nsamples, 3)
.
Here is a slightly more complex example that shows how to plot the distance between the spacecraft and the Sun as a function of time, as well as its speed. The latter quantity is of course most relevant when computing the CMB dipole.
import numpy as np
import litebird_sim as lbs
from astropy.time import Time
import matplotlib.pylab as plt
orbit = lbs.SpacecraftOrbit(start_time=Time("2023-01-01"))
posvel = lbs.spacecraft_pos_and_vel(
orbit,
start_time=orbit.start_time,
time_span_s=3.15e7, # One year
delta_time_s=86_400.0, # One day
)
# posvel.positions_km is a N×3 array containing the XYZ position
# of the spacecraft calculated every day for one year. We compute
# the modulus of the position, which is of course the
# Sun-LiteBIRD distance.
sun_distance_km = np.linalg.norm(posvel.positions_km, axis=1)
# We do the same with the velocities
speed_km_s = np.linalg.norm(posvel.velocities_km_s, axis=1)
# Plot distance and speed as functions of time
_, ax = plt.subplots(nrows=2, ncols=1, figsize=(7, 7))
ax[0].plot(sun_distance_km)
ax[0].set_xlabel("Day")
ax[0].set_ylabel("Distance [km]")
ax[1].plot(speed_km_s)
ax[1].set_xlabel("Day")
ax[1].set_ylabel("Speed [km/s]")
(Source code
, png
, hires.png
, pdf
)
Computing the dipole¶
The CMB dipole is caused by a Doppler shift of the frequencies observed while looking at the CMB blackbody spectrum, according to the formula
where \(T_0\) is the temperature in the rest frame of the CMB, \(\vec \beta = \vec v / c\) is the dimensionless velocity vector, \(\hat n\) is the direction of the line of sight, and \(\gamma = \bigl(1 - \vec\beta \cdot \vec\beta\bigr)^2\).
However, CMB experiments usually employ the linear thermodynamic temperature definition, where temperature differences \(\Delta_1 T\) are related to the actual temperature difference \(\Delta T\) by the relation
where \(x = h \nu / k_B T\),
and \(\mathrm{BB}(\nu, T)\) is the spectral radiance of a black-body according to Planck’s law:
There is no numerical issue in computing the full formula, but often
models use some simplifications, to make the math easier to work on
the blackboard. The LiteBIRD Simulation Framework implements several
simplifications of the formula, which are based on a series expansion
of (1); the caller must pass an object of type
DipoleType
(an enum class), whose value signals
which kind of approximation to use:
The most simple formula uses a series expansion of (1) at the first order:
\[\Delta T(\vec\beta, \hat n) = T_0 \vec\beta\cdot\hat n,\]which is associated to the constant
DipoleType.LINEAR
.The same series expansion for (1), but stopped at the second order (
DipoleType.QUADRATIC_EXACT
):\[\Delta T(\vec\beta, \hat n) = T_0\left(\vec\beta\cdot\hat n + \bigl(\vec\beta\cdot\hat n\bigr)^2\right),\]which discards a \(-T_0\,\beta^2/2\) term (monopole).
The exact formula as in (1) (
DipoleType.TOTAL_EXACT
).Using a series expansion to the second order of (2) instead of (1) and neglecting monopoles (
DipoleTotal.QUADRATIC_FROM_LIN_T
):\[\Delta_2 T(\nu) = T_0 \left(\vec\beta\cdot\hat n + q(x) \bigl(\vec\beta\cdot\hat n\bigr)^2\right),\]where the dependence on the frequency ν is due to the presence of the term \(x = h\nu / k_B T\) in the equation. This is the formula to use if you want the leading frequency-dependent term (second order) without the boosting induced monopoles.
Finally, linearizing (1) through (2) (
DipoleTotal.TOTAL_FROM_LIN_T
):\[\Delta T = \frac{T_0}{f(x)} \left(\frac{\mathrm{BB}\left(T_0 / \gamma\bigl(1 - \vec\beta\cdot\hat n\bigr)\right)}{\mathrm{BB}(T_0)} - 1\right) = \frac{T_0}{f(x)} \left(\frac{\mathrm{BB}\bigl(\nu\gamma(1-\vec\beta\cdot\hat n), T_0\bigr)}{\bigl(\gamma(1-\vec\beta\cdot\hat n)\bigr)^3\mathrm{BB}(T_0)}\right).\]In this case too, the temperature variation depends on the frequency because of (2). This is the formula that is typically used by CMB experiments.
You can add the dipole signal to an existing TOD through the
function add_dipole_to_observations()
, as the following example
shows:
from astropy.time import Time
import numpy as np
import litebird_sim as lbs
import matplotlib.pylab as plt
start_time = Time("2022-01-01")
time_span_s = 120.0 # Two minutes
sampling_hz = 20
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,
)
(obs,) = sim.create_observations(detectors=[det])
sim.compute_pointings()
# Simulate the orbit of the spacecraft and compute positions and
# velocities
orbit = lbs.SpacecraftOrbit(obs.start_time)
pos_vel = lbs.spacecraft_pos_and_vel(orbit, obs, delta_time_s=60.0)
t = obs.get_times()
t -= t[0] # Make `t` start from zero
# Create two plots: the first shows the full two-minute time span, and
# the second one shows a zoom over the very first points of the TOD.
_, axes = plt.subplots(nrows=2, ncols=1, figsize=(10, 12))
# Make a plot of the TOD using all the dipole types
for type_idx, dipole_type in enumerate(lbs.DipoleType):
obs.tod[0][:] = 0 # Reset the TOD
lbs.add_dipole_to_observations(obs, pos_vel, dipole_type=dipole_type)
axes[0].plot(t, obs.tod[0], label=str(dipole_type))
axes[1].plot(t[0:20], obs.tod[0][0:20], label=str(dipole_type))
axes[0].set_xlabel("Time [s]")
axes[0].set_ylabel("Signal [K]")
axes[1].set_xlabel("Time [s]")
axes[1].set_ylabel("Signal [K]")
axes[1].legend()
The example plots two minutes of a simulated timeline for a very simple instrument, and it zooms over the very first points to show that there is indeed some difference in the estimate provided by each method.
Methods of class simulation¶
The class Simulation
provides two simple functions that compute
poisition and velocity of the spacescraft Simulation.compute_pos_and_vel()
,
and add the solar and orbital dipole to all the observations of a given
simulation Simulation.add_dipole()
.
import litebird_sim as lbs
from astropy.time import Time
import numpy as np
start_time = Time("2025-01-01")
time_span_s = 1000.0
sampling_hz = 10.0
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,
)
sim.create_observations(detectors=det)
sim.compute_pointings()
sim.compute_pos_and_vel()
sim.add_dipole()
for i in range(5):
print(f"{sim.observations[0].tod[0][i]:.5e}")
3.44963e-03
3.45207e-03
3.45413e-03
3.45582e-03
3.45712e-03
Note that even if Simulation.compute_pos_and_vel()
is not explicitly
invoked, Simulation.add_dipole()
takes care of that internally initializing
SpacecraftOrbit
and computing positions and velocities.
API reference¶
- class litebird_sim.spacecraft.SpacecraftOrbit(start_time: Time, earth_l2_distance_km: float = 1496509.30522, radius1_km: float = 244450.0, radius2_km: float = 137388.0, ang_speed1_rad_s: float = 4.0266897619299316e-07, ang_speed2_rad_s: float = 3.955023673808657e-07, phase_rad: float = -0.8367806565761614, solar_velocity_km_s: float = 369.816, solar_velocity_gal_lat_rad: float = 0.842173724, solar_velocity_gal_lon_rad: float = 4.6080357444)¶
Bases:
object
A dataclass describing the orbit of the LiteBIRD spacecraft
This structure has the following fields:
start_time: Date and time when the spacecraft starts its nominal orbit
earth_l2_distance_km: distance between the Earth’s barycenter and the L2 point, in km
radius1_km: first radius describing the Lissajous orbit followed by the spacecraft, in km
radius2_km: second radius describing the Lissajous orbit followed by the spacecraft, in km
ang_speed1_rad_s: first angular speed of the Lissajous orbit, in rad/s
ang_speed2_rad_s: second angular speed of the Lissajous orbit, in rad/s
phase_rad: phase difference between the two periodic motions in the Lissajous orbit, in radians
solar_velocity_km_s: velocity of the Sun as estimated from Planck 2018 Solar dipole (see arxiv: 1807.06207)
solar_velocity_gal_lat_rad: galactic latitude direction of the Planck 2018 Solar dipole
solar_velocity_gal_lon_rad: galactic longitude direction of the Planck 2018 Solar dipole
The default values are the nominal numbers of the orbit followed by WMAP, described in Cavaluzzi, Fink & Coyle (2008).
- ang_speed1_rad_s: float = 4.0266897619299316e-07¶
- ang_speed2_rad_s: float = 3.955023673808657e-07¶
- earth_l2_distance_km: float = 1496509.30522¶
- phase_rad: float = -0.8367806565761614¶
- radius1_km: float = 244450.0¶
- radius2_km: float = 137388.0¶
- solar_velocity_ecl_xyz_km_s = array([-359.00346797, 52.57540642, -71.52769031])¶
- solar_velocity_gal_lat_rad: float = 0.842173724¶
- solar_velocity_gal_lon_rad: float = 4.6080357444¶
- solar_velocity_km_s: float = 369.816¶
- start_time: Time¶
- class litebird_sim.spacecraft.SpacecraftPositionAndVelocity(orbit: SpacecraftOrbit, start_time: Time, time_span_s: float, positions_km=None, velocities_km_s=None)¶
Bases:
object
Encode the position/velocity of the spacecraft with respect to the Solar System
This class contains information that characterize the motion of the spacecraft. It is mainly useful to simulate the so-called CMB «orbital dipole» and to properly check the visibility of the Sun, the Moon and inner planets. The coordinate system used by this class is the standard Barycentric Ecliptic reference frame.
The fields of this class are the following:
orbit
: aSpacecraftOrbit
object used to compute the positions andvelocities in this object;
start_time
: the time when the nominal orbit started (aastropy.time.Time
object);
time_span_s
: the time span covered by this object, in seconds;positions_km
: aN×3
matrix, representing a list ofN
XYZ vectorsencoding the position of the spacecraft in the Barycentric Ecliptic reference frame (in kilometers);
velocities_km_s
: aN×3
matrix, representing a list ofN
XYZ vectorsencoding the linear velocity of the spacecraft in the Barycentric Ecliptic reference frame (in km/s).
- compute_velocities(time0: Time, delta_time_s: float, num_of_samples: int)¶
Perform a linear interpolation to sample the satellite velocity in time
Return a N×3 array containing a set of num_of_samples 3D vectors with the velocity of the spacecraft (in km/s) computed every delta_time_s seconds starting from time time0.
- litebird_sim.spacecraft.compute_l2_pos_and_vel(time0: Time, earth_l2_distance_km: float = 1496509.30522)¶
Compute the position and velocity of the L2 Sun-Earth point at a given time.
The L2 point is not calculated using Lagrange’s equations; instead, its distance from the Earth must be provided as the parameter earth_l2_distance_km. The default value is a reasonable estimate. The L2 point is assumed to lie along the line that connects the Solar System Barycenter with the Earth’s gravitational center.
The return value is a 2-tuple containing two NumPy arrays:
A 3D array containing the XYZ components of the vector specifying the position of the L2 point, in km
A 3D array containing the XYZ components of the velocity vector of the L2 point, in km/s
The two vectors are always roughly perpendicular, but they are not exactly due to the irregular motion of the Earth (caused by gravitational interactions with other Solar System bodies, like the Moon and Jupiter).
- litebird_sim.spacecraft.compute_lissajous_pos_and_vel(time0, earth_angle_rad, earth_ang_speed_rad_s, radius1_km, radius2_km, ang_speed1_rad_s, ang_speed2_rad_s, phase_rad)¶
Compute the position and velocity of the spacecraft assuming a Lissajous orbit
The position and velocity are calculated in a reference frame centered on the L2 point, whose axes are aligned with the Solar System Barycenter. This means that the position and velocity of the spacecraft with respect to the Solar System Barycenter itself can be calculated by summing the result of this function with the result of a call to
compute_l2_pos_and_vel()
.
- litebird_sim.spacecraft.compute_start_and_span_for_obs(obs: Observation | List[Observation]) Tuple[Time, float] ¶
Compute the start time and the overall duration in seconds of a set of observations.
The code returns the earliest start time of the observations in obs as well as their overall time span. Gaps between observations are neglected.
- litebird_sim.spacecraft.cycles_per_year_to_rad_per_s(x: float) float ¶
Convert an angular speed from cycles/yr to rad/s
- litebird_sim.spacecraft.get_ecliptic_vec(vec)¶
Convert a coordinate in a XYZ vector expressed in the Ecliptic rest frame
- litebird_sim.spacecraft.spacecraft_pos_and_vel(orbit: SpacecraftOrbit, obs: Observation | List[Observation] | None = None, start_time: Time | None = None, time_span_s: float | None = None, delta_time_s: float = 86400.0) SpacecraftPositionAndVelocity ¶
Compute the position and velocity of the L2 point within some time span
This function computes the XYZ position and velocity of the second Sun-Earth Lagrangean point (L2) over a time span specified either by a
Observation
object/list of objects, or by an explicit pair of values start_time (anastropy.time.Time
object) and time_span_s (length in seconds). The position is specified in the standard Barycentric Ecliptic reference frame.The position of the L2 point is computed starting from the position of the Earth and moving away along the anti-Sun direction by a number of kilometers equal to earth_l2_distance_km.
The result is an object of type
SpacecraftPositionAndVelocity
.If SpacecraftOrbit.solar_velocity_km_s > 0 also the Sun velocity in the rest frame of the CMB is added to the total velocity of the spacecraft.
- litebird_sim.spacecraft.sum_lissajous_pos_and_vel(pos, vel, start_time_s, end_time_s, radius1_km, radius2_km, ang_speed1_rad_s, ang_speed2_rad_s, phase_rad)¶
Add the position and velocity of a Lissajous orbit to some positions/velocities
The pos and vel arrays must have the same shape (N×3, with N being the number of position-velocity pairs); the 3D position and velocity of the Lissajous orbit will be added to these arrays.
The times array must be an array of float values, typically starting from zero, and they must measure the time in seconds. All the other parameters are usually taken from a
SpacecraftOrbit
object.This function has no return value, as the result is stored in pos and vel.
- class litebird_sim.dipole.DipoleType(value)¶
Bases:
IntEnum
Approximation for the Doppler shift caused by the motion of the spacecraft
- LINEAR = 0¶
Linear approximation in β using thermodynamic units:
\[\Delta T(\vec\beta, \hat n) = T_0 \vec\beta\cdot\hat n\]
- QUADRATIC_EXACT = 1¶
Second-order approximation in β using thermodynamic units:
\[\Delta T(\vec\beta, \hat n) = T_0\left(\vec\beta\cdot\hat n + \bigl(\vec\beta\cdot\hat n\bigr)^2\right)\]
- QUADRATIC_FROM_LIN_T = 3¶
Second-order approximation in β using linearized units:
\[\Delta_2 T(\nu) = T_0 \left(\vec\beta\cdot\hat n + q(x) \bigl(\vec\beta\cdot\hat n\bigr)^2\right)\]
- TOTAL_EXACT = 2¶
Exact formula in true thermodynamic units:
\[\frac{T_0}{\gamma \bigl(1 - \vec\beta \cdot \hat n\bigr)}\]
- TOTAL_FROM_LIN_T = 4¶
Full formula in linearized units (the most widely used):
\[\Delta T = \frac{T_0}{f(x)} \left(\frac{\mathrm{BB}\left(T_0 / \gamma\bigl(1 - \vec\beta\cdot\hat n\bigr)\right)}{\mathrm{BB}(T_0)} - 1\right) = \frac{T_0}{f(x)} \left(\frac{\mathrm{BB}\bigl(\nu \gamma(1-\vec\beta\cdot\hat n), T_0\bigr)}{\bigl(\gamma(1- \vec\beta\cdot\hat n)\bigr)^3\mathrm{BB}(t_0)}\right).\]
- litebird_sim.dipole.add_dipole(tod, pointings, velocity, t_cmb_k: float, frequency_ghz: ndarray, dipole_type: DipoleType)¶
Add the CMB dipole to some time-ordered data
This functions modifies the values in tod by adding the contribution of the CMB dipole. Use dipole_type to specify which kind of approximation to use for the dipole component. The pointings argument must be a N×3 matrix containing the pointing information, where N is the size of the tod array. The velocity argument is usually computed through
spacecraft_pos_and_vel()
. Finally, t_cmb_k is the temperature of the monopole and frequency_ghz is an array containing the frequencies of each detector in the TOD.
- litebird_sim.dipole.add_dipole_for_one_detector(tod_det, theta_det, phi_det, velocity, t_cmb_k, nu_hz, f_x, q_x, dipole_type: DipoleType)¶
- litebird_sim.dipole.add_dipole_to_observations(obs: Observation | List[Observation], pos_and_vel: SpacecraftPositionAndVelocity, pointings: ndarray | List[ndarray] | None = None, t_cmb_k: float = 2.72548, dipole_type: DipoleType = DipoleType.TOTAL_FROM_LIN_T, frequency_ghz: None | ndarray = None, component: str = 'tod')¶
Add the CMB dipole to some time-ordered data
This is a wrapper around the
add_dipole()
function that applies to the TOD stored in obs, which can either be oneObservation
instance or a list of observations.By default, the TOD 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 dipole alone cur_obs.dipole_tod = np.zeros_like(cur_obs.tod) # Ask `add_dipole_to_observations` to store the dipole # in `obs.dipole_tod` add_dipole_to_observations(sim.observations, component="dipole_tod")
- litebird_sim.dipole.calculate_beta(theta, phi, v_km_s)¶
Return a 2-tuple containing β·n and β
- litebird_sim.dipole.compute_dipole_for_one_sample_linear(theta, phi, v_km_s, t_cmb_k)¶
- litebird_sim.dipole.compute_dipole_for_one_sample_quadratic_exact(theta, phi, v_km_s, t_cmb_k)¶
- litebird_sim.dipole.compute_dipole_for_one_sample_quadratic_from_lin_t(theta, phi, v_km_s, t_cmb_k, q_x)¶
- litebird_sim.dipole.compute_dipole_for_one_sample_total_exact(theta, phi, v_km_s, t_cmb_k)¶
- litebird_sim.dipole.compute_dipole_for_one_sample_total_from_lin_t(theta, phi, v_km_s, t_cmb_k, nu_hz, f_x, planck_t0)¶
- litebird_sim.dipole.compute_scalar_product(theta, phi, v)¶
Return the scalar (dot) product between a given direction and a velocity
- litebird_sim.dipole.planck(nu_hz, t_k)¶
Return occupation number at frequency nu_hz and temperature t_k