Source code for lazuli_exo.forms

"""Web-form strings -> validated lazuli_exo.simulate() parameters.

Shared by every UI (local Flask demo, the Django simulators site) so that
defaults, ranges, clamping and error messages live in one place. A UI
passes its raw form/query mapping of strings to parse(); it gets typed
values, fatal `errors` (the run cannot proceed) and non-fatal `warnings`
(a value was replaced or clamped and the run proceeds).

Blank optional fields carry meaning: mu blank -> equilibrium-chemistry mu;
log_p_cloud blank -> no deck; log_a_haze blank -> no haze. band is any sncosmo bandpass name or one of the
J/H/Ks/V aliases; it is validated at simulate time.
"""
from collections import namedtuple

from .targets import get_target

ParseResult = namedtuple("ParseResult", "values errors warnings")

# name -> (default, kind, extra). kind: float | int | choice | str | optfloat
# extra: (lo, hi) for numbers, tuple of choices for choice.
FIELDS = {
    "planet_name": ("", "str", None),
    "teff": ("5500", "float", (1000.0, 8000.0)),
    "logg": ("4.5", "float", (2.0, 6.0)),
    "feh": ("0.0", "float", (-2.0, 1.0)),
    "rstar": ("1.0", "float", (0.05, 20.0)),
    "mag": ("", "float", (-2.0, 25.0)),             # required (blank default)
    "band": ("J", "str", None),
    "mag_system": ("vega", "choice", ("vega", "ab")),
    "rp": ("1.0", "float", (0.01, 3.0)),
    "mp": ("1.0", "float", (1e-4, 30.0)),
    "teq": ("1200", "float", (100.0, 4000.0)),
    "t14_h": ("3.0", "float", (0.05, 48.0)),
    "log_met": ("0.0", "float", (-1.0, 3.0)),
    "c_to_o": ("0.55", "float", (0.3, 1.0)),
    "mu": ("", "optfloat", (1.0, 100.0)),
    "log_p_cloud": ("", "optfloat", (-8.0, 1.0)),
    "log_a_haze": ("", "optfloat", (-2.0, 6.0)),
    "haze_slope": ("-4.0", "float", (-10.0, 0.0)),
    "n_transits": ("1", "int", (1, 1000)),
    "resolution": ("100", "int", (10, 500)),
    "seed": ("0", "int", (0, 10 ** 9)),
    "instrument_config": ("cbe", "choice", ("cbe", "eol")),
}
FIELD_DEFAULTS = {k: v[0] for k, v in FIELDS.items()}
REQUIRED = ("mag",)
POSITIVE = ("rstar", "rp", "mp", "teq", "t14_h")
INSTRUMENTS = {"cbe": "lazuli_cbe", "eol": "lazuli_eol"}


def _number(name, raw, kind, lo, hi, errors, warnings):
    cast = int if kind == "int" else float
    try:
        v = cast(float(raw)) if kind == "int" else float(raw)
    except (TypeError, ValueError):
        default = FIELD_DEFAULTS[name]
        if name in REQUIRED or default == "":
            errors.append(f"{name}: {raw!r} is not a number")
            return None
        warnings.append(f"{name}: {raw!r} is not a number, using {default}")
        return cast(default)
    if name in POSITIVE and v <= 0:
        errors.append(f"{name} must be > 0, got {v:g}")   # a typo, not something to clamp
        return v
    if not (lo <= v <= hi):
        clamped = min(max(v, lo), hi)
        warnings.append(f"{name}={v:g} outside [{lo:g}, {hi:g}], clamped to {clamped:g}")
        v = clamped
    return v


[docs] def parse(data): """Parse a mapping of form strings. Missing keys take their defaults.""" values, errors, warnings = {}, [], [] for name, (default, kind, extra) in FIELDS.items(): raw = data.get(name) raw = default if raw is None else str(raw).strip() if kind == "str": values[name] = raw elif kind == "choice": if raw not in extra: if raw != "": warnings.append(f"{name}: {raw!r} is not one of {list(extra)}, using {default!r}") raw = default values[name] = raw elif raw == "": if name in REQUIRED: errors.append(f"{name} is required") values[name] = None else: values[name] = _number(name, raw, kind, *extra, errors, warnings) return ParseResult(values, errors, warnings)
[docs] def to_params(v): """Typed form values -> the lazuli_exo.simulate() parameter dict.""" return dict(name=v["planet_name"] or "custom", scenario="custom", T_eq_K=v["teq"], R_p_RJ=v["rp"], M_p_MJ=v["mp"], R_s_RSun=v["rstar"], teff_K=v["teff"], logg=v["logg"], feh=v["feh"], mag=v["mag"], band=v["band"], mag_system=v["mag_system"], t14_s=v["t14_h"] * 3600.0, log_met=v["log_met"], c_to_o=v["c_to_o"], mu=v["mu"], log_P_cloud_bar=v["log_p_cloud"], log_a_haze=v["log_a_haze"], haze_slope=v["haze_slope"])
[docs] def from_preset(planet, scenario="clear"): """Typed form values prefilled from a showcase planet + scenario.""" from .presets import SCENARIO_PARAMS t = get_target(planet) s = SCENARIO_PARAMS[scenario] base = parse({}).values base.update(planet_name=t["name"], teff=t["T_eff_K"], logg=t["log_g_star"], feh=t["Met_star"], rstar=t["R_s_RSun"], mag=t["J_mag"], band="J", mag_system="vega", rp=t["R_p_RJ"], mp=t["M_p_MJ"], teq=t["T_eq_K"], t14_h=t["t14_s"] / 3600.0, log_met=s["log_met"], c_to_o=s["c_to_o"], mu=None, log_p_cloud=s["log_P_cloud_bar"], log_a_haze=s["log_a_haze"], haze_slope=s["haze_slope"]) return base