Source code for pysm3.models.dipole
import numpy as np
import healpy as hp
from astropy import constants as const
try:
from numpy import trapezoid
except ImportError:
from numpy import trapz as trapezoid
from .. import units as u
from .. import utils
[docs]
class CMBDipole:
"""
Simulate the CMB dipole anisotropy as a full-sky HEALPix map.
The dipole is modeled as a temperature fluctuation due to the observer's motion
with respect to the CMB rest frame, following the relativistic Doppler effect.
Parameters
----------
nside : int
HEALPix NSIDE parameter for the output map (dimensionless).
amp : float
Amplitude of the dipole in micro-Kelvin (uK_CMB).
T_cmb : float
CMB monopole temperature in Kelvin (K_CMB).
dip_lon : float
Galactic longitude of the dipole direction, in degrees (deg).
dip_lat : float
Galactic latitude of the dipole direction, in degrees (deg).
Returns
-------
dipole_map : astropy.units.Quantity
Full-sky HEALPix map (array) of the dipole temperature anisotropy in Kelvin.
Notes
-----
The dipole amplitude is calculated as:
ΔT/T = (v/c) * cos(θ)
where θ is the angle between the dipole direction and the line of sight.
References
----------
For the best-fit parameters, see Table 1 of
"Planck intermediate results. LVII. Joint Planck LFI and HFI data processing"
https://arxiv.org/pdf/2007.04997.pdf
"""
@u.quantity_input
def __init__(
self,
nside: int,
amp,
T_cmb,
dip_lon,
dip_lat,
map_dist=None,
quadrupole_correction: bool = False,
):
self.nside = nside
self.amp = u.Quantity(amp) if not isinstance(amp, u.Quantity) else amp
self.T_cmb = u.Quantity(T_cmb) if not isinstance(T_cmb, u.Quantity) else T_cmb
self.dip_lat = (
u.Quantity(dip_lat) if not isinstance(dip_lat, u.Quantity) else dip_lat
).to_value(u.deg)
self.dip_lon = (
u.Quantity(dip_lon) if not isinstance(dip_lon, u.Quantity) else dip_lon
).to_value(u.deg)
self.map_dist = map_dist
self.quadrupole_correction = quadrupole_correction
[docs]
@u.quantity_input
def get_emission(
self, freqs: u.Quantity[u.GHz], weights=None
) -> u.Quantity[u.uK_RJ]:
"""
Return the dipole emission map, integrating over the bandpass if needed.
Parameters
----------
freqs : Quantity
Frequency or array of frequencies (for bandpass integration).
weights : array-like, optional
Integration weights for the bandpass.
Returns
-------
dipole_map : astropy.units.Quantity
Full-sky HEALPix map (array) of the dipole temperature anisotropy in uK_RJ.
"""
npix = hp.nside2npix(self.nside)
vec = hp.ang2vec(self.dip_lon, self.dip_lat, lonlat=True)
pix_dirs = hp.pix2vec(self.nside, np.arange(npix))
cosθ = np.dot(vec, pix_dirs) # cos(theta)
δ = (self.amp / self.T_cmb).decompose()
β = δ * (δ + 2) / (δ ** 2 + 2 * δ + 2)
γ = 1 / np.sqrt(1 - β ** 2) # Lorentz factor gamma
# this is the temperature fluctuation with no quadrupole correction
# it does not depend on the frequency
ΔT = self.T_cmb / (γ * (1 - β * cosθ)) - self.T_cmb
freqs = utils.check_freq_input(freqs)
weights = utils.normalize_weights(freqs, weights)
emission = []
if self.quadrupole_correction:
# Vectorized calculation for all frequencies
fx = (
const.h
* (freqs * u.GHz).to(u.Hz)
/ (const.k_B * (self.T_cmb.to_value(u.K_CMB) * u.K))
).decompose() # shape: (nfreq,)
fcor = (fx / 2) * (np.exp(fx) + 1) / (np.exp(fx) - 1) # shape: (nfreq,)
bt = β * cosθ # shape: (npix,)
# Broadcast fcor and bt to shape (nfreq, npix)
ΔT_current_freq = self.T_cmb * (
bt[None, :] + fcor[:, None] * bt[None, :] ** 2
) # shape: (nfreq, npix)
# Convert to uK_RJ for each frequency
emission = [
ΔT_current_freq[i].to(
u.uK_RJ, equivalencies=u.cmb_equivalencies(freqs[i] * u.GHz)
)
for i in range(len(freqs))
]
else: # No quadrupole correction
emission = [
ΔT.to(u.uK_RJ, equivalencies=u.cmb_equivalencies(freq * u.GHz))
for freq in freqs
]
if len(freqs) == 1:
result = emission[0]
else:
result = (
trapezoid(
np.stack([e.value for e in emission]) * weights[:, None],
x=freqs,
axis=0,
)
* u.uK_RJ
)
assert not np.isnan(result).any(), "Result contains NaN values"
return result