"""
config_io: bridge between the GUI widgets and the pipeline configuration.
The pipeline is driven entirely by the module-level globals in
``pipeline/phase_config.py``. This module:
* reads those globals to obtain the **default** value of every parameter
(so the GUI never hard-codes defaults that could drift from the pipeline);
* describes each parameter's *group*, *kind* and help text (consumed by the
panels and the ``HelpButton`` popups);
* serialises a flat ``{GLOBAL_NAME: value}`` settings dict to / from JSON.
The subprocess entry point ``runner.py`` loads that JSON, ``setattr``s every
key onto ``phase_config`` and then runs the pipeline, so the set of keys here
must match the global names in ``phase_config.py`` exactly.
"""
from __future__ import annotations
import copy
import json
import os
import sys
from typing import Any, Dict, List
# --------------------------------------------------------------------------- #
# Paths
# --------------------------------------------------------------------------- #
_HERE = os.path.dirname(os.path.abspath(__file__))
PIPELINE_DIR = os.path.join(_HERE, "pipeline")
DATA_DIR = os.path.join(_HERE, "data")
def _import_phase_config():
"""Import the pipeline's ``phase_config`` (flat import) in isolation.
``phase_config`` only depends on the stdlib ``os`` module, so importing it
here is cheap and does not drag in numpy/astropy/matplotlib.
"""
if PIPELINE_DIR not in sys.path:
sys.path.insert(0, PIPELINE_DIR)
import phase_config # noqa: E402 (path set up above)
return phase_config
# --------------------------------------------------------------------------- #
# The list of config globals the GUI exposes (order defines nothing;
# the panels decide layout). Every name here must exist in phase_config.
# --------------------------------------------------------------------------- #
CONFIG_KEYS: List[str] = [
# catalog
"CATALOG_SOURCE", "FETCH_NEA_ONLINE", "NEA_FETCH_TIMEOUT", "CATALOG_DIR",
# filters
"DESERT_FILTER", "EXTRA_FILTERS",
# observatory
"OBSERVATORY",
# time & constraints
"DATE_RANGE", "CONSTRAINTS", "EVENT_CONSTRAINTS",
"TIME_RESOLUTION_MIN", "PRESELECTION_MIN_YEAR",
# custom planets
"CUSTOM_PLANETS",
# telluric
"SKY_TRANSMISSION_FITS", "TELLURIC_LAMBDA_RANGE_NM", "TELLURIC_RV_GRID_KMS",
"TELLURIC_PAD_HOURS", "TELLURIC_EXP_TIME_S",
# output & plot
"OUTPUT_DIR", "AUX_DIR", "DESERT_BOUNDARY_FILE", "KDE_FILE", "P_MAX", "R_MAX",
]
# Enumerated choices used by dropdowns.
CATALOG_SOURCES = ["NEA", "TESS", "BOTH"]
DESERT_FILTERS = ["all", "desert", "nondesert"]
# Columns offered in the EXTRA_FILTERS row editor (NASA-Archive style names).
FILTER_COLUMNS = [
"pl_orbper", "pl_rade", "pl_bmasse", "pl_eqt", "pl_dens",
"st_teff", "st_rad", "st_mass", "sy_jmag", "sy_kmag",
"ra", "dec", "pl_orbsmax", "pl_orbeccen", "disc_year",
]
# Ordered numeric fields of a CUSTOM_PLANETS entry (besides pl_name).
CUSTOM_PLANET_FIELDS = [
"pl_orbper", "pl_rade", "pl_bmasse", "pl_eqt", "st_teff", "st_mass",
"st_rad", "st_jmag", "st_kmag", "ra", "dec", "pl_tranmid",
"pl_orbsmax", "pl_orbeccen", "pl_orblper", "v_sys",
]
TIMEZONES = [
"UTC", "Atlantic/Canary", "America/Santiago", "Pacific/Honolulu",
"Europe/Rome", "Europe/London", "America/New_York", "Australia/Sydney",
]
# --------------------------------------------------------------------------- #
# Built-in observatory / instrument presets for the Observatory tab dropdown.
# Selecting a preset auto-fills every OBSERVATORY field; the extra "New" entry
# lets the user type their own values in the same fields.
# --------------------------------------------------------------------------- #
OBSERVATORY_PRESET_NEW = "New"
OBSERVATORY_PRESETS: Dict[str, Dict[str, Any]] = {
"CRIRES+": {
"name": "VLT",
"telescope": "VLT UT3 (Melipal)",
"instrument": "CRIRES+",
"lat": -24.6275,
"lon": -70.4044,
"alt": 2635,
"timezone": "America/Santiago",
},
"IGRINS-2": {
"name": "Gemini North",
"telescope": "Gemini North",
"instrument": "IGRINS-2",
"lat": 19.8238,
"lon": -155.4691,
"alt": 4213,
"timezone": "Pacific/Honolulu",
},
"GIANO-B": {
"name": "TNG",
"telescope": "TNG",
"instrument": "GIANO-B",
"lat": 28.7569,
"lon": -17.8850,
"alt": 2387,
"timezone": "Atlantic/Canary",
},
}
# --------------------------------------------------------------------------- #
# Help text (parameter -> one-line description) for the HelpButton popups.
# --------------------------------------------------------------------------- #
HELP: Dict[str, Dict[str, str]] = {
"Catalog": {
"CATALOG_SOURCE": "Which cached NASA Exoplanet Archive subset to load: "
"NEA (all confirmed), TESS (TESS-discovered), or BOTH.",
"FETCH_NEA_ONLINE": "If checked, try the NASA TAP service first and fall "
"back to the local CSV on failure.",
"NEA_FETCH_TIMEOUT": "Seconds to wait for the online TAP response.",
"CATALOG_DIR": "Directory holding the PS_latest_{source}.csv cache files.",
},
"Filters": {
"DESERT_FILTER": "Restrict to planets inside ('desert') / outside "
"('nondesert') the Neptunian-desert polygon, or 'all'.",
"EXTRA_FILTERS": "Sequential AND filters on catalog columns. Each row "
"keeps rows where column is within [min, max] (or, with "
"'contains', where the column text matches).",
},
"Observatory": {
"preset": "Pick a built-in telescope/instrument to auto-fill every field "
"below, or 'New' to enter your own values.",
"name": "Short label used in output filenames.",
"telescope": "Telescope name (documentation only).",
"instrument": "Instrument name (documentation only).",
"lat": "Observatory latitude [degrees, +N].",
"lon": "Observatory longitude [degrees, +E].",
"alt": "Observatory altitude [metres].",
"timezone": "IANA timezone name (e.g. America/Santiago) for local-time axes.",
},
"Constraints": {
"start": "Proposal window start date (YYYY-MM-DD).",
"end": "Proposal window end date (YYYY-MM-DD).",
"min_target_alt": "Minimum target elevation [deg] for observability.",
"max_sun_alt": "Maximum Sun altitude [deg]; -10 is NEA twilight, -18 is "
"astronomical night.",
"moon_dist_factor": "Moon-distance guard (0 disables the check).",
"transit": "Fraction of the transit that must be observable (1.0 = full).",
"pre_eclipse": "Fraction of the pre-eclipse window that must be observable.",
"post_eclipse": "Fraction of the post-eclipse window that must be observable.",
"TIME_RESOLUTION_MIN": "Grid step [minutes] for the night-by-night sampling.",
"PRESELECTION_MIN_YEAR": "Only planets whose oldest reference year exceeds "
"this are listed in preselection.csv.",
},
"Custom Planets": {
"CUSTOM_PLANETS": "A JSON list of hand-entered targets injected into the "
"schedule in addition to the filtered catalog. Each list "
"item is one planet object. Leave as [] to schedule only "
"the catalog. For a target already in the catalog, any "
"field you set here overrides the catalog value.",
"pl_name": "Planet name (required); also used for the output subfolder.",
"pl_orbper": "Orbital period [days], sets the phase/ephemeris.",
"pl_rade": "Planet radius [R_earth].",
"pl_bmasse": "Planet mass [M_earth]; if omitted a mass-radius relation is used.",
"pl_eqt": "Equilibrium temperature [K] (drives TSM/ESM).",
"st_teff": "Stellar effective temperature [K].",
"st_mass": "Stellar mass [M_sun] (needed for the Kp computation).",
"st_rad": "Stellar radius [R_sun].",
"st_jmag / st_kmag": "Stellar J / K magnitudes (feed TSM/ESM).",
"ra / dec": "Right ascension / declination [deg] (needed for visibility).",
"pl_tranmid": "Transit mid-time [BJD] (needed for the ephemeris).",
"pl_orbsmax": "Semi-major axis [AU] (optional; else from Kepler's 3rd law).",
"pl_orbeccen": "Orbital eccentricity (optional). Leave it null, or "
"omit it, to treat the orbit as circular; that is also "
"what happens below 0.01, where the eccentric and "
"circular radial-velocity traces are indistinguishable.",
"pl_orblper": "Argument of periastron of the planet [deg] (optional). "
"Leave it null, or omit it, when it is unknown: with a "
"non-zero eccentricity the planetary radial velocity is "
"then shown as an envelope over all possible values "
"rather than assuming one, and the transit geometry "
"(T14, phi_sec) is flagged as unreliable in the log.",
"v_sys": "Systemic radial velocity [km/s] for the telluric-overlap plot.",
"Buttons": "Validate JSON checks the text parses; Insert example appends a "
"template planet; Clear empties the list.",
},
"Telluric": {
"SKY_TRANSMISSION_FITS": "Sky-transmission FITS for the telluric-overlap "
"page. Leave blank to disable that page.",
"TELLURIC_LAMBDA_RANGE_NM": "Wavelength slice (start, stop) [nm] to display.",
"TELLURIC_RV_GRID_KMS": "Radial-velocity grid (start, stop, step) [km/s].",
"TELLURIC_PAD_HOURS": "Hours padded either side of the synthetic window.",
"TELLURIC_EXP_TIME_S": "Synthetic exposure cadence [seconds].",
},
"Run": {
"Overview": "This tab runs the scheduling pipeline with the current form "
"values and shows its progress. Settings are collected from "
"all other tabs, written to a settings.json inside OUTPUT_DIR, "
"and executed in a background process.",
"Run": "Collect the configuration and start the pipeline. Disabled while a "
"run is in progress. If OUTPUT_DIR already holds results, a prompt "
"asks whether to delete them and run a fresh search (No cancels "
"without starting).",
"Stop": "Terminate the running pipeline process.",
"Save preset": "Write the current form values to a JSON file you choose.",
"Load preset": "Repopulate every tab from a previously saved JSON preset.",
"Reset defaults": "Restore all fields to the bundled default configuration.",
"Pipeline log": "Live standard output of the pipeline (catalog loading, "
"per-planet events, saved files, warnings).",
"Generated files": "The PDFs / CSVs produced by the run. Double-click a row "
"to open that file in your system viewer.",
},
"Output & Plot": {
"OUTPUT_DIR": "Root folder for all generated PDFs / CSVs.",
"AUX_DIR": "Folder holding desert_boundaries.txt and the KDE npz.",
"DESERT_BOUNDARY_FILE": "Castro-Gonzalez (2024) desert polygon file.",
"KDE_FILE": "KDE background density for the period-radius landscape.",
"P_MAX": "Landscape x-axis (period) upper limit [days].",
"R_MAX": "Landscape y-axis (radius) upper limit [R_earth].",
},
}
# --------------------------------------------------------------------------- #
# Defaults
# --------------------------------------------------------------------------- #
[docs]
def load_defaults() -> Dict[str, Any]:
"""Return a deep copy of every exposed config global's default value."""
pc = _import_phase_config()
defaults: Dict[str, Any] = {}
for key in CONFIG_KEYS:
defaults[key] = copy.deepcopy(getattr(pc, key))
# Normalise tuples to lists so the value round-trips through the GUI/JSON.
for key in ("TELLURIC_LAMBDA_RANGE_NM", "TELLURIC_RV_GRID_KMS"):
defaults[key] = list(defaults[key])
return defaults
# --------------------------------------------------------------------------- #
# JSON serialisation
# --------------------------------------------------------------------------- #
[docs]
def normalise(values: Dict[str, Any]) -> Dict[str, Any]:
"""Return a JSON-serialisable copy (tuples -> lists)."""
out = copy.deepcopy(values)
for key in ("TELLURIC_LAMBDA_RANGE_NM", "TELLURIC_RV_GRID_KMS"):
if key in out and isinstance(out[key], tuple):
out[key] = list(out[key])
return out
[docs]
def write_settings(path: str, values: Dict[str, Any]) -> None:
with open(path, "w") as f:
json.dump(normalise(values), f, indent=2)
[docs]
def read_settings(path: str) -> Dict[str, Any]:
with open(path) as f:
return json.load(f)