Tables and targets
Everything the runtime computes comes from a small directory of .npz
lookup tables plus a frozen target registry, about 50 MB in total (including
the parametric ζ and PHOENIX grids), committed to the repository at
src/lazuli_exo/data/tables/ and shipped as package data.
Layout
index.json # version, planets, grids, sha256 manifest
atmo/zeta_grid.npz # parametric model: zeta(T, [M/H], C/O, fiducial, lambda), mu
star/phoenix_grid.npz # PHOENIX SEDs on (T_eff, log g, [Fe/H])
star/<planet-slug>.npz # wavelength_um, f_lambda, meta (R = 10 000 host SED)
noise/<planet-slug>.npz # wavelength_um, depth_err_1transit, meta
ld/<planet-slug>.npz # wavelength_um, u1, u2, meta
Scenarios are not table files: they are presets of the parametric model
(lazuli_exo.presets). The per-planet POSEIDON spectra that the
presets are validated against live in precompute/tables/models/ and are
not shipped.
All wavelengths are in micron. meta is a JSON dict stored as a string
array, and every table has one: it is where the provenance and the caveat
flags live.
Slugs come from slugify(): lowercased, with runs of
non-alphanumeric characters collapsed to - ("WASP-39 b" →
wasp-39-b).
The index
index.json is the catalogue: a schema version (currently 4), per
planet its slug and whether noise and limb-darkening tables exist, which
grids are present, and a sha256 manifest of every file.
from lazuli_exo import tables
tdir = tables.resolve_tables_dir(None)
index = tables.load_index(tdir)
index["planets"]["WASP-39 b"]
# {'has_ld': True, 'has_noise': True, 'slug': 'wasp-39-b'}
index["files"]["noise/wasp-39-b.npz"]
# sha256 hex digest; tables.sha256_file(path) recomputes it
load_index() refuses a schema version mismatch
with a clear error rather than misreading tables. The index is regenerated
from what is actually on disk by rebuild_index(),
which every precompute script calls when it finishes, so the catalogue cannot
drift from the files.
Resolving the tables directory
resolve_tables_dir() checks three sources, in order:
an explicit
tables_dir=argument (also accepted byobserve()),the
LAZULI_EXO_TABLESenvironment variable,the
data/tables/directory inside the installed package.
Note
Step 3 works for editable installs, repository checkouts, SHA-pinned VCS
installs and built wheels alike, because the tables are declared as
package data in pyproject.toml. LAZULI_EXO_TABLES is only needed
when a deployment wants the tables somewhere else.
Loading tables
Per-planet loaders return their arrays followed by meta; the grid
loaders return a dict:
wl, f_lambda, meta = tables.load_star(tdir, "WASP-39 b")
wl, sigma_1transit, meta = tables.load_noise(tdir, "WASP-39 b")
wl, u1, u2, meta = tables.load_ld(tdir, "WASP-39 b")
g = tables.load_zeta_grid() # T_K, log_met, c_to_o, wavelength_um, zeta, mu, meta
g = tables.load_phoenix_grid() # teff_K, logg, feh, wavelength_um, log_flam, meta
# validation material only (precompute/tables/, not shipped)
wl, depth, meta = tables.load_model("precompute/tables", "WASP-39 b", "clear")
Planet names are matched against both the display name and the slug. Unknown
planets and unknown scenarios raise KeyError listing what is available;
an indexed-but-missing file raises FileNotFoundError naming the precompute
script to re-run.
The target registry
lazuli_exo.targets is a frozen snapshot of the NASA Exoplanet Archive
PSCompPars table (plus a small set of overrides), generated by
precompute/freeze_targets.py. It is a plain dict: no network access at
runtime, and no dependency on the archive being reachable or unchanged.
from lazuli_exo.targets import get_target
get_target("WASP-39 b")
# {'name': 'WASP-39 b', 'slug': 'wasp-39-b', 'period_d': 4.0552941,
# 'a_over_rs': 11.39, 'inc_deg': 87.32, 'ecc': 0.0, 'w_deg': 90.0,
# 't14_s': 10091.52, 'depth': 0.02122849, 'T_eq_K': 1166.0,
# 'T_eff_K': 5485.0, 'J_mag': 10.663, 'provenance': 'NEA PSCompPars', ...}
The registry feeds the light-curve geometry (see Transit light curves); a planet
missing from it, or missing a_over_rs / inc_deg / period_d, falls
back to the trapezoid model.
Warning
src/lazuli_exo/targets.py is generated. Do not edit it by hand. Re-run precompute/freeze_targets.py to refresh it.
Regenerating the tables
Five steps, in two conda environments, because POSEIDON and slicersim never
need to be installed together (docs/MODEL.md in the repository has the
full detail):
# 1. POSEIDON validation tables (-> precompute/tables/models) + star tables
# (poseidon env; needs the opacity/chemistry inputs)
POSEIDON_input_data=$HOME/inputs/ PYSYN_CDBS=$HOME/inputs/stellar_grids \
~/anaconda3/envs/poseidon/bin/python precompute/make_model_tables.py [planet ...]
# 2. flat (featureless) validation tables (analytic, py313 env)
~/anaconda3/envs/py313/bin/python precompute/make_flat_tables.py
# 3. noise tables (py313 env; needs slicersim)
~/anaconda3/envs/py313/bin/python precompute/make_noise_tables.py
# 4. parametric grids (zeta: poseidon env, ~14 min; PHOENIX: ~1 min), then the gate
POSEIDON_input_data=$HOME/inputs/ ~/anaconda3/envs/poseidon/bin/python precompute/make_zeta_grid.py
PYSYN_CDBS=$HOME/inputs/stellar_grids PYTHONPATH=src python precompute/make_phoenix_grid.py
~/anaconda3/envs/py313/bin/python precompute/validate_zeta_grid.py
# 5. limb-darkening tables (py313 env; needs exotic-ld, network on first
# run; PHOENIX files cache to $EXOTIC_LD_DATA, default
# ~/inputs/exotic_ld_data)
~/anaconda3/envs/py313/bin/python precompute/make_ld_tables.py
Each script calls tables.rebuild_index() when it finishes, so
index.json always reflects what is actually on disk.
Caveat flags recorded in the data
Two of the known approximations are flagged per table, so code can branch on them:
Flag |
Meaning |
|---|---|
|
The target’s \(T_\mathrm{eq}\) fell below the 300 K fastchem grid
floor, so the chemistry uses 300 K ( |
|
|