A structured world model that decomposes its latent space into interpretable components with architectural (not merely learned) guarantees.
- Name / version: Atlas.WM 4.0.0
- Type: Latent-space world model (encoder + structured one-step dynamics)
- Author: Cesar Augusto
- License: MIT
- Frameworks: PyTorch ≥ 2.1; export to ONNX (opset 17)
- Decision record: AD-1 … AD-8 (see
docs/Atlas-WM-v3-Architecture-Plan.md)
| Module | Signature | Role |
|---|---|---|
ContinuousEncoder |
obs[B,6] → z_full[B,64] |
Encodes observations into the structured latent |
StructuredDynamics |
(z, action[B,8]) → z' |
One-step latent transition |
ActionInvarianceCritic |
z_static_immutable → action |
Adversary enforcing identifiability (AD-3) |
PhysicsBeliefEncoder |
obs_window[B,K,6] → z_static_slow[B,8] |
GRU over K consecutive obs → physics belief (Block 14) |
PhysicsHead |
z_static_slow[B,8] → physics_hat[B,n] |
Supervised head; predicts the recoverable physics subset {gravity, friction_box} (Block 14) |
[ z_static (16) | z_dynamic (32) | z_controllable (16) ]
= [ z_static_immutable (8) | z_static_slow (8) ] | ...
| Sub-space | Width | Guarantee |
|---|---|---|
z_static_immutable |
8 | Hard passthrough in dynamics - bit-identical across time (AD-2) |
z_static_slow |
8 | Soft residual; drift penalized by lambda_slow_drift |
z_dynamic |
32 | Autonomous residual evolution |
z_controllable |
16 | Action-conditioned residual |
- In scope: research on disentangled/identifiable world models; studying
domain randomization and latent identifiability of physical parameters;
short-horizon latent rollout on
CruelGridworld-style continuous dynamics. - Out of scope: safety-critical control, real-world robotics deployment, or any setting requiring calibrated uncertainty. This is a research artifact.
- Environment:
CruelGridworld- 3 objects (1 agent + 2 boxes) on a 20×20 grid with non-linear gravity, friction, wall/obstacle bounces; 8 discrete actions; 6-D continuous observation. - Dataset: ~50k random-exploration transitions (
generate_data.py), split 80/10/10, observations normalized to[0, 1]. - Variable-physics variant (Block 12): gravity ∈ [2,8], friction_agent ∈ [0.90,0.99], friction_box ∈ [0.95,0.995] resampled per episode, with optional Gaussian process noise; ground-truth parameters recorded for probing.
- Physics contract tests assert gravity, friction, Newton's third law, bounds, and action effects (tripwire against silent regressions).
- Determinism canary (AD-7): same seed → bit-identical encoder/dynamics
outputs;
z_static_immutableconstant across a rollout. - Rollout drift stays finite and bounded over open-loop horizons.
- Latent probe (Block 12): ridge R² of physics decoded from
z_static_slowvs. the immutable passthrough - direct evidence the decomposition routes variable physics where intended. (R² is near zero on an untrained encoder, as expected; train before interpreting.) - PhysicsBeliefEncoder probe (Block 14): ridge R² of physics decoded from
the GRU's output over a K-step window, targeting all three parameters
(the v3.x
friction_agentexclusion was retracted - see below); the single-step MLP gives R²≈0 by design. - ONNX parity: exported graphs match PyTorch within
rtol=1e-4, and the immutable passthrough is preserved in the exporteddynamics.onnx.
- Toolchain pinned with hashes in
requirements.lock(AD-8); CI installs from it. - Checkpoints stored only as
safetensors(AD-4) with embedded metadata (git_sha,config_hash,env_hash, schema version) and optional HMAC-SHA256 signing (AD-4, key viaATLAS_SIGNING_KEY). - Environment content-addressing via
env_hash(AD-6) flags cross-environment loads.
Retraction (2026-07-02). The Block-14 conclusion that
friction_agentis "not identifiable" is wrong, and the numbers below it are invalid. Two defects were found in the v4 red-team review (docs/v4.0-ROADMAP.md):
- The oracle was broken and unreproducible. The MLP oracle probe that scored R² < 0 was never committed to the repo, and MSE-based fits are destroyed by heavy-tailed wall/obstacle-bounce outliers. A robust closed-form estimator - median of per-step velocity-decay ratios on position-only observations (
scripts/oracle_friction_agent.py) - recoversfriction_agentwith R² = 0.85, MAE = 0.004 over 400 random-policy episodes on the corrected environment (R² = 0.98 with a stricter gravity gate). The agent is the most identifiable parameter, not the least: it is the only object under continuous known excitation.- The environment silently destroyed the box-physics signal. Boxes did not collide with walls and drifted out of the grid (observed range [−62, +77] on a [0, 20] grid), leaving the 1–10-unit gravity interaction band after a few steps. The reported "oracle ceiling of 0.15–0.45 R²" for
gravity/friction_boxmeasured this simulation bug (fixed, v4 B1).Consequences:
friction_agentreturns to the identification target set, and the belief-encoder results (R² ≈ 0–0.15) are void - they were further confounded by a 20× data-scale mismatch between the training and probe pipelines (fixed, v4 B2). No v3.x identifiability claim stands. The v4 re-baseline below replaces them.
Measured on the corrected environment (boxes contained, B1), consistent data pipeline (B2), seeded training (B3), and episode-grouped probe splits (the v3.x sequential splits leaked physics labels across overlapping windows). Dataset: 50k random-policy transitions, ~1,000 episodes of ~50 steps, per-episode physics randomization, process noise σ = 0.05.
| Probe | gravity | friction_agent | friction_box |
|---|---|---|---|
| Closed-form oracle (median-of-ratios, same data) | - | +0.865 | - |
Single-step encoder, z_static_slow |
−0.02 | −0.30 | −0.03 |
Single-step encoder, z_static_immutable |
−0.03 | −0.28 | −0.06 |
| GRU belief encoder (window 20, obs+Δobs+action) | −0.05 | −0.49 | −0.10 |
Two conclusions, both different from the retracted v3.x narrative:
- Identifiability is not data-limited. The closed-form estimator
recovers
friction_agentwith R² = 0.865 (MAE 0.006, 557 episodes) from the same noisy on-disk dataset - the information is present and extractable; the per-episode median averages the process noise out. The v3.x framing ("a data-regime limitation, not a modeling bug") had it backwards. (On a noise-free variant of the dataset the pattern is unchanged: oracle ≈ 0.88, learned encoders still negative.) - The raw-sequence GRU recipe is the bottleneck. Its training loss decreases while cross-episode validation R² stays negative for all three parameters: it memorizes training episodes instead of learning the dynamics → physics mapping. The gap between +0.865 (engineered per-step decay features + robust aggregation) and −0.49 (raw windows) on identical data is the project's clearest signal for what to build next: feed the belief encoder the oracle's dynamics features and train with an episode-contrastive objective (roadmap B10), then compare active-exploration data collection against random policies (B11).
Single-step probes hover at ≈ 0 as expected (physics is invisible in one
frame); mildly negative values reflect ridge variance at ~63 held-out
episodes. Reproduce:
generate_data.py --randomize-physics --process-noise-std 0.05 --episode-reset-prob 0.02 --seed 42 --num-samples 50000, split_data.py,
both trainers, then probe_physics.py (probe table) and the
oracle_friction_agent.estimate_friction_agent per-episode sweep over
data/raw (oracle row).
frame_stack: 2 is now the default model input: the previous same-episode
frame is concatenated to each observation (12-D input), making velocity
observable - a single position frame leaves one-step prediction ill-posed
(finding M2). Measured on the noisy re-baseline dataset (val split,
observation-space next-frame MSE, identical seeds):
| Next-frame predictor | 1 frame | 2 frames |
|---|---|---|
| Linear ridge (information ceiling proxy) | 0.000856 | 0.000274 |
| Trained world model (current v3-era loss) | 0.000921 | 0.001750 |
Two findings:
- The velocity information is real: a linear model improves 3.1× with the stacked input.
- The current training recipe cannot exploit it - the trained world model with 1 frame merely matches its linear ceiling, and with 2 frames lands 6× below it (a plain ridge beats the full encoder→dynamics→decoder stack). This is the same pattern as the oracle-vs-GRU identifiability gap: information present, L2-anchored self-predictive recipe unable to use it. The comparison is re-run in B7 after the objective is replaced (EMA-target / VICReg).
The v3.x objective (online-detached self-predictive target + L2 scale
anchor + a variance hinge on the wrong tensor) is replaced. Two candidates
were ablated, and a prediction-grounding term was added - nothing in the
old loss optimized the decoder∘dynamics composition that inference uses:
| Recipe (obs-space next-frame MSE, val) | 1 frame | 2 frames |
|---|---|---|
| legacy (L2 anchor) | 0.000921 | 0.001750 |
| EMA target + grounding | - | 0.001122 |
| VICReg + grounding (new default) | 0.001055 | 0.000996 |
| Linear ridge ceiling | 0.000856 | 0.000274 |
Findings:
- VICReg wins the ablation: healthy latent dispersion (mean per-dim
std ≈ 0.99) with no scale anchor, while the EMA-target encoder partially
collapses here (std ≈ 0.09).
objective: vicregis the new default;emaremains available. - Frame stacking now helps (0.000996 vs 0.001055) - under the legacy recipe it was 90% worse. The B6 direction is vindicated.
- 43% error reduction vs the legacy 2-frame recipe, with
lambda_latent_l2retired entirely. - Open gap: 3.6× above the 2-frame linear ceiling. The one-step information is demonstrably linearly extractable; remaining suspects are training length/schedule and the structured-latent routing bottleneck. Tracked for B8+ alongside the multi-step rollout loss.
Checkpoint selection and early stopping now use the objective-agnostic observation-space next-frame MSE (latent losses are not comparable across objectives).
Training now runs K-step self-fed rollouts (rollout_k: 4 default): each
predicted latent feeds the next dynamics step, with per-step latent
supervision and prediction grounding. One-step teacher forcing never
exposed the model to its own compounding error - the open-loop regime a
world model is actually used in. scripts/evaluate.py is now a real
evaluator: open-loop obs-space MSE by horizon plus the AD-2 passthrough
check.
Open-loop rollout on the noisy re-baseline val split (VICReg, 2 frames):
| Horizon | one-step trained (K=1) | rollout-trained (K=4) |
|---|---|---|
| 1 | 0.000993 | 0.001157 |
| 4 | 0.010577 | 0.010267 |
| 10 | 0.038915 | 0.035682 |
The classic trade-off, mildly: one-step wins at h=1 (16%), rollout training wins from h≥3 (8% at h=10). Gains are modest - note that the per-step process noise (σ = 0.05) is irreducible and accumulates like a random walk over horizons, so part of the long-horizon error floor cannot be modeled away by any deterministic predictor.
AD-2 verified in the open loop: z_static_immutable max drift over a
10-step self-fed rollout is exactly 0.0 for both models - the passthrough
guarantee now has an end-to-end measurement, not just a single-step
tautology test.
The adversarial ActionInvarianceCritic is retired from training
(module kept, deprecated): with random-policy data the action is sampled
independently of the observation, so I(z_imm(obs); action) = 0 for any
encoder - the adversarial game reduced to an arms race around noise
(finding C4). Action routing is architectural: actions enter only
control_net in StructuredDynamics.
In its place, the immutable anchor implements the intervention loss the v3.x plan prescribed and never shipped (finding C3): z_imm must be invariant within a same-episode window (MSE to the first position) and variant across episodes (VICReg over per-episode batch means - kills the collapsed-constant solution that made the passthrough guarantee vacuous).
Honest measurements:
- Mechanism validated: on synthetic data with observable episode
identity, the across/within episode variance ratio exceeds the 10×
regression floor by orders of magnitude (
tests/test_imm_anchor.py). - This environment carries no signal for it: a single 2-frame input exposes no episode invariants (walls are unobservable; physics requires temporal context), so the ratio stays ≈3 with or without the anchor while h=1 prediction degrades ~25% (0.001165 control vs 0.001454 anchored; the control run also confirms the B9 refactor is clean vs B8's 0.001157).
- Default: disabled on this env (
lambda_imm_* = 0.0), enable ≥ 0.1 when the input exposes episode invariants - belief-encoder integration (B12) or richer environments (B14+). The DoD item "z_imm provably informative" lands there, not here; leaving the anchor on today would be paying real prediction error for provably absent information.
The belief encoder now consumes engineered per-step dynamics features
(atlas_wm/data/dynamics_features.py, 27 dims: the oracle's gated
velocity-decay ratio + its running median, excitation, distances, box
acceleration projections onto attractor directions, 1/d² regressors,
aligned actions) instead of raw obs windows, with a heteroscedastic
head (μ, logσ; Gaussian NLL) and a half-window InfoNCE episode
contrastive. First positive learned physics identification in the
project (supervised head, val, window 40; raw-GRU baseline from B5):
| Parameter | raw-GRU (B5) | belief v2 |
|---|---|---|
| gravity | −0.05 | +0.43 |
| friction_agent | −0.49 | +0.22 |
| friction_box | −0.10 | +0.09 |
What the debugging established (each stage measured):
- Without the oracle's gravity gate on ratio validity, biased "valid" steps poison the GRU's mean-style aggregation (medians resist outliers; GRUs don't). Gravity flipped positive first because its features are action-free.
- An action-alignment off-by-one paired each decay ratio with the next step's action; the unit test initially validated the bug because its fixture used the same shift. Fixed and verified digit-for-digit against the oracle on identical frames.
- The remaining gap to the oracle's 0.865 is evidence length, not modeling: friction_agent lives in [0.90, 0.99] (std 0.025) while the optimal estimator's error over an 18-step window is std 0.043 - SNR < 1, so no learner can score positive there. 38-step windows (window_k 40, now the default) bring the error to the signal's scale; the oracle's 0.865 uses full ~50-step episodes. Ridge probes at window 40 rest on only 42 val episodes and are statistically fragile (±0.5 swings) - the supervised-head val R² above is the stable metric.
Direct consequence for B11: active exploration raises the per-step information rate (more valid steps: excited, boxes distant, no bounces), which shortens the window needed for a given belief quality - measure valid-steps/window under active vs random policies alongside R².
InfoSeekingPolicy (atlas_wm/data/exploration.py) collects data by
greedily maximizing the B10 information-rate proxies (ASID-style, with the
Fisher objective replaced by the measured proxies): a MEASURE phase runs an
oscillating rosette dash in open space (speed ≈ 2 → per-step ratio
error halves; golden-angle axis rotation), alternating with a STIR phase
that drives the nearest box through the gravity band. Available via
generate_data.py --policy active.
Identical belief model + training, only the data collection changes (supervised val R², window 40):
| Parameter | random data | active data |
|---|---|---|
| gravity | 0.43 | 0.67 |
| friction_agent | 0.22 | 0.29 |
| friction_box | 0.09 | 0.07 |
| mean | 0.246 | 0.341 (+39%) |
Episode-level closed-form estimator: active R² 0.956 / MAE 0.0036 vs random 0.914 / 0.0054.
A finding worth publishing on its own: the first policy version dashed along a fixed line, and when that line crossed an unobservable obstacle the agent hit it repeatedly - corrupting the ratio median of the whole episode (episode-level R² collapsed to 0.23 with near-unchanged MAE: a heavy-tail signature). Random walks don't repeat their own mistakes; deterministic information-seeking policies do. The rosette rotation turns such collisions into sparse outliers the median absorbs. Active system identification needs anti-fragility to unobservables, not just information greed.
training.use_belief conditions the world model's z_static_slow on
causal per-row beliefs precomputed by scripts/precompute_belief.py
(GRU forward_sequence; z_t sees only frames ≤ t, prefix-property
tested). Beliefs replace the encoder's slow slice in the start latent AND
in every rollout target, so the dynamics' slow residual learns to track
them. A/B on active data, identical seeds: h=1 MSE −5.8% (0.001080 →
0.001017), h=4 −1.5%. The process-noise random walk dominates the
long-horizon floor here, capping what known physics can buy - revisit in
lower-noise environments (B14+).
- Trained and evaluated only on a synthetic toy environment; no transfer claims.
- Identifiability guarantees on
z_static_immutableare architectural (passthrough) and adversarial (critic);z_static_slowidentifiability is empirical and depends on training quality - verify with the latent probe. - Episode-level physics identification is limited by the data regime - see
Physics identifiability above; only
{gravity, friction_box}are targeted. - No personal or sensitive data is involved.
pip install 'atlas-wm[export]'
python scripts/export_onnx.py \
--checkpoint checkpoints/best_model.safetensors --out-dir export/
# -> export/encoder.onnx, export/dynamics.onnx (compose for rollout)