jet — JUST Emulator Toolkit: a cosmological emulator pipeline for the Jiao Tong University Spectroscopic Telescope (JUST)
Warning
jet is a research project in constant evolution. Features may be experimental
or under development. Check for updates regularly and use with caution.
jet is organized as four modular stages:
| Stage | Responsibility | Status |
|---|---|---|
jet.catalog |
halo catalogs → HOD galaxy catalogs | interface only |
jet.estimator |
galaxy/halo samples → measured statistics | interface only |
jet.emulator |
parameters → statistics; train, save, load, predict | implemented |
jet.inference |
statistics + covariance → parameter posterior | interface only |
jet does not run N-body simulations or halo finders. It takes halo catalogs
produced upstream and works downwards. The shared vocabulary is
jet.spec: a ParameterSpec names and bounds the input axes, a
DataVectorSpec describes the output bins, and every trained model records the
hashes of the specs it was fitted against so a mismatch is caught on load rather
than showing up as a quietly wrong prediction.
Core installs with NumPy and SciPy only, which is enough to load a trained model and predict. Training needs an extra:
pip install jet # numpy + scipy
pip install jet[gp] # + scikit-learn, for Gaussian-process training
pip install jet[nn] # + PyTorch, for neural-network training
pip install jet[inf] # + emcee, for inference
pip install jet[all] # everythingNote
On a cluster, install PyTorch the way the site expects (module or conda) before
installing jet. pip install jet[all] will otherwise replace an
CUDA-matched PyTorch with the generic PyPI build.
From source, for development:
git clone git@github.com:JUST-MOS/jet.git
cd jet
pip install -e ".[test,gp]"
pytestOr straight from GitHub:
pip install git+https://github.com/JUST-MOS/jet.git@<tag>Every bundled emulator (P(k), HMF, ...) is trained on the same flat
w0waCDM cosmology with eight free parameters. The column order of any
theta array is fixed and shared across models:
| # | name | bounds | meaning |
|---|---|---|---|
| 0 | Omegab |
(0.04, 0.06) | baryon density |
| 1 | Omegam |
(0.24, 0.40) | total matter density (not include neutrino) |
| 2 | H0 |
(60.0, 80.0) | Hubble constant [km/s/Mpc] |
| 3 | ns |
(0.92, 1.00) | scalar spectral index |
| 4 | As |
(1.7e-9, 2.5e-9) | primordial amplitude (not A_s×10⁹) |
| 5 | w0 |
(-1.3, -0.7) | dark-energy equation of state today |
| 6 | wa |
(-0.5, 0.5) | running of the equation of state |
| 7 | mnu |
(0.0, 0.3) | sum of neutrino masses [eV] |
Don't memorise the table; read it from the spec, which is the single source of truth for names and bounds:
from jet.emulator.hmf import theta_spec
spec = theta_spec()
print(spec.names) # the 8 names, in column order
print(spec.bounds_array()) # (8, 2) lower/upper bounds per columnNote
The trained weights for the emulators below are not shipped with the
package. They are fetched automatically on first use: simply load the
corresponding emulator on a machine with internet access (e.g. a login
node), and the bundle is downloaded from the project's GitHub Release
and cached locally. Alternatively, download the released .npz files
manually and point JET_DATA_DIR at a directory containing them.
HMFEmulator predicts the cumulative halo abundance n(>= M) and its
derivative dn/dlnM, in the RockstarM200m mass definition, over the same
eight cosmological parameters:
from jet.emulator.hmf import HMFEmulator
## Omegab, Omegacb, H0, ns, As, w0, wa, mnu
theta = [0.049, 0.31, 67.66, 0.9665, 2.1e-9, -1.0, 0.0, 0.06]
hmf = HMFEmulator.load()
n = hmf.number_density(theta, z=[0.0, 1.0, 3.0], M=[1e12, 1e13, 1e14]) # (1, 3, 3)
d = hmf.dndlnM(theta, z=0.5, M=1e13)theta is an (n_samples, 8) array — or a single row — in theta_spec()
order, and the result is (n_samples, n_z, n_M) in (h/Mpc)^3. Multiply by a
volume in (Mpc/h)^3 for an expected halo count. Any redshift or mass inside
the emulator's range may be asked for; both are interpolated.
Two things distinguish this model from the power spectrum:
- It emulates a ratio, not the mass function. Recovering the physical
quantity means multiplying by an analytic Castro23 baseline, which
jet.emulator.hmfcomputes from the bundledP(k)emulator andjet.cosmology. That baseline is why a prediction costs about a third of a second rather than a millisecond. - Its data vector is not a rectangle. The reference trained on a different
mass range at each redshift, so the twelve blocks have different lengths.
jet.emulator.hmf.mass_slices()anddata_slices()give the ranges.
See examples/hmf.ipynb for a runnable walkthrough of the
whole interface.
Note
The emulator only accepts the trainned parameter set. If
you want use sigma8 instead of As, use the following script
to transfer sigma8 to As and input theta_as into the hmf emulator.
import numpy as np
from jet.cosmology import cosmo_parameter_spec
from jet.derived import Sigma8FromAs
from jet.spec import ParameterSpec
# The spec the bundled emulators use, plus sigma8 as an alternative name for As.
spec = ParameterSpec(list(cosmo_parameter_spec()), derived=[Sigma8FromAs()])
frame = spec.frame("sigma8")
# columns: Omegab, Omegam, H0, ns, sigma8, w0, wa, mnu
# Note Omegam only contains the cold dark matter and baryon contributions, not neutrinos.
theta_sigma8 = np.array([[0.049, 0.31, 67.66, 0.9665, 0.81, -1.0, 0.0, 0.06]])
theta_as = frame.to_base(theta_sigma8)
theta_as[0, 4] # array(2.0845e-09)
print(theta_as)
theta = frame.from_base(theta_as) # back to 0.81BCMHFEmulator predicts the cumulative abundance with the baryon correction
model of Arico et al., over four feedback parameters and no cosmological
ones:
from jet.emulator.hmf_bcm import BCMHFEmulator, PARAMETER_DEFAULTS, mass_grid
## logMc, thej, mu, delta
theta_default = [15, 1.5, 3.5, 4.5]
emu = BCMHFEmulator.load()
n = emu.cumulative(theta_default[None, :]) # (1, 46) in (h/Mpc)^3
d = emu.dndlgM(theta_default[None, :]) # dn/dlog10(M)
M = mass_grid() # the 46 masses, in Msun/h — the grid travels in the bundletheta is an (n_samples, 4) array — or a single row — in theta_spec() order:
logMc, thej, mu, delta.
Three things set it apart from the model above:
- It takes no cosmological parameters. This is the one bundled model whose
input is not a cosmology; its four parameters are declared under
block="baryon", so they can be concatenated with a cosmological spec when a joint model is eventually needed. That is whatParameterSpec.subset()is for. - It is one cosmology at one redshift. The weights encode
c0000atz = 0.5and there is no axis along which either can vary. The bundle records both as provenance; neither is extrapolated over. - The mass grid is fixed and rectangular. No interpolation, no baseline, no
per-redshift blocks — the reference is a plain regression of
log10 n(>=M)onto four numbers, and the module is correspondingly small.
See examples/hmf_bcm_example.py for a runnable
walkthrough.
XiHMEmulator predicts the cross-correlation between a halo sample and the
matter field, over the same eight cosmological parameters. The sample can be
selected either by cumulative number density or by mass:
from jet.emulator.xihm import XiHMEmulator
## Omegab, Omegacb, H0, ns, As, w0, wa, mnu
theta = [0.049, 0.31, 67.66, 0.9665, 2.1e-9, -1.0, 0.0, 0.06]
emu = XiHMEmulator.load()
z, r = [0.0, 0.5, 1.5], np.logspace(-1.0, 1.5, 12)
xihm = emu.xihm_lgnbar_threshold(theta, z=z, r=r, lgden=[-5.0, -4.0]) # (n, 2, 3, 12)
xihm = emu.xihm_mass(theta, z=z, r=r, M=[1e13, 1e14]) # (n, 2, 3, 12)
# The three pieces, if you want them separately
box = emu.bias_ratio(theta) # the raw emulated quantity, (n, 2664)
b = emu.brhm(theta, z=z, lgden=[-5.0], r=r) # the bias ratio on your own axes
xi_mm = emu.ximm(theta, z=z, r=r) # linear matter correlation function
bias = emu.bias_lgnbar_threshold(theta, z=z, lgden=[-5.0, -4.0]) # the halo biastheta is an (n_samples, 8) array — or a single row — in theta_spec() order.
lgden is log10 of the cumulative number density in (h/Mpc)^3, so a larger
value is a lower mass threshold and a weaker bias; r is in h^-1 Mpc and
M in Msun/h.
Four things are worth knowing before using it:
- The emulated quantity is a ratio, not the correlation function. The
Gaussian process predicts
B_hm = xi_hm / xi_mm;XiHMEmulatormultiplies back in a matter correlation function it computes by Hankel-transforming the bundled linearP(k)(jet.fourier). At large separations it blends onto a linear-bias baseline,b * P_lintransformed — so this is where the halo bias gets used, andjet.emulator.bhmis what supplies it. z,lgdenandrare interpolation axes, not emulator axes. The Gaussian process sees the eight cosmological parameters only; the other three are handled afterwards by linear interpolation of the box, linear inlog10(r). Asking for a point outside the box warns and extrapolates rather than raising. SetXiHMEmulator(warn_on_extrapolation=False)to silence the whole stack at once.- Density threshold and mass threshold are different samples. Holding
n(>=M)fixed and varyingMis not the same selection as the reverse, soxihm_massconverts by a finite difference inlog10(n), weighted by the density — the same one per cent step the reference uses. - The result is real-space and matter-only. No redshift-space distortions, no HOD, no survey selection.
Neither entry point needs the bias bundle for its emulated term; the baseline is what needs it.
See examples/xihm_example.py for a runnable
walkthrough.
jet/
├── jet/ # the Python package (standard flat layout)
│ ├── spec.py # ParameterSpec / DataVectorSpec, shared by all stages
│ ├── derived.py # derived parameters: sigma8 <- As, h <- H0
│ ├── cosmology.py # background expansion the ported physics needs
│ ├── fourier.py # FFTLog Hankel transforms (P(k) <-> xi(r))
│ ├── emulator/ # transforms, backends, bundle format, metrics
│ │ ├── pklin.py # reader for the bundled linear-theory P(k) emulator
│ │ ├── hmf.py # halo mass function: the emulator and its baseline
│ │ ├── hmf_bcm.py # the same with baryonic feedback: no cosmology at all
│ │ ├── bhm.py # halo bias: the emulator, the baseline, the inversion
│ │ └── xihm.py # halo-matter correlation function, composed from those
│ ├── data/ # bundled weights (not tracked by git; see its README)
│ ├── catalog/ # design stage
│ ├── estimator/ # design stage
│ └── inference/ # design stage
├── tools/ # one-off scripts that regenerate bundled data
│ ├── build_pklin_bundle.py
│ ├── build_hmf_bundle.py
│ ├── build_hmf_bcm_bundle.py
│ └── build_xihm_bundle.py
├── tests/ # unittest suite, run via pytest
├── examples/ # usage notebooks
│ ├── hmf.ipynb # halo mass function quick start
│ ├── hmf_bcm_example.py # baryonic mass function quick start
│ └── xihm_example.py # halo-matter bias ratio quick start
├── environment.yml # shared development environment (conda + pip)
├── VERSION # single source of truth for the version (bump2version-owned)
├── .bumpversion.cfg # version bump configuration
├── pyproject.toml
└── .github/workflows/ # automated version bumping
For the group, one shared conda environment is enough; jet itself is always
installed with pip on top of it:
conda env create -f environment.yml # or: conda activate /path/to/existing/env
conda activate jet
pip install -e ".[test,gp]"If you are the user of Gravity cluster, you can use
source gravity-user.shto load my conda env and setup the path of trained data file for a quick usage.
This section targets advanced users who intend to train their own emulators instead of consuming the pretrained bundles. If you just want to run an existing emulator, skip this entire section.
Warning
The snippet below is illustrative pseudo-code, not a runnable script.
If you actually want to train your own emulator, read the module docstrings
of jet.emulator and the reference builder scripts under tools/ carefully
first. Getting the data-vector layout, the transform chain, or the bundle
format wrong does not raise an error — it silently produces an emulator
that looks fine but predicts garbage.
import numpy as np
from jet.spec import ParameterSpec, Param, DataVectorSpec
from jet.emulator import Emulator
# 1. Declare your specs. The column order of X below must match x_spec
# exactly; the transform chain (e.g. log) is derived from y_spec.log,
# so getting either wrong is a silent failure.
x_spec = ParameterSpec([...]) # <- your input parameters
y_spec = DataVectorSpec("my_stat", n_bins=..., log=True) # <- your data vector
# 2. Your own training set: (n_samples, n_params) and (n_samples, n_bins).
X, y = load_my_training_data() # <- you must provide this
# 3. Build, fit, save. The saved .npz is later picked up by Emulator.load().
emu = Emulator(x_spec, y_spec, backend="gp", n_pca=8)
emu.fit(X, y)
emu.save("my_emulator.npz")Loading needs neither scikit-learn nor PyTorch:
from jet.emulator import Emulator
emulator = Emulator.load("wp.gp.npz") # resolved against $JET_DATA_DIR
emulator.verify(x_spec=x_spec) # optional, catches a column reorderingA parameter that is a function of the others is not a new axis, and jet does
not treat it as one. Declaring it on the spec adds no column — it lets the same
spec be addressed along either parameter, converting at the boundary:
from jet.derived import Sigma8FromAs
x_spec = ParameterSpec(
[
Param("Omegab", ...),
Param("Omegam", ...),
Param("H0", ...),
Param("ns", ...),
Param("As", bounds=(1.7e-9, 2.5e-9)),
Param("w0", ...),
Param("wa", ...),
Param("mnu", ...),
],
derived=[Sigma8FromAs()], # sigma8 <-> As
)
emulator = Emulator(x_spec, y_spec).fit(X, y) # trained on As, as the table has it
emulator.predict(X_new) # X_new is in As
frame = emulator.x_spec.frame("sigma8")
emulator.predict(frame.to_base(X_sigma8)) # driven by sigma8 insteadX keeps its (n_samples, x_spec.dim) shape in either frame, and the two
agree to the accuracy of the conversion. Sigma8FromAs evaluates sigma8
through jet's bundled linear-theory P(k) emulator and inverts with the same
fixed point csstemu uses; see Derived parameters and
the bundled data.
The conversion is applied by you, not by the model: an emulator reads the
axes of its spec and nothing else. That keeps a parameter array to a single
reading at the point it is consumed, and it means a bundle is bound to the axes
alone — declaring or removing a derived parameter never invalidates one. A
loaded model still carries its declarations, so emulator.x_spec.derived_names
tells you which frames are available.
Note
The bundled weights are not part of the distribution. A wheel or a fresh
clone has no jet/data/pklin.gp.npz, so Sigma8FromAs raises
FileNotFoundError explaining how to obtain it. Generate it with
python tools/build_pklin_bundle.py, or point $JET_DATA_DIR at a copy.
Everything else works without it — Omegam needs no data at all.
Trained models are tens of megabytes and are retrained often, so they are not
kept in the repository. Emulator.load accepts either an absolute path or a
bare name, in which case it is looked up under $JET_DATA_DIR:
export JET_DATA_DIR=/share/users/<you>/jet-weightsSince v0.1.0 the weights are also attached to the GitHub Release of each
version, so a plain pip install needs no environment configuration: the first
Emulator.load("<name>.npz") fetches the file from the release into the user
cache ($XDG_CACHE_HOME/jet/data, defaulting to ~/.cache/jet/data), and
every later call reads the cache. The fetch lives in
jet.emulator.resolve_weights_path; disable it with JET_NO_AUTO_FETCH, pin a
release with JET_RELEASE_TAG, and point a private repository's token via
GITHUB_TOKEN / JET_DATA_TOKEN.
Publish the current weights with:
python tools/publish_data.py # uploads jet/data/*.npz to the VERSION tagjet follows semantic versioning with an ACM-style automation:
X.Y.Zfor releases,X.Y.Z-dev.Nduring development.- The
VERSIONfile is the single source of truth and is managed exclusively bybump2version— do not edit it by hand. - Merging to
devauto-bumps the dev version (no tag); merging tomainwith abump:patch/bump:minor/bump:majorlabel creates a tag and a GitHub release, andmainis merged back intodev.
branching model: main for stable releases, dev for ongoing development.
feature/<feature>— new features, merged intodevhotfix/<fix>— urgent bug fixes, merged intomainanddevdocs/**— documentation improvements
Every PR to main must carry exactly one version-bump label. See AGENTS.md for
the full ruleset for AI-assisted coding.
Note
There is currently no lint/test CI workflow: .github/workflows/ only automates
version bumps. Lint and tests are configured in pyproject.toml and are run
locally (ruff check, ruff format --check, pytest).
jet is free software licensed under the BSD-3-Clause license. See LICENSE.