Download code/scripts/diagnostics.py from nottygian/Leplanner: direct link, hf CLI and curl.
- Browser
- Download file 9.38 kB
-
https://huggingface.co/nottygian/Leplanner/resolve/main/code/scripts/diagnostics.py
- Command line
-
hf download hf://nottygian/Leplanner/code/scripts/diagnostics.py
-
curl -L -o diagnostics.py https://huggingface.co/nottygian/Leplanner/resolve/main/code/scripts/diagnostics.py
9.38 kB
| """Diagnostics for horizon-reset procrastination and the K=5 regression. | |
| Two questions, both answered in the world model's latent space against the | |
| same held-out latents used for validation: | |
| 1. Under receding-horizon execution, does the closed loop have a fixed point | |
| outside the success radius? Fitting ``D_{n+1} = c*D_n + b`` gives | |
| ``D* = b/(1-c)`` when ``0 < c < 1``. A positive intercept means each | |
| replan removes a *fraction* of the remaining distance but adds a floor, so | |
| the loop stalls short of the goal no matter how long it runs. | |
| 2. Does refinement keep improving predicted cost past the trained depth K=3 | |
| even as real success degrades? If predicted cost improves while success | |
| drops, the extra refinements are exploiting the world model rather than | |
| planning better. | |
| Question 2 originally measured only the *imagined* side, which cannot separate | |
| "the plan got better" from "the plan left the region where the world model is | |
| trustworthy" — both look like falling predicted cost. The support score | |
| ``r(C, b) = -log beta(b | C) / A`` against the fitted GMM answers that directly: | |
| if the violation fraction rises with k while predicted cost falls, the extra | |
| refinements are buying imagined progress with out-of-distribution actions. | |
| """ | |
| import argparse | |
| import json | |
| import sys | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) | |
| from lejepa_control.data import LatentGoalDataset, split_episodes # noqa: E402 | |
| from lejepa_control.losses import BehaviorDensity # noqa: E402 | |
| from lejepa_control.rollout import goal_distance, rollout_plan # noqa: E402 | |
| from lejepa_control.solver import load_controller # noqa: E402 | |
| from lejepa_control.world_model import load_lewm # noqa: E402 | |
| def parse_args(): | |
| p = argparse.ArgumentParser() | |
| p.add_argument('--controller', default='data/runs/controller/controller.pt') | |
| p.add_argument('--latents', default='data/latents') | |
| p.add_argument('--density', default='data/runs/density/density.pt') | |
| p.add_argument('--samples', type=int, default=256) | |
| p.add_argument('--horizon', type=int, default=5) | |
| p.add_argument('--replans', type=int, default=12) | |
| p.add_argument('--max-k', type=int, default=8) | |
| p.add_argument('--tag', default='main') | |
| p.add_argument('--out', default='data/runs/diagnostics') | |
| return p.parse_args() | |
| def closed_loop_trace(controller, model, batch, device, execute, replans): | |
| """Latent goal distance after each replan, simulating in latent space. | |
| The world model is its own simulator here: whatever the predictor says | |
| the executed blocks caused becomes the next context. That is exactly the | |
| loop the real MPC runs, minus the env, so it isolates the planner's | |
| dynamics from simulator mismatch. | |
| """ | |
| ctx = batch['context'].to(device) | |
| past = batch['past_actions'].to(device) | |
| goal = batch['goal'].to(device) | |
| num_context = controller.num_context | |
| trace = [] | |
| for _ in range(replans): | |
| out = controller(model, ctx, past, goal) | |
| plan = out['plans'][-1] | |
| # execute the first `execute` blocks and advance the latent state | |
| pred, frames = rollout_plan( | |
| model, ctx, past, plan[:, :execute], return_frames=True | |
| ) | |
| trace.append(goal_distance(pred, goal)[:, execute - 1].cpu().numpy()) | |
| ctx = frames[:, -num_context:] | |
| past = torch.cat([past, plan[:, :execute]], dim=1)[:, -(num_context - 1):] | |
| return np.stack(trace, axis=1) # (B, replans) | |
| def fit_contraction(trace): | |
| """Least-squares ``D_{n+1} = c*D_n + b`` over all consecutive pairs.""" | |
| x = trace[:, :-1].ravel() | |
| y = trace[:, 1:].ravel() | |
| A = np.stack([x, np.ones_like(x)], axis=1) | |
| (c, b), *_ = np.linalg.lstsq(A, y, rcond=None) | |
| resid = y - (c * x + b) | |
| ss = 1 - resid.var() / y.var() if y.var() > 0 else float('nan') | |
| fixed = b / (1 - c) if abs(1 - c) > 1e-9 else float('inf') | |
| return float(c), float(b), float(fixed), float(ss) | |
| def refinement_trace(controller, model, batch, device, max_k, | |
| density=None, c95=None): | |
| """Predicted goal cost after each refinement, out to ``max_k``. | |
| Also returns the support-violation fraction per refinement when a density | |
| model is supplied — the imagined cost and the violation fraction moving in | |
| opposite directions is the exploitation signature. | |
| """ | |
| saved = controller.refinements | |
| try: | |
| controller.refinements = max_k | |
| out = controller( | |
| model, | |
| batch['context'].to(device), | |
| batch['past_actions'].to(device), | |
| batch['goal'].to(device), | |
| ) | |
| q = batch['goal_offset'].to(device).clamp(1, controller.horizon) | |
| costs, arrivals, moves, violations, scores = [], [], [], [], [] | |
| prev = None | |
| for k, d in enumerate(out['distances']): | |
| costs.append(float(d[:, -1].mean())) | |
| arrivals.append( | |
| float(d.gather(1, (q - 1).unsqueeze(1)).squeeze(1).mean()) | |
| ) | |
| plan = out['plans'][k] | |
| moves.append( | |
| 0.0 if prev is None else float((plan - prev).abs().mean()) | |
| ) | |
| prev = plan | |
| if density is not None and c95 is not None: | |
| # score every block of the plan at the latent history it is | |
| # actually proposed from, which is what the support term does | |
| ctx = out['contexts'][k].flatten(0, 1) # (B*H, N, D) | |
| score = density.nll_per_dim(ctx, plan.flatten(0, 1)) | |
| violations.append(float((score > c95).float().mean())) | |
| scores.append(float(score.mean())) | |
| return costs, arrivals, moves, violations, scores | |
| finally: | |
| controller.refinements = saved | |
| def main(): | |
| args = parse_args() | |
| device = 'cuda' if torch.cuda.is_available() else 'cpu' | |
| torch.manual_seed(0) | |
| stats = json.loads((Path(args.latents) / 'stats.json').read_text()) | |
| model = load_lewm(device=device) | |
| controller, ckpt = load_controller( | |
| args.controller, latent_dim=stats['latent_dim'], device=device | |
| ) | |
| controller.eval() | |
| print(f'controller step {ckpt["step"]}, K={controller.refinements}') | |
| _, val_eps = split_episodes(stats['n_episodes']) | |
| val = LatentGoalDataset( | |
| args.latents, max_offset=5, episodes=val_eps, horizon=args.horizon | |
| ) | |
| idx = np.random.default_rng(0).choice(len(val), args.samples, replace=False) | |
| batch = { | |
| k: torch.stack([val[int(i)][k] for i in idx]) | |
| for k in ('context', 'past_actions', 'goal', 'goal_offset') | |
| } | |
| density, c95 = None, None | |
| if Path(args.density).exists(): | |
| d_ckpt = torch.load(args.density, map_location=device, weights_only=False) | |
| density = BehaviorDensity( | |
| latent_dim=d_ckpt['latent_dim'], components=d_ckpt['components'] | |
| ) | |
| density.load_state_dict(d_ckpt['state_dict']) | |
| density.to(device).eval().requires_grad_(False) | |
| c95 = float(d_ckpt['c95']) | |
| print(f'support model loaded, c95={c95:.4f}') | |
| else: | |
| print(f'no support model at {args.density} — violation column disabled') | |
| report = {'tag': args.tag, 'checkpoint': args.controller, | |
| 'step': int(ckpt['step'])} | |
| print('\n=== 1. closed-loop contraction ===') | |
| print(f'{"execute":>8}{"D_0":>9}{"D_final":>10}{"c":>8}{"b":>9}' | |
| f'{"D*":>9}{"R^2":>7}') | |
| report['contraction'] = {} | |
| for execute in (1, 5): | |
| trace = closed_loop_trace( | |
| controller, model, batch, device, execute, args.replans | |
| ) | |
| c, b, fixed, r2 = fit_contraction(trace) | |
| m = trace.mean(axis=0) | |
| print(f'{execute:>8}{m[0]:>9.4f}{m[-1]:>10.4f}' | |
| f'{c:>8.4f}{b:>9.4f}{fixed:>9.4f}{r2:>7.3f}') | |
| report['contraction'][f'exec{execute}'] = { | |
| 'c': c, 'b': b, 'fixed_point': fixed, 'r2': r2, | |
| 'mean_trace': [round(float(v), 5) for v in m], | |
| } | |
| print('\n=== 2. refinement past trained depth ===') | |
| costs, arrivals, moves, violations, scores = refinement_trace( | |
| controller, model, batch, device, args.max_k, density, c95 | |
| ) | |
| print(f'{"k":>3}{"terminal":>11}{"arrival":>10}{"dJ":>10}{"|plan chg|":>12}' | |
| f'{"support r":>11}{"viol%":>8}') | |
| for k in range(len(costs)): | |
| dj = '' if k == 0 else f'{costs[k - 1] - costs[k]:+.5f}' | |
| sup = f'{scores[k]:>11.4f}' if scores else f'{"-":>11}' | |
| vio = f'{100 * violations[k]:>8.1f}' if violations else f'{"-":>8}' | |
| print(f'{k:>3}{costs[k]:>11.5f}{arrivals[k]:>10.5f}{dj:>10}' | |
| f'{moves[k]:>12.5f}{sup}{vio}') | |
| report['refinement'] = { | |
| 'terminal': costs, 'arrival': arrivals, 'plan_change': moves, | |
| 'support_score': scores, 'violation': violations, 'c95': c95, | |
| } | |
| out = Path(args.out) | |
| out.mkdir(parents=True, exist_ok=True) | |
| with (out / 'diagnostics.jsonl').open('a') as f: | |
| f.write(json.dumps(report) + '\n') | |
| print(f'\nwrote {out / "diagnostics.jsonl"}') | |
| if __name__ == '__main__': | |
| main() | |