Spaces:
Running on Zero
Running on Zero
Download tcp4d/state_normalization.py from HarrisonPENG/TCP4D-DROID: direct link, hf CLI and curl.
- Browser
- Download file 7.66 kB
-
https://huggingface.co/spaces/HarrisonPENG/TCP4D-DROID/resolve/main/tcp4d/state_normalization.py
- Command line
-
hf download hf://spaces/HarrisonPENG/TCP4D-DROID/tcp4d/state_normalization.py
-
curl -L -o state_normalization.py https://huggingface.co/spaces/HarrisonPENG/TCP4D-DROID/resolve/main/tcp4d/state_normalization.py
7.66 kB
| """Real RoboTwin joint/gripper state IO and train-anchor-only normalization.""" | |
| from __future__ import annotations | |
| from contextlib import ExitStack | |
| import json | |
| from pathlib import Path | |
| from tempfile import TemporaryDirectory | |
| import numpy as np | |
| import torch | |
| STATE_NORM_FILENAME = "state_norm.json" | |
| STATE_LAYOUT = [f"{arm}.{name}" for arm in ("left", "right") | |
| for name in (*[f"joint_{i}" for i in range(6)], "gripper")] | |
| JOINT_INDICES = [i for i in range(14) if i not in (6, 13)] | |
| def read_robot_states(episode_path, num_frames=None): | |
| path = Path(episode_path) / "robot_state.npy" | |
| if not path.is_file(): | |
| raise ValueError(f"V2 requires real current robot state: missing {path}") | |
| state = np.load(path, mmap_mode="r", allow_pickle=False) | |
| if state.ndim != 2 or state.shape[1] != 14 or (num_frames is not None and len(state) != num_frames): | |
| raise ValueError(f"Expected frame-aligned robot_state.npy [T,14]: {path}, got {state.shape}") | |
| return state | |
| def validate_robot_states(values): | |
| values = np.asarray(values) | |
| if values.ndim != 2 or values.shape[1] != 14 or not np.isfinite(values).all(): | |
| raise ValueError("Current real robot states must be finite [N,14]") | |
| if np.any((values[:, [6, 13]] < 0) | (values[:, [6, 13]] > 1)): | |
| raise ValueError("Current real robot state grippers must be in [0,1]") | |
| def state_parameters(q01, q99, minimum, maximum): | |
| q01, q99, minimum, maximum = [np.asarray(x, dtype=np.float64) for x in (q01, q99, minimum, maximum)] | |
| if any(x.shape != (1, 12) or not np.isfinite(x).all() for x in (q01, q99, minimum, maximum)): | |
| raise ValueError("State joint statistics must be finite [1,12]") | |
| if np.any((minimum > q01) | (q01 > q99) | (q99 > maximum)): | |
| raise ValueError("Invalid state quantile bounds") | |
| fallback = q99 - q01 < 1e-4 | |
| low, high = np.where(fallback, minimum, q01), np.where(fallback, maximum, q99) | |
| offset, scale = np.full((1, 14), 0.5), np.full((1, 14), 0.5) | |
| offset[:, JOINT_INDICES] = (low + high) / 2 | |
| scale[:, JOINT_INDICES] = np.maximum(high - low, 1e-3) / 2 | |
| return offset, scale | |
| def validate_state_norm(record): | |
| expected = {"version": 1, "layout": STATE_LAYOUT, "statistics_scope": "train_valid_current_frames", | |
| "joint_indices": JOINT_INDICES, "minimum_span": 1e-3, "fallback_span": 1e-4} | |
| for key, value in expected.items(): | |
| if record.get(key) != value: | |
| raise ValueError(f"Invalid state normalization {key}") | |
| if not isinstance(record.get("count"), int) or record["count"] < 1: | |
| raise ValueError("State statistics require valid training frames") | |
| offset, scale = state_parameters(*(record[key] for key in ("q01", "q99", "min", "max"))) | |
| for name, expected in (("offset", offset), ("scale", scale)): | |
| actual = np.asarray(record[name], dtype=np.float32) | |
| if actual.shape != (1, 14) or not np.isfinite(actual).all() or not np.array_equal(actual, expected.astype(np.float32)): | |
| raise ValueError(f"State normalization {name} disagrees with quantile/gripper rules") | |
| return record | |
| def compute_state_norm(mixture, scratch_dir): | |
| """Exact quantiles per joint, bounded RAM; each eligible current frame once.""" | |
| count, sources = 0, [] | |
| minimum, maximum = np.full(12, np.inf), np.full(12, -np.inf) | |
| with TemporaryDirectory(prefix=".state-norm-", dir=scratch_dir) as tmp: | |
| paths = [Path(tmp) / f"joint-{i}.bin" for i in range(12)] | |
| with ExitStack() as stack: | |
| handles = [stack.enter_context(path.open("wb")) for path in paths] | |
| for source in mixture.sources: | |
| if source.weight <= 0: | |
| continue | |
| if source.dataset.split != "train": | |
| raise ValueError("State normalization may only use the training split") | |
| source_count = 0 | |
| for values in source.dataset.iter_current_robot_states(): | |
| validate_robot_states(values) | |
| joints = np.asarray(values, dtype=np.float32)[:, JOINT_INDICES] | |
| if not len(joints): | |
| continue | |
| minimum, maximum = np.minimum(minimum, joints.min(0)), np.maximum(maximum, joints.max(0)) | |
| count += len(joints) | |
| source_count += len(joints) | |
| for column, handle in enumerate(handles): | |
| np.ascontiguousarray(joints[:, column]).tofile(handle) | |
| sources.append({"name": source.name, "count": source_count, "selection": source.dataset.manifest()}) | |
| if not count: | |
| raise ValueError("No valid training current frames for state normalization") | |
| ranks = np.array([0.01, 0.99]) * (count - 1) | |
| lower, upper = np.floor(ranks).astype(np.int64), np.ceil(ranks).astype(np.int64) | |
| quantiles = np.empty((2, 12)) | |
| for column, path in enumerate(paths): | |
| values = np.memmap(path, mode="r+", dtype=np.float32, shape=(count,)) | |
| values.partition(np.unique(np.r_[lower, upper])) | |
| lo, hi = values[lower].astype(np.float64), values[upper].astype(np.float64) | |
| quantiles[:, column] = lo + (hi - lo) * (ranks - lower) | |
| del values | |
| offset, scale = state_parameters(quantiles[:1], quantiles[1:], minimum[None], maximum[None]) | |
| return validate_state_norm({"version": 1, "layout": STATE_LAYOUT, "joint_indices": JOINT_INDICES, | |
| "statistics_scope": "train_valid_current_frames", "minimum_span": 1e-3, "fallback_span": 1e-4, | |
| "count": count, "sources": sources, "q01": quantiles[:1].tolist(), "q99": quantiles[1:].tolist(), | |
| "min": minimum[None].tolist(), "max": maximum[None].tolist(), | |
| "offset": offset.tolist(), "scale": scale.tolist()}) | |
| def save_state_norm(directory, record): | |
| validate_state_norm(record) | |
| path = Path(directory) / STATE_NORM_FILENAME | |
| temporary = path.with_suffix(".json.tmp") | |
| temporary.write_text(json.dumps(record, indent=2, allow_nan=False), encoding="utf-8") | |
| temporary.replace(path) | |
| def load_checkpoint_state_norm(checkpoint): | |
| path = Path(checkpoint).expanduser() | |
| directory = path if path.is_dir() else path.parent | |
| record = json.loads((directory / STATE_NORM_FILENAME).read_text()) | |
| return validate_state_norm(record) | |
| def assert_state_norm_matches(policy, record): | |
| validate_state_norm(record) | |
| for name in ("offset", "scale"): | |
| actual = getattr(policy, f"state_{name}").detach().float().cpu() | |
| if not torch.equal(actual, torch.tensor(record[name], dtype=torch.float32)): | |
| raise ValueError(f"Checkpoint state_{name} differs from {STATE_NORM_FILENAME}") | |
| def initialize_state_normalization(policy, dataset, config, accelerator): | |
| from accelerate.utils import broadcast_object_list | |
| result = [None] | |
| if accelerator.is_main_process: | |
| try: | |
| root = Path(config["output_dir"]) | |
| root.mkdir(parents=True, exist_ok=True) | |
| record = load_checkpoint_state_norm(config["resume"]) if config.get("resume") else compute_state_norm(dataset, root) | |
| save_state_norm(root, record) | |
| result[0] = {"record": record} | |
| except Exception as exc: | |
| result[0] = {"error": f"{type(exc).__name__}: {exc}"} | |
| broadcast_object_list(result) | |
| if "error" in result[0]: | |
| raise ValueError(result[0]["error"]) | |
| record = result[0]["record"] | |
| if config.get("resume"): | |
| assert_state_norm_matches(policy, record) | |
| else: | |
| policy.set_state_stats(record["offset"], record["scale"]) | |
| return record | |