Architecture¶
liulu enforces a strict separation between physics (astrophysical content)
and numerics (pure numerical routines), wired together by a single driver.
Respecting this split is the main way the codebase stays extensible — violating
it is the main way to break it.
liulu/
├── model.py # StreamingModel: the driver (wires physics + numerics)
├── physics/ # astrophysical content only
│ ├── base.py # ABCs: RealSpaceCorrelation, PairwiseVelocityMoments, VelocityPDF
│ ├── real_space.py # xi(r) providers: TabulatedXi, LinearTheoryXi, HalofitXi
│ ├── pairwise_velocity.py# TabulatedMoments (r-t pairwise velocity moments)
│ ├── velocity_pdf.py # GaussianPDF, SkewTPDF, NIGPDF, GeneralizedHyperbolicPDF, TabulatedGHPDF
│ ├── los_projection.py # project_to_los: r-t moments -> LOS moments (geometry)
│ └── cosmology.py # Cosmology(FlatLambdaCDM) helper
├── numerics/ # no cosmology knowledge; operates on callables & arrays
│ ├── interpolation.py # log_linear / log_spline / loglog interpolators
│ └── hankel.py # pk_to_xi, xi_to_pk
└── io/ # readers / writers for tables
The two design rules¶
Physics layer — no raw numerics
Classes in liulu/physics/ expose __call__ / evaluate and contain
astrophysical content only. No np.trapz/scipy.integrate, no FFTs, no
grid construction. New physics models are new classes inheriting the ABCs in
physics/base.py.
Numerics layer — no cosmology
Functions in liulu/numerics/ are pure numerical routines operating on
callables and arrays. They take signatures like f, a, b, tol — never a
Cosmology or a moments object, and they have no knowledge of what \(\xi\) or
\(\mathcal{P}\) mean.
The driver StreamingModel is the only place the two layers
meet, via constructor injection: you build the physics components, hand them
to the model, and the model applies the numerical integration strategy.
Data flow¶
user supplies physics layer driver
┌───────────────┐ ┌──────────────────────────────┐ ┌──────────────┐
│ xi_real │──▶│ │ │ │
│ (RealSpace…) │ │ │ │ │
│ │ │ PairwiseVelocityMoments │ │ StreamingModel│
│ moments │──▶│ --project_to_los--> │──▶│ integrates │──▶ xi^s(s_perp,s_par)
│ (Pairwise…) │ │ LOS moments │ │ over y │ multipoles xi_0,2,4
│ │ │ --VelocityPDF.__call__--> │ │ │
│ │ │ P(v_los | r_perp, r_par) │ │ │
└───────────────┘ └──────────────────────────────┘ └──────────────┘
- You provide a real-space
xi_realand aPairwiseVelocityMoments. - The physics layer projects radial-tangential moments to the line of sight via
project_to_los. - A
VelocityPDFevaluates \(\mathcal{P}(v_\text{los}\mid r_\perp,r_\parallel)\). StreamingModelintegrates \([1+\xi(r)]\,\mathcal{P}\) over \(y\) and projects to multipoles.
Extending the package¶
- New \(\xi(r)\) source → subclass
RealSpaceCorrelation. - New velocity model → subclass
VelocityPDF(and/orPairwiseVelocityMoments). - New numerical strategy (interpolation, quadrature) → add to
numerics/with a callable/array signature, then inject it.
project_to_los is not an ABC — it is geometry (a binomial expansion), not a
model choice. Extend it by adding higher-order \(c_{ij}\) methods to the moments
class, never by hand-unrolling per-order formulae.