feat(tracking): 汇总 v0.1.3 回放与 ACT 准备进度

新增专家轨迹诊断、原视频估计相机入口和状态参考ACT数据门禁/训练链路;更新版本与进度文档。

验证:120项CPU/USD回归和4步synthetic CPU smoke通过;1333帧默认GUI历史回放PASS。原视频相机完整回放超时124,策略GPU E2E未执行,完整pre-commit工具缺失。

兼容性:HDF5、Cartpole、USD和控制阈值保持不变。本提交为实验进度快照,不宣称完整发布验收通过;数据、视频和权重不纳入。
This commit is contained in:
2026-09-15 16:29:17 +08:00
parent 8e7ab5fc76
commit f5d84e26f6
26 changed files with 2662 additions and 7 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
# Semantic Versioning is used: https://semver.org/
version = "0.1.2"
version = "0.1.3"
# Description
category = "isaaclab"
@@ -0,0 +1,5 @@
"""State-only reference imitation, separate from Isaac Lab task registration.
Importing this package does not launch Kit or initialize CUDA. It is not a robot
hardware controller, grasping task, or implementation of the full DexSchema.
"""
@@ -0,0 +1,182 @@
"""Manifest-bound geometry/posture encoding and bounded reference target adapter.
Shared geometry is root-link pose relative to the episode's initial root frame.
Joint layout/coupling lives ONLY in this adapter. Targets are state-derived
proxies, not measured actuator commands. No hardware or collision guarantees.
"""
import numpy as np
from dex_workbench_tracking.control import bounded, rotation_error
from dex_workbench_tracking.identity import manifest_side
from dex_workbench_tracking.trajectory import require
def qmul(a, b):
return np.r_[a[0] * b[0] - np.dot(a[1:], b[1:]), a[0] * b[1:] + b[0] * a[1:] + np.cross(a[1:], b[1:])]
def qmatrix(q):
q = np.asarray(q, dtype=float)
require(q.shape == (4,) and np.isfinite(q).all() and abs(np.linalg.norm(q) - 1) < 1e-4, "Unit wxyz required")
w, x, y, z = q / np.linalg.norm(q)
return np.array(
[
[1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)],
[2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)],
[2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)],
]
)
def matrixq(matrix):
"""Rotation matrix -> unit wxyz, stable at half turns (largest component branch)."""
m = np.asarray(matrix, dtype=float)
candidates = np.array(
[
1 + np.trace(m),
1 + m[0, 0] - m[1, 1] - m[2, 2],
1 - m[0, 0] + m[1, 1] - m[2, 2],
1 - m[0, 0] - m[1, 1] + m[2, 2],
]
)
i = int(candidates.argmax())
s = 2 * np.sqrt(max(candidates[i], 0))
require(s > 1e-8, "Degenerate rotation")
if i == 0:
q = [s / 4, (m[2, 1] - m[1, 2]) / s, (m[0, 2] - m[2, 0]) / s, (m[1, 0] - m[0, 1]) / s]
elif i == 1:
q = [(m[2, 1] - m[1, 2]) / s, s / 4, (m[0, 1] + m[1, 0]) / s, (m[0, 2] + m[2, 0]) / s]
elif i == 2:
q = [(m[0, 2] - m[2, 0]) / s, (m[0, 1] + m[1, 0]) / s, s / 4, (m[1, 2] + m[2, 1]) / s]
else:
q = [(m[1, 0] - m[0, 1]) / s, (m[0, 2] + m[2, 0]) / s, (m[1, 2] + m[2, 1]) / s, s / 4]
q = np.asarray(q)
return q / np.linalg.norm(q)
def rotation6d(values):
a, b = np.asarray(values, dtype=float).reshape(2, 3)
require(np.isfinite(a).all() and np.isfinite(b).all(), "Nonfinite policy rotation")
require(np.linalg.norm(a) > 1e-6, "Degenerate policy rotation first axis")
a = a / np.linalg.norm(a)
b = b - np.dot(a, b) * a
require(np.linalg.norm(b) > 1e-6, "Degenerate policy rotation second axis")
b = b / np.linalg.norm(b)
return np.column_stack((a, b, np.cross(a, b)))
class Adapter:
def __init__(self, manifest):
self.side = manifest_side(manifest)
self.names = [j["name"] for j in manifest["joints"]]
require(len(set(self.names)) == len(self.names) and len(self.names) > 0, "Unique joint names required")
self.lower = np.array([j["lower_rad"] for j in manifest["joints"]], dtype=float)
self.upper = np.array([j["upper_rad"] for j in manifest["joints"]], dtype=float)
require(
np.isfinite(self.lower).all() and np.isfinite(self.upper).all() and (self.lower <= self.upper).all(),
"Invalid limits",
)
self.mimic = manifest.get("source_urdf", {}).get("mimic", [])
followers = [eq["joint"] for eq in self.mimic]
require(len(followers) == len(set(followers)), "Duplicate follower")
self.master_names = [n for n in self.names if n not in followers]
self.columns = [self.names.index(n) for n in self.master_names]
self.target_lower = self.lower[self.columns].copy()
self.target_upper = self.upper[self.columns].copy()
self.rate_multiplier = np.ones(len(self.columns))
for eq in self.mimic:
require(
eq["joint"] in self.names and eq["reference"] in self.master_names,
"Only inspected direct master/follower equations supported",
)
m, o = eq["multiplier"], eq["offset_rad"]
require(np.isfinite(m) and np.isfinite(o) and m != 0, "Invalid mimic coefficients")
child, master = self.names.index(eq["joint"]), self.master_names.index(eq["reference"])
bounds = sorted([(self.lower[child] - o) / m, (self.upper[child] - o) / m])
self.target_lower[master] = max(self.target_lower[master], bounds[0])
self.target_upper[master] = min(self.target_upper[master], bounds[1])
self.rate_multiplier[master] = max(self.rate_multiplier[master], abs(m))
require((self.target_lower <= self.target_upper).all(), "Infeasible coupled limits")
self.spec = {
"hand_side": self.side,
"asset_sha256": manifest["asset_sha256"],
"root_link": manifest["root_link"],
"joint_names": self.names,
"master_names": self.master_names,
"lower_rad": self.lower.tolist(),
"upper_rad": self.upper.tolist(),
"mimic": self.mimic,
}
self.action_dim = 9 + len(self.columns)
# Current root pose/all state joints + terminal geometric/posture goal + phase/duration.
self.observation_dim = 9 + len(self.names) + self.action_dim + 2
def encode_pose(self, position, quaternion, origin):
p0, q0 = origin
r0 = qmatrix(q0)
relative = r0.T @ qmatrix(quaternion)
return np.r_[r0.T @ (np.asarray(position) - p0), relative[:, 0], relative[:, 1]]
def action(self, position, quaternion, joints, origin):
return np.r_[self.encode_pose(position, quaternion, origin), np.asarray(joints)[self.columns]].astype(
np.float32
)
def observation(self, position, quaternion, joints, origin, goal, elapsed, duration):
require(duration > 0 and 0 <= elapsed <= duration + 1e-8, "Invalid trajectory phase")
result = np.r_[
self.encode_pose(position, quaternion, origin), joints, goal, min(elapsed / duration, 1), duration
]
require(result.shape == (self.observation_dim,) and np.isfinite(result).all(), "Invalid policy observation")
return result.astype(np.float32)
def expand(self, masters):
result = np.zeros(len(self.names), dtype=float)
result[self.columns] = masters
for eq in self.mimic:
result[self.names.index(eq["joint"])] = (
eq["multiplier"] * result[self.names.index(eq["reference"])] + eq["offset_rad"]
)
return result
def decode(self, action, origin):
a = np.asarray(action, dtype=float)
require(a.shape == (self.action_dim,) and np.isfinite(a).all(), "Invalid policy action")
p0, q0 = origin
r0 = qmatrix(q0)
return p0 + r0 @ a[:3], matrixq(r0 @ rotation6d(a[3:9])), a[9:]
class TargetLimiter:
"""Bound commands at each physics step; never teleport simulated state."""
def __init__(self, adapter, limits, position, quaternion, joints):
self.adapter, self.limits = adapter, limits
self.origin = np.array(position, dtype=float)
self.position, self.quaternion = np.array(position, dtype=float), np.array(quaternion, dtype=float)
self.masters = np.clip(np.asarray(joints)[adapter.columns], adapter.target_lower, adapter.target_upper)
self.limited_steps = 0
self.steps = 0
def step(self, desired, dt):
require(np.isfinite(dt) and dt > 0, "Positive command dt required")
position, quaternion, masters = desired
limits, adapter = self.limits, self.adapter
require(all(np.isfinite(v).all() for v in desired), "Nonfinite desired command")
bounded_position = self.origin + bounded(np.asarray(position) - self.origin, limits.workspace_radius)
p = self.position + bounded(bounded_position - self.position, limits.reference_speed * dt)
error = rotation_error(quaternion, self.quaternion)
angle = np.linalg.norm(error)
limited = min(angle, limits.reference_angular_speed * dt)
increment = np.r_[np.cos(limited / 2), error * (np.sin(limited / 2) / angle if angle > 1e-12 else 0.5)]
q = qmul(increment, self.quaternion)
q /= np.linalg.norm(q)
clipped = np.clip(masters, adapter.target_lower, adapter.target_upper)
maximum = min(limits.reference_joint_speed, limits.finger_velocity) * dt / adapter.rate_multiplier
m = self.masters + np.clip(clipped - self.masters, -maximum, maximum)
self.limited_steps += int(
np.linalg.norm(p - position) > 1e-8 or angle - limited > 1e-8 or np.max(np.abs(m - masters)) > 1e-8
)
self.steps += 1
self.position, self.quaternion, self.masters = p, q, m
return p.copy(), q.copy(), adapter.expand(m)
@@ -0,0 +1,193 @@
"""Offline ACT preparation/training commands. No command here starts Isaac Sim."""
import argparse
import importlib.util
import json
import platform
import sys
from dataclasses import replace
from pathlib import Path
import numpy as np
import torch
from dex_workbench_tracking.cli import publish_validated, synthetic
from dex_workbench_tracking.trajectory import ContractError, Demonstrations, Episode, require
from .config import Config
from .data import digest, prepare
from .engine import json_write, load_checkpoint, score, train
def read_json(path):
return json.loads(Path(path).read_text(encoding="utf-8"))
def provenance(metadata):
package = Path(__file__).parent
metadata["implementation_sha256"] = {p.name: digest(p) for p in sorted(package.glob("*.py"))}
metadata["python"] = platform.python_version()
metadata["numpy"] = np.__version__
metadata["torch"] = str(torch.__version__)
return metadata
def evaluate(checkpoint, hdf5, manifest, partition):
model, config, adapter, normalization, metadata = load_checkpoint(checkpoint, manifest)
require(
digest(hdf5) == metadata["hdf5_sha256"], "Evaluation HDF5 must match checkpoint; re-review new data explicitly"
)
_, datasets, fitted, _ = prepare(
hdf5, manifest, metadata["splits"], config, metadata["data_review"], synthetic_smoke=metadata["synthetic_smoke"]
)
require(partition in ("validation", "test") and partition in datasets, "Select a nonempty held-out split")
for key in normalization:
np.testing.assert_array_equal(normalization[key], fitted[key])
torch.set_num_threads(1)
metric = score(model, datasets[partition], config.batch_size, "cpu")
return {
"status": "PASS",
"scope": "offline_heldout_reference_prediction_not_dynamic_success",
"split": partition,
"windows": len(datasets[partition]),
"l1_normalized_z0": metric,
"checkpoint_sha256": digest(checkpoint),
"hdf5_sha256": digest(hdf5),
"synthetic_smoke": metadata["synthetic_smoke"],
"simulation_e2e": "NOT_RUN",
}
def smoke(manifest, output):
output = Path(output)
output.mkdir(parents=True, exist_ok=False)
side = manifest.get("hand_side", "left")
config = replace(
Config(),
hand_side=side,
chunk_size=4,
execute_steps=2,
hidden_dim=32,
heads=4,
layers=1,
latent_dim=4,
batch_size=4,
max_updates=4,
evaluate_every=2,
max_seconds=120,
)
original = synthetic(manifest)
episode = original.episodes["demo_000000"]
episodes = {}
for index, factor in enumerate((1.0, 1.3, 1.6)):
position = episode.wrist_position.copy()
position[:, 0] *= factor
episodes[f"demo_{index:06d}"] = Episode(
episode.time.copy(),
position,
episode.wrist_quaternion.copy(),
episode.joint_position.copy() * factor,
episode.valid.copy(),
)
data = Demonstrations(dict(original.metadata), original.joint_names, original.world_from_source.copy(), episodes)
data.metadata["source_description"] += "; THREE ANALYTIC CPU SMOKE FIXTURES, not independent expert recordings"
split = {
"schema": "l20_episode_splits_v1",
"train": ["demo_000000"],
"validation": ["demo_000001"],
"test": ["demo_000002"],
"episode_groups": {name: f"analytic_fixture_{i}" for i, name in enumerate(episodes)},
}
hdf5 = output / "synthetic.hdf5"
publish_validated(hdf5, data, manifest)
json_write(output / "splits.json", split)
json_write(output / "config.json", config.to_dict())
adapter, datasets, normalization, metadata = prepare(hdf5, manifest, split, config, synthetic_smoke=True)
result = train(adapter, datasets, normalization, provenance(metadata), config, output / "train", device="cpu")
evaluation = evaluate(output / "train/last.pt", hdf5, manifest, "test")
json_write(output / "offline-evaluation.json", evaluation)
return {
"status": "PASS",
"scope": "synthetic_CPU_training_save_reload_evaluate_only",
"training": result,
"evaluation": evaluation,
"simulation_e2e": "NOT_RUN",
"output": str(output),
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
doctor = commands.add_parser("doctor", help="CPU dependency check; CUDA probing is opt-in")
doctor.add_argument("--cuda", action="store_true")
for command in ("preflight", "train"):
item = commands.add_parser(command)
item.add_argument("--hdf5", type=Path, required=True)
item.add_argument("--manifest", type=Path, required=True)
item.add_argument("--config", type=Path, required=True)
item.add_argument("--splits", type=Path, required=True)
item.add_argument("--data-review", type=Path, required=True)
item.add_argument("--output", type=Path, required=True)
if command == "train":
item.add_argument("--device", choices=("cpu", "cuda"), default="cpu")
item.add_argument(
"--execute", action="store_true", help="Acknowledge configured updates/time/device budget"
)
evaluation = commands.add_parser("evaluate", help="CPU held-out prediction, not simulation rollout")
evaluation.add_argument("--checkpoint", type=Path, required=True)
evaluation.add_argument("--hdf5", type=Path, required=True)
evaluation.add_argument("--manifest", type=Path, required=True)
evaluation.add_argument("--split", choices=("validation", "test"), default="test")
evaluation.add_argument("--output", type=Path, required=True)
test = commands.add_parser("smoke", help="Four tiny CPU updates on labeled synthetic fixtures; no GPU/Kit")
test.add_argument("--manifest", type=Path, required=True)
test.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
try:
if args.command == "doctor":
require(sys.version_info >= (3, 12), "Python >=3.12 required")
major_minor = tuple(int(v) for v in torch.__version__.split("+")[0].split(".")[:2])
require(major_minor >= (2, 6), "Torch >=2.6 required for tensor-only checkpoint loading")
if args.cuda:
require(torch.cuda.is_available(), "CUDA unavailable; do not install/replace Isaac Torch automatically")
report = {
"status": "PASS",
"scope": "dependency_imports_only",
"python": platform.python_version(),
"torch": str(torch.__version__),
"numpy": np.__version__,
"isaaclab_discoverable": importlib.util.find_spec("isaaclab") is not None,
"cuda": "PASS" if args.cuda else "NOT_RUN",
"simulation_e2e": "NOT_RUN",
}
elif args.command == "smoke":
report = smoke(read_json(args.manifest), args.output)
elif args.command == "evaluate":
report = evaluate(args.checkpoint, args.hdf5, read_json(args.manifest), args.split)
json_write(args.output, report)
else:
if args.command == "train":
require(args.execute, "Training requires --execute; review configured compute budget first")
config = Config.load(args.config)
manifest, split, review = (read_json(p) for p in (args.manifest, args.splits, args.data_review))
adapter, datasets, normalization, metadata = prepare(args.hdf5, manifest, split, config, review)
metadata = provenance(metadata)
if args.command == "preflight":
report = {
"status": "PASS",
"scope": "data_and_training_configuration_only",
"config": config.to_dict(),
"metadata": metadata,
"windows": {k: len(v) for k, v in datasets.items()},
"simulation_e2e": "NOT_RUN",
}
json_write(args.output, report)
else:
report = train(adapter, datasets, normalization, metadata, config, args.output, args.device)
print(json.dumps(report, allow_nan=False))
except (ContractError, OSError, KeyError, TypeError, ValueError, RuntimeError, AssertionError) as error:
parser.exit(1, f"FAIL: {error}\n")
if __name__ == "__main__":
main()
@@ -0,0 +1,69 @@
"""Versioned, bounded configuration for goal-conditioned reference ACT."""
import json
from dataclasses import asdict, dataclass
from pathlib import Path
import numpy as np
from dex_workbench_tracking.trajectory import require
CONTRACT = "l20_goal_reference_act_v1"
CHECKPOINT = "l20_reference_act_checkpoint_v1"
@dataclass(frozen=True)
class Config:
contract: str = CONTRACT
hand_side: str = "right"
control_hz: int = 30
chunk_size: int = 16
execute_steps: int = 4
hidden_dim: int = 128
heads: int = 4
layers: int = 2
latent_dim: int = 32
batch_size: int = 32
learning_rate: float = 0.0001
kl_weight: float = 10.0
max_updates: int = 1000
evaluate_every: int = 100
max_seconds: float = 600.0
max_total_frames: int = 500000
workspace_radius: float = 0.8
seed: int = 42
def __post_init__(self):
require(self.contract == CONTRACT, "Unsupported observation/action contract")
require(self.hand_side in ("left", "right"), "Explicit left/right side required")
for key in (
"control_hz",
"chunk_size",
"execute_steps",
"hidden_dim",
"heads",
"layers",
"latent_dim",
"batch_size",
"max_updates",
"evaluate_every",
"max_total_frames",
"seed",
):
value = getattr(self, key)
require(type(value) is int and value > 0, f"{key} must be a positive integer")
require(240 % self.control_hz == 0, "control_hz must divide the validated 240Hz physics rate")
require(self.execute_steps <= self.chunk_size <= 256, "Require execute_steps <= chunk_size <= 256")
require(self.hidden_dim % self.heads == 0 and self.hidden_dim <= 512, "Invalid transformer width/heads")
require(self.layers <= 6 and self.latent_dim <= 256 and self.batch_size <= 512, "Model/batch resource cap")
require(self.max_updates <= 500000 and self.max_total_frames <= 2000000, "Training/data resource cap")
for key in ("learning_rate", "kl_weight", "max_seconds", "workspace_radius"):
value = getattr(self, key)
require(type(value) in (int, float) and np.isfinite(value) and value > 0, f"Invalid {key}")
require(self.max_seconds <= 86400, "Maximum one-day process budget; choose bounded experiments")
def to_dict(self):
return asdict(self)
@classmethod
def load(cls, path):
return cls(**json.loads(Path(path).read_text(encoding="utf-8")))
@@ -0,0 +1,168 @@
"""Episode/group-isolated reference windows; training-only normalization."""
import hashlib
from dataclasses import replace
from pathlib import Path
import numpy as np
import torch
from dex_workbench_tracking.control import Limits, validate_reference
from dex_workbench_tracking.trajectory import load, require, sample
from torch.utils.data import Dataset
from .adapter import Adapter
def digest(path):
value = hashlib.sha256()
with Path(path).open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
value.update(block)
return value.hexdigest()
def validate_splits(data, split):
require(split.get("schema") == "l20_episode_splits_v1", "Unsupported split schema")
require(
set(split) == {"schema", "train", "validation", "test", "episode_groups"}, "Unexpected/missing split fields"
)
groups = split["episode_groups"]
require(isinstance(groups, dict) and set(groups) == set(data.episodes), "Provide capture group for every episode")
require(all(isinstance(v, str) and v.strip() for v in groups.values()), "Nonempty source capture groups required")
require(split["train"] and split["validation"], "At least one independent train and validation episode required")
seen, group_owner, geometry_owner = set(), {}, {}
for partition in ("train", "validation", "test"):
require(isinstance(split[partition], list), "Split entries must be episode lists")
for name in split[partition]:
require(
isinstance(name, str) and name in data.episodes and name not in seen,
"Unknown/overlapping episode split",
)
seen.add(name)
group = groups[name]
require(group_owner.get(group, partition) == partition, "Capture group leakage across splits")
group_owner[group] = partition
# Catch exact renamed/slowdown copies even if the caller supplies false group IDs.
ep = data.episodes[name]
fingerprint = hashlib.sha256()
for field in (ep.wrist_position, ep.wrist_quaternion, ep.joint_position, ep.valid):
fingerprint.update(np.ascontiguousarray(field).tobytes())
fingerprint = fingerprint.hexdigest()
require(
geometry_owner.get(fingerprint, partition) == partition, "Duplicate trajectory geometry across splits"
)
geometry_owner[fingerprint] = partition
require(seen == set(data.episodes), "Every episode must have an explicit split; no silently ignored data")
def validate_review(review, data_hash, manifest):
require(review.get("schema") == "l20_training_review_v1", "Training review sidecar required")
require(review.get("hdf5_sha256") == data_hash, "Review must bind exact HDF5 bytes")
require(review.get("asset_sha256") == manifest["asset_sha256"], "Review asset identity mismatch")
for flag in ("coordinate_and_scale_reviewed", "reference_state_targets_accepted", "capture_groups_reviewed"):
require(review.get(flag) is True, f"BLOCKED: explicit data-owner approval missing: {flag}")
for key in ("reviewer", "evidence"):
require(isinstance(review.get(key), str) and review[key].strip(), f"Review {key} required")
class Windows(Dataset):
def __init__(self, series, names, chunk_size, normalization=None):
self.series, self.names, self.chunk_size = series, list(names), chunk_size
self.normalization = normalization
self.ends = np.cumsum([len(series[n][0]) - 1 for n in names])
require(len(self.ends) > 0 and self.ends[-1] > 0, "Empty partition")
def __len__(self):
return int(self.ends[-1])
def __getitem__(self, index):
require(0 <= index < len(self), "Window index out of range")
i = int(np.searchsorted(self.ends, index, side="right"))
start = index - (int(self.ends[i - 1]) if i else 0)
obs, actions = self.series[self.names[i]]
stop = min(start + 1 + self.chunk_size, len(actions))
length = stop - start - 1
target = np.repeat(actions[stop - 1 : stop], self.chunk_size, axis=0)
target[:length] = actions[start + 1 : stop]
mask = np.arange(self.chunk_size) < length
x = obs[start].copy()
if self.normalization is not None:
n = self.normalization
x = (x - n["observation_mean"]) / n["observation_std"]
target = (target - n["action_mean"]) / n["action_std"]
return (
torch.from_numpy(x.astype(np.float32)),
torch.from_numpy(target.astype(np.float32)),
torch.from_numpy(mask),
)
def fit_normalization(series, train_names):
result = {}
for label, column in (("observation", 0), ("action", 1)):
# Fit each TRAIN frame once; not padded windows and never validation/test.
values = np.concatenate([series[n][column][:-1] if column == 0 else series[n][column][1:] for n in train_names])
require(np.isfinite(values).all(), "Nonfinite training features")
result[label + "_mean"] = values.mean(axis=0, dtype=np.float64).astype(np.float32)
result[label + "_std"] = np.maximum(values.std(axis=0, dtype=np.float64), 1e-4).astype(np.float32)
return result
def prepare(hdf5, manifest, split, config, review=None, *, synthetic_smoke=False):
require(Path(hdf5).stat().st_size <= 2 * 1024**3, "Single HDF5 exceeds 2GiB preflight cap; shard explicitly")
data = load(hdf5, manifest)
require(data.metadata["hand_side"] == config.hand_side, "Config/data hand side mismatch")
data_hash = digest(hdf5)
if synthetic_smoke:
require(data.metadata["provenance"] == "synthetic", "Smoke cannot relabel real expert data")
else:
require(data.metadata["provenance"] == "expert_retargeted", "Real training requires expert_retargeted data")
validate_review(review or {}, data_hash, manifest)
validate_splits(data, split)
adapter = Adapter(manifest)
limits = replace(Limits(), workspace_radius=config.workspace_radius)
total, series, counts = 0, {}, {}
for name, episode in data.episodes.items():
validate_reference(episode, limits) # No implicit slowdown, clipping or invalid-gap bridging.
steps = int(round(episode.time[-1] * config.control_hz))
require(
steps >= 1 and abs(steps / config.control_hz - episode.time[-1]) < 1e-8,
f"{name}: duration must end on the configured control grid; no silent tail trimming",
)
total += steps + 1
require(total <= config.max_total_frames, "Resampled dataset exceeds configured frame budget")
query = np.arange(steps + 1, dtype=float) / config.control_hz
query[-1] = episode.time[-1] # Only the checked floating-point grid roundoff, not time warping.
ep = sample(episode, query)
origin = ep.wrist_position[0], ep.wrist_quaternion[0]
goal = adapter.action(ep.wrist_position[-1], ep.wrist_quaternion[-1], ep.joint_position[-1], origin)
observations, actions = [], []
for t, p, q, joints in zip(ep.time, ep.wrist_position, ep.wrist_quaternion, ep.joint_position, strict=True):
observations.append(adapter.observation(p, q, joints, origin, goal, t, ep.time[-1]))
actions.append(adapter.action(p, q, joints, origin))
series[name] = np.asarray(observations), np.asarray(actions)
counts[name] = {
"source_frames": len(episode.time),
"control_frames": len(ep.time),
"duration_s": float(ep.time[-1]),
}
normalization = fit_normalization(series, split["train"])
datasets = {
key: Windows(series, split[key], config.chunk_size, normalization)
for key in ("train", "validation", "test")
if split[key]
}
metadata = {
"hdf5_sha256": data_hash,
"provenance": data.metadata["provenance"],
"splits": split,
"data_review": review if not synthetic_smoke else None,
"synthetic_smoke": synthetic_smoke,
"episodes": counts,
"adapter": adapter.spec,
"limits_uncalibrated": vars(limits),
"target_semantics": "next_reference_root_pose_and_independent_q_target_proxy_not_measured_commands",
"observation_semantics": "current_start_relative_pose_all_q_terminal_goal_phase_duration",
"normalization_source": "train_frames_only",
}
return adapter, datasets, normalization, metadata
@@ -0,0 +1,183 @@
"""Bounded offline training, trusted tensor-only checkpoints and deterministic inference."""
import json
import os
import random
import time
from pathlib import Path
import numpy as np
import torch
from dex_workbench_tracking.trajectory import require
from torch.utils.data import DataLoader
from .adapter import Adapter
from .config import CHECKPOINT, Config
from .data import digest
from .model import ReferenceACT, objective
def json_write(path, value):
with Path(path).open("x", encoding="utf-8") as stream:
json.dump(value, stream, indent=2, allow_nan=False)
def score(model, dataset, batch_size, device, deadline=None):
model.eval()
total, count = 0.0, 0
with torch.inference_mode():
for obs, target, valid in DataLoader(dataset, batch_size=batch_size, shuffle=False, num_workers=0):
require(deadline is None or time.monotonic() < deadline, "Training/evaluation wall-time budget exhausted")
prediction = model(obs.to(device))[0]
error = (prediction - target.to(device)).abs() * valid.to(device).unsqueeze(-1)
require(torch.isfinite(error).all().item(), "Nonfinite held-out prediction")
total += error.sum().item()
count += valid.sum().item() * target.shape[-1]
return total / count
def checkpoint_payload(model, config, normalization, metadata, update):
return {
"format": CHECKPOINT,
"config": config.to_dict(),
"metadata": metadata,
"update": update,
"normalization": {k: torch.tensor(v.copy()) for k, v in normalization.items()},
"state_dict": {k: v.detach().cpu().clone() for k, v in model.state_dict().items()},
"torch_version": str(torch.__version__),
}
def save_checkpoint(path, payload):
"""Owned new run directory: atomic replace of this run's best/last checkpoint only."""
path = Path(path)
temporary = path.with_suffix(".pending")
with temporary.open("xb") as stream:
torch.save(payload, stream)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
def load_checkpoint(path, manifest):
require(Path(path).stat().st_size <= 512 * 1024**2, "Checkpoint exceeds 512MiB local loading cap")
# Caller-supplied local file only; no unsafe custom pickle globals.
payload = torch.load(path, map_location="cpu", weights_only=True)
require(payload.get("format") == CHECKPOINT, "Unsupported checkpoint")
config = Config(**payload["config"])
adapter = Adapter(manifest)
require(payload["metadata"]["adapter"] == adapter.spec, "Checkpoint embodiment/asset/adapter mismatch")
require(config.hand_side == adapter.side, "Checkpoint config side mismatch")
normalization = {k: v.cpu().numpy() for k, v in payload["normalization"].items()}
require(
set(normalization) == {"observation_mean", "observation_std", "action_mean", "action_std"},
"Normalization fields mismatch",
)
for key, value in normalization.items():
dim = adapter.observation_dim if key.startswith("observation") else adapter.action_dim
require(value.shape == (dim,) and np.isfinite(value).all(), "Invalid checkpoint normalization")
if key.endswith("std"):
require((value > 0).all(), "Normalization scales must be positive")
model = ReferenceACT(adapter.observation_dim, adapter.action_dim, config)
model.load_state_dict(payload["state_dict"], strict=True)
require(all(torch.isfinite(p).all().item() for p in model.parameters()), "Nonfinite checkpoint parameters")
model.eval()
return model, config, adapter, normalization, payload["metadata"]
def predict(model, normalization, observation):
obs = np.asarray(observation, dtype=np.float32)
require(
obs.shape == normalization["observation_mean"].shape and np.isfinite(obs).all(), "Invalid inference observation"
)
obs = (obs - normalization["observation_mean"]) / normalization["observation_std"]
with torch.inference_mode():
result = model(torch.from_numpy(obs[None]))[0][0].cpu().numpy()
result = result * normalization["action_std"] + normalization["action_mean"]
require(np.isfinite(result).all(), "Nonfinite policy prediction")
return result
def train(adapter, datasets, normalization, metadata, config, output, device="cpu"):
require(device in ("cpu", "cuda"), "Choose explicit cpu or cuda device")
require(device != "cuda" or torch.cuda.is_available(), "CUDA unavailable; no silent CPU fallback")
if metadata["synthetic_smoke"]:
require(
device == "cpu" and config.max_updates <= 10 and config.hidden_dim <= 64 and config.max_seconds <= 120,
"Synthetic smoke is CPU-only and tightly bounded",
)
output = Path(output)
output.mkdir(parents=True, exist_ok=False) # Never mix new data/config/checkpoints with an old run.
json_write(output / "run.json", {"config": config.to_dict(), "metadata": metadata, "device": device})
random.seed(config.seed)
np.random.seed(config.seed)
torch.manual_seed(config.seed)
# Keep CPU smoke inexpensive; this only affects this dedicated CLI/test process.
torch.set_num_threads(1)
model = ReferenceACT(adapter.observation_dim, adapter.action_dim, config).to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=config.learning_rate, weight_decay=1e-4)
generator = torch.Generator().manual_seed(config.seed)
started = time.monotonic()
deadline = started + config.max_seconds
best, completed = float("inf"), 0
last_loss = None
try:
with (output / "metrics.jsonl").open("x", encoding="utf-8") as log:
for update in range(1, config.max_updates + 1):
require(time.monotonic() < deadline, "Training wall-time budget exhausted; incomplete run")
indices = torch.randint(len(datasets["train"]), (config.batch_size,), generator=generator).tolist()
batch = [datasets["train"][index] for index in indices]
obs, target, valid = (torch.stack(values).to(device) for values in zip(*batch, strict=True))
model.train()
optimizer.zero_grad(set_to_none=True)
prediction, mu, logvar = model(obs, target, valid)
loss, l1, kl = objective(prediction, target, valid, mu, logvar, config.kl_weight)
require(torch.isfinite(loss).item(), "Nonfinite training loss")
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0, error_if_nonfinite=True)
optimizer.step()
completed = update
last_loss = {"update": update, "loss": loss.item(), "l1": l1.item(), "kl": kl.item()}
if update % config.evaluate_every == 0 or update == config.max_updates:
validation = score(model, datasets["validation"], config.batch_size, device, deadline)
last_loss["validation_l1_normalized_z0"] = validation
if validation < best:
best = validation
save_checkpoint(
output / "best.pt", checkpoint_payload(model, config, normalization, metadata, update)
)
log.write(json.dumps(last_loss, allow_nan=False) + "\n")
log.flush()
model.eval()
payload = checkpoint_payload(model, config, normalization, metadata, completed)
save_checkpoint(output / "last.pt", payload)
reloaded = ReferenceACT(adapter.observation_dim, adapter.action_dim, config)
reloaded.load_state_dict(
torch.load(output / "last.pt", map_location="cpu", weights_only=True)["state_dict"], strict=True
)
reloaded.eval()
probe = datasets["validation"][0][0].unsqueeze(0)
with torch.inference_mode():
expected = model.cpu()(probe)[0]
actual = reloaded(probe)[0]
torch.testing.assert_close(actual, expected, rtol=0, atol=1e-6)
require(time.monotonic() <= deadline, "Training exceeded budget during finalization")
result = {
"status": "PASS",
"scope": "bounded_offline_reference_act_training_not_policy_quality",
"synthetic_smoke": metadata["synthetic_smoke"],
"completed_updates": completed,
"elapsed_seconds": time.monotonic() - started,
"last_metrics": last_loss,
"best_validation_l1_normalized": best,
"best_checkpoint_sha256": digest(output / "best.pt"),
"last_checkpoint_sha256": digest(output / "last.pt"),
"checkpoint_reload_prediction_max_abs_diff": float((actual - expected).abs().max()),
"simulation_e2e": "NOT_RUN",
"hardware_and_training_quality_validated": False,
}
json_write(output / "result.json", result)
return result
except BaseException as error:
json_write(output / "failure.json", {"status": "FAIL", "completed_updates": completed, "error": str(error)})
raise
@@ -0,0 +1,51 @@
"""State-only ACT-style CVAE Transformer (no vision backbone or official-weight compatibility)."""
import torch
from torch import nn
class ReferenceACT(nn.Module):
def __init__(self, observation_dim, action_dim, config):
super().__init__()
self.config = config
width = config.hidden_dim
self.observation = nn.Linear(observation_dim, width)
self.action = nn.Linear(action_dim, width)
self.cls = nn.Parameter(torch.zeros(1, 1, width))
self.posterior_position = nn.Parameter(torch.randn(1, config.chunk_size + 2, width) * 0.02)
encoder = nn.TransformerEncoderLayer(
width, config.heads, width * 4, dropout=0, batch_first=True, norm_first=True
)
self.posterior = nn.TransformerEncoder(encoder, config.layers, enable_nested_tensor=False)
self.distribution = nn.Linear(width, config.latent_dim * 2)
self.latent = nn.Linear(config.latent_dim, width)
self.queries = nn.Parameter(torch.randn(1, config.chunk_size, width) * 0.02)
decoder = nn.TransformerDecoderLayer(
width, config.heads, width * 4, dropout=0, batch_first=True, norm_first=True
)
self.decoder = nn.TransformerDecoder(decoder, config.layers)
self.output = nn.Linear(width, action_dim)
def forward(self, observation, actions=None, valid=None):
batch = len(observation)
obs = self.observation(observation).unsqueeze(1)
mu = logvar = None
if actions is not None:
tokens = torch.cat((self.cls.expand(batch, -1, -1), obs, self.action(actions)), dim=1)
padding = torch.cat((torch.zeros((batch, 2), dtype=torch.bool, device=obs.device), ~valid), dim=1)
encoded = self.posterior(tokens + self.posterior_position, src_key_padding_mask=padding)[:, 0]
mu, logvar = self.distribution(encoded).chunk(2, dim=-1)
logvar = logvar.clamp(-10, 10)
z = mu + torch.exp(0.5 * logvar) * torch.randn_like(mu)
else:
# Deterministic ACT inference: posterior is never supplied reference future actions.
z = observation.new_zeros((batch, self.config.latent_dim))
memory = torch.cat((obs, self.latent(z).unsqueeze(1)), dim=1)
prediction = self.output(self.decoder(self.queries.expand(batch, -1, -1), memory))
return prediction, mu, logvar
def objective(prediction, actions, valid, mu, logvar, kl_weight):
reconstruction = ((prediction - actions).abs() * valid.unsqueeze(-1)).sum() / (valid.sum() * actions.shape[-1])
kl = -0.5 * (1 + logvar - mu.square() - logvar.exp()).sum(dim=-1).mean()
return reconstruction + kl_weight * kl, reconstruction, kl
@@ -0,0 +1,77 @@
"""Receding action-chunk policy for the existing bounded Isaac Lab replay runner."""
import torch
from dex_workbench_tracking.trajectory import require
from .adapter import TargetLimiter
from .data import digest
from .engine import load_checkpoint, predict
class ClosedLoopReferenceACT:
def __init__(self, checkpoint, manifest, hdf5, episode_name, episode, limits):
# This optional branch owns the dedicated evaluation process; avoid a large
# CPU thread pool for tiny, latency-sensitive chunk inference.
torch.set_num_threads(1)
self.model, self.config, self.adapter, self.normalization, self.metadata = load_checkpoint(checkpoint, manifest)
require(
hdf5 is not None and digest(hdf5) == self.metadata["hdf5_sha256"],
"Policy evaluation must bind exact trained dataset",
)
split = self.metadata["splits"]
require(
episode_name in split["validation"] + split["test"] and episode_name not in split["train"],
"Policy evaluation requires a held-out episode, never train replay",
)
require(vars(limits) == self.metadata["limits_uncalibrated"], "Policy/runtime controller limits mismatch")
self.episode, self.limits = episode, limits
self.checkpoint_hash = digest(checkpoint)
self.physics_per_control = 240 // self.config.control_hz
self.duration = float(episode.time[-1])
self.origin = episode.wrist_position[0], episode.wrist_quaternion[0]
self.goal = self.adapter.action(
episode.wrist_position[-1], episode.wrist_quaternion[-1], episode.joint_position[-1], self.origin
)
self.reset()
def reset(self):
e = self.episode
self.limiter = TargetLimiter(
self.adapter, self.limits, e.wrist_position[0], e.wrist_quaternion[0], e.joint_position[0]
)
self.chunk = None
self.next_step = 0
self.inference_calls = 0
self.desired = None
def target(self, step, pose, joints):
require(step == self.next_step, "Policy clock must advance one physics step at a time; reset explicitly")
if step % self.physics_per_control == 0:
control_step = step // self.physics_per_control
slot = control_step % self.config.execute_steps
if slot == 0:
observation = self.adapter.observation(
pose[:3], pose[3:], joints, self.origin, self.goal, step / 240, self.duration
)
self.chunk = predict(self.model, self.normalization, observation)
self.inference_calls += 1
self.desired = self.adapter.decode(self.chunk[slot], self.origin)
result = self.limiter.step(self.desired, 1 / 240)
self.next_step += 1
return result
def summary(self):
return {
"checkpoint_sha256": self.checkpoint_hash,
"contract": self.config.contract,
"control_hz": self.config.control_hz,
"chunk_size": self.config.chunk_size,
"execute_steps": self.config.execute_steps,
"inference_calls": self.inference_calls,
"limited_target_steps": self.limiter.limited_steps,
"physics_steps": self.limiter.steps,
"synthetic_smoke_checkpoint": self.metadata["synthetic_smoke"],
"observation_source": "measured_current_state_plus_terminal_goal_phase_duration",
"future_reference_used_for_policy": "terminal_goal_only; no teacher forcing",
"limiting_is_not_force_saturation_telemetry": True,
}
@@ -0,0 +1,216 @@
"""CPU-only reference diagnostics; no dynamics, identity overrides or data rewriting.
Requires a schema/manifest-validated, fully valid episode. Finite differences are
reference estimates, not measured velocities. Slowdown bounds cover reference
speed gates only, never force/torque, tracking quality, workspace or hardware.
"""
import argparse
import hashlib
import json
from dataclasses import replace
from pathlib import Path
import numpy as np
from .cli import stretch_time
from .control import Limits, rotation_error, validate_reference
from .trajectory import ContractError, load, require, validate_against_manifest
def stats(values):
values = np.asarray(values, dtype=np.float64)
require(values.size > 0 and np.isfinite(values).all(), "Finite nonempty diagnostic values required")
return {
"min": float(values.min()),
"median": float(np.median(values)),
"p95": float(np.percentile(values, 95)),
"max": float(values.max()),
}
def speed_summary(values, time, limit):
"""Unweighted interval statistics, with zero-based source frame locations."""
index = int(np.argmax(values))
return {
**stats(values),
"reference_limit": limit,
"peak_interval_frames": [index, index + 1],
"peak_interval_time_s": [float(time[index]), float(time[index + 1])],
"over_limit_interval_starts": np.flatnonzero(values > limit).tolist(),
"speed_only_slowdown_lower_bound": max(1.0, float(np.max(values)) / limit),
}
def diagnose(data, manifest, episode_name, limits=None, factors=(1, 60, 69, 70, 75, 80)):
"""No implicit name reorder, invalid-gap bridging, clipping or asset rebinding."""
limits = Limits() if limits is None else limits
validate_against_manifest(data, manifest)
episode = data.episodes[episode_name]
require(episode.valid.all(), "Offline diagnostic needs one fully valid episode; segment explicitly")
time = episode.time
dt = np.diff(time)
require(len(dt) > 0 and np.isfinite(dt).all() and (dt > 0).all(), "Increasing finite time required")
# Promote float32 payload before subtraction to avoid extra cancellation rounding.
position = episode.wrist_position.astype(np.float64)
joints = episode.joint_position.astype(np.float64)
linear = np.diff(position, axis=0) / dt[:, None]
angular = (
np.array(
[
rotation_error(b, a)
for a, b in zip(episode.wrist_quaternion[:-1], episode.wrist_quaternion[1:], strict=True)
]
)
/ dt[:, None]
)
joint_velocity = np.diff(joints, axis=0) / dt[:, None]
speed = np.linalg.norm(linear, axis=1)
omega = np.linalg.norm(angular, axis=1)
qspeed = np.abs(joint_velocity)
displacement = np.linalg.norm(position - position[0], axis=1)
lower = np.array([j["lower_rad"] for j in manifest["joints"]])
upper = np.array([j["upper_rad"] for j in manifest["joints"]])
followers = {eq["joint"] for eq in manifest.get("source_urdf", {}).get("mimic", [])}
per_joint = {}
for column, name in enumerate(data.joint_names):
per_joint[name] = {
"role": "model_follower" if name in followers else "model_independent_target_not_hardware_mapping",
"position_range_rad": [float(joints[:, column].min()), float(joints[:, column].max())],
"minimum_limit_margin_rad": float(
np.minimum(joints[:, column] - lower[column], upper[column] - joints[:, column]).min()
),
"speed_rad_s": speed_summary(qspeed[:, column], time, limits.reference_joint_speed),
}
mimic = {}
for eq in manifest.get("source_urdf", {}).get("mimic", []):
child, parent = (data.joint_names.index(eq[key]) for key in ("joint", "reference"))
residual = joints[:, child] - eq["multiplier"] * joints[:, parent] - eq["offset_rad"]
mimic[eq["joint"]] = {
"max_abs_residual_rad": float(np.abs(residual).max()),
"peak_frame": int(np.argmax(np.abs(residual))),
"reference_tolerance_rad": 1e-3,
"leader_range_rad": float(np.ptp(joints[:, parent])),
}
summaries = {
"translation_m_s": speed_summary(speed, time, limits.reference_speed),
"rotation_rad_s": speed_summary(omega, time, limits.reference_angular_speed),
"all_joint_max_rad_s": speed_summary(qspeed.max(axis=1), time, limits.reference_joint_speed),
}
speed_bound = max(s["speed_only_slowdown_lower_bound"] for s in summaries.values())
candidates = []
for factor in factors:
# Reuse the real float32 reference gate, rather than treating our double
# precision estimates as an exact reproduction of boundary comparisons.
stretched = stretch_time(data, factor).episodes[episode_name]
try:
validate_reference(stretched, limits)
gate_status, reason = "PASS", None
except ContractError as error:
gate_status, reason = "FAIL", str(error)
candidates.append(
{
"factor": float(factor),
"duration_s": float(stretched.time[-1]),
"estimated_peak_translation_m_s": float(speed.max() / factor),
"estimated_peak_rotation_rad_s": float(omega.max() / factor),
"estimated_peak_joint_rad_s": float(qspeed.max() / factor),
"reference_gate": gate_status,
"first_gate_failure": reason,
"dynamics": "NOT_RUN",
}
)
# Midpoint finite differences describe reference roughness only. The replay
# interpolator is piecewise linear/SLERP: knot acceleration is not bounded by
# these estimates, and no dynamics/force inference is justified from them.
roughness = None
if len(dt) > 1:
midpoint_dt = (dt[:-1] + dt[1:]) / 2
roughness = {
"translation_m_s2": stats(np.linalg.norm(np.diff(linear, axis=0), axis=1) / midpoint_dt),
"rotation_rad_s2": stats(np.linalg.norm(np.diff(angular, axis=0), axis=1) / midpoint_dt),
"all_joint_max_rad_s2": stats((np.abs(np.diff(joint_velocity, axis=0)) / midpoint_dt[:, None]).max(axis=1)),
}
return {
"report_version": "l20_reference_diagnostic_v1",
"status": "PASS",
"status_scope": "offline report generated; NOT dynamic or replay approval",
"asset_compatibility": "PASS_MANIFEST_ONLY_NOT_USD_REINSPECTION",
"episode": episode_name,
"hand_side": data.metadata["hand_side"],
"asset_sha256": data.metadata["asset_sha256"],
"provenance": data.metadata["provenance"],
"frames": len(time),
"valid_frames": int(episode.valid.sum()),
"duration_s": float(time[-1]),
"sample_dt_s": stats(dt),
"effective_sample_hz": float(1 / np.median(dt)),
"quaternion_max_norm_error": float(
np.abs(np.linalg.norm(episode.wrist_quaternion.astype(float), axis=1) - 1).max()
),
"limits_uncalibrated": vars(limits),
"speed_estimates": summaries,
"speed_only_slowdown_lower_bound": speed_bound,
"workspace": {
"max_displacement_from_start_m": float(displacement.max()),
"peak_frame": int(displacement.argmax()),
"radius_m": limits.workspace_radius,
"outside_frames": np.flatnonzero(displacement > limits.workspace_radius).tolist(),
"fixable_by_time_stretch": False,
"path_length_m": float(np.linalg.norm(np.diff(position, axis=0), axis=1).sum()),
},
"joints": per_joint,
"mimic": mimic,
"midpoint_acceleration_estimates_not_physical_bounds": roughness,
"intervals": [
{
"frames": [i, i + 1],
"time_s": [float(time[i]), float(time[i + 1])],
"translation_m_s": float(speed[i]),
"rotation_rad_s": float(omega[i]),
"max_joint_rad_s": float(qspeed[i].max()),
"fastest_joint": data.joint_names[int(qspeed[i].argmax())],
}
for i in range(len(dt))
],
"candidates": candidates,
"dynamic_replay": "NOT_RUN",
"limitations": [
"Derived reference differences, not measured hardware or simulator velocities.",
"No position/rotation rescaling, recentering, smoothing or invalid-gap interpolation.",
"Speed-only lower bound is necessary for source intervals, not a dynamics guarantee.",
"Piecewise interpolation has velocity jumps; midpoint accelerations are not knot bounds.",
"No force/torque saturation, collision, calibration or tracking stability measured.",
"Candidate reference gate is the existing validate_reference, not full runtime acceptance.",
],
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input", type=Path)
parser.add_argument("--manifest", type=Path, required=True)
parser.add_argument("--episode", default="demo_000000")
parser.add_argument("--output", type=Path, required=True, help="New JSON path, never overwrite")
parser.add_argument("--workspace-radius", type=float, help="Comparison only; does not change runtime defaults")
parser.add_argument("--factors", nargs="+", type=float, default=[1, 60, 69, 70, 75, 80])
args = parser.parse_args()
try:
manifest = json.loads(args.manifest.read_text())
data = load(args.input, manifest)
limits = Limits()
if args.workspace_radius is not None:
limits = replace(limits, workspace_radius=args.workspace_radius)
report = diagnose(data, manifest, args.episode, limits, args.factors)
report["input_hdf5_sha256"] = hashlib.sha256(args.input.read_bytes()).hexdigest()
report["manifest_file_sha256"] = hashlib.sha256(args.manifest.read_bytes()).hexdigest()
encoded = json.dumps(report, indent=2, allow_nan=False) + "\n"
with args.output.open("x", encoding="utf-8") as stream:
stream.write(encoded)
print(json.dumps({"status": "PASS", "scope": report["status_scope"], "output": str(args.output)}))
except (ContractError, OSError, KeyError, TypeError, ValueError) as error:
parser.exit(1, f"FAIL: {error}\n")
if __name__ == "__main__":
main()
@@ -0,0 +1,65 @@
"""Presentation-only estimated source camera; no physics or reference changes."""
import numpy as np
def load_camera(path, reference_time, time_factor):
"""Validate source timestamps against the replay's explicitly stretched time."""
if not np.isfinite(time_factor) or time_factor < 1:
raise ValueError("Camera time factor must be finite and >= 1")
with np.load(path, allow_pickle=False) as data:
time = data["time"].copy()
poses = data["final_world_from_camera"].copy()
inverse = data["camera_from_final_world"].copy()
k = data["K"].copy()
if time.ndim != 1 or len(time) < 2 or poses.shape != (len(time), 4, 4):
raise ValueError("Invalid camera array shapes")
if not all(np.isfinite(a).all() for a in (time, poses, inverse, k)):
raise ValueError("Nonfinite camera data")
if not (np.diff(time) > 0).all() or time[0] != 0:
raise ValueError("Invalid camera timestamps")
np.testing.assert_allclose(time * time_factor, reference_time, atol=1e-9, rtol=0)
np.testing.assert_allclose(poses @ inverse, np.broadcast_to(np.eye(4), poses.shape), atol=2e-6, rtol=0)
rotation = poses[:, :3, :3]
np.testing.assert_allclose(rotation.transpose(0, 2, 1) @ rotation,
np.broadcast_to(np.eye(3), rotation.shape), atol=3e-6, rtol=0)
np.testing.assert_allclose(np.linalg.det(rotation), 1, atol=3e-6, rtol=0)
np.testing.assert_allclose(poses[:, 3], np.tile([0, 0, 0, 1], (len(time), 1)), atol=1e-9)
# This delivery uses centered 1280x720 full-image intrinsics, no crop/distortion.
if k.shape != (3, 3) or k[0, 0] <= 0 or k[1, 1] <= 0:
raise ValueError("Invalid camera intrinsics")
np.testing.assert_allclose(k, [[k[0, 0], 0, 640], [0, k[1, 1], 360], [0, 0, 1]], atol=1e-9)
return time * time_factor, poses, k
def usd_pose(cv_pose):
"""OpenCV +Y down/+Z forward to USD +Y up/-Z forward, column-vector SE3."""
return cv_pose @ np.diag([1.0, -1.0, -1.0, 1.0])
class SourceCamera:
def __init__(self, stage, path, reference_time, time_factor):
from pxr import Gf, UsdGeom
from omni.kit.viewport.utility import get_active_viewport
self.time, self.poses, k = load_camera(path, reference_time, time_factor)
self.Gf = Gf
camera = UsdGeom.Camera.Define(stage, "/World/SourceVideoCamera")
camera.CreateProjectionAttr("perspective")
camera.CreateFocalLengthAttr(20.0)
camera.CreateHorizontalApertureAttr(20.0 * 1280 / k[0, 0])
camera.CreateVerticalApertureAttr(20.0 * 720 / k[1, 1])
camera.CreateClippingRangeAttr(Gf.Vec2f(0.01, 100.0))
self.op = UsdGeom.Xformable(camera.GetPrim()).AddTransformOp()
viewport = get_active_viewport()
if viewport is None:
raise RuntimeError("Source camera requires an active Kit viewport")
viewport.set_texture_resolution((1280, 720))
viewport.camera_path = str(camera.GetPath())
self.update(0)
def update(self, replay_time):
# Hold each exported source frame until the next timestamp (no guessed poses).
index = int(np.clip(np.searchsorted(self.time, replay_time, side="right") - 1, 0, len(self.time) - 1))
matrix = usd_pose(self.poses[index])
self.op.Set(self.Gf.Matrix4d(*matrix.T.reshape(-1).tolist()))
+41
View File
@@ -4,6 +4,47 @@ Changelog
Unreleased
~~~~~~~~~~
0.1.3 (2026-09-15)
~~~~~~~~~~~~~~~~~~
Experimental progress snapshot; not a full simulation release-gate PASS.
Added
^^^^^
* Add bounded state-only reference ACT (CVAE Transformer) training, train-only
normalization, capture-group/episode splits, hash-bound data-review requirements,
tensor-only checkpoint loading and offline held-out evaluation. Provide explicit
configuration templates, an Isaac-Python wrapper and synthetic CPU smoke.
* Add an opt-in checkpoint branch to the existing bounded tracking runner with
measured-state feedback, terminal goal/phase conditioning, receding action chunks
and manifest-bound coupled position/rate limits. Existing runtime assertions remain;
the new policy GPU E2E and real-data training are NOT_RUN. No new Gym/PPO task or
hardware mapping is claimed; existing tracking HDF5 schema and Cartpole are unchanged.
* Add a CPU-only, identity-validated reference diagnostic with source-interval
speeds, workspace/limit/mimic metrics and actual existing reference-gate checks
for in-memory slowdown candidates. Preserve inputs and refuse output overwrites.
Document the right expert trajectory's translation-dominated ~69.37x speed-only
slowdown bound; this is not dynamic feasibility or original-speed acceptance.
Runtime controls, assets and data schema remain unchanged.
* Record one separately authorized right expert-reference replay at 75x slowdown:
full 2x30000 steps at 240Hz, exit0/PASS in 364 seconds with unchanged controls
and assertions; 98 CPU/USD regressions pass. Preserve the upstream world-frame
and metric-scale verification blocker; no original-speed, GUI, task or hardware claim.
* Add opt-in estimated source-video camera replay with explicit OpenCV-to-USD
axes, centered 1280x720 intrinsics, source-frame hold and validated time stretch.
Camera data is external; no calibration or pixel-alignment claim. The first
full GUI run reached GUI_READY but timed out at 600 seconds (exit 124); full
camera replay acceptance is FAIL, not PASS.
* Record the new 1333-frame right reference: verified source model binding and
unchanged geometry, 3x slowdown / 0.4 m workspace, full default-camera GUI
replay 2x31968 steps at 240 Hz, exit 0/PASS. Maximum wrist position error is
3.582 mm; this does not validate original speed, object contact or grasp success.
* Verify 120 CPU/USD regression tests without skips and a fresh four-update
synthetic CPU ACT save/reload/held-out smoke. Full pre-commit is BLOCKED by
missing tooling; policy GPU E2E and real-data training remain NOT_RUN.
0.1.2 (2026-09-14)
~~~~~~~~~~~~~~~~~~
+5 -2
View File
@@ -24,7 +24,7 @@ INSTALL_REQUIRES = [
# Installation operation
setup(
name="dex_workbench",
packages=["dex_workbench", "dex_workbench_tracking"],
packages=["dex_workbench", "dex_workbench_tracking", "dex_workbench_imitation"],
author=EXTENSION_TOML_DATA["package"]["author"],
maintainer=EXTENSION_TOML_DATA["package"]["maintainer"],
url=EXTENSION_TOML_DATA["package"]["repository"],
@@ -32,7 +32,10 @@ setup(
description=EXTENSION_TOML_DATA["package"]["description"],
keywords=EXTENSION_TOML_DATA["package"]["keywords"],
install_requires=INSTALL_REQUIRES,
extras_require={"tracking": ["numpy>=1.26", "h5py>=3.10"]},
extras_require={
"tracking": ["numpy>=1.26", "h5py>=3.10"],
"imitation": ["numpy>=1.26", "h5py>=3.10", "torch>=2.6"],
},
license="Apache-2.0",
include_package_data=True,
python_requires=">=3.12",
@@ -0,0 +1,427 @@
"""Analytic CPU tests; no Kit, GPU, real demonstrations or policy-quality claims."""
import copy
import importlib.util
import json
import subprocess
import sys
import tempfile
import unittest
from dataclasses import replace
from pathlib import Path
import numpy as np
import torch
from dex_workbench_imitation.adapter import Adapter, TargetLimiter, matrixq, qmatrix, qmul, rotation6d
from dex_workbench_imitation.cli import evaluate, smoke
from dex_workbench_imitation.config import Config
from dex_workbench_imitation.data import digest, fit_normalization, prepare, validate_review, validate_splits
from dex_workbench_imitation.engine import load_checkpoint, predict, train
from dex_workbench_imitation.model import ReferenceACT, objective
from dex_workbench_imitation.policy import ClosedLoopReferenceACT
from dex_workbench_tracking.cli import synthetic, write
from dex_workbench_tracking.control import Limits, rotation_error
from dex_workbench_tracking.trajectory import ContractError, Demonstrations, Episode, load
ROOT = Path(__file__).resolve().parents[3]
def manifest_fixture():
return {
"manifest_version": "l20_asset_manifest_v1",
"hand_side": "left",
"asset_sha256": "a" * 64,
"root_link": "analytic_base",
"joints": [
{"name": "follower", "lower_rad": 0.0, "upper_rad": 0.8},
{"name": "leader", "lower_rad": 0.0, "upper_rad": 1.0},
],
"source_urdf": {"mimic": [{"joint": "follower", "reference": "leader", "multiplier": 2.0, "offset_rad": 0.0}]},
}
def fixtures(manifest):
data = synthetic(manifest)
ep = data.episodes["demo_000000"]
episodes = {}
for i, factor in enumerate((1.0, 1.3, 1.6)):
p = ep.wrist_position.copy()
p[:, 0] *= factor
episodes[f"demo_{i:06d}"] = Episode(
ep.time.copy(), p, ep.wrist_quaternion.copy(), ep.joint_position * factor, ep.valid.copy()
)
data = Demonstrations(dict(data.metadata), data.joint_names, data.world_from_source.copy(), episodes)
split = {
"schema": "l20_episode_splits_v1",
"train": ["demo_000000"],
"validation": ["demo_000001"],
"test": ["demo_000002"],
"episode_groups": {n: f"analytic_{i}" for i, n in enumerate(episodes)},
}
return data, split
class AdapterTests(unittest.TestCase):
def setUp(self):
self.adapter = Adapter(manifest_fixture())
def test_half_turn_and_sign_equivalent_geometry_round_trip(self):
for q in ([1.0, 0, 0, 0], [0.0, 1, 0, 0], [0.0, 0, 1, 0], [0.0, 0, 0, 1], [0.5, 0.5, 0.5, 0.5]):
with self.subTest(q=q):
np.testing.assert_allclose(qmatrix(matrixq(qmatrix(q))), qmatrix(q), atol=1e-12)
np.testing.assert_allclose(qmatrix(-np.array(q)), qmatrix(q), atol=1e-12)
r = qmatrix(q)
np.testing.assert_allclose(rotation6d(np.r_[r[:, 0], r[:, 1]]), r, atol=1e-12)
def test_start_relative_encoding_invariant_to_global_rigid_transform(self):
origin = np.array([1.0, 2, 3]), np.array([1.0, 0, 0, 0])
p, q, joints = np.array([1.1, 2.2, 3.3]), np.array([0.5, 0.5, 0.5, 0.5]), np.array([0.4, 0.2])
a = self.adapter.action(p, q, joints, origin)
global_q = np.array([0.5, -0.5, 0.5, 0.5])
r, shift = qmatrix(global_q), np.array([4.0, 5, 6])
transformed = self.adapter.action(
r @ p + shift, qmul(global_q, q), joints, (r @ origin[0] + shift, qmul(global_q, origin[1]))
)
np.testing.assert_allclose(a, transformed, atol=1e-6)
p2, q2, m = self.adapter.decode(a, origin)
np.testing.assert_allclose(p2, p, atol=1e-6)
np.testing.assert_allclose(qmatrix(q2), qmatrix(q), atol=1e-6)
np.testing.assert_allclose(self.adapter.expand(m), joints, atol=1e-6)
def test_degenerate_action_is_rejected_not_silently_repaired(self):
for values in ([0] * 6, [1, 0, 0, 2, 0, 0], [float("nan")] * 6):
with self.assertRaises(ContractError):
rotation6d(values)
def test_coupled_limit_and_all_state_joint_rate_envelope(self):
self.assertEqual(self.adapter.master_names, ["leader"])
np.testing.assert_array_equal(self.adapter.target_upper, [0.4])
limiter = TargetLimiter(self.adapter, Limits(), [0, 0, 0], [1, 0, 0, 0], [0, 0])
desired = np.array([10.0, 0, 0]), np.array([0.0, 0, 0, 1]), np.array([10.0])
p0, q0, j0 = np.zeros(3), np.array([1.0, 0, 0, 0]), np.zeros(2)
for _ in range(250):
p, q, j = limiter.step(desired, 1 / 240)
self.assertLessEqual(np.linalg.norm(p - p0), 0.05 / 240 + 1e-12)
self.assertLessEqual(np.linalg.norm(rotation_error(q, q0)), 0.5 / 240 + 1e-12)
self.assertLessEqual(np.max(np.abs(j - j0)), 0.5 / 240 + 1e-12)
self.assertLessEqual(np.linalg.norm(p), 0.1 + 1e-12)
self.assertAlmostEqual(j[0], 2 * j[1])
p0, q0, j0 = p, q, j
self.assertEqual(limiter.limited_steps, 250)
def test_negative_mimic_intersection_and_unsupported_cascade(self):
manifest = manifest_fixture()
manifest["source_urdf"]["mimic"][0].update(multiplier=-2.0, offset_rad=0.8)
adapter = Adapter(manifest)
np.testing.assert_allclose(adapter.target_lower, [0])
np.testing.assert_allclose(adapter.target_upper, [0.4])
manifest["source_urdf"]["mimic"][0]["reference"] = "follower"
with self.assertRaises(ContractError):
Adapter(manifest)
class DatasetTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.path = Path(self.directory.name) / "data.hdf5"
self.manifest = manifest_fixture()
self.data, self.split = fixtures(self.manifest)
self.config = replace(Config(), hand_side="left", chunk_size=4)
write(self.path, self.data)
def prepared(self, **kwargs):
return prepare(self.path, self.manifest, self.split, self.config, synthetic_smoke=True, **kwargs)
def test_future_windows_padding_and_split_isolation(self):
_, datasets, norm, metadata = self.prepared()
training = datasets["train"]
self.assertEqual(training.names, ["demo_000000"])
obs, actions, valid = training[len(training) - 1]
self.assertEqual(valid.tolist(), [True, False, False, False])
n = norm
actual = actions[0].numpy() * n["action_std"] + n["action_mean"]
np.testing.assert_allclose(actual, training.series["demo_000000"][1][-1], atol=1e-6)
self.assertEqual(metadata["episodes"]["demo_000000"]["control_frames"], 61)
self.assertTrue(torch.isfinite(obs).all())
def test_normalization_cannot_see_heldout_values(self):
_, datasets, norm, _ = self.prepared()
series = copy.deepcopy(datasets["train"].series)
for name in self.split["validation"] + self.split["test"]:
series[name] = (series[name][0] * 10000, series[name][1] * 10000)
new = fit_normalization(series, self.split["train"])
for k in norm:
np.testing.assert_array_equal(norm[k], new[k])
def test_cross_partition_episode_group_and_duplicate_geometry_rejected(self):
for kind in ("episode", "group", "geometry", "unassigned"):
split, data = copy.deepcopy(self.split), copy.deepcopy(self.data)
if kind == "episode":
split["test"] = split["train"]
elif kind == "group":
split["episode_groups"]["demo_000001"] = "analytic_0"
elif kind == "geometry":
data.episodes["demo_000001"] = copy.deepcopy(data.episodes["demo_000000"])
else:
split["test"] = []
with self.subTest(kind=kind), self.assertRaises(ContractError):
validate_splits(data, split)
def test_review_requires_hash_owner_and_all_flags(self):
review = {
"schema": "l20_training_review_v1",
"hdf5_sha256": digest(self.path),
"asset_sha256": "a" * 64,
"coordinate_and_scale_reviewed": True,
"reference_state_targets_accepted": True,
"capture_groups_reviewed": True,
"reviewer": "analytic test fixture only",
"evidence": "test",
}
validate_review(review, digest(self.path), self.manifest)
for key in (
"hdf5_sha256",
"coordinate_and_scale_reviewed",
"reference_state_targets_accepted",
"capture_groups_reviewed",
"reviewer",
):
bad = dict(review)
bad[key] = False
with self.subTest(key=key), self.assertRaises(ContractError):
validate_review(bad, digest(self.path), self.manifest)
def test_real_training_cannot_accept_synthetic_or_unreviewed_expert_flags(self):
with self.assertRaisesRegex(ContractError, "expert_retargeted"):
prepare(self.path, self.manifest, self.split, self.config, {})
# Deliberately forged expert flag in an analytic contract test, never real training data.
self.data.metadata["provenance"] = "expert_retargeted"
expert = self.path.with_name("expert-flag-contract-fixture.hdf5")
write(expert, self.data)
with self.assertRaisesRegex(ContractError, "review"):
prepare(expert, self.manifest, self.split, self.config, {})
with self.assertRaisesRegex(ContractError, "relabel"):
prepare(expert, self.manifest, self.split, self.config, synthetic_smoke=True)
def test_speed_invalid_gap_off_grid_and_frame_budget_fail_closed(self):
for kind in ("speed", "gap", "offgrid", "budget"):
data = copy.deepcopy(self.data)
config = self.config
e = data.episodes["demo_000000"]
if kind == "speed":
e.wrist_position[1, 0] = 0.09
elif kind == "gap":
e.valid[1] = False
elif kind == "offgrid":
e.time[-1] += 0.001
else:
config = replace(config, max_total_frames=10)
path = self.path.with_name(kind + ".hdf5")
write(path, data)
with self.subTest(kind=kind), self.assertRaises(ContractError):
prepare(path, self.manifest, self.split, config, synthetic_smoke=True)
def test_prepare_does_not_modify_file_or_manifest(self):
before, manifest = self.path.read_bytes(), copy.deepcopy(self.manifest)
self.prepared()
self.assertEqual(before, self.path.read_bytes())
self.assertEqual(manifest, self.manifest)
def test_config_rejects_invalid_dimensions_and_resource_values(self):
for fields in (
{"control_hz": 29},
{"hidden_dim": 31},
{"execute_steps": 17},
{"max_updates": True},
{"max_seconds": float("nan")},
{"max_total_frames": 2000001},
):
with self.subTest(fields=fields), self.assertRaises(ContractError):
Config(**fields)
class ModelTests(unittest.TestCase):
def test_real_cvae_gradient_padding_and_deterministic_zero_latent_inference(self):
torch.manual_seed(42)
torch.set_num_threads(1)
config = replace(Config(), hidden_dim=32, layers=1, latent_dim=4, chunk_size=4, execute_steps=2)
model = ReferenceACT(6, 3, config)
obs, target = torch.randn(2, 6), torch.randn(2, 4, 3)
valid = torch.tensor([[True, True, False, False], [True, False, False, False]])
pred, mu, logvar = model(obs, target, valid)
changed = target.clone()
changed[~valid] = 10000
_, mu2, lv2 = model(obs, changed, valid)
torch.testing.assert_close(mu, mu2, rtol=0, atol=1e-6)
torch.testing.assert_close(logvar, lv2, rtol=0, atol=1e-6)
loss, _, _ = objective(pred, target, valid, mu, logvar, config.kl_weight)
loss.backward()
self.assertTrue(all(p.grad is None or torch.isfinite(p.grad).all() for p in model.parameters()))
model.eval()
with torch.inference_mode():
a = model(obs)[0]
b = model(obs)[0]
torch.testing.assert_close(a, b, rtol=0, atol=0)
class TrainingAndPolicyTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.directory = tempfile.TemporaryDirectory()
cls.path = Path(cls.directory.name) / "smoke"
cls.manifest = manifest_fixture()
cls.result = smoke(cls.manifest, cls.path)
cls.checkpoint = cls.path / "train/last.pt"
@classmethod
def tearDownClass(cls):
cls.directory.cleanup()
def test_four_update_save_reload_and_heldout_evaluation(self):
self.assertEqual(self.result["training"]["completed_updates"], 4)
self.assertEqual(self.result["training"]["checkpoint_reload_prediction_max_abs_diff"], 0)
self.assertEqual(self.result["evaluation"]["split"], "test")
self.assertEqual(self.result["simulation_e2e"], "NOT_RUN")
self.assertTrue(self.checkpoint.stat().st_size > 0)
model, config, adapter, norm, _ = load_checkpoint(self.checkpoint, self.manifest)
values = predict(model, norm, norm["observation_mean"])
self.assertEqual(values.shape, (config.chunk_size, adapter.action_dim))
with self.assertRaises(ContractError):
evaluate(self.checkpoint, self.path / "synthetic.hdf5", self.manifest, "train")
def test_checkpoint_side_identity_and_bad_normalization_rejected(self):
manifest = copy.deepcopy(self.manifest)
manifest["asset_sha256"] = "b" * 64
with self.assertRaisesRegex(ContractError, "mismatch"):
load_checkpoint(self.checkpoint, manifest)
payload = torch.load(self.checkpoint, weights_only=True)
payload["normalization"]["action_std"][0] = 0
path = self.path / "invalid-norm.pt"
torch.save(payload, path)
with self.assertRaisesRegex(ContractError, "positive"):
load_checkpoint(path, self.manifest)
def test_policy_reset_clock_rate_limits_and_no_future_teacher_forcing(self):
hdf5 = self.path / "synthetic.hdf5"
ep = load(hdf5, self.manifest).episodes["demo_000001"]
limits = replace(Limits(), workspace_radius=0.8)
policy = ClosedLoopReferenceACT(self.checkpoint, self.manifest, hdf5, "demo_000001", ep, limits)
pose = np.r_[ep.wrist_position[0], ep.wrist_quaternion[0]]
traces = []
for repetition in range(2):
policy.reset()
trace = []
for step in range(128):
# Analytic observations only: not a simulated physics trace.
p, q, j = policy.target(step, pose, ep.joint_position[0])
self.assertAlmostEqual(j[0], 2 * j[1])
self.assertTrue((j >= policy.adapter.lower - 1e-8).all())
self.assertTrue((j <= policy.adapter.upper + 1e-8).all())
trace.append(np.r_[p, q, j])
traces.append(trace)
np.testing.assert_array_equal(traces[0], traces[1])
self.assertEqual(policy.summary()["inference_calls"], 8)
self.assertEqual(policy.summary()["physics_steps"], 128)
with self.assertRaisesRegex(ContractError, "clock"):
policy.target(130, pose, ep.joint_position[0])
with self.assertRaisesRegex(ContractError, "held-out"):
ClosedLoopReferenceACT(self.checkpoint, self.manifest, hdf5, "demo_000000", ep, limits)
with self.assertRaisesRegex(ContractError, "limits"):
ClosedLoopReferenceACT(self.checkpoint, self.manifest, hdf5, "demo_000001", ep, Limits())
def test_run_directory_is_never_overwritten_and_budget_failure_is_not_pass(self):
with self.assertRaises(FileExistsError):
smoke(self.manifest, self.path)
config = replace(
Config(),
hand_side="left",
hidden_dim=32,
layers=1,
latent_dim=4,
chunk_size=4,
execute_steps=2,
batch_size=2,
max_updates=1,
max_seconds=1e-12,
)
split = json.loads((self.path / "splits.json").read_text())
adapter, datasets, norm, metadata = prepare(
self.path / "synthetic.hdf5", self.manifest, split, config, synthetic_smoke=True
)
output = self.path / "deadline-failure"
with self.assertRaisesRegex(ContractError, "budget"):
train(adapter, datasets, norm, metadata, config, output)
self.assertFalse((output / "result.json").exists())
self.assertEqual(json.loads((output / "failure.json").read_text())["status"], "FAIL")
def test_cli_heldout_eval_and_missing_inputs_fail_without_kit(self):
manifest = self.path / "manifest.json"
manifest.write_text(json.dumps(self.manifest))
output = self.path / "cli-evaluation.json"
base = [sys.executable, "-m", "dex_workbench_imitation.cli"]
result = subprocess.run(
base
+ [
"evaluate",
"--checkpoint",
str(self.checkpoint),
"--hdf5",
str(self.path / "synthetic.hdf5"),
"--manifest",
str(manifest),
"--output",
str(output),
],
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(json.loads(output.read_text())["simulation_e2e"], "NOT_RUN")
result = subprocess.run(
base
+ [
"preflight",
"--hdf5",
"missing.hdf5",
"--manifest",
str(manifest),
"--config",
str(ROOT / "configs/imitation/l20_right_act.json"),
"--splits",
str(self.path / "splits.json"),
"--data-review",
str(ROOT / "configs/imitation/data_review.example.json"),
"--output",
str(self.path / "missing.json"),
],
capture_output=True,
text=True,
)
self.assertNotEqual(result.returncode, 0)
self.assertFalse((self.path / "missing.json").exists())
class EntryTests(unittest.TestCase):
def test_optional_policy_argument_preserves_default_and_requires_full_hdf5_before_kit(self):
script = ROOT / "scripts/tracking/track_l20.py"
spec = importlib.util.spec_from_file_location("act_entry_test", script)
entry = importlib.util.module_from_spec(spec)
spec.loader.exec_module(entry)
parser = entry.build_parser(lambda parser: None)
args = parser.parse_args(["asset.usda", "--manifest", "manifest.json"])
self.assertIsNone(args.policy_checkpoint)
args = parser.parse_args(["asset.usda", "--manifest", "manifest.json", "--policy-checkpoint", "checkpoint.pt"])
self.assertEqual(args.policy_checkpoint, Path("checkpoint.pt"))
# AST/source invariant: original runtime assertion thresholds have not been disabled.
source = script.read_text()
for assertion in (
"residual < 0.002",
"position_error < 0.05 and angle_error < 0.5 and joint_error < 0.2",
"np.testing.assert_allclose(reset_states[0], reset_states[1], atol=1e-6, rtol=0)",
):
self.assertIn(assertion, source)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,40 @@
"""CPU regression for camera axes and explicit slow-replay synchronization."""
import tempfile
import unittest
from pathlib import Path
import numpy as np
from dex_workbench_tracking.source_camera import load_camera, usd_pose
class SourceCameraTests(unittest.TestCase):
def test_axes_and_projection(self):
cv = np.eye(4)
cv[:3, 3] = [1, 2, 3]
usd = usd_pose(cv)
np.testing.assert_array_equal(usd[:3, 3], cv[:3, 3])
point = cv @ np.array([.1, .2, 1, 1])
np.testing.assert_allclose(np.linalg.inv(usd) @ point, [.1, -.2, -1, 1])
def test_time_binding_and_invalid_rotation(self):
with tempfile.TemporaryDirectory() as folder:
path = Path(folder) / 'camera.npz'
poses = np.tile(np.eye(4), (2, 1, 1))
k = np.array([[1000, 0, 640], [0, 1000, 360], [0, 0, 1]])
np.savez(path, time=[0, 1], final_world_from_camera=poses,
camera_from_final_world=poses, K=k)
time, _, _ = load_camera(path, np.array([0, 3]), 3)
np.testing.assert_array_equal(time, [0, 3])
with self.assertRaises(AssertionError):
load_camera(path, np.array([0, 3]), 1)
with self.assertRaises(ValueError):
load_camera(path, np.array([0, 3]), float('nan'))
poses[:, 0, 0] = -1
np.savez(path, time=[0, 1], final_world_from_camera=poses,
camera_from_final_world=poses, K=k)
with self.assertRaises(AssertionError):
load_camera(path, np.array([0, 3]), 3)
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,212 @@
"""CPU analytic tests: offline reference metrics never authorize physical replay."""
import copy
import json
import subprocess
import sys
import tempfile
import unittest
from dataclasses import replace
from pathlib import Path
import numpy as np
from dex_workbench_tracking.cli import stretch_time, write
from dex_workbench_tracking.control import Limits
from dex_workbench_tracking.diagnostic import diagnose
from dex_workbench_tracking.identity import RIGHT_URDF_SHA
from dex_workbench_tracking.trajectory import ContractError, Demonstrations, Episode, load
class DiagnosticTests(unittest.TestCase):
def setUp(self):
self.manifest = {
"manifest_version": "l20_asset_manifest_v1",
"hand_side": "right",
"asset_sha256": "a" * 64,
"root_link": "hand_base_link",
"joints": [
{"name": "follower", "lower_rad": 0, "upper_rad": 2},
{"name": "leader", "lower_rad": 0, "upper_rad": 1},
],
"source_urdf": {
# Identity token required by right-side schema; geometry below is synthetic.
"sha256": RIGHT_URDF_SHA,
"mimic": [
{"joint": "follower", "reference": "leader", "multiplier": 2, "offset_rad": 0},
],
},
}
time = np.array([0, 0.25, 1.0], dtype=np.float64)
position = np.zeros((3, 3), dtype=np.float32)
position[:, 0] = 0.2 * time
quaternion = np.zeros((3, 4), dtype=np.float32)
quaternion[:, 0], quaternion[:, 3] = np.cos(time / 2), np.sin(time / 2)
joints = np.array([time * 0.8, time * 0.4], dtype=np.float32).T
self.data = Demonstrations(
{
"schema_version": "l20_tracking_v1",
"embodiment": "L20",
"hand_side": "right",
"asset_sha256": "a" * 64,
"root_link": "hand_base_link",
"provenance": "synthetic",
"metric_scale_provenance": "Analytic test, not measured",
"scale_to_meters": 1.0,
"source_description": "Analytic nonuniform samples",
},
("follower", "leader"),
np.eye(4),
{"demo_000000": Episode(time, position, quaternion, joints, np.ones(3, dtype=bool))},
)
def report(self, **kwargs):
return diagnose(self.data, self.manifest, "demo_000000", **kwargs)
def test_nonuniform_si_derivatives_and_passive_joint_roles(self):
report = self.report()
self.assertAlmostEqual(report["speed_estimates"]["translation_m_s"]["max"], 0.2, places=6)
self.assertAlmostEqual(report["speed_estimates"]["rotation_rad_s"]["max"], 1.0, places=6)
self.assertAlmostEqual(report["speed_estimates"]["all_joint_max_rad_s"]["max"], 0.8, places=6)
self.assertAlmostEqual(report["speed_only_slowdown_lower_bound"], 4.0, places=6)
self.assertEqual(report["joints"]["follower"]["role"], "model_follower")
self.assertEqual(report["mimic"]["follower"]["max_abs_residual_rad"], 0)
self.assertEqual(report["sample_dt_s"]["min"], 0.25)
def test_workspace_cannot_be_fixed_by_slowdown(self):
report = self.report(factors=(1, 80))
self.assertEqual(report["workspace"]["outside_frames"], [2])
self.assertFalse(report["workspace"]["fixable_by_time_stretch"])
self.assertEqual(report["candidates"][1]["reference_gate"], "FAIL")
self.assertIn("workspace", report["candidates"][1]["first_gate_failure"])
extended = self.report(limits=replace(Limits(), workspace_radius=0.8), factors=(80,))
self.assertEqual(extended["candidates"][0]["reference_gate"], "PASS")
self.assertEqual(extended["dynamic_replay"], "NOT_RUN")
self.assertEqual(extended["candidates"][0]["dynamics"], "NOT_RUN")
def test_slowdown_scaling_and_no_input_mutation(self):
before = copy.deepcopy(self.data)
report = self.report()
slow = diagnose(stretch_time(self.data, 10), self.manifest, "demo_000000")
for metric in report["speed_estimates"]:
self.assertAlmostEqual(
slow["speed_estimates"][metric]["max"] * 10, report["speed_estimates"][metric]["max"]
)
self.assertEqual(self.data.metadata, before.metadata)
for field in vars(self.data.episodes["demo_000000"]):
np.testing.assert_array_equal(
getattr(self.data.episodes["demo_000000"], field), getattr(before.episodes["demo_000000"], field)
)
def test_peak_interval_locations_and_limit_margin(self):
episode = self.data.episodes["demo_000000"]
episode.wrist_position[1, 0] = 0.15
report = self.report()
metric = report["speed_estimates"]["translation_m_s"]
self.assertEqual(metric["peak_interval_frames"], [0, 1])
self.assertEqual(metric["peak_interval_time_s"], [0, 0.25])
self.assertEqual(metric["over_limit_interval_starts"], [0, 1])
self.assertEqual(report["joints"]["leader"]["minimum_limit_margin_rad"], 0)
def test_identity_side_order_limits_mimic_fail_closed(self):
for mutation in ("hash", "side", "order", "limits", "mimic"):
manifest = copy.deepcopy(self.manifest)
if mutation == "hash":
manifest["asset_sha256"] = "b" * 64
elif mutation == "side":
manifest["hand_side"] = "left"
manifest["source_urdf"].pop("sha256")
elif mutation == "order":
manifest["joints"].reverse()
elif mutation == "limits":
manifest["joints"][1]["upper_rad"] = 0.1
else:
manifest["source_urdf"]["mimic"][0]["multiplier"] = 1.5
with self.subTest(mutation=mutation), self.assertRaises(ContractError):
diagnose(self.data, manifest, "demo_000000")
def test_invalid_gap_and_invalid_factors_rejected(self):
for factor in (0, 0.5, float("inf"), float("nan")):
with self.subTest(factor=factor), self.assertRaises(ContractError):
self.report(factors=(factor,))
self.data.episodes["demo_000000"].valid[1] = False
with self.assertRaisesRegex(ContractError, "fully valid"):
self.report()
def test_two_frame_and_stationary_episode(self):
episode = self.data.episodes["demo_000000"]
episode = Episode(*(getattr(episode, field)[:2].copy() for field in vars(episode)))
episode.wrist_position[:] = 0
episode.wrist_quaternion[:] = [1, 0, 0, 0]
episode.joint_position[:] = 0
self.data.episodes["demo_000000"] = episode
report = self.report()
self.assertEqual(report["speed_only_slowdown_lower_bound"], 1)
self.assertIsNone(report["midpoint_acceleration_estimates_not_physical_bounds"])
# Stationary data can satisfy the reference gate, not the runtime motion checks.
self.assertEqual(report["candidates"][0]["reference_gate"], "PASS")
self.assertEqual(report["dynamic_replay"], "NOT_RUN")
def test_cli_no_overwrite_and_rejects_hash_without_output(self):
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
source, manifest, output = (directory / name for name in ("input.hdf5", "manifest.json", "report.json"))
write(source, self.data)
manifest.write_text(json.dumps(self.manifest))
before = source.read_bytes()
command = [
sys.executable,
"-m",
"dex_workbench_tracking.diagnostic",
str(source),
"--manifest",
str(manifest),
"--output",
str(output),
]
first = subprocess.run(command, capture_output=True, text=True)
self.assertEqual(first.returncode, 0, first.stderr)
self.assertEqual(json.loads(output.read_text())["dynamic_replay"], "NOT_RUN")
report_bytes = output.read_bytes()
second = subprocess.run(command, capture_output=True, text=True)
self.assertNotEqual(second.returncode, 0)
self.assertEqual(output.read_bytes(), report_bytes)
self.assertEqual(source.read_bytes(), before)
self.manifest["asset_sha256"] = "b" * 64
manifest.write_text(json.dumps(self.manifest))
rejected = directory / "rejected.json"
command[-1] = str(rejected)
third = subprocess.run(command, capture_output=True, text=True)
self.assertNotEqual(third.returncode, 0)
self.assertIn("hash mismatch", third.stderr)
self.assertFalse(rejected.exists())
def test_loader_rejects_bad_time_and_nan_before_diagnostic(self):
with tempfile.TemporaryDirectory() as temporary:
for kind in ("duplicate_time", "nan"):
data = copy.deepcopy(self.data)
episode = data.episodes["demo_000000"]
if kind == "duplicate_time":
episode.time[1] = 0
else:
episode.wrist_position[1, 0] = np.nan
path = Path(temporary) / (kind + ".hdf5")
write(path, data)
with self.subTest(kind=kind), self.assertRaises(ContractError):
load(path, self.manifest)
def test_import_does_not_load_simulator_or_torch(self):
result = subprocess.run(
[
sys.executable,
"-c",
"import sys; import dex_workbench_tracking.diagnostic; "
"assert not any(n in sys.modules for n in ('pxr', 'torch', 'isaacsim', 'isaaclab', 'omni'))",
],
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 0, result.stderr)
if __name__ == "__main__":
unittest.main()