XiangpengYang commited on
Commit
5b21b68
·
1 Parent(s): a0288c0

feat: add validated UR action inference

Browse files
Files changed (2) hide show
  1. inference.py +133 -0
  2. tests/test_inference.py +66 -0
inference.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Validated π₀.₅ UR inference and serializable result formatting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import random
7
+ import tempfile
8
+ from dataclasses import dataclass
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+ from PIL import Image
13
+
14
+
15
+ ACTION_LABELS = ("dx", "dy", "dz", "droll", "dpitch", "dyaw", "gripper")
16
+ ACTION_HORIZON = 10
17
+ MODEL_ACTION_DIM = 32
18
+ BASE_SEED = 42
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class PredictionResult:
23
+ actions: pd.DataFrame
24
+ json_path: str
25
+ status: str
26
+
27
+
28
+ def _as_rgb_uint8(value) -> np.ndarray:
29
+ if isinstance(value, Image.Image):
30
+ return np.asarray(value.convert("RGB"), dtype=np.uint8)
31
+ array = np.asarray(value)
32
+ if np.issubdtype(array.dtype, np.floating):
33
+ maximum = float(np.nanmax(array)) if array.size else 0.0
34
+ if maximum <= 1.0:
35
+ array = array * 255.0
36
+ array = np.clip(array, 0, 255).astype(np.uint8)
37
+ return np.asarray(Image.fromarray(array).convert("RGB"), dtype=np.uint8)
38
+
39
+
40
+ def _validate_state(values) -> np.ndarray:
41
+ try:
42
+ state = np.asarray(values, dtype=np.float32)
43
+ except (TypeError, ValueError) as exc:
44
+ raise ValueError("state must contain seven numeric values") from exc
45
+ if state.shape != (7,):
46
+ raise ValueError("state must contain exactly seven values")
47
+ if not np.isfinite(state).all():
48
+ raise ValueError("state must contain seven finite values")
49
+ return state
50
+
51
+
52
+ def _validate_trial_index(value) -> int:
53
+ if isinstance(value, bool) or not isinstance(value, (int, np.integer)):
54
+ raise ValueError("trial index must be a non-negative integer")
55
+ result = int(value)
56
+ if result < 0:
57
+ raise ValueError("trial index must be a non-negative integer")
58
+ return result
59
+
60
+
61
+ def set_seed(seed: int) -> None:
62
+ random.seed(seed)
63
+ np.random.seed(seed)
64
+ try:
65
+ import torch
66
+
67
+ torch.manual_seed(seed)
68
+ if torch.cuda.is_available():
69
+ torch.cuda.manual_seed_all(seed)
70
+ except ImportError:
71
+ pass
72
+
73
+
74
+ def run_prediction(
75
+ policy,
76
+ fixed_image,
77
+ wrist_image,
78
+ instruction,
79
+ state_values,
80
+ trial_index,
81
+ model_id,
82
+ checkpoint_path,
83
+ ) -> PredictionResult:
84
+ if fixed_image is None:
85
+ raise ValueError("fixed-camera image is required")
86
+ if wrist_image is None:
87
+ raise ValueError("wrist-camera image is required")
88
+ if not isinstance(instruction, str) or not instruction.strip():
89
+ raise ValueError("task instruction is required")
90
+
91
+ prompt = instruction.strip()
92
+ state = _validate_state(state_values)
93
+ trial = _validate_trial_index(trial_index)
94
+ seed = BASE_SEED + trial
95
+ set_seed(seed)
96
+ noise = np.random.default_rng(seed).standard_normal(
97
+ (ACTION_HORIZON, MODEL_ACTION_DIM), dtype=np.float32
98
+ )
99
+ observation = {
100
+ "observation/image": _as_rgb_uint8(fixed_image),
101
+ "observation/wrist_image": _as_rgb_uint8(wrist_image),
102
+ "observation/state": state,
103
+ "prompt": prompt,
104
+ }
105
+ result = policy.infer(observation, noise=noise)
106
+ actions = np.asarray(result.get("actions"))
107
+ expected_shape = (ACTION_HORIZON, len(ACTION_LABELS))
108
+ if actions.shape != expected_shape:
109
+ raise RuntimeError(f"policy returned action shape {actions.shape}; expected {expected_shape}")
110
+ if not np.isfinite(actions).all():
111
+ raise RuntimeError("policy returned non-finite actions")
112
+
113
+ table = pd.DataFrame(actions, columns=ACTION_LABELS)
114
+ payload = {
115
+ "model_id": model_id,
116
+ "checkpoint_path": checkpoint_path,
117
+ "instruction": prompt,
118
+ "state": state.tolist(),
119
+ "trial_index": trial,
120
+ "seed": seed,
121
+ "action_labels": list(ACTION_LABELS),
122
+ "actions": actions.tolist(),
123
+ }
124
+ with tempfile.NamedTemporaryFile(
125
+ mode="w", suffix=".json", delete=False, encoding="utf-8"
126
+ ) as stream:
127
+ json.dump(payload, stream, indent=2, ensure_ascii=False)
128
+ json_path = stream.name
129
+ return PredictionResult(
130
+ actions=table,
131
+ json_path=json_path,
132
+ status=f"Prediction complete (seed={seed}, action_shape={expected_shape}).",
133
+ )
tests/test_inference.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import tempfile
3
+ import unittest
4
+
5
+ import numpy as np
6
+ from PIL import Image
7
+
8
+
9
+ class FakePolicy:
10
+ def __init__(self, actions=None):
11
+ self.observation = None
12
+ self.noise = None
13
+ self.actions = np.ones((10, 7)) if actions is None else actions
14
+
15
+ def infer(self, observation, *, noise=None):
16
+ self.observation = observation
17
+ self.noise = noise
18
+ return {"actions": self.actions}
19
+
20
+
21
+ class InferenceTests(unittest.TestCase):
22
+ def test_prediction_maps_ur_observation_and_writes_json(self):
23
+ from inference import ACTION_LABELS, run_prediction
24
+
25
+ policy = FakePolicy()
26
+ image = Image.new("RGB", (8, 6), "red")
27
+ result = run_prediction(
28
+ policy, image, image, "pick up", [1, 2, 3, 4, 5, 6, 0], 2,
29
+ "owner/model", "checkpoint",
30
+ )
31
+ self.assertEqual(tuple(result.actions.columns), ACTION_LABELS)
32
+ self.assertEqual(
33
+ policy.observation["observation/state"].tolist(),
34
+ [1, 2, 3, 4, 5, 6, 0],
35
+ )
36
+ self.assertEqual(policy.observation["observation/image"].dtype, np.uint8)
37
+ self.assertEqual(policy.observation["prompt"], "pick up")
38
+ self.assertEqual(policy.noise.shape, (10, 32))
39
+ with open(result.json_path, encoding="utf-8") as stream:
40
+ self.assertEqual(json.load(stream)["seed"], 44)
41
+
42
+ def test_same_trial_uses_same_diffusion_noise(self):
43
+ from inference import run_prediction
44
+
45
+ first, second = FakePolicy(), FakePolicy()
46
+ image = Image.new("RGB", (8, 6))
47
+ arguments = (image, image, "task", [0] * 7, 3, "model", "checkpoint")
48
+ run_prediction(first, *arguments)
49
+ run_prediction(second, *arguments)
50
+ np.testing.assert_array_equal(first.noise, second.noise)
51
+
52
+ def test_prediction_rejects_invalid_state_and_action_shape(self):
53
+ from inference import run_prediction
54
+
55
+ image = Image.new("RGB", (8, 6))
56
+ with self.assertRaisesRegex(ValueError, "seven"):
57
+ run_prediction(FakePolicy(), image, image, "task", [1, 2], 0, "m", "c")
58
+ with self.assertRaisesRegex(RuntimeError, r"\(10, 7\)"):
59
+ run_prediction(
60
+ FakePolicy(np.zeros((8, 7))), image, image, "task", [0] * 7,
61
+ 0, "m", "c",
62
+ )
63
+
64
+
65
+ if __name__ == "__main__":
66
+ unittest.main()