Skip to content

Commit 3ef0a3d

Browse files
authored
Merge pull request #55 from aurascoper/fix/pin-episodes-and-paired-endpoint
fix(worldmodel): pin the episode set, per-episode CEM noise, paired endpoint
2 parents 0e0bd81 + c646918 commit 3ef0a3d

2 files changed

Lines changed: 59 additions & 6 deletions

File tree

‎WorldModel/EXPERIMENT_LEDGER.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,11 @@ Two reviewers verified the delivered synthetic A/Bs are sound (symlog threaded i
272272
- **Node 14's measurements stand.** `forward_eval.py` contains no `torch.load`; it trains in-process via `train_jepa`, so nothing it reported came from a restored checkpoint. Only the parenthetical account of the hazard was wrong.
273273
- **Consequence for the benchmark repair.** The registered acceptance criterion — that the repair must separate zero from random from MSE *on existing data* — cannot be run against v1 checkpoints at all. It would be scoring an untrained goal encoder. `eeg_jepa.py` now writes format **v2** with `target_encoder_state_dict`; a v2 restore is exact (verified: max parameter difference 0.00e+00 against the original target). Acceptance runs must use v2 checkpoints or run in-process.
274274
- **Correction B (measurement, refines node 14's "0.15 to 0.45" reading).** That swing was attributed to the episode-limited binomial noise of estimating the baseline at one draw per episode. Decomposed, it is roughly **half** that and half genuine episode-set difficulty, because `starts` and `goal_pos` are redrawn from `random.Random(seed)` so each seed scores a different episode set. Holding the episode set fixed and varying only the random draw: sd **0.0750** at reps=1, **0.0054** at reps=100, **0.0022** at reps=500. Varying the episode set with reps pinned at 100: sd **0.0853**, essentially the original spread. So raising reps removes one half for the price of env steps — the baseline touches no encoder, predictor or CEM — and decoupling the episode seed from the model seed is required for the other half.
275+
276+
## Corrections to node 14, part 2 (2026-08-02)
277+
278+
- **Node 14's `success_rate` values are superseded, deliberately.** Per-episode CEM generators change the planner's sampling noise, so the recorded 32d/64d rates no longer reproduce. The EPISODE SET for model-seed 0 is unchanged — `evaluate()` previously passed `seed = model_seed + 2` and the new fixed `episode_seed` is 2 — but every seed now scores that same set, where before each scored its own. Node 14's *conclusions* are untouched: the frame ratio, the Procrustes residual and the one-frame remedy were all measured within a run, not across seeds.
279+
- **The baseline is now a constant.** Across 10 runs (2 latent dims x 5 seeds) `random_baseline_success` is 0.303 for every one, against 0.254-0.483 before. It is a property of the episode set, as it should be, and no longer contributes anything to arm-vs-baseline comparisons.
280+
- **The per-episode CEM generator is not cosmetic.** A single generator consumed sequentially made the planning noise on episode i depend on the draws taken by episodes 0..i-1, i.e. on `n_samples * cem_iters * horizon`. Any arm touching a CEM knob got different noise on the same episode. Nodes 10 and 11 swept exactly those knobs. That sweep was therefore not the paired comparison it appeared to be — it is not necessarily wrong, but its "within seed noise" reading rested on an assumption that did not hold.
281+
- **The continuous endpoint helps, by less than predicted.** Measured paired, ld32 vs ld64 on identical episodes and identical planner noise: mean |t| of 0.62 for the per-episode binary against 0.88 for per-episode `log(d_final/d_start)`, a factor of **1.43**, implying median n for |t|=2 of ~248 versus ~154. A power simulation had suggested nearer 3-4x. Only 3-8 of 20 pairs are discordant, which is why the binary endpoint is weak and is the mechanism the simulation got roughly right while overstating the size. Both endpoints are now returned per episode so this can be recomputed rather than re-derived.
282+
- **`log_distance_ratio_mean` is reported, NOT adopted as the adjudicator.** Switching the primary endpoint in the same change that repins the episode set would confound the two. Adoption is a separate pre-registered decision, and it still has to clear the blind criterion: separate zero from random from MSE on existing data.

‎WorldModel/forward_eval.py‎

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,8 @@ def _cem_plan(model, z0_lat, zg_lat, device, *, horizon, cem_iters, n_samples, e
319319

320320
@torch.no_grad()
321321
def _mpc_success(model, train_mean, train_std, device, *, n_episodes=20, horizon=6,
322-
cem_iters=3, n_samples=64, elite_frac=0.2, mode="signal", seed=2,
323-
goal_offset=0.4, goal_tol=0.15, baseline_reps=100,
322+
cem_iters=3, n_samples=64, elite_frac=0.2, mode="signal",
323+
episode_seed=2, goal_offset=0.4, goal_tol=0.15, baseline_reps=100,
324324
log_features: bool = False,
325325
log_epsilon: float = 1e-6, symlog: bool = False) -> dict[str, Any]:
326326
"""Goal-conditioned latent MPC/CEM planning success. Per episode: sample a start
@@ -329,9 +329,13 @@ def _mpc_success(model, train_mean, train_std, device, *, n_episodes=20, horizon
329329
true final pos reached the goal. A random-action policy is the baseline — the
330330
JEPA-planned success rate beating it is the signal that the latent dynamics are
331331
useful for control. Returns success_rate, mean_final_distance, random_baseline."""
332-
rng = random.Random(seed)
333-
gen = torch.Generator()
334-
gen.manual_seed(seed + 7)
332+
# The episode set, the baseline draws and the planner's sampling noise all
333+
# derive from `episode_seed` and NOTHING else. Previously `evaluate()` passed
334+
# `seed = model_seed + 2`, so every model seed scored a DIFFERENT episode set:
335+
# "which episodes" was confounded with "which model", and the between-set
336+
# difficulty spread (sd 0.0853, measured) was being read as model variance.
337+
# The benchmark's episodes are a property of the benchmark, not of the run.
338+
rng = random.Random(episode_seed)
335339
starts = [_sample_latent(rng) for _ in range(n_episodes)]
336340
goal_pos = [starts[i]["pos"] + rng.uniform(-goal_offset, goal_offset) for i in range(n_episodes)]
337341
goals = [{**starts[i], "pos": goal_pos[i]} for i in range(n_episodes)]
@@ -352,7 +356,18 @@ def _enc(states, use_target: bool = False):
352356

353357
successes, dists = 0, []
354358
rand_per_episode: list[float] = []
359+
start_dists: list[float] = []
360+
log_ratios: list[float] = []
355361
for i in range(n_episodes):
362+
# A generator PER EPISODE, seeded from the episode identity alone. A
363+
# single generator consumed sequentially made the planning noise on
364+
# episode i depend on how many draws episodes 0..i-1 had taken, which is
365+
# a function of n_samples * cem_iters * horizon. Any arm that touched a
366+
# CEM knob therefore got different noise on the SAME episode, silently
367+
# defeating pairing even with the episode set fixed. Nothing in the panel
368+
# would have revealed it.
369+
gen = torch.Generator()
370+
gen.manual_seed(episode_seed * 1_000_003 + i)
356371
plan = _cem_plan(model, z0[i], zg[i], device, horizon=horizon, cem_iters=cem_iters,
357372
n_samples=n_samples, elite_frac=elite_frac, gen=gen)
358373
z = dict(starts[i])
@@ -362,6 +377,24 @@ def _enc(states, use_target: bool = False):
362377
dists.append(d)
363378
if d < goal_tol:
364379
successes += 1
380+
# Paired continuous endpoint: log(d_final / d_start), per episode.
381+
#
382+
# `success_rate` thresholds a continuous distance at goal_tol=0.15 and
383+
# throws away everything else. Simulating a 10% distance improvement
384+
# with shared episode difficulty, power at n=20 is 17.7% for a paired
385+
# continuous statistic against 0.5% for paired binary; at n=200 it is
386+
# 85.1% against 23.4%. The distance is already computed and returned --
387+
# it simply was not the adjudicator.
388+
#
389+
# The RATIO, not the difference: episodes differ enormously in how far
390+
# the goal starts, so a raw difference is dominated by episode
391+
# difficulty. The log makes "halved the distance" the same effect size
392+
# wherever it happens, which is what pairing needs. Both terms are
393+
# clamped because goal_offset can draw a start essentially on top of
394+
# its goal, and log(0) would take the whole panel with it.
395+
d0 = max(abs(starts[i]["pos"] - goal_pos[i]), 1e-6)
396+
start_dists.append(d0)
397+
log_ratios.append(math.log(max(d, 1e-6) / d0))
365398
# The random baseline is estimated AFTER the model arm, over
366399
# `baseline_reps` rollouts per episode rather than one. It touches no
367400
# encoder, no predictor and no CEM — only `_step` with uniform actions —
@@ -400,6 +433,18 @@ def _enc(states, use_target: bool = False):
400433
"random_baseline_stderr": rand_se,
401434
"random_baseline_reps": baseline_reps,
402435
"random_baseline_per_episode": rand_per_episode,
436+
# Primary candidate for the repaired benchmark. Reported alongside
437+
# success_rate rather than replacing it -- adopting it as THE adjudicator
438+
# is a separate, pre-registered decision, and switching endpoints while
439+
# also changing the episode set would confound the two.
440+
# Per-episode finals, so a downstream paired analysis can compute EITHER
441+
# endpoint exactly instead of approximating the binary one from the two
442+
# marginal rates. Without this the paired binary sd has to be guessed.
443+
"final_distance_per_episode": dists,
444+
"log_distance_ratio_mean": float(np.mean(log_ratios)),
445+
"log_distance_ratio_per_episode": log_ratios,
446+
"start_distance_per_episode": start_dists,
447+
"episode_seed": episode_seed,
403448
"frame": frame,
404449
}
405450

@@ -463,7 +508,7 @@ def evaluate(
463508
)
464509
mpc = _mpc_success(
465510
model, dataset.mean, dataset.std, device,
466-
n_episodes=mpc_episodes, horizon=mpc_horizon, mode=mode, seed=seed + 2,
511+
n_episodes=mpc_episodes, horizon=mpc_horizon, mode=mode,
467512
cem_iters=mpc_cem_iters, n_samples=mpc_n_samples, elite_frac=mpc_elite_frac,
468513
log_features=log_features, log_epsilon=log_epsilon, symlog=symlog,
469514
)

0 commit comments

Comments
 (0)