lenspyx.lensing

CMB lensing operations: lensed map synthesis and related utilities.

This module provides functions for computing gravitationally lensed CMB maps from spherical harmonic coefficients. Gravitational lensing remaps the CMB temperature and polarization by deflecting photon paths according to the intervening mass distribution.

Overview

Gravitational lensing modifies the observed CMB by deflecting photon paths. The lensing operation remaps the CMB according to:

\[X^{\text{lensed}}(\hat{n}) = X^{\text{unlensed}}(\hat{n} + \nabla\phi(\hat{n}))\]

where \(\phi\) is the lensing potential and \(X\) represents T, Q, or U Stokes polarization modes.

For polarization, the Stokes parameters Q and U are additionally rotated by the lensing-induced angle \(\gamma\).

The implementation uses exact (non-perturbative) lensing via interpolation on the sphere, with configurable accuracy through the epsilon parameter.

API Reference

Main Functions

lenspyx.lensing.synfast(cls: dict, lmax=None, mmax=None, geometry=('healpix', {'nside': 2048}), epsilon=1e-07, nthreads=0, alm=False, seed=None, verbose=0)[source]

Generate lensed CMB realizations from power spectra.

Creates correlated Gaussian random fields on the sphere and applies gravitational lensing according to the input power spectra. This is the standard way to generate lensed CMB simulations.

Parameters:
  • cls (dict) –

    Dictionary of auto- and cross-power spectra with string keys. Recognized field labels (case-insensitive):

    • ’T’ or ‘t’ : CMB temperature (spin-0 intensity)

    • ’E’ or ‘e’ : E-mode polarization

    • ’B’ or ‘b’ : B-mode polarization

    • ’P’ or ‘p’ : Lensing potential \(\phi\)

    • ’O’ or ‘o’ : Lensing curl potential \(\Omega\)

    Keys are two-character strings like ‘TT’, ‘TE’, ‘EE’, ‘PP’, etc. Arrays must be \(C_\ell\) (not \(D_\ell = \ell(\ell+1)C_\ell/(2\pi)\)).

    Important:

    • If auto-spectrum ‘AA’ is absent, field ‘A’ is assumed zero

    • If neither ‘PP’ nor ‘OO’ are present, output maps are unlensed

    • All relevant cross-spectra must be provided for correlated fields

  • lmax (int, optional) – Band-limit of unlensed alms. If None, inferred from length of input spectra.

  • mmax (int, optional) – Maximum azimuthal mode number. If None, defaults to lmax.

  • geometry (tuple of (str, dict), optional) – Pixelization: (geometry_name, parameters). Default: (‘healpix’, {‘nside’: 2048})

  • epsilon (float, optional) – Target numerical accuracy for lensing. Default: 1e-7. Execution time has weak dependence on this.

  • nthreads (int, optional) – Number of threads for SHTs. If 0, uses os.cpu_count(). Default: 0.

  • alm (bool, optional) – If True, also returns unlensed alms. Default: False.

  • seed (int, optional) – Random number generator seed for reproducible results. Default: None (random).

  • verbose (int, optional) – Print timing information if non-zero. Default: 0.

Returns:

  • maps (dict) – Dictionary of lensed maps:

    • ’T’ : Lensed temperature (if ‘TT’ was in cls and non-zero)

    • ’QU’ : Lensed Q and U Stokes parameters, shape (2, npix) (if ‘EE’ or ‘BB’ were in cls and non-zero)

  • alms (tuple, optional (if alm=True)) – Tuple of (alm_arrays, field_labels) where:

    • alm_arrays : shape (nfields, nalm) with unlensed alms

    • field_labels : string indicating field ordering (e.g., ‘tebp’)

Examples

Generate lensed CMB with temperature and polarization:

