pre_run_loader — the config gateway

Purpose

The single gateway through which every sim run and analysis job gets its configuration.
Merges a run-spec YAML over the defaults, applies ordered config overrides into the cfg
namespace, and resolves the metrics.yaml (catalog) and scoreboard.yaml (judgment) specs.

Role in the system

  • RunSpecLoader(stem).prepare() is called by runner / orchestrator at the head of the
    pipeline (pre_run_loader → runner → logger → NPZ → data_analyzer → plotter/star_reporter).
  • MetricSpecLoader is consumed by metric_catalog (and verified by metric_alignment) — the
    what is measured catalog feeding data_analyzer.
  • ScoreboardSpecLoader is the parser for the how it is judged layer; the completeness contract in
    logger (scoreboard_metric_coverage) and the console run_scoreboard.py read these specs.
  • Reads YAML via utils.infra.load_spec/RecursiveNamespace; builds gains/dt/camera derived params
    via utils.parameter_loader.

Inputs / Outputs

  • In: a run-spec stem (analysis_YAMLs/<stem>.yaml), the default run + figure specs, the global
    parameters.yaml, and YAMLs_by_domain/{metrics,scoreboard}.yaml; an optional RunContext (sweep
    parameter/value, run dir).
  • Out: a Package of {spec, cfg, ctx, run_name, run_dir} for the runner; a MetricSpec tuple +
    source config; a ScoreboardSpec tuple + by-name index.

Key methods / functions

  • RunSpecLoader.prepare — resolve spec, name, context, and overridden cfg for one run — analysis/pre_run_loader.py:298
  • RunSpecLoader.ordered_overrides — run-spec overrides, then context pairs (context wins) — analysis/pre_run_loader.py:330
  • RunSpecLoader.apply_overrides — set each override; re-derive gains/dt/camera only when gated — analysis/pre_run_loader.py:340
  • RunSpecLoader.set_config_value — validated dotted-path set (the only sanctioned hasattr traversal) — analysis/pre_run_loader.py:357
  • MetricSpecLoader.walk_metric_specs — recurse the catalog tree, inheriting context per leaf — analysis/pre_run_loader.py:118
  • ScoreboardSpecLoader.load_scoreboard — resolve gates/weights/direction into specs — analysis/pre_run_loader.py:223
  • merge_mapping_defaults — recursive deep-merge of raw spec over defaults — analysis/pre_run_loader.py:274

Footguns

Snapshot traps: post-load overrides on baked params are silently inert

Three families of leaves are compiled/baked at load time and an override against the snapshot
does nothing unless a re-derive fires. apply_overrides detects each prefix and re-derives only then
(byte-identical otherwise):

  • Gains (controller.{com,base,arm}.gains.*) → compiled to matrices; override the built
    cfg.controller.gains.base.K, not the raw leaf. Re-derived by build_controller_gains.
  • dt (simulation.time.dt) → feeds EE-goal alphas, startup step count, CoM blend baked in
    finalize; a dt/2 sweep would integrate at the new step but steer by default-dt references.
    Re-derived by rederive_dt_dependent.
  • Camera (...sampling.radius_scale, targeting.reach.*, targeting.camera.*) → camera_radius
    and the cos/FOV constants baked at load; a radius_scale sweep once ran SILENTLY as the default
    rs0.40 (Jun 19). Re-derived by rederive_camera_radius.

(analysis/INSIGHTS.md [footgun] ×3; MEMORY controller-gains-built-matrix-path.)

set_config_value uses hasattr deliberately

This is the only permitted exception to the YAML.md dot-access rule: it must validate an
arbitrary user-supplied dotted path before setting it, raising loudly on a typo. Elsewhere, use
dot access, never hasattr/fallback getattr. (analysis/INSIGHTS.md [config])

Catalog vs judgment are separate files with separate parsers

metrics.yaml is the variable catalog (what is measured); scoreboard.yaml is the judgment layer
(reduction, direction, gate, weight, meaning, after: window). MetricSpecLoader and
ScoreboardSpecLoader own them respectively — don’t cross the wires. A catalog leaf missing
variable: raises via reject_missing_variable. (analysis/INSIGHTS.md [config])

Pseudocode (prepare one run)

spec  = load_spec(stem.yaml, merge_dict=default_run + default_figure)   # deep-merged defaults
ctx   = ctx or RunContext(name=run_name)                               # name | sweep param/value | run_dir
cfg   = load_parameters()                                              # global parameters.yaml
overrides = list(spec.run.config_overrides) + ctx.(param,value) pairs  # context pairs win (appended last)
for o in overrides: set_config_value(cfg, o.path, o.value)            # hasattr-validated dotted set
if any gain leaf touched:   cfg.controller.gains = build_controller_gains(...)   # else byte-identical
if dt touched:              rederive_dt_dependent(cfg)
if any camera leaf touched: rederive_camera_radius(cfg)
return Package(spec, cfg, ctx, run_name, run_dir)

runner · orchestrator · logger · metric_catalog · data_analyzer · terminology