True-Policy Exact Shield Baseline¶
IPPO_TrueShielded is a separate baseline from the learned WM/OM shielding path.
It is also distinct from the theorem-facing
exact-anytime-adversarial diagnostic. This
baseline marginalizes opponent actions using current live policies. The anytime
certificate instead preserves every opponent-action row and takes their
adversarial maximum; do not use this true-policy bundle as evidence for an
arbitrary-opponent guarantee.
Intent¶
This baseline answers a narrower question than the main architecture:
- what happens if the shield has exact environment transition probabilities
- and exact access to the current opponent policies used by IPPO training
It is meant as a reference baseline, not as a replacement for world modelling or opponent modelling.
Artifact flow¶
The baseline uses a dedicated cache under exports/.../true_shield/:
- build or load
true_transition_graph.pkl - validate it against
true_transition_graph.metadata.pkl - create a
TruePolicyTransitionShieldaround the labelled env - initialize that wrapper with a conservative one-level bundle built from uniform opponent policies
- let IPPO push live opponent policies into the wrapper during training
Unlike IPPO_Shielded, this path does not read wm/env_transition_graph.pkl
or om/iop_stack.pt.
The true-graph cache is validated against the current public-state shape, a small environment dynamics signature, and source hashes for the true/exact graph builders. If an environment's public Markov contract or cached dynamics-relevant configuration changes, the cached true graph is rebuilt automatically instead of being silently reused.
Graph validation, construction, and publication are serialized by a process lock next to true_transition_graph.pkl, because IPPO, Lagrangian, and CPO post-processors for one environment may become eligible together. The graph and metadata sidecar are each staged in the cache directory and atomically replaced while the lock is held. A truncated legacy graph or metadata pickle is treated as a miss and rebuilt.
true_transition_graph.pkl is intentionally stored as a plain pickled
src.joint_graph.common.Graph, because the runtime wrapper reloads that path
through src.joint_graph.load_graph(...). Cache validation metadata lives in
the sidecar file instead of wrapping the graph object. The loader still accepts
the older wrapped cache artifact and migrates it forward on first reuse.
Exact dynamics graph¶
src.true_shield.exact_graph.build_true_transition_graph(...) dispatches over
the same supported env families as the exact joint-graph legality path:
- matrix envs
markov_stag_huntgatheringpursuitice_duel
For deterministic envs, the true graph is the legal graph annotated with
env_prob=1.0. For stochastic envs, the builder reuses the exact successor
distribution helpers from src/joint_graph/exact/* and stores those
probabilities directly on the graph edges.
Rewards are zero-filled and intentionally ignored by the shield path. The graph exists only to support exact unsafe-reachability analysis.
Live policy refresh semantics¶
TruePolicyTransitionShield reuses TransitionShield, but with exactly one
level and no opponent-mixture updates.
- there is no
iop_stack.pt - there is no posterior update after each environment step
level_flooris always effectively0
The wrapper exposes maybe_refresh_true_shield(update_idx, policies). Plain
IPPO calls that hook:
- once immediately after runtime initialization with
update_idx=0 - once after every PPO update cycle with the incremented update index
The refresh interval is configured in PPO-update units via
true_policy_refresh_interval_updates. On refresh, the wrapper rebuilds a
one-level bundle from the cached exact graph plus the latest live
PolicyAdapters for all non-focal agents, but it does not immediately replace
the active shield bundle. The rebuilt bundle is stored as pending and installed
at the next reset().
This reset-boundary install is intentional. The shield carries a shrinking eventual-risk budget within an episode; replacing the true-policy model under that partially spent budget invalidates the old budget certificate. Deferring the swap keeps each episode certified under one fixed true-policy bundle while still letting PPO updates schedule fresher bundles for subsequent episodes.
Large gridworld exact graphs, especially Markov Stag Hunt, should use a coarse
refresh interval. Rebuilding the true-policy bundle requires marginalizing the
exact joint graph and rerunning sound value iteration. The MSH experiment
defaults to 64 PPO updates between live-policy refreshes, which gives a first
live-policy refresh around 131k environment steps with the current rollouts=2048
setting.
The true-policy bundle builder now marginalizes the exact joint graph directly
against the supplied opponent policies. It no longer creates a full intermediate
joint graph annotated with opp_prob/prob, which reduces peak memory on large
graphs while preserving the induced focal-MDP semantics used by the shield.
The notebook-side true-shield factory also probes reset() once during setup.
That means impossible initial budgets now fail in the setup phase with the same
infeasibility diagnostics as learned shielding, instead of failing only after
training has already started.
Why this stays separate¶
Keeping IPPO_TrueShielded separate avoids muddying the main pipeline:
- WM/OM shielding still measures learned-model performance
- true shielding measures an oracle-style upper bound with exact dynamics and exact opponent policies
That separation is useful when comparing:
- learned uncertainty vs exact dynamics
- learned opponent reasoning vs true live policies
- architectural gaps in the learned pipeline
It does not guarantee feasibility. If the exact environment plus safety label already makes the reset state violate the requested eventual-risk budget, true shielding will fail too.
One concrete cache failure mode was found in 2-agent Pursuit after switching to
terminal-on-collision dynamics. The old true_transition_graph.pkl had been
built before that switch and still let collision states transition onward, so
the setup probe reported reset risk 1.0 even though the live env passed
terminates_after_collision=True. A fresh true graph has collision states as
self-looping safe terminal states; under the uniform bootstrap opponent policy,
the default reset state's eventual unsafe risk is about 2.07e-4, so the
max_risk=0.2 setup is feasible. The expanded cache metadata prevents reusing
that stale graph class.
That repeated-game caveat still applies in general, but the current matrix envs have env-specific repairs:
- Bertrand now treats only
price_waras unsafe. - Congestion now includes a safe
Detourfallback action.
So both exact baselines are feasible again at max_risk=0.2; see
repeated-matrix-feasibility.md.