Source code for specsrbench.figures

"""One module per paper figure, and the registry the CLI dispatches through.

Each module exposes ``build(cache=None, outdir=None) -> Path``: it draws its
figure, prints the numbers the paper quotes from it, and returns the path it
wrote.  Nothing else in the package imports one figure from another, so a
figure can be rebuilt on its own without dragging the rest of the suite in.

These were six Jupyter notebooks until 2026-08-25.  Notebooks were a poor fit
for a product that has to rebuild identically: their outputs are committed
alongside their code, so a stale number can sit in a diff looking like data;
they cannot be imported, so five of the six carried a copied ``load()`` helper
and colour table; and "run all the figures" meant driving ``nbconvert`` and
parsing stdout to find out whether it had worked.
"""
from __future__ import annotations

from importlib import import_module
from pathlib import Path

__all__ = ["REGISTRY", "build", "build_all", "FIGURE_FILES"]

#: figure name -> (module, output filename).  The name is what the CLI takes.
REGISTRY: dict[str, tuple[str, str]] = {
    "toy":          ("fig1_toy_methods",   "fig_toy_1d.pdf"),
    "qualitative":  ("fig2_qualitative",   "fig_jades_qualitative.pdf"),
    "residuals":    ("fig3_residual_maps", "fig_residual_maps.pdf"),
    "mae":          ("fig4_mae_summary",   "fig_mae_summary.pdf"),
    "per-line-snr": ("fig5_per_line_snr",  "fig_jades_per_line_snr.pdf"),
    "redshift":     ("fig6_redshift_mae",  "fig_redshift_mae.pdf"),
}

#: Paper figure number -> registry name, for ``specsrbench figures 4``.
BY_NUMBER: dict[str, str] = {
    "1": "toy", "2": "qualitative", "3": "residuals",
    "4": "mae", "5": "per-line-snr", "6": "redshift",
}

FIGURE_FILES: tuple[str, ...] = tuple(f for _, f in REGISTRY.values())


def resolve(name: str) -> str:
    """Accept a registry name, a paper figure number, or ``fig4``."""
    n = name.strip().lower().removeprefix("figure").removeprefix("fig").strip("_- ")
    if n in REGISTRY:
        return n
    if name.strip().lower() in REGISTRY:
        return name.strip().lower()
    if n in BY_NUMBER:
        return BY_NUMBER[n]
    raise KeyError(
        f"unknown figure {name!r}; choose from "
        f"{', '.join(REGISTRY)} or 1-{len(BY_NUMBER)}")


[docs] def build(name: str, cache=None, outdir: Path | None = None) -> Path: """Build one figure by name or number.""" key = resolve(name) module, _ = REGISTRY[key] mod = import_module(f".{module}", __package__) return mod.build(cache=cache, outdir=outdir)
[docs] def build_all(cache=None, outdir: Path | None = None, skip: tuple[str, ...] = ()) -> dict[str, Path]: """Build every figure, in paper order, returning what was written.""" out: dict[str, Path] = {} for number, key in BY_NUMBER.items(): if key in skip: continue print(f"\n{'=' * 78}\nFigure {number}: {key}\n{'=' * 78}") out[key] = build(key, cache=cache, outdir=outdir) return out