>>> from lenspyx.lensing import synfast
>>> from lenspyx.utils import camb_clfile
>>> # Load power spectra
>>> cls = camb_clfile('cosmo_params.ini')
>>> # Generate lensed realization
>>> maps = synfast(cls, lmax=3000, geometry=('healpix', {'nside': 2048}),
...                nthreads=8, seed=42)
>>> t_lensed = maps['T']
>>> q_lensed, u_lensed = maps['QU']

Generate and return unlensed alms:

>>> maps, (alms, labels) = synfast(cls, lmax=3000, alm=True, seed=42)
>>> # labels might be 'tebp' for T, E, B, phi
>>> if 't' in labels:
...     alm_t = alms[labels.index('t')]

Generate unlensed maps (no lensing potential in cls):

>>> cls_unlensed = {'tt': cl_tt, 'ee': cl_ee, 'te': cl_te}
>>> maps_unlensed = synfast(cls_unlensed, lmax=2000)

Notes

The function:

  1. Generates correlated Gaussian random alms from the input \(C_\ell\)

  2. If lensing potentials (P or O) are present, computes deflection field

  3. Applies exact (non-perturbative) lensing via interpolation

  4. Returns lensed maps on the specified geometry

The lensing deflection is computed as:

\[d_{\ell m} = \sqrt{\ell(\ell+1)} \left(\phi_{\ell m} + i \Omega_{\ell m}\right)\]

See also

alm2lenmap

Lens pre-existing alms

lenspyx.utils.camb_clfile

Read CAMB power spectrum files

lenspyx.lensing.alm2lenmap(alm, dlms, geometry: tuple[str, dict] = ('healpix', {'nside': 2048}), epsilon=1e-07, verbose=0, nthreads: int = 0, pol=True)[source]

Compute lensed CMB maps from unlensed alms and deflection field.

This function performs exact (non-perturbative) gravitational lensing of CMB temperature and polarization maps using interpolation on the deflected sphere.

Parameters:
  • alm (array_like or list of array_like) –

    Unlensed CMB spherical harmonic coefficients. Can be:

    • Single array: Temperature only (spin-0)

    • List of 2 arrays: [T, E] if pol=True, otherwise two spin-0 fields

    • List of 3 arrays: [T, E, B] if pol=True, otherwise three spin-0 fields

  • dlms (array_like or list of array_like) –

    Spin-1 deflection field in harmonic space:

    • Single array: Gradient-only deflection \(\sqrt{\ell(\ell+1)}\phi_{\ell m}\)

    • List of 2 arrays: [gradient, curl] deflections where curl is \(\sqrt{\ell(\ell+1)}\Omega_{\ell m}\)

    The curl can be omitted if zero for slightly faster transforms.

  • geometry (tuple of (str, dict), optional) – Sphere pixelization: (geometry_name, parameters). Default: (‘healpix’, {‘nside’: 2048})

  • epsilon (float, optional) – Target numerical accuracy of the result. Default: 1e-7. Execution time has only weak dependence on this parameter.

  • verbose (int, optional) – If non-zero, prints timing and diagnostic information. Default: 0.

  • nthreads (int, optional) – Number of threads to use. If 0, uses os.cpu_count(). Default: 0.

  • pol (bool, optional) – If True, interprets input arrays as CMB fields (T, E, B) and returns lensed T, Q, U. If False, performs spin-0 transforms only. Default: True.

Returns:

maps – Lensed maps, each an array of size npix from the input geometry:

  • If pol=True and 2-3 input alms: Returns (T, Q, U) tuple

  • If pol=False or single alm: Returns single map or list of maps

Return type:

tuple or array_like

Examples

Lens temperature and polarization:

