runner — sim runner / controller dispatch front-end

Purpose

Thin front-end from a run-spec YAML to a finished run: load the spec, build a RunContract,
dispatch to the right controller (base/com/arm) or a guidance-only rollout, return a RunResult.
One run in, one logged result out — it owns dispatch, not the analyze→plot→report loop.

Role in the system

  • Driven by orchestrator (the pipeline loop): orchestrator builds contexts and calls Runner.run;
    runner is never reached across module boundaries by logger/plotter/reporter.
  • Spec loading delegates to pre_run_loader (RunSpecLoader.prepare → spec, built cfg, ctx, names).
  • Live path constructs a GNC controller — base_controller (base), com_controller (com),
    or breve_controller (arm, the live workhorse) — and calls its run_all.
  • Guidance-only path uses ee_guidance + guidance_rollout (build_guidance_only_rollout): selection /
    coverage without controller dynamics.
  • Output flows into logger (Logger/LogStore) → NPZ → data_analyzer.

Inputs / Outputs

  • In: a run-spec stem (default "run"); optional per-run override ctx (a config-override object).
  • Out: RunResult — the prepared contract (spec, cfg, ctx, run_name, run_dir) plus the
    populated logger and log_path.

Key methods / functions

  • Runner.run — prepare → safety-check → execute → wrap as RunResultanalysis/runner.py:77
  • Runner.prepared_context — prepared contract for an override ctx, cached by id(ctx)analysis/runner.py:61
  • Runner.confirm_live_run_is_safe — interactive guard before a full run discards its log — analysis/runner.py:83
  • Runner.run_live — branch to controller vs guidance-only rollout — analysis/runner.py:98
  • Runner.make_controller — select the controller class by controller.nameanalysis/runner.py:107
  • Runner.run_guidance_only — EE guidance rollout with no controller dynamics — analysis/runner.py:133
  • RunContract / RunResult — the prepared-inputs / finished-run dataclasses — analysis/runner.py:14 / :25

Footguns

make_controller mutates sys.path (GNC sibling imports)

GNC modules import each other with bare sibling names (from com_guidance import …), so make_controller
inserts GNC/ at the front of sys.path before importing any controller. The insertion is guarded to be
idempotent. (analysis/INSIGHTS.md [run][io])

prepared_context caches by id(ctx) — re-prepares on identity drift

Prepared contracts are cached keyed on id(ctx), and re-prepared if the cached object identity no longer
matches. This prevents a reused Runner from serving stale config across runs with different override
dicts. (analysis/INSIGHTS.md [run])

A full run with save_log=False and no debug_time_limit would silently discard everything

confirm_live_run_is_safe is the last safety net before a long sim throws away all its output: it
prompts interactively and aborts on any non-Y input. A debug_time_limit (short debug run) or
save_log=True bypasses the prompt. (analysis/INSIGHTS.md [run])

Pseudocode (one run)

Runner(stem):
    prepared = RunSpecLoader(stem).prepare()      # spec, built cfg, ctx, run_name/dir
    self.prepared_run = RunContract(prepared)

run(ctx=None):
    prepared = prepared_context(ctx)              # cache by id(ctx); re-prepare on drift
    confirm_live_run_is_safe(prepared)            # abort a logless full run unless confirmed
    run_data = run_live(prepared):
        if not controller.enable:  return guidance_only rollout
        ctrl = make_controller(prepared)          # base | com | arm  (inserts GNC/ on sys.path)
        ctrl.run_all(debug_time_limit)
    return RunResult(prepared, run_data.logger, run_data.log_path)

orchestrator · pre_run_loader · logger · breve_controller · com_controller ·
base_controller · ee_guidance · guidance_rollout · data_analyzer · terminology