react-manip 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- assets/materials/realistic/common/ceramic.png +0 -0
- assets/materials/realistic/common/cream-plaster.png +0 -0
- assets/materials/realistic/common/gray-felt.png +0 -0
- assets/materials/realistic/common/gray-plaster.png +0 -0
- assets/materials/realistic/common/gray-woodgrain.png +0 -0
- assets/materials/realistic/common/light-gray-floor-tile.png +0 -0
- assets/materials/realistic/common/light-gray-plaster.png +0 -0
- assets/materials/realistic/common/metal.png +0 -0
- assets/materials/realistic/common/oak-wood.png +0 -0
- assets/materials/realistic/common/steel-brushed.png +0 -0
- assets/materials/realistic/common/steel-scratched.png +0 -0
- assets/materials/realistic/common/varnished-wood-panels.png +0 -0
- assets/materials/realistic/common/walnut-wood.png +0 -0
- assets/materials/realistic/common/white-bricks.png +0 -0
- assets/materials/realistic/common/wood-tiles.png +0 -0
- assets/materials/realistic/tiles074/Tiles074_2K_Color.png +0 -0
- assets/materials/realistic/woodfloor049/WoodFloor049_2K_Color.png +0 -0
- configs/box_insertion/profile_default.json +204 -0
- react_manip/__init__.py +12 -0
- react_manip/asset_cli.py +18 -0
- react_manip/builtin.py +168 -0
- react_manip/cameras.py +301 -0
- react_manip/category_objects.py +248 -0
- react_manip/core/__init__.py +60 -0
- react_manip/core/aggregate.py +59 -0
- react_manip/core/compatibility.py +64 -0
- react_manip/core/contracts.py +42 -0
- react_manip/core/geometry.py +269 -0
- react_manip/core/language.py +116 -0
- react_manip/core/metrics.py +196 -0
- react_manip/core/observation.py +59 -0
- react_manip/core/protocol.py +91 -0
- react_manip/core/registry.py +94 -0
- react_manip/core/runner.py +498 -0
- react_manip/core/scenarios.py +110 -0
- react_manip/core/specs.py +203 -0
- react_manip/core/timeouts.py +44 -0
- react_manip/core/visual_randomization.py +261 -0
- react_manip/evaluation/__init__.py +4 -0
- react_manip/evaluation/metrics.py +17 -0
- react_manip/evaluation/protocols.py +326 -0
- react_manip/evaluation/viewer.py +240 -0
- react_manip/instances/__init__.py +6 -0
- react_manip/instances/candidate_bank.py +109 -0
- react_manip/instances/public.py +95 -0
- react_manip/instances/records.py +345 -0
- react_manip/instances/runtime.py +306 -0
- react_manip/instances/suite.py +231 -0
- react_manip/instances/trajectory.py +59 -0
- react_manip/instances/variation.py +118 -0
- react_manip/legacy.py +318 -0
- react_manip/policies/__init__.py +5 -0
- react_manip/policies/base.py +79 -0
- react_manip/rendering.py +58 -0
- react_manip/robot_registry.py +31 -0
- react_manip/robots/__init__.py +33 -0
- react_manip/robots/adapters.py +135 -0
- react_manip/robots/agilex_adapter.py +35 -0
- react_manip/robots/agilex_description.py +74 -0
- react_manip/robots/agilex_robosuite.py +246 -0
- react_manip/robots/arm_actions.py +17 -0
- react_manip/robots/base.py +119 -0
- react_manip/robots/catalog.py +18 -0
- react_manip/robots/legacy_agilex_robosuite.py +169 -0
- react_manip/robots/mobile_base.py +33 -0
- react_manip/robots/mounts.py +80 -0
- react_manip/robots/resolution.py +103 -0
- react_manip/robots/sawyer_robosuite.py +147 -0
- react_manip/robots/sensors.py +137 -0
- react_manip/utils/__init__.py +1 -0
- react_manip/visual_task.py +50 -0
- react_manip-0.1.0.dist-info/METADATA +446 -0
- react_manip-0.1.0.dist-info/RECORD +99 -0
- react_manip-0.1.0.dist-info/WHEEL +5 -0
- react_manip-0.1.0.dist-info/entry_points.txt +2 -0
- react_manip-0.1.0.dist-info/licenses/LICENSE +131 -0
- react_manip-0.1.0.dist-info/top_level.txt +5 -0
- scripts/add_phone_lean_to_bank.py +242 -0
- scripts/agilex_wheeled_builder.py +133 -0
- scripts/audit_task_solvability.py +150 -0
- scripts/build_agilex_assets.py +317 -0
- scripts/build_legacy_agilex_assets.py +244 -0
- scripts/evaluate_all_tasks.py +309 -0
- scripts/evaluate_policy.py +281 -0
- scripts/piperx_assets.lock.json +181 -0
- scripts/pygame_demo.py +360 -0
- scripts/pygame_task_debug.py +242 -0
- scripts/regenerate_instance_bank.py +192 -0
- scripts/remove_task_from_bank.py +103 -0
- scripts/select_task_instances.py +64 -0
- scripts/verify_curling_reachability.py +206 -0
- scripts/verify_piperx_tasks.py +81 -0
- tasks/box_insertion/__init__.py +3 -0
- tasks/box_insertion/assets/scene.xml +50 -0
- tasks/box_insertion/env.py +642 -0
- tasks/box_insertion/evaluation.json +16 -0
- tasks/box_insertion/instance_contract.json +321 -0
- tasks/box_insertion/scoring.py +23 -0
- tasks/box_insertion/task_spec.json +333 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Versioned physical adapters; all runtime sampling is confined to generation.
|
|
2
|
+
|
|
3
|
+
Initial state uses MuJoCo's version-bound integration vector. Controllers start
|
|
4
|
+
from reset targets at that state; this is an initial-state format, not a generic
|
|
5
|
+
mid-trajectory checkpoint. No simulator state is assigned during env.step().
|
|
6
|
+
"""
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import sys
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from .records import VERSION, SUPPORTED_TASKS, digest, seal, validate_pair, validate_record, validate_parameters
|
|
17
|
+
|
|
18
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
19
|
+
from react_manip.robots.sensors import policy_camera_ids
|
|
20
|
+
|
|
21
|
+
_SOURCE_DRIFT_WARNED: set[str] = set()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _check_runtime_identity(actual, expected, task_id):
|
|
25
|
+
"""Validate compiled runtime fields while tolerating evaluator-only drift.
|
|
26
|
+
|
|
27
|
+
Saved instances contain a broad source digest. Changes to the viewer or
|
|
28
|
+
evaluator control flow must not invalidate already-recorded physical
|
|
29
|
+
scenes, so a source-digest-only difference is reported as a warning. Model,
|
|
30
|
+
controller, camera, and physics identity changes remain hard failures.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
mismatches = [key for key in expected if actual.get(key) != expected.get(key)]
|
|
34
|
+
mismatches.extend(key for key in actual if key not in expected)
|
|
35
|
+
if not mismatches:
|
|
36
|
+
return
|
|
37
|
+
if set(mismatches) == {'source_digest'}:
|
|
38
|
+
if task_id not in _SOURCE_DRIFT_WARNED:
|
|
39
|
+
warnings.warn(
|
|
40
|
+
f'{task_id}: saved source digest differs from the current evaluator; '
|
|
41
|
+
'continuing because compiled runtime identity matches',
|
|
42
|
+
RuntimeWarning,
|
|
43
|
+
stacklevel=3,
|
|
44
|
+
)
|
|
45
|
+
_SOURCE_DRIFT_WARNED.add(task_id)
|
|
46
|
+
return
|
|
47
|
+
raise ValueError('compiled model/controller/runtime mismatch')
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def source_digest(task_id):
|
|
51
|
+
paths = list((ROOT / 'react_manip').rglob('*.py')) + list((ROOT / 'tasks' / task_id).rglob('*.py'))
|
|
52
|
+
paths += list((ROOT / 'tasks' / task_id).rglob('*.xml'))
|
|
53
|
+
return digest({str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(paths)})
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _json(value):
|
|
57
|
+
if isinstance(value, np.ndarray):
|
|
58
|
+
return value.tolist()
|
|
59
|
+
if isinstance(value, np.generic):
|
|
60
|
+
return value.item()
|
|
61
|
+
if isinstance(value, dict):
|
|
62
|
+
return {str(k): _json(v) for k, v in value.items()}
|
|
63
|
+
if isinstance(value, (list, tuple)):
|
|
64
|
+
return [_json(v) for v in value]
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _model_digest(env):
|
|
69
|
+
# Hash compiled geometry/dynamics/assets, not XML temporary file names.
|
|
70
|
+
model = env.sim.model._model
|
|
71
|
+
names = ('qpos0 body_pos body_quat body_mass body_inertia body_ipos body_iquat '
|
|
72
|
+
'jnt_type jnt_axis jnt_pos jnt_range dof_damping dof_frictionloss dof_armature '
|
|
73
|
+
'geom_type geom_bodyid geom_pos geom_quat geom_size geom_friction geom_solref geom_solimp '
|
|
74
|
+
'geom_contype geom_conaffinity geom_condim pair_friction pair_solref pair_solimp '
|
|
75
|
+
'actuator_gainprm actuator_biasprm actuator_dynprm actuator_ctrlrange actuator_forcerange '
|
|
76
|
+
'eq_data eq_type eq_obj1id eq_obj2id mesh_vert mesh_face mesh_normal '
|
|
77
|
+
'cam_bodyid cam_pos cam_quat cam_fovy light_pos light_dir light_diffuse light_specular light_ambient '
|
|
78
|
+
'geom_rgba geom_matid mat_rgba mat_texid mat_texrepeat mat_emission mat_specular mat_shininess mat_reflectance tex_data').split()
|
|
79
|
+
h = hashlib.sha256()
|
|
80
|
+
for name in names:
|
|
81
|
+
value = getattr(model, name, None)
|
|
82
|
+
if value is not None:
|
|
83
|
+
array = np.ascontiguousarray(value)
|
|
84
|
+
h.update(name.encode()); h.update(str((array.shape, str(array.dtype))).encode()); h.update(array.tobytes())
|
|
85
|
+
h.update(bytes(model.names))
|
|
86
|
+
h.update(str((model.nq, model.nv, model.na, model.nu, model.opt.timestep,
|
|
87
|
+
model.opt.integrator, model.opt.solver, model.opt.iterations,
|
|
88
|
+
model.opt.tolerance, tuple(model.opt.gravity))).encode())
|
|
89
|
+
return h.hexdigest()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def spec_for(task_id):
|
|
93
|
+
from react_manip.core.specs import load_task_spec
|
|
94
|
+
if task_id not in SUPPORTED_TASKS:
|
|
95
|
+
raise ValueError('task has not passed the instance adapter gate')
|
|
96
|
+
return load_task_spec(ROOT / 'tasks' / task_id / 'task_spec.json')
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def contract_for(task_id):
|
|
100
|
+
return json.loads((ROOT / 'tasks' / task_id / 'instance_contract.json').read_text())
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _construct(task_id, scenario, robot_id=None, episode=None, *, render=False):
|
|
104
|
+
from react_manip.builtin import discover_task_definitions
|
|
105
|
+
definition = next((s, f) for s, f in discover_task_definitions(task_ids=(task_id,)) if s.task_id == task_id)
|
|
106
|
+
if episode is None:
|
|
107
|
+
episode = deepcopy(scenario.metadata.get('initial_episode_scene'))
|
|
108
|
+
extra = {'episode_instance': episode} if task_id in ('phone_lean','leveling_board1','rope_hang','laptop_into_drawer') else {}
|
|
109
|
+
env = definition[1](**extra, scenario=scenario, robot_id=robot_id, seed=scenario.seed,
|
|
110
|
+
initialization_noise=None, has_renderer=render, has_offscreen_renderer=False,
|
|
111
|
+
use_camera_obs=False)
|
|
112
|
+
return env
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def runtime_identity(env, task_id):
|
|
116
|
+
import mujoco
|
|
117
|
+
import robosuite
|
|
118
|
+
raw = getattr(env, 'env', env)
|
|
119
|
+
spec = spec_for(task_id)
|
|
120
|
+
controllers = [r.composite_controller.composite_controller_specific_config
|
|
121
|
+
if hasattr(r.composite_controller, 'composite_controller_specific_config') else {}
|
|
122
|
+
for r in raw.robots]
|
|
123
|
+
# Per-arm config is the concrete config supplied to controller construction.
|
|
124
|
+
controllers += [getattr(r, 'controller_config', {}) for r in raw.robots]
|
|
125
|
+
for robot in raw.robots:
|
|
126
|
+
controllers.append({name: {
|
|
127
|
+
'class': type(part).__module__ + '.' + type(part).__name__,
|
|
128
|
+
**{key: _json(getattr(part, key)) for key in (
|
|
129
|
+
'input_min', 'input_max', 'output_min', 'output_max', 'kp', 'kd',
|
|
130
|
+
'damping_ratio', 'control_dim', 'input_ref_frame', 'input_type', 'impedance_mode')
|
|
131
|
+
if hasattr(part, key)}} for name, part in robot.composite_controller.part_controllers.items()})
|
|
132
|
+
return dict(python='.'.join(map(str, sys.version_info[:2])), numpy=np.__version__,
|
|
133
|
+
mujoco=mujoco.__version__, robosuite=robosuite.__version__,
|
|
134
|
+
source_digest=source_digest(task_id), control_hz=spec.control_hz,
|
|
135
|
+
horizon_sec=spec.horizon_sec, physics_timestep=float(raw.sim.model.opt.timestep),
|
|
136
|
+
model_digest=_model_digest(raw),
|
|
137
|
+
action_layout=_json(raw.robots[0].composite_controller._action_split_indexes),
|
|
138
|
+
controller_digest=digest(_json(controllers)), cameras=list(policy_camera_ids(raw.robot_id if hasattr(raw, 'robot_id') else spec.supported_robots[0])))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def capture_state(env):
|
|
142
|
+
import mujoco
|
|
143
|
+
raw = getattr(env, 'env', env)
|
|
144
|
+
model, data = raw.sim.model._model, raw.sim.data._data
|
|
145
|
+
state_spec = int(mujoco.mjtState.mjSTATE_INTEGRATION)
|
|
146
|
+
vector = np.empty(mujoco.mj_stateSize(model, state_spec))
|
|
147
|
+
mujoco.mj_getState(model, data, vector, state_spec)
|
|
148
|
+
return dict(format='mujoco.integration.v1', state_spec=state_spec, integration_state=vector.tolist())
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ReplayEnv:
|
|
156
|
+
"""Bind explicit episodes to an instance; never forward their files to policy."""
|
|
157
|
+
def __init__(self, instance, *, cameras=True, native_actions=False):
|
|
158
|
+
self._native_actions = native_actions
|
|
159
|
+
validate_record(instance)
|
|
160
|
+
spec = spec_for(instance['task_id'])
|
|
161
|
+
if digest(spec.to_dict()) != instance['task_spec_digest'] or digest(contract_for(spec.task_id)) != instance['contract_digest']:
|
|
162
|
+
raise ValueError('TaskSpec/instance contract version mismatch')
|
|
163
|
+
validate_parameters(instance['scenario']['parameters'], contract_for(spec.task_id))
|
|
164
|
+
from react_manip.core.scenarios import Scenario
|
|
165
|
+
data = instance['scenario']
|
|
166
|
+
scenario = Scenario(**{k: deepcopy(v) for k, v in data.items() if k != 'digest'})
|
|
167
|
+
self._instance = deepcopy(instance)
|
|
168
|
+
self._cameras = cameras
|
|
169
|
+
self._episode = None
|
|
170
|
+
self._wrapped = _construct(spec.task_id, scenario, instance['robot_id'])
|
|
171
|
+
self.env = self._wrapped.env # policy geometry filter expects the task here
|
|
172
|
+
try:
|
|
173
|
+
self._wrapped.reset()
|
|
174
|
+
_check_runtime_identity(runtime_identity(self._wrapped, spec.task_id),
|
|
175
|
+
instance['runtime'], spec.task_id)
|
|
176
|
+
if cameras:
|
|
177
|
+
self._wrapped.set_camera_observations(instance['runtime']['cameras'])
|
|
178
|
+
except BaseException:
|
|
179
|
+
self.close()
|
|
180
|
+
raise
|
|
181
|
+
|
|
182
|
+
def __getattr__(self, name):
|
|
183
|
+
return getattr(self._wrapped, name)
|
|
184
|
+
|
|
185
|
+
def bind(self, episode):
|
|
186
|
+
validate_pair(self._instance, episode)
|
|
187
|
+
if 'variation' in episode or (self._episode and 'variation' in self._episode):
|
|
188
|
+
from react_manip.core.scenarios import Scenario
|
|
189
|
+
from .variation import episode_scenario
|
|
190
|
+
data = self._instance['scenario']
|
|
191
|
+
scenario = Scenario(**{k: deepcopy(v) for k,v in data.items() if k != 'digest'})
|
|
192
|
+
if 'variation' in episode:
|
|
193
|
+
scenario = episode_scenario(scenario, episode['variation'])
|
|
194
|
+
candidate = _construct(self._instance['task_id'], scenario,
|
|
195
|
+
self._instance['robot_id'], episode=episode.get('scene'))
|
|
196
|
+
try:
|
|
197
|
+
candidate.reset()
|
|
198
|
+
expected = episode.get('runtime', self._instance['runtime'])
|
|
199
|
+
_check_runtime_identity(runtime_identity(candidate, self._instance['task_id']),
|
|
200
|
+
expected, self._instance['task_id'])
|
|
201
|
+
if self._cameras:
|
|
202
|
+
candidate.set_camera_observations(expected['cameras'])
|
|
203
|
+
except BaseException:
|
|
204
|
+
candidate.close()
|
|
205
|
+
raise
|
|
206
|
+
self._wrapped.close()
|
|
207
|
+
self._wrapped, self.env = candidate, candidate.env
|
|
208
|
+
if self._instance['task_id'] == 'phone_lean':
|
|
209
|
+
# Poses reference the explicit Episode scene.
|
|
210
|
+
self.env.episode_instance = deepcopy(episode['scene'])
|
|
211
|
+
self.env.episode_seed = episode['scene']['episode_seed']
|
|
212
|
+
self.env.layout_seed = self.env.episode_seed
|
|
213
|
+
if self._instance['task_id'] in ('leveling_board1','rope_hang','laptop_into_drawer'):
|
|
214
|
+
self.env.episode_instance = deepcopy(episode['scene'])
|
|
215
|
+
self._episode = deepcopy(episode)
|
|
216
|
+
|
|
217
|
+
def reset(self, *, seed=None, **kwargs):
|
|
218
|
+
if seed is not None or kwargs:
|
|
219
|
+
raise ValueError('explicit episode replay forbids seed/options overrides')
|
|
220
|
+
if self._episode is None:
|
|
221
|
+
raise ValueError('bind an Episode Instance before reset')
|
|
222
|
+
import mujoco
|
|
223
|
+
self._wrapped.reset()
|
|
224
|
+
self._wrapped._episode_index = 0
|
|
225
|
+
raw = self.env
|
|
226
|
+
saved = self._episode['initial_state']
|
|
227
|
+
if self._instance['task_id'] in ('rail_ball_to_cup', 'pitch_pot', 'conveyor_grasp', 'turntable_gamble1', 'ring_toss1', 'box_insertion', 'car_drive2', 'rope_hang', 'pan_flip', 'button_indicator_matching', 'laptop_into_drawer', 'find_object_in_drawer2', 'tilte_maze'):
|
|
228
|
+
# State must match the resolved model's reset, including Episode
|
|
229
|
+
# parameters; arbitrary resealed robot/object pose edits are forbidden.
|
|
230
|
+
if saved != capture_state(self._wrapped):
|
|
231
|
+
raise ValueError(f"{self._instance['task_id']} forbids Episode initial-state variation")
|
|
232
|
+
model, data = raw.sim.model._model, raw.sim.data._data
|
|
233
|
+
if saved['state_spec'] != int(mujoco.mjtState.mjSTATE_INTEGRATION):
|
|
234
|
+
raise ValueError('initial state mask mismatch')
|
|
235
|
+
vector = np.asarray(saved['integration_state'], dtype=float)
|
|
236
|
+
if vector.shape != (mujoco.mj_stateSize(model, saved['state_spec']),) or not np.isfinite(vector).all():
|
|
237
|
+
raise ValueError('initial state shape/value mismatch')
|
|
238
|
+
# This v1 format supports reset states only, not arbitrary task checkpoints.
|
|
239
|
+
if vector[0] != 0:
|
|
240
|
+
raise ValueError('initial state time must be zero')
|
|
241
|
+
mujoco.mj_setState(model, data, vector, saved['state_spec'])
|
|
242
|
+
raw.sim.forward()
|
|
243
|
+
raw.timestep = 0
|
|
244
|
+
raw.cur_time = 0
|
|
245
|
+
raw.done = False
|
|
246
|
+
for robot in raw.robots:
|
|
247
|
+
robot.composite_controller.update_state()
|
|
248
|
+
robot.composite_controller.reset()
|
|
249
|
+
for controller in robot.composite_controller.part_controllers.values():
|
|
250
|
+
if hasattr(controller, 'reset_goal'):
|
|
251
|
+
controller.reset_goal()
|
|
252
|
+
if self._instance['task_id'] == 'car_drive2':
|
|
253
|
+
# Native reset restores Python vehicle state; sync moving controller origins.
|
|
254
|
+
raw._set_car_pose()
|
|
255
|
+
# Resetting targets may touch controls; restore the exact integration input.
|
|
256
|
+
mujoco.mj_setState(model, data, vector, saved['state_spec'])
|
|
257
|
+
raw.sim.forward()
|
|
258
|
+
if self._instance['task_id'] in ('button_indicator_matching', 'find_object_in_drawer2'):
|
|
259
|
+
raw.set_episode_targets(self._episode['round_plan']['targets'])
|
|
260
|
+
raw._episode_index = 0
|
|
261
|
+
else:
|
|
262
|
+
raw._attempt = 1
|
|
263
|
+
raw._update_observables(force=True)
|
|
264
|
+
obs = raw._get_observations(force_update=True)
|
|
265
|
+
if self._instance['task_id'] in ('button_indicator_matching', 'conveyor_grasp'):
|
|
266
|
+
obs = raw._observation(obs)
|
|
267
|
+
elif self._instance['task_id'] == 'box_insertion':
|
|
268
|
+
obs = raw._observation()
|
|
269
|
+
prompt = self._wrapped.get_language_prompt()
|
|
270
|
+
self._wrapped.language_prompt = prompt
|
|
271
|
+
# mj_forward refreshes warm-start acceleration by tiny roundoff amounts.
|
|
272
|
+
# Restore the recorded integration input after all derived observations.
|
|
273
|
+
obs = self._wrapped._with_camera_observations(obs)
|
|
274
|
+
mujoco.mj_setState(model, data, vector, saved['state_spec'])
|
|
275
|
+
return obs, {
|
|
276
|
+
'success_state': False, 'terminal_success': False, 'events': [],
|
|
277
|
+
'language_prompt': prompt['text'], 'language_prompt_structured': prompt}
|
|
278
|
+
|
|
279
|
+
def step(self, action):
|
|
280
|
+
action = np.asarray(action, dtype=float)
|
|
281
|
+
low,high=-np.ones(self._wrapped.action_dim),np.ones(self._wrapped.action_dim)
|
|
282
|
+
if self._native_actions:
|
|
283
|
+
robot=self.env.robots[0]
|
|
284
|
+
for arm in robot.arms:
|
|
285
|
+
part=robot.part_controllers[arm]
|
|
286
|
+
if part.name=='JOINT_POSITION' and getattr(part,'input_type',None)=='absolute':
|
|
287
|
+
s,e=robot.composite_controller._action_split_indexes[arm]
|
|
288
|
+
limits=self.env.sim.model.jnt_range[part.joint_indexes['joints']]
|
|
289
|
+
low[s:e],high[s:e]=limits[:,0],limits[:,1]
|
|
290
|
+
if (action.shape != low.shape or not np.isfinite(action).all()
|
|
291
|
+
or np.any(action<low-1e-8) or np.any(action>high+1e-8)):
|
|
292
|
+
raise ValueError('action must be finite and inside the recorded controller bounds')
|
|
293
|
+
result = self._wrapped.step(action)
|
|
294
|
+
if not np.isfinite(self.env.sim.data.qpos).all() or not np.isfinite(self.env.sim.data.qvel).all():
|
|
295
|
+
raise ValueError('non-finite simulation state; evaluation is invalid')
|
|
296
|
+
# Refresh language after dynamic round transitions; do not send the future plan.
|
|
297
|
+
prompt = self._wrapped.get_language_prompt()
|
|
298
|
+
self._wrapped.language_prompt = prompt
|
|
299
|
+
obs, reward, terminated, truncated, info = result
|
|
300
|
+
if self._instance['task_id'] == 'rope_hang' and (info.get('failure') or {}).get('reason') in ('physics_instability', 'non_finite_state'):
|
|
301
|
+
raise ValueError('numerical rope simulation failure; evaluation is invalid')
|
|
302
|
+
info = dict(info, language_prompt=prompt['text'], language_prompt_structured=prompt)
|
|
303
|
+
return obs, reward, terminated, truncated, info
|
|
304
|
+
|
|
305
|
+
def close(self):
|
|
306
|
+
self._wrapped.close()
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Manifest execution. Files stay on the evaluator side of the policy boundary."""
|
|
2
|
+
import json
|
|
3
|
+
import math
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .records import VERSION, SUPPORTED_TASKS, load_suite, save_record, seal
|
|
8
|
+
from react_manip.core.timeouts import (
|
|
9
|
+
DEFAULT_TASK_WALL_TIMEOUT_SEC,
|
|
10
|
+
USE_DEFAULT_TASK_WALL_TIMEOUT,
|
|
11
|
+
resolve_task_wall_timeout_sec,
|
|
12
|
+
validate_timeout,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
PROTOCOL = dict(name='practice_exam', practice_episodes=10, exam_episodes=5,
|
|
16
|
+
exam_adaptation='online', invalid='abort_group',
|
|
17
|
+
clock='synchronous_simulation', score='mean_exam_task_score', policy_track='public')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def evaluate(path, policy_factory, *, cameras=True, runner=None, env_factory=None, on_result=None,
|
|
25
|
+
trace_dir=None, visualize=False, video_dir=None, policy_factory_for_robot=None,
|
|
26
|
+
close_policy=False, task_wall_timeout_sec=USE_DEFAULT_TASK_WALL_TIMEOUT,
|
|
27
|
+
episode_wall_timeout_sec=None):
|
|
28
|
+
if (visualize or video_dir is not None) and not cameras:
|
|
29
|
+
raise ValueError('visualization and video require policy images')
|
|
30
|
+
from react_manip.evaluation.viewer import EvaluationViewer
|
|
31
|
+
with EvaluationViewer(enabled=visualize, video_dir=video_dir) as viewer:
|
|
32
|
+
return _evaluate(path, policy_factory, cameras=cameras, runner=runner,
|
|
33
|
+
env_factory=env_factory, on_result=on_result,
|
|
34
|
+
policy_factory_for_robot=policy_factory_for_robot, close_policy=close_policy,
|
|
35
|
+
trace_dir=trace_dir, viewer=viewer if viewer.active else None,
|
|
36
|
+
task_wall_timeout_sec=task_wall_timeout_sec,
|
|
37
|
+
episode_wall_timeout_sec=episode_wall_timeout_sec)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _evaluate(path, policy_factory, *, cameras=True, runner=None, env_factory=None,
|
|
41
|
+
on_result=None, trace_dir=None, viewer=None, policy_factory_for_robot=None, close_policy=False,
|
|
42
|
+
task_wall_timeout_sec=USE_DEFAULT_TASK_WALL_TIMEOUT, episode_wall_timeout_sec=None):
|
|
43
|
+
"""Fresh policy per instance; all fifteen episodes retain learned state."""
|
|
44
|
+
from react_manip.core.runner import BenchmarkRunner
|
|
45
|
+
from react_manip.evaluation.protocols import _EpisodePolicy
|
|
46
|
+
from react_manip.evaluation.viewer import SkipEpisode
|
|
47
|
+
from .runtime import ReplayEnv, spec_for, contract_for
|
|
48
|
+
manifest, groups = load_suite(path)
|
|
49
|
+
if trace_dir is not None:
|
|
50
|
+
trace_dir = Path(trace_dir)
|
|
51
|
+
trace_dir.mkdir(parents=True, exist_ok=False)
|
|
52
|
+
runner = runner or BenchmarkRunner()
|
|
53
|
+
env_factory = env_factory or ReplayEnv
|
|
54
|
+
explicit_episode_timeout = validate_timeout(episode_wall_timeout_sec, 'episode_wall_timeout_sec')
|
|
55
|
+
task_started = {}
|
|
56
|
+
task_timeouts = {}
|
|
57
|
+
timed_out_tasks = set()
|
|
58
|
+
rows, outcomes = [], []
|
|
59
|
+
for instance, episodes in groups:
|
|
60
|
+
task_id = instance['task_id']
|
|
61
|
+
task_started.setdefault(task_id, time.perf_counter())
|
|
62
|
+
task_timeouts.setdefault(task_id, resolve_task_wall_timeout_sec(task_id, task_wall_timeout_sec))
|
|
63
|
+
task_timeout = task_timeouts[task_id]
|
|
64
|
+
task_deadline = None if task_timeout is None else task_started[task_id] + task_timeout
|
|
65
|
+
if task_id in timed_out_tasks or (task_deadline is not None and time.perf_counter() >= task_deadline):
|
|
66
|
+
timed_out_tasks.add(task_id)
|
|
67
|
+
outcomes.append({'task_instance_id': instance['task_instance_id'], 'valid': False,
|
|
68
|
+
'exam_scores': [], 'error': 'wall_clock_timeout',
|
|
69
|
+
'completed_episodes': 0, 'skipped_episodes': len(episodes), 'score': None,
|
|
70
|
+
'timeout_scope': 'task'})
|
|
71
|
+
continue
|
|
72
|
+
group_rows, error, env, policy = [], None, None, None
|
|
73
|
+
try:
|
|
74
|
+
policy = policy_factory_for_robot(instance['robot_id']) if policy_factory_for_robot else policy_factory()
|
|
75
|
+
for name in ('begin_episode', 'end_episode'):
|
|
76
|
+
if not callable(getattr(policy, name, None)):
|
|
77
|
+
raise TypeError(f'policy requires {name}')
|
|
78
|
+
if getattr(policy, 'requires_geometry_truth', False):
|
|
79
|
+
raise ValueError('public track rejects privileged geometry policies')
|
|
80
|
+
env = env_factory(instance, cameras=cameras)
|
|
81
|
+
spec = spec_for(instance['task_id'])
|
|
82
|
+
score_key = contract_for(instance['task_id'])['evaluation']['score_key']
|
|
83
|
+
for index, episode in enumerate(episodes):
|
|
84
|
+
if task_deadline is not None and time.perf_counter() >= task_deadline:
|
|
85
|
+
timed_out_tasks.add(task_id)
|
|
86
|
+
error = 'wall_clock_timeout'
|
|
87
|
+
break
|
|
88
|
+
phase = 'practice' if index < 10 else 'exam'
|
|
89
|
+
env.bind(episode)
|
|
90
|
+
# Policy RNG seed is protocol-owned and is not the private environment seed.
|
|
91
|
+
session = _EpisodePolicy(policy, phase, index if index < 10 else index-10, index)
|
|
92
|
+
writer = None
|
|
93
|
+
if viewer:
|
|
94
|
+
viewer.begin_episode(instance['task_id'], episode['episode_id'], phase,
|
|
95
|
+
index if index < 10 else index-10, spec.control_hz)
|
|
96
|
+
callback = None
|
|
97
|
+
if trace_dir is not None:
|
|
98
|
+
from .trajectory import TrajectoryWriter, RecordingPolicy
|
|
99
|
+
writer = TrajectoryWriter(trace_dir / episode['episode_id'], {
|
|
100
|
+
'manifest_digest': manifest['content_digest'], 'task_instance_digest': instance['content_digest'],
|
|
101
|
+
'episode_digest': episode['content_digest'], 'runtime': episode.get('runtime', instance['runtime']),
|
|
102
|
+
'phase': phase, 'policy_id': session.policy_id,
|
|
103
|
+
'policy_provenance': getattr(policy, 'provenance', None),
|
|
104
|
+
'policy_track': 'public', 'cameras_enabled': cameras,
|
|
105
|
+
'action_application': 'reject invalid; no clipping',
|
|
106
|
+
'control_hz': spec.control_hz})
|
|
107
|
+
session = RecordingPolicy(session, writer)
|
|
108
|
+
def callback(current_env, step, result):
|
|
109
|
+
from react_manip.core.observation import policy_observation
|
|
110
|
+
writer.write({'type': 'transition', 'tick': step-1,
|
|
111
|
+
'sim_time_sec': float(current_env.env.sim.data.time),
|
|
112
|
+
'next_observation': policy_observation(result.observation, spec, current_env),
|
|
113
|
+
'reward': result.reward, 'terminated': result.terminated,
|
|
114
|
+
'truncated': result.truncated, 'info': result.info})
|
|
115
|
+
trace_callback = callback
|
|
116
|
+
def callback(current_env, step, result):
|
|
117
|
+
if trace_callback:
|
|
118
|
+
trace_callback(current_env, step, result)
|
|
119
|
+
if viewer and (result.done or step >= round(spec.horizon_sec * spec.control_hz)):
|
|
120
|
+
from react_manip.core.observation import policy_observation
|
|
121
|
+
viewer.observe(current_env, policy_observation(result.observation, spec, current_env), result.info)
|
|
122
|
+
skipped = False
|
|
123
|
+
try:
|
|
124
|
+
remaining = None if task_deadline is None else max(1e-6, task_deadline - time.perf_counter())
|
|
125
|
+
runner_episode_timeout = explicit_episode_timeout
|
|
126
|
+
if runner_episode_timeout is None:
|
|
127
|
+
runner_episode_timeout = getattr(runner, 'episode_wall_timeout_sec', None)
|
|
128
|
+
if remaining is not None:
|
|
129
|
+
runner_episode_timeout = remaining if runner_episode_timeout is None else min(runner_episode_timeout, remaining)
|
|
130
|
+
result = runner.evaluate(spec, lambda **kwargs: env, session, env=env, seed=None,
|
|
131
|
+
robot_id=instance['robot_id'], camera_observations=cameras, stop_on_done=True, step_callback=callback,
|
|
132
|
+
observation_callback=viewer.observe if viewer else None,
|
|
133
|
+
horizon_sec=instance['runtime']['horizon_sec'],
|
|
134
|
+
control_hz=instance['runtime']['control_hz'], score_key=score_key,
|
|
135
|
+
episode_wall_timeout_sec=runner_episode_timeout)
|
|
136
|
+
if writer:
|
|
137
|
+
writer.write({'type': 'episode_result', **result.to_dict()})
|
|
138
|
+
except SkipEpisode:
|
|
139
|
+
# A skipped episode is recorded and the next saved
|
|
140
|
+
# episode is evaluated. It intentionally makes the group
|
|
141
|
+
# invalid for protocol scoring, but must not abort the
|
|
142
|
+
# remaining episodes or groups.
|
|
143
|
+
skipped = True
|
|
144
|
+
if writer:
|
|
145
|
+
writer.write({'type': 'skipped', 'reason': 'viewer_skip'})
|
|
146
|
+
try:
|
|
147
|
+
session.end_episode(skipped=True, passed=False)
|
|
148
|
+
except Exception:
|
|
149
|
+
# The skip action itself is still honored; policy
|
|
150
|
+
# cleanup errors are represented by the invalid row.
|
|
151
|
+
pass
|
|
152
|
+
except Exception as exc:
|
|
153
|
+
if writer:
|
|
154
|
+
writer.write({'type': 'invalid', 'reason': f'{type(exc).__name__}: {exc}'})
|
|
155
|
+
raise
|
|
156
|
+
finally:
|
|
157
|
+
if viewer:
|
|
158
|
+
viewer.end_episode()
|
|
159
|
+
if writer:
|
|
160
|
+
writer.close()
|
|
161
|
+
if skipped:
|
|
162
|
+
row = {'task_id': instance['task_id'], 'task_instance_id': instance['task_instance_id'],
|
|
163
|
+
'task_instance_digest': instance['content_digest'],
|
|
164
|
+
'episode_id': episode['episode_id'], 'episode_digest': episode['content_digest'],
|
|
165
|
+
'phase': phase, 'episode_index': index, 'skipped': True,
|
|
166
|
+
'error': 'skipped_by_viewer', 'passed': False, 'episode_score': None}
|
|
167
|
+
rows.append(row)
|
|
168
|
+
group_rows.append(row)
|
|
169
|
+
if on_result:
|
|
170
|
+
on_result(row)
|
|
171
|
+
continue
|
|
172
|
+
row = {**result.to_dict(), 'phase': phase, 'episode_index': index,
|
|
173
|
+
'task_instance_id': instance['task_instance_id'],
|
|
174
|
+
'task_instance_digest': instance['content_digest'],
|
|
175
|
+
'episode_id': episode['episode_id'], 'episode_digest': episode['content_digest']}
|
|
176
|
+
if 'error' not in row:
|
|
177
|
+
value = row.get('episode_score')
|
|
178
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
|
|
179
|
+
row['error'] = 'missing or non-finite episode_score'
|
|
180
|
+
rows.append(row); group_rows.append(row)
|
|
181
|
+
if on_result:
|
|
182
|
+
on_result(row)
|
|
183
|
+
if 'error' in row:
|
|
184
|
+
if row.get('error') == 'wall_clock_timeout':
|
|
185
|
+
timed_out_tasks.add(task_id)
|
|
186
|
+
raise ValueError(row['error'])
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
error = f'{type(exc).__name__}: {exc}'
|
|
189
|
+
finally:
|
|
190
|
+
try:
|
|
191
|
+
if env is not None:env.close()
|
|
192
|
+
finally:
|
|
193
|
+
if close_policy and policy is not None:
|
|
194
|
+
try:getattr(policy,'shutdown',policy.close)()
|
|
195
|
+
except Exception as exc:error=f'Policy cleanup failed: {type(exc).__name__}: {exc}'
|
|
196
|
+
valid = (error is None and len(group_rows) == 15 and
|
|
197
|
+
not any('error' in row or row.get('skipped') for row in group_rows))
|
|
198
|
+
exam_scores = [r.get('episode_score') for r in group_rows[10:]]
|
|
199
|
+
score = sum(exam_scores)/5 if valid else None
|
|
200
|
+
outcomes.append({'task_instance_id': instance['task_instance_id'], 'valid': valid,
|
|
201
|
+
'exam_scores': exam_scores, 'error': error, 'completed_episodes': len(group_rows), 'score': score,
|
|
202
|
+
'skipped_episodes': sum(bool(r.get('skipped')) for r in group_rows),
|
|
203
|
+
'timeout_scope': 'task' if task_id in timed_out_tasks else None})
|
|
204
|
+
if viewer and viewer.aborted:
|
|
205
|
+
break
|
|
206
|
+
valid = len(outcomes) == len(groups) and all(g['valid'] for g in outcomes)
|
|
207
|
+
skipped_episodes = sum(len(episodes) for instance, episodes in groups
|
|
208
|
+
if instance['task_id'] in timed_out_tasks)
|
|
209
|
+
task_wall_time_sec = {task_id: time.perf_counter() - started
|
|
210
|
+
for task_id, started in task_started.items()}
|
|
211
|
+
return {'schema_version': VERSION, 'suite_id': manifest['suite_id'], 'manifest_digest': manifest['content_digest'],
|
|
212
|
+
'protocol': manifest['protocol'], 'valid': valid,
|
|
213
|
+
'mode': 'public_visual' if cameras else 'state_only_smoke',
|
|
214
|
+
'protocol_score': sum(g['score'] for g in outcomes)/len(outcomes) if valid and cameras else None,
|
|
215
|
+
'official_score': None, # Pilot contracts have not passed publication gates.
|
|
216
|
+
'groups': outcomes, 'episodes': rows,
|
|
217
|
+
'limitations': ['in-process policy state isolation relies on adapter contract',
|
|
218
|
+
'pilot suite; task solvability and publication gates are separate'],
|
|
219
|
+
'wall_clock_timeout': bool(timed_out_tasks),
|
|
220
|
+
'timeout_scope': 'task' if timed_out_tasks else None,
|
|
221
|
+
'task_wall_timeout_sec': (DEFAULT_TASK_WALL_TIMEOUT_SEC
|
|
222
|
+
if task_wall_timeout_sec is USE_DEFAULT_TASK_WALL_TIMEOUT
|
|
223
|
+
else validate_timeout(task_wall_timeout_sec, 'task_wall_timeout_sec')),
|
|
224
|
+
'task_wall_timeout_sec_by_task': task_timeouts,
|
|
225
|
+
'task_wall_time_sec': task_wall_time_sec,
|
|
226
|
+
'completed_episodes': len(rows),
|
|
227
|
+
'skipped_episodes': sum(bool(row.get('skipped')) for row in rows) + max(0, skipped_episodes - sum(
|
|
228
|
+
1 for row in rows if row.get('task_instance_id') in {
|
|
229
|
+
instance['task_instance_id'] for instance, episodes in groups
|
|
230
|
+
if instance['task_id'] in timed_out_tasks
|
|
231
|
+
}))}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Evaluator-owned, optional trajectories with public observations and array files."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TrajectoryWriter:
|
|
9
|
+
def __init__(self, directory, metadata):
|
|
10
|
+
self.directory = Path(directory)
|
|
11
|
+
self.directory.mkdir(parents=True, exist_ok=False)
|
|
12
|
+
self.stream = (self.directory / 'trajectory.jsonl').open('x', encoding='utf-8')
|
|
13
|
+
self.index = 0
|
|
14
|
+
self.write({'type': 'header', 'schema_version': 'cl-manip.trajectory.v1', **metadata})
|
|
15
|
+
|
|
16
|
+
def write(self, record):
|
|
17
|
+
arrays = {}
|
|
18
|
+
def encode(value):
|
|
19
|
+
if isinstance(value, np.ndarray):
|
|
20
|
+
name = f'a{len(arrays)}'
|
|
21
|
+
if value.dtype.hasobject:
|
|
22
|
+
raise ValueError('trajectory refuses object arrays')
|
|
23
|
+
arrays[name] = value
|
|
24
|
+
return {'array_file': f'{self.index:06d}.npz', 'array_key': name,
|
|
25
|
+
'dtype': str(value.dtype), 'shape': list(value.shape)}
|
|
26
|
+
if isinstance(value, np.generic):
|
|
27
|
+
return value.item()
|
|
28
|
+
if isinstance(value, dict):
|
|
29
|
+
return {str(k): encode(v) for k, v in value.items()}
|
|
30
|
+
if isinstance(value, (tuple, list)):
|
|
31
|
+
return [encode(v) for v in value]
|
|
32
|
+
return value
|
|
33
|
+
encoded = encode(record)
|
|
34
|
+
line = json.dumps(encoded, ensure_ascii=False, allow_nan=False)
|
|
35
|
+
if arrays:
|
|
36
|
+
np.savez_compressed(self.directory / f'{self.index:06d}.npz', **arrays)
|
|
37
|
+
self.stream.write(line + '\n')
|
|
38
|
+
self.stream.flush()
|
|
39
|
+
self.index += 1
|
|
40
|
+
|
|
41
|
+
def close(self):
|
|
42
|
+
self.stream.close()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RecordingPolicy:
|
|
46
|
+
"""Log policy inputs before act; successful ticks are logged by the evaluator."""
|
|
47
|
+
def __init__(self, policy, writer):
|
|
48
|
+
self.policy, self.writer, self.tick = policy, writer, 0
|
|
49
|
+
|
|
50
|
+
def __getattr__(self, key):
|
|
51
|
+
return getattr(self.policy, key)
|
|
52
|
+
|
|
53
|
+
def act(self, observation, info):
|
|
54
|
+
self.writer.write({'type': 'policy_input', 'tick': self.tick,
|
|
55
|
+
'observation': observation, 'info': info})
|
|
56
|
+
action = self.policy.act(observation, info)
|
|
57
|
+
self.writer.write({'type': 'requested_action', 'tick': self.tick, 'action': action})
|
|
58
|
+
self.tick += 1
|
|
59
|
+
return action
|