Note
This page is generated from the Jupyter notebook
notebooks/07_custom_planet.ipynb in the
repository. Download it and run it interactively to experiment.
7. A custom planet, end to end
The showcase tables cover 14 planets. lazuli_exo.simulate covers any transiting planet from a dozen numbers. This notebook builds the parameter dict by hand for GJ 9827 d, a warm sub-Neptune that is not in the registry, runs it, and then shows the same thing through the form/report layer a web page uses.
Values below are approximate literature numbers, rounded, for illustration (radius 2.0 R⊕, mass 3.4 M⊕, T_eq ≈ 680 K, P = 6.2 d; K6V host, J = 7.98). simulate needs slicersim and sncosmo installed (the noise is computed live); import lazuli_exo itself stays numpy-only.
[1]:
import numpy as np
import matplotlib.pyplot as plt
try:
plt.style.use("gks")
except OSError:
pass
import lazuli_exo
from lazuli_exo import forms, report
R_EARTH_RJ, M_EARTH_MJ = 1 / 11.209, 1 / 317.83
params = dict(
name="GJ 9827 d",
# star
teff_K=4294.0, logg=4.66, feh=-0.28, R_s_RSun=0.602, mag=7.984, band="J", mag_system="vega",
# planet
R_p_RJ=2.02 * R_EARTH_RJ, M_p_MJ=3.42 * M_EARTH_MJ, T_eq_K=680.0, t14_s=1.22 * 3600,
# atmosphere (solar-metallicity, cloud-free H2/He to start)
log_met=0.0, c_to_o=0.55, mu=None, log_P_cloud_bar=None, log_a_haze=None, haze_slope=-4.0,
)
obs = lazuli_exo.simulate(params, R=100, n_transits=10)
a, n = obs.meta["atmosphere"], obs.meta["noise"]
print(f"g = {a['g_ms2']:.1f} m/s2 H = {a['H_m']/1e3:.0f} km mu = {a['mu_used']:.2f} ({a['mu_source']})")
print(f"feature amplitude {a['feature_amplitude_ppm']:.0f} ppm, median error {np.median(obs.depth_err)*1e6:.0f} ppm over 10 transits")
print(f"J = {n['mag_input']} Vega -> {n['mag_ab']:.2f} AB; readout {n['nmd_used']}, saturated px {n['nsaturated_detpx']}")
print(obs.detect())
g = 8.2 m/s2 H = 295 km mu = 2.32 (chemistry)
feature amplitude 924 ppm, median error 26 ppm over 10 transits
J = 7.984 Vega -> 8.88 AB; readout [2, 1, 0], saturated px 0
{'template_snr_diagonal': 32.888673743561014, 'band_snr': {'Na': 6.7144868206849395, 'K': 3.996727332735979, 'H2O': 21.64870359040673, 'CH4': 15.369272894534555, 'CO2': 10.659073018459495}}
[2]:
fig, ax = plt.subplots(figsize=(8, 3.8))
obs.plot(ax=ax, seed=1)
fig.tight_layout()
Heavier atmosphere, and a cloud deck
Two edits a user would make: a water-rich envelope (μ = 10) and a deck at 10 mbar. The scale height shrinks with μ, and the deck hides everything below it; the derived numbers say how much.
[3]:
variants = {
"H2/He, clear": {},
"mu = 10, clear": dict(mu=10.0),
"H2/He, deck at 10 mbar": dict(log_P_cloud_bar=-2.0),
"mu = 10, deck at 10 mbar": dict(mu=10.0, log_P_cloud_bar=-2.0),
}
fig, ax = plt.subplots(figsize=(8, 4))
for label, edit in variants.items():
o = lazuli_exo.simulate({**params, **edit}, R=100, n_transits=10)
amp = o.meta["atmosphere"]["feature_amplitude_ppm"]
snr = o.detect()["template_snr_diagonal"]
ax.plot(o.wavelength, o.depth * 1e6, lw=1.2, label=f"{label}: {amp:.0f} ppm, S/N {snr:.1f}")
ax.set_xlabel("wavelength [µm]"); ax.set_ylabel("transit depth [ppm]"); ax.legend(frameon=False, fontsize=8)
fig.tight_layout()
The same run through the web layer
A web page never builds the params dict directly. It hands the raw form strings to lazuli_exo.forms.parse, which applies defaults, clamps ranges (with warnings) and rejects what cannot run (errors); forms.to_params maps the typed values onto simulate; lazuli_exo.report.summary turns the result into plain JSON for plotting. The local Flask applet and the Django simulators page both use exactly this path.
[4]:
form = {"planet_name": "GJ 9827 d", "teff": "4294", "logg": "4.66", "feh": "-0.28", "rstar": "0.602",
"mag": "7.984", "band": "J", "mag_system": "vega", "rp": f"{2.02 * R_EARTH_RJ:.4f}",
"mp": f"{3.42 * M_EARTH_MJ:.5f}", "teq": "680", "t14_h": "1.22",
"log_met": "0.0", "c_to_o": "0.55", "mu": "", "log_p_cloud": "-2", "log_a_haze": "",
"n_transits": "10", "resolution": "100", "seed": "1", "instrument_config": "cbe",
"logg_typo": "ignored", "c_to_o": "1.7"} # one out-of-range value on purpose
res = forms.parse(form)
print("errors: ", res.errors)
print("warnings:", res.warnings)
o = lazuli_exo.simulate(forms.to_params(res.values), R=res.values["resolution"], n_transits=res.values["n_transits"])
s = report.summary(o, seed=res.values["seed"])
print(sorted(s))
print(s["result"])
for row in s["derived"]:
print(f" {row['label']:18s} {row['value']}" + (f" [{row['cls']}]" if row["cls"] else ""))
errors: []
warnings: ['c_to_o=1.7 outside [0.3, 1], clamped to 1']
['R', 'caveats', 'depth', 'depth_err', 'derived', 'feature_snr', 'flat_depth', 'model_depth', 'model_wavelength', 'n_transits', 'noise_validated', 'planet', 'points', 'result', 'scenario', 'seed', 'wavelength']
{'template_snr_diagonal': 14.372176958372272, 'feature_amplitude_ppm': 675.1787062867755, 'median_err_ppm': 26.470302572313823, 'n_bins': 145}
g 8.2 m/s²
H 294 km (H/Rp = 0.0228)
mu 2.32 (chemistry)
T for chemistry 680 K
feature amplitude 675 ppm (0.40–1.70 µm)
column slope 0.81
stellar SED phoenix: feh -0.28 clamped to 0 [warn]
magnitude 7.98 J vega → 8.88 AB (offset +0.899)
readout 2:1:0, t_obs = 8.5 s, lazuli_cbe
saturated pixels 0
slicersim 1.3.4
Summary
A dozen numbers describe the system;
simulatereturns the sameSimulatedSpectrumthe showcase tables do, sodetect(),plot()andsimulate_points()work unchanged.μ and the deck pressure are the two knobs that move a sub-Neptune’s amplitude the most; the derived panel reports H, H/R_p and the μ source.
forms+reportare the whole web contract: strings in, JSON out.