Source code for uvex_transients.cli.pipeline

"""
Plain functions implementing each CLI stage, kept free of `click` so they're directly testable.

`uvex_transients.cli.main`'s subcommands are thin wrappers around these -- each one parses
arguments, calls one of these, and reports a short summary.
"""

from collections.abc import Iterable
from pathlib import Path

from astropy.table import QTable

from ..simulation.event_catalog import EventCatalog
from ..simulation.exposure_catalog import ExposureCatalog
from ..simulation.photometry_catalog import PhotometryCatalog
from ..simulation.yield_table import YieldTable
from .config import RunConfig


[docs] def run_generate(config: RunConfig) -> EventCatalog: """ Sample a Monte Carlo `EventCatalog` per the config's ``generate:`` section. Parameters ---------- config : RunConfig The parsed run-config. Returns ------- EventCatalog The generated catalog. """ settings = config.generate return config.simulator.generate_events( time_bins=settings.time_bins, nside=settings.nside, order=settings.order, downsample=settings.downsample, )
[docs] def run_exposure(config: RunConfig) -> ExposureCatalog: """ Tabulate an `ExposureCatalog` per the config's ``generate:`` section. Uses the same ``time_bins``/``nside``/``order`` as `run_generate`, so both describe the same footprint query -- see `SurveySimulator.compute_effective_exposure`. Parameters ---------- config : RunConfig The parsed run-config. Returns ------- ExposureCatalog The tabulated per-(transient type, time bin) exposure. """ settings = config.generate return config.simulator.compute_effective_exposure( time_bins=settings.time_bins, nside=settings.nside, order=settings.order, )
[docs] def run_yield_summary( config: RunConfig, raw: EventCatalog, detected: EventCatalog, exposure: ExposureCatalog, ) -> YieldTable: """ Build a `YieldTable` per the config's ``yield:`` section (Clopper-Pearson confidence level). Parameters ---------- config : RunConfig The parsed run-config. raw : EventCatalog The feasible (pre-cut) catalog -- typically `run_generate`'s own output. detected : EventCatalog The detected (post-cut) catalog -- typically `run_cuts`'s own output. exposure : ExposureCatalog Typically `run_exposure`'s own output. Returns ------- YieldTable One row per transient type in `config.transients`. """ settings = config.yield_config return raw.compute_yield_summary(detected, exposure, config.transients, confidence=settings.confidence)
[docs] def run_cuts(config: RunConfig, catalog: EventCatalog, names: list[str] | None = None) -> EventCatalog: """ Run one or more of the config's declared ``cuts:`` against `catalog`, chained in order. Parameters ---------- config : RunConfig The parsed run-config. catalog : EventCatalog The catalog to filter. names : list of str, optional Which of the config's `cuts:` keys to run, and in what order. If `None` (the default), runs every declared cut, in the order it was declared in the config. Returns ------- EventCatalog The filtered catalog. """ cuts = config.cuts if names is None: selected = list(cuts) else: unknown = [name for name in names if name not in cuts] if unknown: raise ValueError(f"Unknown cut key(s) {unknown}; available: {list(cuts)}.") selected = list(names) for key in selected: spec = cuts[key] catalog = config.simulator.run_cut(spec.type, catalog, config.mission, **spec.params) return catalog
[docs] def run_photometry(config: RunConfig, catalog: EventCatalog) -> QTable: """ Run synthetic photometry over every event in `catalog` per the config's ``photometry:`` section. Parameters ---------- config : RunConfig The parsed run-config. catalog : EventCatalog The catalog of events to simulate photometry for. Returns ------- ~astropy.table.QTable One row per (event, time, band) synthetic observation. """ settings = config.photometry return catalog.simulate_photometry( config.mission, config.transients, config.schedule, bands=settings.bands, n_sigma=settings.n_sigma, )
[docs] def run_detection_counts( config: RunConfig, catalog: EventCatalog, exposure: ExposureCatalog, photometry: PhotometryCatalog | QTable, ) -> QTable: """ Build the detection-count estimator table per the config's ``detection_counts:`` section. Parameters ---------- config : RunConfig The parsed run-config. catalog : EventCatalog The full per-type event list `photometry` was computed over -- forwarded to `PhotometryCatalog.compute_detection_count_table`. exposure : ExposureCatalog Typically `run_exposure`'s own output. photometry : PhotometryCatalog or ~astropy.table.QTable Typically `run_photometry`'s own output, or a `PhotometryCatalog` wrapping it; a bare `QTable` (the shape `run_photometry` itself returns) is wrapped automatically. Returns ------- astropy.table.QTable One row per ``(transient_type, n_detections)`` pair; see `PhotometryCatalog.compute_detection_count_table`'s own docstring for the column list. """ settings = config.detection_counts if not isinstance(photometry, PhotometryCatalog): photometry = PhotometryCatalog(table=photometry) return photometry.compute_detection_count_table( catalog, exposure, config.transients, snr_threshold=settings.snr_threshold, confidence=settings.confidence, )
[docs] def dry_run_report( config: RunConfig, command: str, outputs: Iterable[Path] = (), overwrite: bool = False, cut_names: list[str] | None = None, ) -> tuple[list[str], bool]: """ Validate `config` for `command` and describe what it would do, without sampling or writing anything. Resolving each section the command needs is itself the validation: an unknown transient class, a bad parameter override, an unknown cut type or cut name, an unknown mission, or an unreadable schedule all raise exactly as they would in a real run. The (potentially large) schedule *is* loaded, but no events are sampled and no output file is created. Parameters ---------- config : RunConfig The parsed run-config. command : {"generate", "cut", "photometry", "detection-counts", "run"} Which command is being dry-run; decides which config sections are validated. ``"run"`` validates ``generate:``, ``cuts:`` (if declared), ``photometry:``, and ``detection_counts:`` (if declared). outputs : iterable of pathlib.Path, optional The files the real command would write, checked for collisions. overwrite : bool, optional Whether the real command would be run with ``--overwrite``. cut_names : list of str, optional For ``"cut"``, which of the config's cuts would run (default: every declared cut). Returns ------- lines : list of str The report, one line per entry. ok : bool `False` if the real command would fail on an output file that already exists. """ source = f" (config: {config._source})" if config._source else "" lines = [f"dry run: {command}{source}"] lines.append(f"mission: {config.mission.name}") lines.append(f"schedule: {len(config.schedule)} scheduled actions") transients = config.transients lines.append(f"transients ({len(transients)}):") for key, transient in transients.items(): lines.append( f" {key:<20s} {type(transient).__name__:<34s} sed={type(transient.sed).__name__:<32s} " f"z<={transient.redshift_limit} duration={transient.duration_limit}" ) if command in ("generate", "run"): settings = config.generate seed = (config._raw.get("generate") or {}).get("seed") lines.append( f"generate: time_bins={settings.time_bins}, nside={settings.nside or 'default'}, " f"order={settings.order or 'default'}, downsample={settings.downsample or 'none'}, seed={seed}" ) if command == "cut" or (command == "run" and config.has_section("cuts")): cuts = config.cuts selected = list(cuts) if cut_names is None or command == "run" else list(cut_names) unknown = [name for name in selected if name not in cuts] if unknown: raise ValueError(f"Unknown cut key(s) {unknown}; available: {list(cuts)}.") lines.append(f"cuts ({len(selected)}, in order):") lines.extend(f" {key:<20s} {cuts[key].type:<20s} {cuts[key].params}" for key in selected) if command in ("photometry", "run"): settings = config.photometry lines.append(f"photometry: bands={settings.bands or 'every band'}, n_sigma={settings.n_sigma or 'default'}") if command == "run": lines.append(f"yield: confidence={config.yield_config.confidence}") if command == "detection-counts" or (command == "run" and config.has_section("detection_counts")): settings = config.detection_counts lines.append(f"detection_counts: snr_threshold={settings.snr_threshold}, confidence={settings.confidence}") ok = True outputs = list(outputs) if outputs: lines.append("outputs:") for path in outputs: if path.exists() and not overwrite: lines.append(f" WOULD FAIL {path} already exists (pass --overwrite)") ok = False else: lines.append(f" would write {path}") lines.append("nothing was sampled or written.") return lines, ok