| """Validate the latent dataset against the raw HDF5 and the world model.
|
|
|
| The indexing here is easy to get subtly wrong: pixels are strided by frameskip
|
| while actions are not, and the block leaving frame k must be the actions that
|
| actually produced frame k+1. This checks that alignment against ground truth,
|
| then confirms the frozen predictor can reach the sampled goals — if teacher
|
| forcing on real actions doesn't reduce the goal distance, the goals aren't
|
| reachable and the controller has no learnable signal.
|
| """
|
|
|
| import sys
|
| from pathlib import Path
|
|
|
| import h5py
|
| import hdf5plugin
|
| import numpy as np
|
| import torch
|
|
|
| sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
| from lejepa_control.data import (
|
| FRAMESKIP,
|
| NUM_CONTEXT,
|
| LatentGoalDataset,
|
| split_episodes,
|
| )
|
| from lejepa_control.rollout import goal_distance, rollout_plan
|
| from lejepa_control.world_model import load_lewm
|
|
|
| H5 = 'data/swm_home/datasets/pusht_expert_train.h5'
|
| IMAGENET_MEAN = torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1)
|
| IMAGENET_STD = torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1)
|
|
|
|
|
| def main():
|
| device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| torch.manual_seed(0)
|
|
|
| ds = LatentGoalDataset('data/latents', max_offset=5)
|
| print(f'{len(ds)} clips over {len(ds.episodes)} episodes')
|
|
|
| f = h5py.File(H5, 'r')
|
| model = load_lewm(device=device)
|
|
|
|
|
| sample_idx = [0, 12345, 999999]
|
| for idx in sample_idx:
|
| ep = int(ds.clip_episode[idx])
|
| start = int(ds.clip_start[idx])
|
| base = int(ds.offsets[ep])
|
| rows = [base + start + i * FRAMESKIP for i in range(NUM_CONTEXT)]
|
|
|
| px = torch.from_numpy(f['pixels'][rows[0] : rows[-1] + 1 : FRAMESKIP])
|
| px = px.permute(0, 3, 1, 2).float() / 255.0
|
| px = ((px - IMAGENET_MEAN) / IMAGENET_STD).unsqueeze(0).to(device)
|
| with torch.no_grad():
|
| fresh = model.encode({'pixels': px})['emb'][0].cpu()
|
|
|
| cached = ds[idx]['context']
|
| rel = (cached - fresh).norm() / fresh.norm()
|
| assert rel < 0.02, f'latent cache mismatch at {idx}: {rel}'
|
| print(f'latent cache matches fresh encode (rel err < 2%) at {sample_idx}')
|
|
|
|
|
| idx = 12345
|
| ep = int(ds.clip_episode[idx])
|
| start = int(ds.clip_start[idx])
|
| base = int(ds.offsets[ep])
|
| item = ds[idx]
|
| for i in range(NUM_CONTEXT - 1):
|
| lo = base + start + i * FRAMESKIP
|
| raw = f['action'][lo : lo + FRAMESKIP]
|
| want = ((raw - ds.action_mean) / ds.action_std).reshape(-1)
|
| got = item['past_actions'][i].numpy()
|
| assert np.allclose(got, want, atol=1e-5), f'block {i} misaligned'
|
| print('past action blocks align with raw actions between context frames')
|
|
|
|
|
| for max_offset in (2, 3, 5):
|
| ds.set_max_offset(max_offset)
|
| offsets = [int(ds[i]['goal_offset']) for i in range(200)]
|
| assert min(offsets) >= 1 and max(offsets) <= max_offset, offsets
|
| print(f' max_offset={max_offset}: sampled {sorted(set(offsets))}')
|
|
|
|
|
| ds.set_max_offset(5)
|
| loader = torch.utils.data.DataLoader(ds, batch_size=64, shuffle=True)
|
| batch = next(iter(loader))
|
|
|
| ctx = batch['context'].to(device)
|
| past = batch['past_actions'].to(device)
|
| goal = batch['goal'].to(device)
|
| real = batch['real_action'].to(device)
|
|
|
|
|
| H = 5
|
| plan = torch.zeros(ctx.size(0), H, 10, device=device)
|
| plan[:, 0] = real
|
| with torch.no_grad():
|
| pred = rollout_plan(model, ctx, past, plan)
|
| d_real = goal_distance(pred, goal)
|
|
|
| zero = torch.zeros_like(plan)
|
| d_zero = goal_distance(rollout_plan(model, ctx, past, zero), goal)
|
|
|
|
|
| d_start = (ctx[:, -1] - goal).pow(2).mean(-1)
|
|
|
| print(f'start distance to goal {d_start.mean():.4f}')
|
| print(f'after real 1st block (d_1) {d_real[:, 0].mean():.4f}')
|
| print(f'after zero 1st block (d_1) {d_zero[:, 0].mean():.4f}')
|
| assert d_real[:, 0].mean() < d_start.mean(), (
|
| 'real actions do not move toward the goal — indexing is likely wrong'
|
| )
|
| assert d_real[:, 0].mean() < d_zero[:, 0].mean(), (
|
| 'real actions are no better than doing nothing'
|
| )
|
| print('reachability ok: real actions reduce latent goal distance')
|
|
|
|
|
| ds.set_max_offset(1)
|
| batch = next(iter(torch.utils.data.DataLoader(ds, batch_size=64, shuffle=True)))
|
| ctx = batch['context'].to(device)
|
| past = batch['past_actions'].to(device)
|
| goal = batch['goal'].to(device)
|
| plan = batch['real_action'].to(device).unsqueeze(1)
|
| with torch.no_grad():
|
| d1 = goal_distance(rollout_plan(model, ctx, past, plan), goal)[:, 0]
|
| d0 = (ctx[:, -1] - goal).pow(2).mean(-1)
|
| print(f'1-step goal: before {d0.mean():.4f} -> after {d1.mean():.4f} '
|
| f'({100 * (1 - d1.mean() / d0.mean()):.0f}% closed)')
|
| assert d1.mean() < 0.5 * d0.mean(), 'one-step goals not being reached'
|
|
|
| print('ALL DATA TESTS PASSED')
|
|
|
|
|
| if __name__ == '__main__':
|
| main()
|
|
|