"""APE model and compact cache used by the isolated Keyframe baseline.""" from __future__ import annotations import json from dataclasses import dataclass from pathlib import Path import numpy as np import torch from torch import nn class ActionCopycat(nn.Module): """Official one-hidden-layer action-only copycat architecture.""" def __init__(self, action_dim: int = 7, history: int = 9, hidden: int = 300): super().__init__() self.net = nn.Sequential( nn.Linear(action_dim * history, hidden), nn.LeakyReLU(0.2, inplace=True), nn.Linear(hidden, action_dim), ) def forward(self, action_history: torch.Tensor) -> torch.Tensor: return self.net(action_history) def q99_normalize( actions: np.ndarray, statistics: dict, *, binary_action_indices: tuple[int, ...] = (), ) -> np.ndarray: """Apply q01/q99 normalization with an explicit binary-action contract. A zero percentile span is not sufficient evidence that an action dimension is binary. This matters for RoboTwin absolute qpos, where a joint can be constant in a dataset shard while remaining a continuous joint. Such continuous, zero-span dimensions are mapped to zero; only explicitly declared indices use the binary threshold. """ actions = np.asarray(actions, dtype=np.float32) q01 = np.asarray(statistics["q01"], dtype=np.float32) q99 = np.asarray(statistics["q99"], dtype=np.float32) if actions.shape[-1] != len(q01) or q01.shape != q99.shape: raise ValueError( f"action/statistics shape mismatch: actions={actions.shape}, " f"q01={q01.shape}, q99={q99.shape}" ) binary = np.zeros(len(q01), dtype=bool) for index in binary_action_indices: if index < 0 or index >= len(q01): raise ValueError(f"binary action index {index} is out of range") binary[index] = True continuous = ~binary scaled = continuous & (q01 != q99) constant = continuous & (q01 == q99) normalized = np.zeros_like(actions, dtype=np.float32) normalized[..., scaled] = ( 2.0 * (actions[..., scaled] - q01[scaled]) / (q99[scaled] - q01[scaled]) - 1.0 ) normalized[..., scaled] = np.clip( normalized[..., scaled], -2.2, 2.2 ) normalized[..., constant] = 0.0 normalized[..., binary] = ( actions[..., binary] > 0.5 ).astype(np.float32) return normalized def history_target(actions: np.ndarray, anchor: int, history: int = 9): """Repeat the earliest physical action at a trajectory boundary.""" indices = np.arange(anchor - history, anchor, dtype=np.int64) indices = np.clip(indices, 0, len(actions) - 1) return actions[indices].reshape(-1), actions[anchor] @dataclass(frozen=True) class CacheRecord: ape: float weight: float class KeyframeWeightCache: """Read a compact NPZ cache without altering sampling.""" def __init__(self, path: str | Path): self.path = Path(path) payload = np.load(self.path, allow_pickle=False) self.metadata = json.loads(str(payload["metadata_json"].item())) if self.metadata.get("method") != "Keyframe-Focused-adapted": raise RuntimeError("unexpected keyframe cache method") self._datasets = {} for index, name in enumerate(self.metadata["dataset_names"]): trajectory_ids = payload[f"d{index}_trajectory_ids"].astype(np.int64) offsets = payload[f"d{index}_offsets"].astype(np.int64) anchors = payload[f"d{index}_anchors"].astype(np.int64) ape = payload[f"d{index}_ape"].astype(np.float32) weights = payload[f"d{index}_weights"].astype(np.float32) if not (len(anchors) == len(ape) == len(weights)): raise RuntimeError(f"cache array lengths differ for {name}") if len(offsets) != len(trajectory_ids) + 1 or int(offsets[-1]) != len(anchors): raise RuntimeError(f"cache offsets are invalid for {name}") if np.any(~np.isfinite(ape)) or np.any(~np.isfinite(weights)): raise RuntimeError(f"cache contains non-finite values for {name}") if np.any(weights <= 0): raise RuntimeError(f"cache contains non-positive weights for {name}") self._datasets[name] = { "trajectory_to_row": { int(trajectory_id): row for row, trajectory_id in enumerate(trajectory_ids) }, "offsets": offsets, "anchors": anchors, "ape": ape, "weights": weights, } def lookup(self, dataset_name: str, trajectory_id: int, base_index: int) -> CacheRecord: try: dataset = self._datasets[dataset_name] row = dataset["trajectory_to_row"][int(trajectory_id)] except KeyError as exc: raise KeyError( f"keyframe cache has no trajectory {(dataset_name, trajectory_id)}" ) from exc start, end = dataset["offsets"][row : row + 2] local = dataset["anchors"][start:end] position = int(np.searchsorted(local, int(base_index))) if position >= len(local) or int(local[position]) != int(base_index): raise KeyError( f"keyframe cache has no anchor {(dataset_name, trajectory_id, base_index)}" ) absolute = int(start + position) return CacheRecord( ape=float(dataset["ape"][absolute]), weight=float(dataset["weights"][absolute]), )