Initial pi0.5 minimal validation

This commit is contained in:
seeker
2026-07-11 20:04:18 +08:00
commit ffbcca8d2b
20 changed files with 2354 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
from __future__ import annotations
import json
from pathlib import Path
import numpy as np
import torch
from PIL import Image
from pi05mini.data import Pi05OverfitDataset, load_manifest, materialize_subset
from pi05mini.tokenizer import CoordinateTokenizer, MiniTokenizer, SubtaskVocabulary
def _make_source_project(root: Path) -> Path:
processed = root / "data" / "processed"
image_dir = processed / "images"
trajectory_dir = processed / "trajectories"
image_dir.mkdir(parents=True)
trajectory_dir.mkdir(parents=True)
image_path = image_dir / "sample.jpg"
pixels = np.zeros((48, 80, 3), dtype=np.uint8)
pixels[..., 0] = 64
pixels[..., 1] = np.arange(80, dtype=np.uint8)[None, :]
Image.fromarray(pixels).save(image_path)
history_x = np.linspace(-7.0, 0.0, 8, dtype=np.float32)
history = np.stack((history_x, 0.02 * history_x, np.zeros(8)), axis=-1)
future_x = np.linspace(1.0, 32.0, 16, dtype=np.float32)
future = np.stack((future_x, 0.002 * future_x**2, np.zeros(16)), axis=-1)
trajectory_path = trajectory_dir / "sample.npz"
np.savez_compressed(trajectory_path, history_xyz=history, future_xyz=future)
manifest = processed / "overfit_manifest.jsonl"
row = {
"clip_id": "clip-a",
"t0_us": 1234,
"reasoning": "Steer left through the construction lane.",
"image_path": "data/processed/images/sample.jpg",
"trajectory_path": "data/processed/trajectories/sample.npz",
}
manifest.write_text(json.dumps(row) + "\n", encoding="utf-8")
return manifest
def test_coordinate_tokens_are_literal_bytes_and_round_trip() -> None:
tokenizer = CoordinateTokenizer(x_range=(-10.0, 10.0), y_range=(-5.0, 5.0))
coordinates = torch.tensor([[-10.0, -5.0], [0.0, 0.0], [10.0, 5.0]])
tokens = tokenizer.encode(coordinates)
assert tokens.dtype == torch.long
assert tokens.tolist()[0] == [0, 0]
assert tokens.tolist()[-1] == [255, 255]
assert 0 <= int(tokens.min()) <= int(tokens.max()) <= 255
decoded = tokenizer.decode(tokens)
assert torch.allclose(decoded, coordinates, atol=10.1 / 255.0)
def test_subtask_vocabulary_and_prefix_contract() -> None:
tokenizer = MiniTokenizer()
history = torch.zeros(8, 2)
prefix = tokenizer.build_prefix(history)
assert prefix.shape == (20,)
assert prefix[:3].tolist() == [tokenizer.BOS, tokenizer.IMAGE, tokenizer.STATE]
assert prefix[-1].item() == tokenizer.SUBTASK_QUERY
assert prefix[3:-1].min() >= 0 and prefix[3:-1].max() <= 255
target = tokenizer.encode_subtask("Steer left around the cones")
assert tokenizer.subtasks.decode(target) == "steer_left"
assert SubtaskVocabulary().classify("Slow down for a pedestrian") == "slow_down"
def test_materialize_and_dataset_are_self_contained_and_deterministic(tmp_path: Path) -> None:
source_manifest = _make_source_project(tmp_path / "source")
copied_manifest = materialize_subset(source_manifest, tmp_path / "target", max_samples=1)
records = load_manifest(copied_manifest)
assert len(records) == 1
assert records[0].image_path.is_file()
assert records[0].trajectory_path.is_file()
dataset = Pi05OverfitDataset(
copied_manifest,
image_size=(32, 48),
augmentation_factor=3,
seed=19,
)
same_dataset = Pi05OverfitDataset(
copied_manifest,
image_size=(32, 48),
augmentation_factor=3,
seed=19,
action_statistics=dataset.action_normalizer.as_dict(),
)
assert len(dataset) == 3
original = dataset[0]
augmented = dataset[1]
repeated = same_dataset[1]
assert original["image"].shape == (3, 32, 48)
assert original["prefix_tokens"].shape == (20,)
assert original["actions"].shape == (16, 2)
assert original["subtask_target"].shape == ()
assert original["subtask_label"] == "steer_left"
assert original["reasoning"].startswith("Steer left")
assert original["sample_id"].endswith(":aug0")
assert augmented["sample_id"].endswith(":aug1")
assert not torch.equal(original["future_xy"], augmented["future_xy"])
assert torch.equal(augmented["future_xy"], repeated["future_xy"])
assert torch.equal(augmented["image"], repeated["image"])
reconstructed = dataset.action_normalizer.decode(
original["actions"], original["history_xy"]
)
assert torch.allclose(reconstructed, original["future_xy"], atol=1e-5)
statistics_path = dataset.save_action_statistics(tmp_path / "action_stats.json")
statistics = json.loads(statistics_path.read_text(encoding="utf-8"))
assert len(statistics["mean"]) == 2
assert len(statistics["scale"]) == 2
assert min(statistics["scale"]) >= 0.05