>>> from lenspyx.lensing import alm2lenmap
>>> from lenspyx.utils_hp import Alm
>>> import numpy as np
>>> lmax = 3000
>>> nalm = Alm.getsize(lmax, lmax)
>>> # Create unlensed alms
>>> alm_t = np.random.randn(nalm) + 1j * np.random.randn(nalm)
>>> alm_e = np.random.randn(nalm) + 1j * np.random.randn(nalm)
>>> alm_b = np.zeros(nalm, dtype=complex)
>>> # Create deflection from lensing potential
>>> phi_lm = np.random.randn(nalm) + 1j * np.random.randn(nalm)
>>> dlm = utils_hp.almxfl(phi_lm, np.sqrt(np.arange(lmax+1) * np.arange(1, lmax+2)), None, False)
>>> # Compute lensed maps
>>> t_lens, q_lens, u_lens = alm2lenmap([alm_t, alm_e, alm_b], dlm,
...                                      geometry=('healpix', {'nside': 2048}),
...                                      nthreads=8)

Lens temperature only:

>>> t_lens = alm2lenmap(alm_t, dlm, nthreads=8)

Notes

The lensing operation remaps the CMB according to:

\[X^{\text{lensed}}(\hat{n}) = X^{\text{unlensed}}(\hat{n} + \alpha(\hat{n}))\]

where \(\alpha\) is the lensing deflection vector field and \(X\) is T, Q, or U, or another field.

For polarization, the Stokes parameters are rotated by the lensing-induced angle.

See also

alm2lenmap_spin

Lens arbitrary spin-weight fields

synfast

Generate lensed realizations from power spectra

dlm2angles

Get deflected angles from deflection alms

lenspyx.lensing.alm2lenmap_spin(gclm: ndarray, dlms: ndarray, spin: int, geometry: tuple[str, dict] = ('healpix', {'nside': 2048}), epsilon: float = 1e-07, verbose=0, nthreads: int = 0)[source]

Compute lensed spin-weighted map from gradient/curl modes and deflection field.

This function remaps arbitrary spin-s fields.

Parameters:
  • gclm (array_like or list of array_like) –

    Unlensed gradient and curl mode alms (E and B modes for spin-2).

    • Single array: Gradient-only (e.g., E-mode only)

    • List of 2 arrays: [gradient, curl] (e.g., [E, B])

  • dlms (array_like or list of array_like) –

    Spin-1 deflection field in harmonic space:

    • Single array: Gradient-only deflection \(\sqrt{\ell(\ell+1)}\phi_{\ell m}\)

    • List of 2 arrays: [gradient, curl] deflections

  • spin (int) –

    Spin weight of the field to deflect (≥ 0). Examples:

    • spin=0 : Scalar field (temperature)

    • spin=2 : Polarization (most common)

    • spin≥3 : Higher-spin fields

  • geometry (tuple of (str, dict), optional) – Sphere pixelization. Default: (‘healpix’, {‘nside’: 2048})

  • epsilon (float, optional) – Target numerical accuracy. Default: 1e-7

  • verbose (int, optional) – Print timing information if non-zero. Default: 0

  • nthreads (int, optional) – Number of threads. If 0, uses os.cpu_count(). Default: 0

Returns:

maps – Lensed maps with shape (2, npix) containing the real and imaginary parts of the spin-s field (or shape (1, npix) for spin-0).

Return type:

array_like

Notes

For a spin-s field, the lensing operation includes both deflection and rotation:

