| """Offline goal-conditioned samples drawn from the cached PushT latents.
|
|
|
| A sample is a 3-frame latent history, the two action blocks that connect
|
| those frames, a goal latent taken from 1-``max_offset`` transitions ahead in
|
| the *same* episode, and the real action block leaving the current frame (used
|
| only to fit the behavior-density model, never as a controller target).
|
|
|
| Everything is in latent space, so this never touches the 46 GB image file.
|
| """
|
|
|
| import json
|
| from pathlib import Path
|
|
|
| import numpy as np
|
| import torch
|
| from torch.utils.data import Dataset
|
|
|
| NUM_CONTEXT = 3
|
| FRAMESKIP = 5
|
|
|
|
|
| class LatentGoalDataset(Dataset):
|
| """Latent history/goal pairs sampled from cached expert trajectories.
|
|
|
| Args:
|
| root: Directory written by ``scripts/encode_latents.py``.
|
| max_offset: Largest goal distance, in world-model transitions. Set by
|
| the training curriculum (1-2, then 1-3, then 1-5).
|
| episodes: Episode indices this split may draw from.
|
| horizon: Plan horizon, reserved so a full plan stays inside the episode.
|
| """
|
|
|
| def __init__(
|
| self, root, max_offset=5, episodes=None, horizon=5, in_memory=True
|
| ):
|
| root = Path(root)
|
| stats = json.loads((root / 'stats.json').read_text())
|
| self.latent_dim = stats['latent_dim']
|
|
|
|
|
|
|
| mode = None if in_memory else 'r'
|
| self.latents = np.load(root / 'latents.npy', mmap_mode=mode)
|
| self.actions = np.load(root / 'actions.npy', mmap_mode=mode)
|
| self.lengths = np.load(root / 'ep_len.npy')
|
| self.offsets = np.load(root / 'ep_offset.npy')
|
|
|
| self.action_mean = np.asarray(stats['action_mean'], dtype=np.float32)
|
| self.action_std = np.asarray(stats['action_std'], dtype=np.float32)
|
|
|
| self.horizon = horizon
|
| self.set_max_offset(max_offset)
|
|
|
| if episodes is None:
|
| episodes = np.arange(len(self.lengths))
|
| self.episodes = np.asarray(episodes)
|
|
|
|
|
|
|
|
|
|
|
|
|
| counts = (self.lengths[self.episodes] - NUM_CONTEXT * FRAMESKIP).clip(
|
| min=0
|
| )
|
| self.clip_episode = np.repeat(self.episodes, counts)
|
| self.clip_start = np.concatenate(
|
| [np.arange(c) for c in counts if c > 0]
|
| ).astype(np.int64)
|
|
|
| def set_max_offset(self, max_offset):
|
| """Update the curriculum's furthest goal, in world-model transitions."""
|
| self.max_offset = max_offset
|
|
|
| def __len__(self):
|
| return len(self.clip_start)
|
|
|
| def _block(self, base, local_step):
|
| """Normalized action block leaving the frame at ``local_step``.
|
|
|
| Matches ``swm.data.Dataset.__getitem__``: pixels are strided by
|
| frameskip while actions are not, so the block leaving frame ``k`` is
|
| the raw actions ``[start + 5k, start + 5k + 5)``.
|
| """
|
| lo = base + local_step
|
| raw = np.asarray(self.actions[lo : lo + FRAMESKIP], dtype=np.float32)
|
| return ((raw - self.action_mean) / self.action_std).reshape(-1)
|
|
|
| def __getitem__(self, idx):
|
| ep = int(self.clip_episode[idx])
|
| start = int(self.clip_start[idx])
|
| base = int(self.offsets[ep])
|
| length = int(self.lengths[ep])
|
|
|
|
|
| rows = [base + start + i * FRAMESKIP for i in range(NUM_CONTEXT)]
|
| ctx = np.asarray(self.latents[rows], dtype=np.float32)
|
|
|
|
|
| past = np.stack(
|
| [
|
| self._block(base, start + i * FRAMESKIP)
|
| for i in range(NUM_CONTEXT - 1)
|
| ]
|
| )
|
|
|
|
|
|
|
| current = start + (NUM_CONTEXT - 1) * FRAMESKIP
|
| reach = (length - 1 - current) // FRAMESKIP
|
| high = min(self.max_offset, reach)
|
| offset = int(torch.randint(1, high + 1, (1,)).item())
|
| goal = np.asarray(
|
| self.latents[base + current + offset * FRAMESKIP], dtype=np.float32
|
| )
|
|
|
|
|
| real = self._block(base, current)
|
|
|
| return {
|
| 'context': torch.from_numpy(ctx),
|
| 'past_actions': torch.from_numpy(past),
|
| 'goal': torch.from_numpy(goal),
|
| 'real_action': torch.from_numpy(real),
|
| 'goal_offset': torch.tensor(offset, dtype=torch.long),
|
| }
|
|
|
|
|
| def split_episodes(num_episodes, val_fraction=0.05, seed=0):
|
| """Deterministic train/val split over episode indices."""
|
| rng = np.random.default_rng(seed)
|
| perm = rng.permutation(num_episodes)
|
| n_val = max(1, int(num_episodes * val_fraction))
|
| return perm[n_val:], perm[:n_val]
|
|
|