Tracer Protocol (Photometric Observables)¶
Protocol Definition: cloelib.observables.tracer.Tracer
Tracers define window functions for photometric surveys—how galaxies are distributed in redshift and how they trace the matter field.
Required Property¶
perturbations: Reference to a Perturbations object
Tracers need perturbations to compute power spectra and growth.
Required Methods¶
get_window(z)¶
Compute the window function W(z) at given redshifts.
Returns: Window function values, shape depends on number of redshift bins
_window_integrand(z, zprime)¶
Window integrand for Limber integration.
Used internally by summary statistics calculators.
_get_prefactor(ell)¶
Compute prefactor for Limber approximation.
Handles different tracer types (shear has extra factors, galaxy clustering doesn't).
Existing Tracer Implementations¶
ShearTracer¶
For weak gravitational lensing (cosmic shear) measurements.
Location: cloelib/observables/photo.py
What it does:
- Computes lensing window function W^κ(z)
- Includes lensing efficiency
- Handles intrinsic alignments
- Applies nuisance parameters (multiplicative bias, photo-z errors)
Example:
from cloelib.cosmology.camb_cosmology import CAMBBackground, CAMBPerturbations
from cloelib.observables.photo import ShearTracer
import numpy as np
# Set up cosmology
bg = CAMBBackground(H0=67.5, Omega_b0=0.0492, ...)
pert = CAMBPerturbations(background=bg)
# Define redshift distribution (normalized.)
z = np.linspace(0.01, 3.0, 100)
dndz = np.exp(-((z - 0.7) / 0.3)**2) # Gaussian n(z)
dndz = dndz / np.trapz(dndz, z) # Normalize
# Multiple tomographic bins
dndz_bins = np.array([
np.exp(-((z - 0.5) / 0.2)**2),
np.exp(-((z - 1.0) / 0.3)**2),
])
dndz_bins = dndz_bins / np.trapz(dndz_bins, z, axis=1)[:, np.newaxis]
# Nuisance parameters
nuisance = {
'multiplicative_bias_1': 0.0,
'multiplicative_bias_2': 0.0,
'dz_shear_1': 0.0, # Photo-z bias
'dz_shear_2': 0.0,
'AIA': 1.0, # Intrinsic alignment amplitude
'CIA': 0.0164, # IA normalization
'EtaIA': -0.41, # IA redshift evolution
}
# Create tracer
tracer = ShearTracer(
perturbations=pert,
dndz=dndz_bins,
z=z,
nuisance_params=nuisance,
)
# Get window function
window = tracer.get_window(z) # Shape: (n_bins, len(z))
Special Features:
- Lensing efficiency calculation
- Intrinsic alignment modeling (NLA model)
- Photo-z error handling
- Multiplicative shear bias
PositionsTracer¶
For galaxy clustering (galaxy positions) measurements.
Location: cloelib/observables/photo.py
What it does:
- Computes galaxy clustering window function W^g(z)
- Handles galaxy bias
- Applies magnification bias
- Handles photo-z uncertainties
Example:
from cloelib.observables.photo import PositionsTracer
# Nuisance for clustering
nuisance = {
'bias_1': 1.5, # Galaxy bias
'bias_2': 1.8,
'dz_clustering_1': 0.0,
'dz_clustering_2': 0.0,
'magnification_bias_1': 0.0,
'magnification_bias_2': 0.0,
}
tracer = PositionsTracer(
perturbations=pert,
dndz=dndz_bins,
z=z,
nuisance_params=nuisance,
)
window = tracer.get_window(z)
CMBLensingTracer¶
For CMB weak gravitational lensing (convergence) measurements.
Location: cloelib/observables/cmb.py
What it does:
- Computes lensing window function W^κ(z)
Example:
from cloelib.observables.cmb import CMBLensingTracer
z = np.arange(1e-3,pert.background.z_star,0.01)
tracer = CMBLensingTracer(
perturbations=pert,
z=z,
)
window = tracer.get_window(z)
Adding Your Own Tracer¶
To add a new type of photometric observable, follow these steps.
Step 1: Create Your Tracer Class¶
# cloelib/observables/my_new_tracer.py
from cloelib.observables.tracer import Tracer
from cloelib.cosmology.cosmology import Perturbations
import numpy as np
import jax.numpy as jnp
class CMBLensingTracer:
"""Tracer for CMB lensing convergence."""
def __init__(
self,
perturbations: Perturbations,
z_cmb: float = 1100.0, # CMB redshift
):
"""
Initialize CMB lensing tracer.
Args:
perturbations: Perturbations object
z_cmb: Redshift of last scattering surface
"""
self.perturbations = perturbations
self.background = perturbations.background
self.z_cmb = z_cmb
self.prefact_toggle = 1 # Include lensing prefactor
def get_window(self, z: np.ndarray) -> np.ndarray:
"""
Compute CMB lensing window function.
The window peaks at z ~ z_cmb/2 (halfway to CMB).
"""
# Comoving distances
chi_z = self.background.comoving_distance(z)
chi_cmb = self.background.comoving_distance(self.z_cmb)
# CMB lensing efficiency: peaks halfway
efficiency = (chi_cmb - chi_z) / chi_cmb * chi_z / chi_cmb
efficiency[z >= self.z_cmb] = 0.0 # No lensing beyond CMB
# Hubble parameter
H_z = self.background.hubble_parameter(z, units="1/Mpc")
# Window function
window = 1.5 * self.background.Omega_m(0.0) * (1 + z) * H_z * efficiency
return window
def _window_integrand(self, z: np.ndarray, zprime: np.ndarray) -> np.ndarray:
"""Window integrand for Limber integration."""
# This is used by AngularTwoPoint
# Usually just returns get_window for the z argument
return self.get_window(z)
def _get_prefactor(self, ell: np.ndarray) -> np.ndarray:
"""Prefactor for Limber approximation."""
# CMB lensing has same prefactor as shear
return ell * (ell + 1)
Step 2: Add to Package¶
Update cloelib/observables/__init__.py:
from cloelib.observables.my_new_tracer import CMBLensingTracer
__all__ = [
# ... existing exports
"CMBLensingTracer",
]
Step 3: Write Tests¶
# tests/test_my_new_tracer.py
import pytest
import numpy as np
from cloelib.cosmology.camb_cosmology import CAMBBackground, CAMBPerturbations
from cloelib.observables.my_new_tracer import CMBLensingTracer
def test_cmb_lensing_tracer():
"""Test CMB lensing tracer."""
bg = CAMBBackground(H0=67.5, ...)
pert = CAMBPerturbations(background=bg)
tracer = CMBLensingTracer(perturbations=pert)
z = np.linspace(0.1, 2.0, 50)
window = tracer.get_window(z)
# Check shape
assert window.shape == z.shape
# Window should peak somewhere between 0 and z_cmb
peak_idx = np.argmax(window)
assert 0 < z[peak_idx] < tracer.z_cmb
# Window should be zero beyond z_cmb
z_high = np.array([1200.0])
assert tracer.get_window(z_high)[0] == 0.0
Tips & Tricks¶
Protocol Compliance¶
Always verify your implementation:
Nuisance Parameters¶
Keep nuisance parameters in a dictionary:
nuisance = {
'bias': 1.5,
'm_bias': 0.01,
'photo_z_error': 0.03,
}
tracer = MyTracer(perturbations=pert, nuisance_params=nuisance)
This makes it easy to vary parameters in MCMC!
Performance¶
These calculations are invoked frequently during likelihood evaluation:
from functools import lru_cache
class MyTracer:
@lru_cache(maxsize=128)
def _get_window_cached(self, z_tuple):
z = np.array(z_tuple)
return self._compute_window(z)
def get_window(self, z):
return self._get_window_cached(tuple(z.flat))
JAX Compatibility¶
If using JAX, avoid Python control flow:
# ❌ Bad (won't JIT)
if z > 1.0:
result = compute_high_z(z)
else:
result = compute_low_z(z)
# Good (JIT-able)
result = jnp.where(z > 1.0, compute_high_z(z), compute_low_z(z))
Next Steps¶
Ready to compute final statistics with your observables?
- Summary Statistics – Combine tracers into C_ℓ and multipoles
- Perturbations – Review structure formation
- Background – Review the foundation
- API Reference – Full technical details
- Back to Observables – Review all oobservables
- Back to Overview – Review the architecture