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 ------ .. code-block:: text 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/.npz # wavelength_um, f_lambda, meta (R = 10 000 host SED) noise/.npz # wavelength_um, depth_err_1transit, meta ld/.npz # wavelength_um, u1, u2, meta Scenarios are not table files: they are presets of the parametric model (:mod:`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 :func:`~lazuli_exo.tables.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. .. code-block:: python 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 :func:`~lazuli_exo.tables.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 :func:`~lazuli_exo.tables.rebuild_index`, which every precompute script calls when it finishes, so the catalogue cannot drift from the files. Resolving the tables directory ------------------------------ :func:`~lazuli_exo.tables.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: .. code-block:: python 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 ------------------- :mod:`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. .. code-block:: python 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 :doc:`lightcurves`); 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): .. code-block:: bash # 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: .. list-table:: :header-rows: 1 :widths: 34 66 * - Flag - Meaning * - ``meta["atmosphere"]["T_clamped"]`` - The target's :math:`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 :math:`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 :doc:`noise`.