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:

  1. an explicit tables_dir= argument (also accepted by observe()),

  2. the LAZULI_EXO_TABLES environment variable,

  3. 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

meta["atmosphere"]["T_clamped"]

The target’s \(T_\mathrm{eq}\) fell below the 300 K fastchem grid floor, so the chemistry uses 300 K (T_chem_K) while the scale height uses the true \(T_\mathrm{eq}\). True for K2-18 b, LHS 1140 b, TRAPPIST-1 e.

meta["noise"]["fully_desaturated"]

False when the detector ramp retained saturated pixels; the count is in nsaturated_detpx. observe() also raises a UserWarning. See The noise model.