\[{}_s X^{\text{lensed}}(\hat{n}) = e^{is\gamma(\hat{n})} {}_s X^{\text{unlensed}}(\hat{n}')\]

where \(\gamma\) is the rotation angle induced by lensing and \(\hat{n}' = \hat{n} + \alpha(\hat{n})\) is the deflected direction.

If curl modes are zero (for either the deflection or the field alms), they can be omitted, resulting in slightly faster transforms.

Examples

>>> from lenspyx.lensing import alm2lenmap_spin
>>> import numpy as np
>>> # Lens polarization (spin-2)
>>> e_lm = np.random.randn(nalm) + 1j * np.random.randn(nalm)
>>> b_lm = np.zeros_like(e_lm)
>>> dlm =almxfl(phi_lm, np.sqrt(np.arange(lmax+1) * np.arange(1, lmax+2), None, False)
>>> q_u_lensed = alm2lenmap_spin([e_lm, b_lm], dlm, spin=2, nthreads=8)
>>> q_lensed = q_u_lensed[0]
>>> u_lensed = q_u_lensed[1]

See also

alm2lenmap

same for spin-0 field

dlm2angles

Get deflected angles and rotation

lenspyx.lensing.dlm2angles(dlms: ndarray, geometry: Geom, mmax=None, nthreads: int = 0, calc_rotation=False)[source]

Convert lensing deflection alms to deflected angles.

Computes the deflected pointing directions (and optionally rotation angles) from the deflection field spherical harmonic coefficients.

Parameters:
  • dlms (array_like) –

    Spin-1 deflection field in harmonic space. Can be:

    • Single array: gradient-only deflection \(\sqrt{\ell(\ell+1)}\phi_{\ell m}\) where \(\phi\) is the lensing potential

    • Two arrays: [gradient, curl] with gradient as above and curl \(\sqrt{\ell(\ell+1)}\Omega_{\ell m}\) where \(\Omega\) is the curl potential

    The curl can be omitted if zero, resulting in slightly faster execution.

  • geometry (Geom) – Sphere pixelization geometry (iso-latitude ring structure).

  • mmax (int, optional) – Maximum m value of dlms. If None, assumes mmax = lmax.

  • nthreads (int, optional) – Number of threads to use. If 0, uses os.cpu_count().

  • calc_rotation (bool, optional) –

    If True, also computes the rotation angle \(\gamma\) by which to rotate non-zero spin fields after deflection:

    \[{}_s P(\hat{n}) \rightarrow e^{is\gamma(\hat{n})} {}_s P(\hat{n}')\]

    Default: False

Returns:

angles – Array of shape (npix, 2) or (npix, 3) containing:

  • Column 0: Deflected colatitude \(\theta'\) (radians)

  • Column 1: Deflected longitude \(\phi'\) (radians)

  • Column 2: Rotation angle \(\gamma\) (radians, only if calc_rotation=True)

Return type:

array_like

Examples

>>> from lenspyx.lensing import dlm2angles
>>> from lenspyx.remapping.utils_geom import Geom
>>> import numpy as np
>>> # Create deflection from lensing potential
>>> lmax = 2048
>>> phi_lm = np.random.randn(Alm.getsize(lmax, lmax)) + \
...          1j * np.random.randn(Alm.getsize(lmax, lmax))
>>> utils_hp.almxfl(phi_lm, np.sqrt(np.arange(lmax+1) * np.arange(1, lmax+2)), None, False)
>>> # Get deflected angles
>>> geom = Geom.get_healpix_geometry(nside=1024)
>>> angles = dlm2angles(dlm, geom, nthreads=8)
>>> theta_deflected = angles[:, 0]
>>> phi_deflected = angles[:, 1]
>>> # With rotation angles for polarization
>>> angles_rot = dlm2angles(dlm, geom, calc_rotation=True, nthreads=8)
>>> gamma = angles_rot[:, 2]

Notes

The deflection field is derived from the lensing potentials via:

\[d_{\ell m} = \sqrt{\ell(\ell+1)} \left(\phi_{\ell m} + i \Omega_{\ell m}\right)\]

See also

alm2lenmap

Compute lensed maps directly

lenspyx.remapping.utils_geom.Geom

Geometry class

Examples

Generate a lensed CMB realization

from lenspyx.lensing import synfast
from lenspyx.utils import camb_clfile

# Load CMB power spectra (including lensing potential)
cls = camb_clfile('cosmo2017_10K_acc3_lensedCls.dat')
# Must contain 'tt', 'ee', 'bb', 'te', 'pp' keys

# Generate lensed realization
maps = synfast(cls, lmax=3000,
               geometry=('healpix', {'nside': 2048}),
               nthreads=8, seed=42, verbose=1)

# Access lensed maps
t_lensed = maps['T']         # Temperature
q_lensed = maps['QU'][0]     # Q Stokes parameter
u_lensed = maps['QU'][1]     # U Stokes parameter

Lens existing unlensed alms

from lenspyx.lensing import alm2lenmap
from lenspyx.utils_hp import Alm, almxfl
import numpy as np

lmax = 3000
nalm = Alm.getsize(lmax, lmax)

# Create or load unlensed alms
alm_t = np.load('unlensed_alm_t.npy')
alm_e = np.load('unlensed_alm_e.npy')
alm_b = np.zeros(nalm, dtype=complex)  # Unlensed B=0

# Create deflection from lensing potential
phi_lm = np.load('phi_lm.npy')
L = np.arange(lmax + 1, dtype=float)
deflection_factor = np.sqrt(L * (L + 1))
dlm = almxfl(phi_lm, deflection_factor, None, False)

# Compute lensed maps
t_lens, q_lens, u_lens = alm2lenmap([alm_t, alm_e, alm_b], dlm,
                                    geometry=('healpix', {'nside': 2048}),
                                    epsilon=1e-7, nthreads=8, verbose=1)

Compute deflected angles from lensing potential

from lenspyx.lensing import dlm2angles
from lenspyx.remapping.utils_geom import Geom
from lenspyx.utils_hp import Alm
import numpy as np

# Lensing potential alms
phi_lm = np.load('phi_lm.npy')
lmax = Alm.getlmax(phi_lm.size, mmax=None)

# Convert to deflection
dlm = phi_lm * np.sqrt(np.arange(lmax+1) * np.arange(1, lmax+2))

# Get geometry
geom = Geom.get_healpix_geometry(nside=1024)

# Compute deflected angles (with rotation for polarization)
angles = dlm2angles(dlm, geom, calc_rotation=True, nthreads=8)

theta_deflected = angles[:, 0]  # Deflected colatitude
phi_deflected = angles[:, 1]    # Deflected longitude
gamma_rotation = angles[:, 2]   # Rotation angle for spin fields

Lens arbitrary spin-weighted fields

from lenspyx.lensing import alm2lenmap_spin
from lenspyx.utils_hp import almxfl
import numpy as np

# For spin-2 field (e.g., polarization)
e_lm = np.load('e_mode_alms.npy')
b_lm = np.load('b_mode_alms.npy')

# Deflection field
dlm = almxfl(phi_lm, np.sqrt(np.arange(lmax+1) * np.arange(1, lmax+2)), None, False)

# Lens the spin-2 field
qu_lensed = alm2lenmap_spin([e_lm, b_lm], dlm, spin=2,
                            geometry=('healpix', {'nside': 2048}),
                            nthreads=8)

q_lensed = qu_lensed[0]
u_lensed = qu_lensed[1]

Use different geometries

from lenspyx.lensing import synfast

# HEALPix geometry (most common)
maps_hp = synfast(cls, lmax=2000,
                 geometry=('healpix', {'nside': 1024}))

# Gauss-Legendre geometry (for high-accuracy applications)
maps_gl = synfast(cls, lmax=2000,
                 geometry=('gl', {'lmax': 2000}))

Notes

  • The lensing implementation is exact (non-perturbative), using interpolation

  • The epsilon parameter controls numerical accuracy

  • For reproducible results, always set the seed parameter

  • The deflection field is \(d_{\ell m} = \sqrt{\ell(\ell+1)}\left(\phi_{\ell m} + i \Omega_{\ell m}\right)\)

References

See Also

  • Jupyter notebook: examples/demo_lenspyx.ipynb for interactive examples

  • lenspyx.remapping.deflection_029 : Low-level lensing implementation

  • lenspyx.experimental : Optimized transforms for partial-sky data