Final-Policy Evaluation¶
Training-window reward and cost average a sequence of changing policies. They do not measure the safety of the policy at the final update. Final-policy evaluation freezes each trained policy and measures it separately, on fresh evaluation seeds, without adding episodes to the training history.
There are two distinct measurements:
| Measurement | What it is | Available for |
|---|---|---|
| Live frozen rollouts | IID episodes of the frozen joint policy in the real environment. Reports reward, violations per step, survival, and U_H = P(T_unsafe <= H) at declared horizons, with Wilson intervals. |
Every algorithm, including OmSh |
| Exact reachability | Finite- and infinite-horizon unsafe reachability of a stationary frozen policy on the exact transition graph. | IPPO, IPPO-Lagrangian, CPO |
Neither measurement turns a baseline into a guaranteed method. The exact result is a property of one frozen policy in one exact benchmark model. It says nothing about safety during training or about later opponent drift.
Enable It in Training¶
Set the evaluation keys on the experiment notebook. With
final_policy_eval_episodes=0 (the default), no frozen evaluation runs.
uv run python notebooks/matrix/chicken/experiments_mo.py \
--run-ippo-shielded=true --run-cpo=true \
--experiment-tag=chicken_final_v1 --seed-offset=2200 --num-runs=5 \
--timesteps=250000 \
--final-policy-eval-episodes=1000 \
--final-policy-eval-seed-offset=2200000
| Key | Meaning | Default |
|---|---|---|
final_policy_eval_episodes |
Frozen evaluation episodes per run; 0 disables it. |
0 (Congestion 2,000) |
final_policy_eval_horizons |
Horizons H at which U_H is reported. |
(1, 10, 50, 100, 200) |
final_policy_eval_seed_offset |
First evaluation seed. Run i uses its own block. |
1,000,000 |
final_policy_eval_confidence |
Wilson interval level. | 0.95 |
Congestion has a 64-step episode cap, so its campaign uses horizons
(1, 2, 10, 64). Keep the evaluation seed block disjoint from every other
campaign so live samples are independent.
Per-run reports are written below
experiments/runs/<tag>/final_policy/<artifact>_run<N>.json. Evaluation time
is recorded separately from training time.
Evaluate as Trained¶
Shielded IPPO samples only within the shield's admissible set, so a frozen
OmSh policy is evaluated with the same action masks it was trained under.
Every live report records "action_selection": "training_masks", and reports
without it are rejected for OmSh.
Reports before 2026-09-24
The earlier evaluator sampled unmasked logits and let the shield rewrite inadmissible proposals. Safety was unaffected, but reward measured the shield's replacement rule rather than the trained policy. Re-evaluate such conditions without retraining:
uv run python scripts/reevaluate_omsh_final_policies.py \
--environment-name markov_stag_hunt --tag <tag> \
--artifact-stem ippo_shielded_histories
Superseded reports move to final_policy/superseded_unmasked/; nothing is
deleted.
OmSh is stateful: its opponent posterior, monotone floor, and patience
counters persist across environment resets. The training checkpoint stores
that post-training controller state, and the evaluator restores the same
snapshot before every episode. Replicates are therefore IID
(episode_sampling=iid_controller_snapshot) rather than one adaptive
sequence.
Exact Post-Processing for Baselines¶
The exact reachability columns come from a separate post-processor. For a complete tag, one job evaluates IPPO, IPPO-Lagrangian, and CPO sequentially, so the exact graph is loaded once:
uv run python scripts/evaluate_final_campaign_baselines.py \
--environment-name chicken --tag <tag> \
--horizons 1 10 50 100 200 \
--evaluation-seed-offset 2200000 --episodes 1000
Congestion uses --horizons 1,2,10,64 --evaluation-seed-offset 4200000
--episodes 2000, and at any fleet size other than two also --n-agents N.
When the evaluation seed block matches the training-time live reports, those
reports are reused and only the exact quantities are computed. The output is
final_policy/<stem>_safety.json beside the per-run reports. Memory follows
the exact graph: about 64 GB for Markov Stag Hunt, 32 GB for Gathering, 16 GB
for Pursuit, and 8 GB otherwise.
Executed OmSh has no exact column. Its actions depend on the carried budget, posterior, and floor, which the public-state graph does not represent. The evaluator rejects the bare neural actor rather than labelling it OmSh. For OmSh, report the live frozen risk next to its model-relative certificate and the risk-dominance audit.
Summarize a Campaign¶
uv run python scripts/summarize_final_policy_campaign.py \
--tag <tag> --exports-root exports --expected-runs 5
It writes final_policy_campaign.{json,md}, final_policy_compact.tex,
final_policy_all_conditions.tex, and timing tables below
exports/reports/<tag>/. The compact table gives each primary method as
reward per step / worst-seed U_H / exact eventual unsafe reach.
A condition is complete only with exactly --expected-runs valid reports. An
environment enters the paper tables only when IPPO, IPPO-Lagrangian, CPO, and
OmSh-Monotone are all complete at the same largest horizon. Partial campaigns
still render, with explicit missing and incomplete status.
Seed Shards¶
Long OmSh conditions can run one seed per job. Give each shard its own
one-run tag, trainer seed s + i, and evaluation offset
base + i * episodes, so the merged result equals a serial five-seed run:
uv run python scripts/merge_final_policy_shards.py <destination_tag> \
--shard-tag-template '<shard_prefix>_seed{trainer_seed}' \
--seed-offset 2200 --expected-runs 5 \
--evaluation-seed-offset 2200000 --evaluation-episodes 1000
The template may use {run_index} and {trainer_seed}. Run the merge
against a scratch exports root if the destination tag also holds baseline
results.
If the destination's seed 0 came from a serial job stopped before it wrote its completion manifest, merge with `--recover-missing-manifests --risk-threshold
`. The manifest is rebuilt only if the checkpoint, trainer seed, and controller state match the live report. A merged tag holds only the reports; checkpoints stay in the shard tags. Anything that replays checkpoints, such as the risk-dominance audit, runs once per tag and is then combined; see [Analysis and Audits](analysis-and-audits.md#audits-of-frozen-deployments). If a job wrote its checkpoint but died before publishing the report or manifest, `scripts/recover_omsh_final_policy_checkpoint.py` (OmSh) and `evaluate_final_policy_checkpoints.py --live-only` (baselines) replay the declared evaluation from the checkpoint. Recovery needs a schema-3 checkpoint with the controller state. ## Paired Comparisons Several scripts compare frozen evaluations on the same trainer seeds: | Script | Compares | | --- | --- | | `compare_final_policy_campaigns.py` | Two campaigns that differ only in the world model. | | `summarize_replacement_ablation.py` | Parameterized shield variants against the pure OmSh control. | | `decide_replacement_screen.py` | Applies a predeclared promotion rule to a paired screen. | | `summarize_patience_screen.py` | Floor-patience arms against the patience-64 control. | | `summarize_padded_runs.py` | Padded-shield runs against the campaign OmSh runs. | See [Analysis and Audits](analysis-and-audits.md) for the full catalogue and the maintainer note on [frozen final-policy safety](../AI/reinforcement-learning/final-policy-safety-evaluation.md) for checkpoint schemas and solver details.