def doc_theme():
return theme_minimal() + theme(
panel_grid_minor=element_line(color="gray", linetype="--"),
)Simulating a Weak-Lensing Galaxy Catalog
Purpose
Builds a mock weak-lensing galaxy catalog from the position, redshift, and shape factor calculators described in The Galaxy Population & Weak-Lensing Framework. Read that page first for the vocabulary used here: Factor, Pop, Obs, and the per-galaxy Data.
The orchestrator that wires the three factors together has not been built yet, so this tutorial threads the per-galaxy data between them by hand. That also shows exactly what each factor needs from its neighbours.
Setting Up the Lens and the Models
We need a lens (a single NFW halo at a fixed redshift and mass, observed through its projected surface mass density) and three population models: the redshift distribution of the source sample, its photo-\(z\) error kernel, and the intrinsic-ellipticity distribution of the sources.
import numpy as np
from IPython.display import HTML
from numcosmo_py import Nc, Ncm
from numcosmo_py.catalog import catalog_to_table
Ncm.cfg_init()
def show_table(table, float_format="%.4f", max_rows=10):
"""Render an astropy Table as a compact HTML table."""
return HTML(table.to_pandas().to_html(float_format=float_format, max_rows=max_rows))
cosmo = Nc.HICosmoDEXcdm.new()
dist = Nc.Distance.new(100.0)
hms = Nc.HaloCMParam.new(Nc.HaloMassSummaryMassDef.MEAN, 200.0)
hms.param_set_by_name("log10MDelta", 14.0)
density_profile = Nc.HaloDensityProfileNFW.new(hms)
halo_position = Nc.HaloPosition.new(dist)
halo_position.param_set_by_name("z", 0.3)
halo_position.prepare(cosmo)
surface_mass_density = Nc.WLSurfaceMassDensity.new(dist)
pop_z = Nc.GalaxyRedshiftPopLSSTSRD.new_y1_source()
obs_z = Nc.GalaxyRedshiftObsGauss.new()
pop_shape = Nc.GalaxyShapePopGauss.new()
pop_shape["sigma"] = 0.3All seven models are collected into one NcmMSet: every calculator below reads whichever of these it needs by resolving them from this same mset, rather than holding a reference of its own.
mset = Ncm.MSet.empty_new()
for model in (cosmo, density_profile, halo_position, surface_mass_density,
pop_z, obs_z, pop_shape):
mset.set(model)Building the Three Factors
Each factor is a plain calculator (not a model, not in the mset): a flat footprint for the position, the composed true-\(z \otimes\) photo-\(z\) scheme for the redshift, and the variance-add marginalization for the shape.
position_factor = Nc.GalaxyPositionFactorFlat.new(-0.2, 0.2, -0.2, 0.2)
redshift_factor = Nc.GalaxyRedshiftFactorComposed.new(0.0, 20.0)
shape_factor = Nc.GalaxyShapeFactorVarAdd.new(Nc.GalaxyWLObsEllipConv.TRACE_DET)Each factor’s data_new allocates the per-galaxy fragment for that factor. The shape factor’s fragment is built from the position and redshift fragments, since the shape likelihood needs the projected radius (from ra/dec) and the source redshift z (see the framework page’s Z \(\Rightarrow\) P \(\Rightarrow\) S pipeline) — this one call is the whole “orchestration” this tutorial needs to do by hand:
pos_data = Nc.GalaxyPositionFactorData.new(position_factor, mset)
z_data = Nc.GalaxyRedshiftFactorData.new(redshift_factor, mset)
shape_data = Nc.GalaxyShapeFactorData.new(
shape_factor, mset, pos_data, z_data
)Generating the Catalog
Every factor exposes the columns its fragment needs through required_columns; concatenating them (skipping repeats) gives exactly the columns a storage catalog must provide, with no factor needing to know the others’ column names:
columns = []
for cols in (
Nc.GalaxyPositionFactorData.required_columns(pos_data),
Nc.GalaxyRedshiftFactorData.required_columns(z_data),
Nc.GalaxyShapeFactorData.required_columns(shape_data),
):
for col in cols:
if col not in columns:
columns.append(col)
print(columns)['ra', 'dec', 'z', 'zp', 'sigma0', 'epsilon_int_1', 'epsilon_int_2', 'epsilon_obs_1', 'epsilon_obs_2', 'std_noise', 'c1', 'c2', 'm']
The storage object is NcGalaxyWLObs — a plain named-column catalog, agnostic to which calculators fill it in.
n_galaxies = 8
wl_obs = Nc.GalaxyWLObs.new(
Nc.GalaxyWLObsEllipConv.TRACE_DET,
Nc.WLEllipticityFrame.CELESTIAL,
n_galaxies,
columns,
)For each galaxy we draw a position and a redshift, fix the noise/calibration inputs (std_noise, c1, c2, m — these stay plain scalars on the shape factor’s data, not a pluggable model, for now), draw the shape, and write the three fragments into their row. gen is the same code path integ uses below for the likelihood — see Generation and Likelihood Share One Engine.
rng = Ncm.RNG.seeded_new(None, 123)
for i in range(n_galaxies):
position_factor.gen(mset, pos_data, rng)
redshift_factor.gen(mset, z_data, rng)
shape_factor.data_set(
shape_data, 0.0, 0.0, 0.03, 0.0, 0.0, 0.0,
Nc.WLEllipticityFrame.CELESTIAL,
)
shape_factor.gen(mset, shape_data, rng)
pos_data.write_row(wl_obs, i)
z_data.write_row(wl_obs, i)
shape_data.write_row(wl_obs, i)NcGalaxyWLObs derives from NcmCatalog, so the generic catalog_to_table helper (used in the halo/cluster mock tutorial) works here unchanged:
Code
show_table(catalog_to_table(wl_obs))| ra | dec | z | zp | sigma0 | epsilon_int_1 | epsilon_int_2 | epsilon_obs_1 | epsilon_obs_2 | std_noise | c1 | c2 | m | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.0852 | 0.0786 | 0.3102 | 0.3102 | 0.0000 | 0.4265 | -0.0163 | 0.4787 | -0.0188 | 0.0300 | 0.0000 | 0.0000 | 0.0000 |
| 1 | 0.0319 | 0.0739 | 0.1700 | 0.1700 | 0.0000 | -0.4682 | 0.0472 | -0.4322 | 0.0201 | 0.0300 | 0.0000 | 0.0000 | 0.0000 |
| 2 | 0.0376 | -0.0408 | 0.2626 | 0.2626 | 0.0000 | -0.4247 | -0.0081 | -0.3701 | -0.0827 | 0.0300 | 0.0000 | 0.0000 | 0.0000 |
| 3 | 0.0961 | 0.0898 | 0.1354 | 0.1354 | 0.0000 | -0.2198 | 0.3891 | -0.1855 | 0.3490 | 0.0300 | 0.0000 | 0.0000 | 0.0000 |
| 4 | 0.1108 | -0.1632 | 0.3993 | 0.3993 | 0.0000 | 0.2055 | 0.4641 | 0.1693 | 0.4415 | 0.0300 | 0.0000 | 0.0000 | 0.0000 |
| 5 | 0.0774 | 0.1574 | 1.2337 | 1.2337 | 0.0000 | -0.0089 | 0.5179 | 0.0256 | 0.5060 | 0.0300 | 0.0000 | 0.0000 | 0.0000 |
| 6 | 0.1667 | 0.1465 | 0.8541 | 0.8541 | 0.0000 | -0.0158 | 0.1423 | -0.0976 | 0.1831 | 0.0300 | 0.0000 | 0.0000 | 0.0000 |
| 7 | -0.0869 | 0.0412 | 1.0196 | 1.0196 | 0.0000 | -0.1114 | -0.9073 | -0.0664 | -0.8821 | 0.0300 | 0.0000 | 0.0000 | 0.0000 |
Reading a Row Back
read_row is the inverse of write_row: it re-populates a fragment from one row of the catalog. This is exactly what a likelihood evaluation does when loading an observed catalog rather than a generated one — a fragment does not care whether the values it is holding were drawn or measured.
pos_data_readback = Nc.GalaxyPositionFactorData.new(position_factor, mset)
pos_data_readback.read_row(wl_obs, 0)
print(f"row 0: ra={pos_data_readback.ra:.4f}, dec={pos_data_readback.dec:.4f}")row 0: ra=0.0852, dec=0.0786
Evaluating the Shape Likelihood Factor
The same shape_factor that generated the mock also evaluates the per-galaxy likelihood factor \(p(\epsilon_\mathrm{obs} \mid z)\) as a function of the source redshift. prepare_data_array resolves the per-galaxy geometry caches described on the framework page (projected radius, surface-density optimization structs) once, and integ returns the reusable integrand callback:
shape_factor.prepare_data_array(mset, [shape_data], True, True)
integrand = shape_factor.integ(mset, False)
z_grid = np.linspace(0.05, 1.5, 5)
for z in z_grid:
print(f"z={z:.2f} -> p(eps_obs | z) = {integrand.eval(z, shape_data):.4e}")z=0.05 -> p(eps_obs | z) = 2.3663e-02
z=0.41 -> p(eps_obs | z) = 2.3470e-02
z=0.78 -> p(eps_obs | z) = 2.3232e-02
z=1.14 -> p(eps_obs | z) = 2.3148e-02
z=1.50 -> p(eps_obs | z) = 2.3106e-02
In a full likelihood this integrand would be combined with the redshift factor’s own \(p(z_p, z)\) and integrated over \(z\) once per galaxy — the single outer integral the orchestrator (once built) will perform automatically.
Summary
We built a lens and three population models, wired one factor per observable (position, redshift, shape), generated a mock catalog by calling each factor’s gen and writing the results into a shared NcGalaxyWLObs, read a row back to confirm the round trip, and evaluated the shape factor’s own likelihood integrand for the very galaxies it had just generated. See The Galaxy Population & Weak-Lensing Framework for why the pieces are shaped this way.
API Reference
- factors:
NcGalaxyPositionFactorFlat,NcGalaxyRedshiftFactorComposed,NcGalaxyShapeFactorVarAdd - population/observable models:
NcGalaxyRedshiftPopLSSTSRD,NcGalaxyRedshiftObsGauss,NcGalaxyShapePopGauss - lens:
NcHaloPosition,NcHaloDensityProfileNFW,NcWLSurfaceMassDensity - storage:
NcGalaxyWLObs,NcmCatalog,NcmMSet
The catalog_to_table helper lives in the numcosmo_py.catalog subpackage.