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.
Files changed (99) hide show
  1. assets/materials/realistic/common/ceramic.png +0 -0
  2. assets/materials/realistic/common/cream-plaster.png +0 -0
  3. assets/materials/realistic/common/gray-felt.png +0 -0
  4. assets/materials/realistic/common/gray-plaster.png +0 -0
  5. assets/materials/realistic/common/gray-woodgrain.png +0 -0
  6. assets/materials/realistic/common/light-gray-floor-tile.png +0 -0
  7. assets/materials/realistic/common/light-gray-plaster.png +0 -0
  8. assets/materials/realistic/common/metal.png +0 -0
  9. assets/materials/realistic/common/oak-wood.png +0 -0
  10. assets/materials/realistic/common/steel-brushed.png +0 -0
  11. assets/materials/realistic/common/steel-scratched.png +0 -0
  12. assets/materials/realistic/common/varnished-wood-panels.png +0 -0
  13. assets/materials/realistic/common/walnut-wood.png +0 -0
  14. assets/materials/realistic/common/white-bricks.png +0 -0
  15. assets/materials/realistic/common/wood-tiles.png +0 -0
  16. assets/materials/realistic/tiles074/Tiles074_2K_Color.png +0 -0
  17. assets/materials/realistic/woodfloor049/WoodFloor049_2K_Color.png +0 -0
  18. configs/box_insertion/profile_default.json +204 -0
  19. react_manip/__init__.py +12 -0
  20. react_manip/asset_cli.py +18 -0
  21. react_manip/builtin.py +168 -0
  22. react_manip/cameras.py +301 -0
  23. react_manip/category_objects.py +248 -0
  24. react_manip/core/__init__.py +60 -0
  25. react_manip/core/aggregate.py +59 -0
  26. react_manip/core/compatibility.py +64 -0
  27. react_manip/core/contracts.py +42 -0
  28. react_manip/core/geometry.py +269 -0
  29. react_manip/core/language.py +116 -0
  30. react_manip/core/metrics.py +196 -0
  31. react_manip/core/observation.py +59 -0
  32. react_manip/core/protocol.py +91 -0
  33. react_manip/core/registry.py +94 -0
  34. react_manip/core/runner.py +498 -0
  35. react_manip/core/scenarios.py +110 -0
  36. react_manip/core/specs.py +203 -0
  37. react_manip/core/timeouts.py +44 -0
  38. react_manip/core/visual_randomization.py +261 -0
  39. react_manip/evaluation/__init__.py +4 -0
  40. react_manip/evaluation/metrics.py +17 -0
  41. react_manip/evaluation/protocols.py +326 -0
  42. react_manip/evaluation/viewer.py +240 -0
  43. react_manip/instances/__init__.py +6 -0
  44. react_manip/instances/candidate_bank.py +109 -0
  45. react_manip/instances/public.py +95 -0
  46. react_manip/instances/records.py +345 -0
  47. react_manip/instances/runtime.py +306 -0
  48. react_manip/instances/suite.py +231 -0
  49. react_manip/instances/trajectory.py +59 -0
  50. react_manip/instances/variation.py +118 -0
  51. react_manip/legacy.py +318 -0
  52. react_manip/policies/__init__.py +5 -0
  53. react_manip/policies/base.py +79 -0
  54. react_manip/rendering.py +58 -0
  55. react_manip/robot_registry.py +31 -0
  56. react_manip/robots/__init__.py +33 -0
  57. react_manip/robots/adapters.py +135 -0
  58. react_manip/robots/agilex_adapter.py +35 -0
  59. react_manip/robots/agilex_description.py +74 -0
  60. react_manip/robots/agilex_robosuite.py +246 -0
  61. react_manip/robots/arm_actions.py +17 -0
  62. react_manip/robots/base.py +119 -0
  63. react_manip/robots/catalog.py +18 -0
  64. react_manip/robots/legacy_agilex_robosuite.py +169 -0
  65. react_manip/robots/mobile_base.py +33 -0
  66. react_manip/robots/mounts.py +80 -0
  67. react_manip/robots/resolution.py +103 -0
  68. react_manip/robots/sawyer_robosuite.py +147 -0
  69. react_manip/robots/sensors.py +137 -0
  70. react_manip/utils/__init__.py +1 -0
  71. react_manip/visual_task.py +50 -0
  72. react_manip-0.1.0.dist-info/METADATA +446 -0
  73. react_manip-0.1.0.dist-info/RECORD +99 -0
  74. react_manip-0.1.0.dist-info/WHEEL +5 -0
  75. react_manip-0.1.0.dist-info/entry_points.txt +2 -0
  76. react_manip-0.1.0.dist-info/licenses/LICENSE +131 -0
  77. react_manip-0.1.0.dist-info/top_level.txt +5 -0
  78. scripts/add_phone_lean_to_bank.py +242 -0
  79. scripts/agilex_wheeled_builder.py +133 -0
  80. scripts/audit_task_solvability.py +150 -0
  81. scripts/build_agilex_assets.py +317 -0
  82. scripts/build_legacy_agilex_assets.py +244 -0
  83. scripts/evaluate_all_tasks.py +309 -0
  84. scripts/evaluate_policy.py +281 -0
  85. scripts/piperx_assets.lock.json +181 -0
  86. scripts/pygame_demo.py +360 -0
  87. scripts/pygame_task_debug.py +242 -0
  88. scripts/regenerate_instance_bank.py +192 -0
  89. scripts/remove_task_from_bank.py +103 -0
  90. scripts/select_task_instances.py +64 -0
  91. scripts/verify_curling_reachability.py +206 -0
  92. scripts/verify_piperx_tasks.py +81 -0
  93. tasks/box_insertion/__init__.py +3 -0
  94. tasks/box_insertion/assets/scene.xml +50 -0
  95. tasks/box_insertion/env.py +642 -0
  96. tasks/box_insertion/evaluation.json +16 -0
  97. tasks/box_insertion/instance_contract.json +321 -0
  98. tasks/box_insertion/scoring.py +23 -0
  99. tasks/box_insertion/task_spec.json +333 -0
@@ -0,0 +1,118 @@
1
+ """Explicit, evaluator-private Episode variations for four feedback tasks.
2
+
3
+ Task parameters remain the baseline. Resolved variations are saved, validated,
4
+ and applied before compilation; no sampling takes place during replay or step.
5
+ """
6
+ from copy import deepcopy
7
+ import numpy as np
8
+
9
+ TASKS = ('leveling_board1', 'conveyor_grasp', 'box_insertion', 'car_drive2')
10
+ DOMAINS = {
11
+ 'leveling_board1': {'ordinary_dimension_m': [.06, .14], 'thin_dimensions_m': [.03, .04, .05]},
12
+ 'conveyor_grasp': {'belt_speed_m_s': [.08, .20], 'target_size_scale': [.8, 1.15]},
13
+ 'box_insertion': {'friction_multiplier': [.6, 1.4], 'friction_limits': [.01, .6], 'mass_scale': [.7, 1.3]},
14
+ 'car_drive2': {'nominal_speed_m_s': [.9, 1.15]},
15
+ }
16
+
17
+
18
+ def _number(value, lo, hi):
19
+ if type(value) not in (int, float) or not np.isfinite(value) or not lo-1e-12 <= value <= hi+1e-12:
20
+ raise ValueError('Episode parameter outside its finite sampling domain')
21
+
22
+
23
+ def sample_variation(task_id, base, seed, index, attempt=0):
24
+ """Each consecutive five Episodes cover five shuffled strata, in both phases.
25
+
26
+ Rejection retries stay in the original stratum. Seeds and indices are private.
27
+ Independent dimensions use independently shuffled strata.
28
+ """
29
+ if task_id not in TASKS or not 0 <= index < 15:
30
+ raise ValueError('unsupported Episode variation request')
31
+ rng = np.random.default_rng(np.random.SeedSequence([seed, index, attempt, 261012]))
32
+ axis = 0
33
+ def draw(lo, hi):
34
+ nonlocal axis
35
+ order = np.random.default_rng(np.random.SeedSequence([seed, index//5, axis, 73])).permutation(5)
36
+ axis += 1
37
+ u = (int(order[index % 5]) + rng.uniform(.05, .95))/5
38
+ return float(lo + (hi-lo)*u)
39
+ if task_id == 'leveling_board1':
40
+ from tasks.leveling_board1.instances import SHIM_NAMES
41
+ useful = rng.choice(SHIM_NAMES, 3, replace=False).tolist()
42
+ heights = rng.permutation([.03,.04,.05])
43
+ axes = {n:int(rng.integers(3)) for n in useful}
44
+ dims = {n:[draw(.06,.14) for _ in range(3)] for n in SHIM_NAMES}
45
+ for n,h in zip(useful, heights):
46
+ dims[n][axes[n]] = float(h)
47
+ result = dict(shim_dimensions=dims, solution_axes=axes)
48
+ elif task_id == 'conveyor_grasp':
49
+ result = dict(belt_speed_m_s=draw(.08,.20),
50
+ target_size_scales=[draw(.8,1.15) for _ in range(base['task']['n_objects'])])
51
+ elif task_id == 'box_insertion':
52
+ result = dict(mass_scale=draw(.7,1.3), sliding_friction={
53
+ side:draw(max(.01,base['block_friction'][side]*.6), min(.6,base['block_friction'][side]*1.4))
54
+ for side in ('left','right')})
55
+ else:
56
+ result = dict(nominal_speed_m_s=draw(.9,1.15))
57
+ effective_parameters(task_id, base, result)
58
+ return result
59
+
60
+
61
+ def effective_parameters(task_id, base, variation):
62
+ """Validate a strict whitelist and derive resolved parameters from the base."""
63
+ p = deepcopy(base)
64
+ expected = {
65
+ 'leveling_board1': {'shim_dimensions','solution_axes'},
66
+ 'conveyor_grasp': {'belt_speed_m_s','target_size_scales'},
67
+ 'box_insertion': {'mass_scale','sliding_friction'},
68
+ 'car_drive2': {'nominal_speed_m_s'},
69
+ }
70
+ if task_id not in expected or not isinstance(variation,dict):
71
+ raise ValueError('invalid Episode variation fields')
72
+ if set(variation) != expected[task_id]:
73
+ raise ValueError('invalid Episode variation fields')
74
+ v = deepcopy(variation)
75
+ if task_id == 'leveling_board1':
76
+ from tasks.leveling_board1.instances import validate_task_instance
77
+ p.update(v)
78
+ validate_task_instance(p)
79
+ elif task_id == 'conveyor_grasp':
80
+ _number(v['belt_speed_m_s'], .08,.20)
81
+ scales = v['target_size_scales']
82
+ if not isinstance(scales,list) or len(scales) != base['task']['n_objects']:
83
+ raise ValueError('one size scale per target is required')
84
+ for scale in scales:
85
+ _number(scale,.8,1.15)
86
+ p['belt']['speed'] = v['belt_speed_m_s']
87
+ elif task_id == 'box_insertion':
88
+ _number(v['mass_scale'],.7,1.3)
89
+ if not isinstance(v['sliding_friction'],dict) or set(v['sliding_friction']) != {'left','right'}:
90
+ raise ValueError('both block contact patches are required')
91
+ for side, value in v['sliding_friction'].items():
92
+ _number(value,max(.01,base['block_friction'][side]*.6),min(.6,base['block_friction'][side]*1.4))
93
+ p['block_friction'][side] = value
94
+ else:
95
+ _number(v['nominal_speed_m_s'],.9,1.15)
96
+ p['vehicle']['nominal_speed_m_s'] = v['nominal_speed_m_s']
97
+ return p
98
+
99
+
100
+ def episode_scenario(scenario, variation):
101
+ resolved = deepcopy(scenario)
102
+ resolved.parameters = effective_parameters(scenario.task_id, scenario.parameters, variation)
103
+ resolved.metadata.pop('initial_episode_scene', None)
104
+ resolved.metadata['episode_variation'] = deepcopy(variation)
105
+ return resolved
106
+
107
+
108
+ def construct_sampled_episode(scenario, robot, index, scene_seed, *, render=False):
109
+ """Task-default practice/exam uses the same sampler as saved question banks."""
110
+ from .runtime import _construct
111
+ variation = sample_variation(scenario.task_id, scenario.parameters, scenario.seed, index)
112
+ resolved = episode_scenario(scenario, variation)
113
+ scene = None
114
+ if scenario.task_id == 'leveling_board1':
115
+ from tasks.leveling_board1.instances import sample_episode_instance
116
+ scene = sample_episode_instance(resolved.parameters, scene_seed)
117
+ env = _construct(scenario.task_id, resolved, robot, episode=scene, render=render)
118
+ return env, variation
react_manip/legacy.py ADDED
@@ -0,0 +1,318 @@
1
+ """Compatibility helpers for migrating existing robosuite tasks.
2
+
3
+ The v1 environments remain untouched. These helpers only provide metadata
4
+ and factory conventions so they can be evaluated by the v2 runner while task
5
+ implementations are migrated one at a time.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Callable, Mapping
11
+
12
+ from .core.protocol import normalize_reset, normalize_step
13
+ from .core.language import runtime_language_prompt
14
+
15
+
16
+ class TaskRobotProxy:
17
+ """Embodiment context for task-level environments without arm physics.
18
+
19
+ Native scene tasks (for example vehicle or board actuators) can accept a
20
+ robot selection while keeping their explicit task-level action API. This
21
+ proxy makes that choice visible to generic frontends and benchmark logs;
22
+ it intentionally exposes no EEF/controller attributes, so callers cannot
23
+ mistake it for a simulated manipulator.
24
+ """
25
+
26
+ def __init__(self, robot_id: str):
27
+ from .robots.catalog import normalize_robot_id
28
+ self.robot_id = normalize_robot_id(robot_id)
29
+ # Context is a label, not a loaded robot model. Do not load assets or
30
+ # fabricate joints/arms for a native task-level scene.
31
+ self.name = self.robot_id
32
+ self.arms = ()
33
+
34
+ @property
35
+ def spec(self):
36
+ from .robots.base import RobotSpec
37
+ return RobotSpec(
38
+ robot_id=self.robot_id, arm_names=(),
39
+ arm_dof=(), gripper_type=None,
40
+ control_modes=("task_level",), capabilities=frozenset({"task_level_proxy"}),
41
+ )
42
+
43
+
44
+ def attach_robot_context(env: Any, robot_id: str | None) -> Any:
45
+ """Attach a normalized robot context to native task-level environments."""
46
+ if robot_id is None:
47
+ return env
48
+ from .robots.catalog import normalize_robot_id
49
+ normalized = normalize_robot_id(robot_id)
50
+ env.robot_id = normalized
51
+ if getattr(env, 'robots', None):
52
+ return env
53
+ # Do not populate ``env.robots``: frontends reserve that attribute for
54
+ # physically compiled robot models with controllers and EEF sites.
55
+ env.robot_context = TaskRobotProxy(normalized)
56
+ return env
57
+
58
+
59
+ class _NativeModelView:
60
+ """Expose a raw ``mujoco.MjModel`` through robosuite's model surface."""
61
+
62
+ def __init__(self, model: Any):
63
+ self._model = model
64
+
65
+ def __getattr__(self, name: str) -> Any:
66
+ return getattr(self._model, name)
67
+
68
+
69
+ class _NativeSimView:
70
+ # Marker consumed by frontends that need to distinguish a raw MuJoCo
71
+ # scene from robosuite's MjSim wrapper. The model is intentionally still
72
+ # exposed through the same ``sim.model / sim.data`` surface.
73
+ native_mujoco = True
74
+
75
+ def __init__(self, env: Any):
76
+ self.model = _NativeModelView(env.model)
77
+ self.data = env.data
78
+
79
+
80
+ class LegacyTaskAdapter:
81
+ """Thin wrapper adding a v2-friendly signal surface to an existing env."""
82
+
83
+ def __init__(self, env: Any):
84
+ self.env = env
85
+ self._counter_snapshot: dict[str, float] = {}
86
+ self._episode_index = -1
87
+ self.language_prompt: dict[str, Any] = {}
88
+ self._camera_renderers = {}
89
+ self._camera_observation_ids = ()
90
+ self._camera_resolution = (640, 480)
91
+
92
+ def get_camera_specs(self):
93
+ from .cameras import camera_specs
94
+ return camera_specs(self.env)
95
+
96
+ def get_camera_calibration(self, camera_id, *, width=640, height=480):
97
+ """Internal diagnostics only: optical +Z forward, +Y down."""
98
+ import mujoco
99
+ import numpy as np
100
+ from .cameras import raw_sim
101
+ _, model, data = raw_sim(self.env)
102
+ cid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_CAMERA, camera_id)
103
+ if cid < 0:
104
+ raise ValueError(f'Unknown camera: {camera_id}')
105
+ f = .5 * height / np.tan(np.deg2rad(model.cam_fovy[cid] / 2))
106
+ rotation = data.cam_xmat[cid].reshape(3, 3) @ np.diag([1., -1., -1.])
107
+ world_from_optical = np.eye(4)
108
+ world_from_optical[:3, :3] = rotation
109
+ world_from_optical[:3, 3] = data.cam_xpos[cid]
110
+ return dict(K=np.array([[f, 0, width / 2], [0, f, height / 2], [0, 0, 1]]),
111
+ world_from_optical=world_from_optical, width=width, height=height)
112
+
113
+ def render_camera(self, camera_id='workspace_oblique', *, width=640, height=480):
114
+ from react_manip.rendering import NativeRGBRenderer
115
+ key = (int(width), int(height))
116
+ if key not in self._camera_renderers:
117
+ self._camera_renderers[key] = NativeRGBRenderer(*key)
118
+ return self._camera_renderers[key].render(self.sim, camera_id).copy()
119
+
120
+ def get_camera_observation(self, camera_ids=None, *, width=640, height=480):
121
+ if camera_ids is None:
122
+ camera_ids = [s['id'] for s in self.get_camera_specs() if s['policy_default']]
123
+ elif isinstance(camera_ids, str):
124
+ camera_ids = (camera_ids,)
125
+ return {name: self.render_camera(name, width=width, height=height) for name in camera_ids}
126
+
127
+ def set_camera_observations(self, camera_ids=(), *, width=640, height=480):
128
+ """Opt-in RGB under observation['images']; no implicit room/debug input."""
129
+ if isinstance(camera_ids, str):
130
+ camera_ids = (camera_ids,)
131
+ available = {s['id'] for s in self.get_camera_specs()}
132
+ missing = set(camera_ids) - available
133
+ if missing:
134
+ raise ValueError(f'Unavailable cameras: {sorted(missing)}')
135
+ if width <= 0 or height <= 0:
136
+ raise ValueError('camera resolution must be positive')
137
+ self._camera_observation_ids = tuple(camera_ids)
138
+ self._camera_resolution = (int(width), int(height))
139
+
140
+ def _with_camera_observations(self, observation):
141
+ if not self._camera_observation_ids:
142
+ return observation
143
+ if not isinstance(observation, Mapping):
144
+ observation = {'state': observation}
145
+ width, height = self._camera_resolution
146
+ return dict(observation, images=self.get_camera_observation(
147
+ self._camera_observation_ids, width=width, height=height))
148
+
149
+ @property
150
+ def robot_id(self) -> str | None:
151
+ explicit = getattr(self.env, "robot_id", None)
152
+ if explicit:
153
+ return str(explicit)
154
+ robots = getattr(self.env, "robots", ()) or ()
155
+ if robots:
156
+ from .robots.resolution import robot_id_from_model
157
+ try:
158
+ return robot_id_from_model(getattr(robots[0], 'name', ''))
159
+ except ValueError:
160
+ pass
161
+ return None
162
+
163
+ @property
164
+ def task_id(self) -> str:
165
+ explicit = getattr(self.env, "task_id", None)
166
+ if explicit:
167
+ return str(explicit)
168
+ name = self.env.__class__.__name__
169
+ import re
170
+ return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
171
+
172
+ def get_language_prompt(self) -> dict[str, Any]:
173
+ """Return the current episode's structured language instruction."""
174
+ return runtime_language_prompt(self.env)
175
+
176
+ _EVENT_COUNTERS = (
177
+ ("in_box_count", "object_in_box"),
178
+ ("hits", "hit"),
179
+ ("counted", "juggle"),
180
+ ("op", "round"),
181
+ )
182
+
183
+ @property
184
+ def action_dim(self) -> int:
185
+ return int(self.env.action_dim)
186
+
187
+ @property
188
+ def sim(self) -> Any:
189
+ """Expose the simulator for read-only recorders and diagnostics."""
190
+ sim = getattr(self.env, "sim", None)
191
+ if sim is not None:
192
+ return sim
193
+ if hasattr(self.env, "model") and hasattr(self.env, "data"):
194
+ return _NativeSimView(self.env)
195
+ return self.env
196
+
197
+ def __getattr__(self, name: str) -> Any:
198
+ return getattr(self.env, name)
199
+
200
+ def reset(self, *args: Any, **kwargs: Any) -> Any:
201
+ self._counter_snapshot.clear()
202
+ self._episode_index += 1
203
+ value = self.env.reset(*args, **kwargs)
204
+ sim = self.sim
205
+ model = getattr(sim, 'model', None)
206
+ if model is not None and hasattr(model, 'ncam'):
207
+ from .cameras import configure_fixed_cameras, configure_policy_camera
208
+ configure_policy_camera(self.env)
209
+ configure_fixed_cameras(self.env)
210
+ for attr, _ in self._EVENT_COUNTERS:
211
+ current = getattr(self.env, attr, None)
212
+ if isinstance(current, (int, float)):
213
+ self._counter_snapshot[attr] = float(current)
214
+ self._counter_snapshot["drop_count"] = float(getattr(self.env, "drop_count", 0))
215
+ prompt = self.get_language_prompt()
216
+ self.language_prompt = prompt
217
+ if isinstance(value, tuple) and len(value) == 2 and isinstance(value[1], dict):
218
+ info = dict(value[1])
219
+ info["language_prompt"] = prompt["text"]
220
+ info["language_prompt_structured"] = prompt
221
+ return self._with_camera_observations(value[0]), info
222
+ return self._with_camera_observations(value)
223
+
224
+ def step(self, action: Any) -> Any:
225
+ value = normalize_step(self.env.step(action))
226
+ info = dict(value.info)
227
+ self.language_prompt = self.get_language_prompt()
228
+ info["language_prompt"] = self.language_prompt["text"]
229
+ info["language_prompt_structured"] = self.language_prompt
230
+ events = list(_as_events(info))
231
+ for attr, event_name in self._EVENT_COUNTERS:
232
+ value_now = getattr(self.env, attr, None)
233
+ if isinstance(value_now, (int, float)):
234
+ previous = self._counter_snapshot.get(attr, float(value_now))
235
+ delta = int(max(0, float(value_now) - previous))
236
+ events.extend([event_name] * delta)
237
+ self._counter_snapshot[attr] = float(value_now)
238
+ # Common v1 task fields use ``in`` or a positive score for one-shot
239
+ # events but did not expose the new ``events`` key.
240
+ if info.get("in") is True:
241
+ events.append("in")
242
+ if isinstance(info.get("score"), (int, float)) and info["score"] > 0:
243
+ events.append("score")
244
+ if events:
245
+ info["events"] = events
246
+ if getattr(self.env, "drop_count", 0) > self._counter_snapshot.get("drop_count", 0):
247
+ info.setdefault("failure", {"recoverable": True})
248
+ self._counter_snapshot["drop_count"] = float(getattr(self.env, "drop_count", 0))
249
+ if "success_state" not in info:
250
+ if hasattr(self.env, "_check_success"):
251
+ info["success_state"] = bool(self.env._check_success())
252
+ else:
253
+ info["success_state"] = bool(value.reward > 0)
254
+ if "success" not in info and "terminal_success" not in info:
255
+ info["terminal_success"] = bool(info["success_state"] and value.done)
256
+ sim = getattr(self.env, "sim", None)
257
+ cfrc = getattr(getattr(sim, "data", None), "cfrc_ext", None)
258
+ if cfrc is not None:
259
+ try:
260
+ info["peak_force_N"] = float(abs(cfrc).max())
261
+ except (AttributeError, TypeError, ValueError):
262
+ pass
263
+ return self._with_camera_observations(value.observation), value.reward, value.terminated, value.truncated, info
264
+
265
+ def close(self) -> None:
266
+ for renderer in self._camera_renderers.values():
267
+ renderer.close()
268
+ self._camera_renderers.clear()
269
+ self.env.close()
270
+
271
+ def render(self, *args: Any, **kwargs: Any) -> Any:
272
+ """Forward interactive rendering to the wrapped robosuite env."""
273
+
274
+ viewer = getattr(self.env, "viewer", None)
275
+ update_fn = getattr(viewer, "update", None)
276
+ if callable(update_fn):
277
+ # robosuite's native ``mjviewer`` has a no-op render() method;
278
+ # update() is the operation that creates / presents the window.
279
+ return update_fn()
280
+ render_fn = getattr(self.env, "render", None)
281
+ if not callable(render_fn):
282
+ return None
283
+ return render_fn(*args, **kwargs)
284
+
285
+
286
+ def legacy_factory(
287
+ constructor: Callable[..., Any],
288
+ *,
289
+ robot_keyword: str = "robots",
290
+ default_robot: str | None = None,
291
+ fixed_kwargs: Mapping[str, Any] | None = None,
292
+ ) -> Callable[..., LegacyTaskAdapter]:
293
+ """Create a factory compatible with :class:`BenchmarkRunner`."""
294
+
295
+ fixed_kwargs = dict(fixed_kwargs or {})
296
+
297
+ def factory(*, seed: int | None = None, robot_id: str | None = None, **kwargs: Any) -> LegacyTaskAdapter:
298
+ params = dict(fixed_kwargs)
299
+ params.update(kwargs)
300
+ if seed is not None:
301
+ params.setdefault("seed", seed)
302
+ if robot_id is not None or default_robot is not None:
303
+ params.setdefault(robot_keyword, robot_id or default_robot)
304
+ return LegacyTaskAdapter(constructor(**params))
305
+
306
+ return factory
307
+
308
+
309
+ def _as_events(info: Mapping[str, Any]) -> list[Any]:
310
+ events = info.get("events", info.get("event", ()))
311
+ if events is None:
312
+ return []
313
+ if isinstance(events, (str, bytes)):
314
+ return [events]
315
+ try:
316
+ return list(events)
317
+ except TypeError:
318
+ return [events]
@@ -0,0 +1,5 @@
1
+ """Policy interfaces and small reference policies."""
2
+
3
+ from .base import CallablePolicy, Policy, RandomPolicy
4
+
5
+ __all__ = ["CallablePolicy", "Policy", "RandomPolicy"]
@@ -0,0 +1,79 @@
1
+ """Policy adapters kept independent from model frameworks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from typing import Any, Callable, Mapping
7
+
8
+ import numpy as np
9
+
10
+
11
+ class Policy:
12
+ """Small base class for model adapters.
13
+
14
+ External models may subclass this class or simply expose compatible
15
+ ``reset`` and ``act`` methods (duck typing is supported by the runner).
16
+ """
17
+
18
+ policy_id = "policy"
19
+
20
+ def reset(self, *, seed: int | None = None, task_spec: Any = None) -> None:
21
+ del seed, task_spec
22
+
23
+ def act(self, observation: Any, info: Mapping[str, Any] | None = None) -> Any:
24
+ raise NotImplementedError
25
+
26
+
27
+ class RandomPolicy(Policy):
28
+ """Reference policy used for smoke tests and legacy comparisons."""
29
+
30
+ policy_id = "random"
31
+
32
+ def __init__(self, action_dim: int | None = None, seed: int | None = None):
33
+ self.action_dim = action_dim
34
+ self.rng = np.random.default_rng(seed)
35
+
36
+ def set_action_dim(self, action_dim: int) -> None:
37
+ self.action_dim = int(action_dim)
38
+
39
+ def reset(self, *, seed: int | None = None, task_spec: Any = None) -> None:
40
+ del task_spec
41
+ if seed is not None:
42
+ self.rng = np.random.default_rng(seed)
43
+
44
+ def begin_episode(self, *, seed=None, **kwargs):
45
+ self.reset(seed=seed)
46
+
47
+ def end_episode(self, **feedback):
48
+ pass
49
+
50
+ def get_adaptation_state(self):
51
+ # RandomPolicy has no learned state. Its RNG is reseeded per episode.
52
+ return None
53
+
54
+ def set_adaptation_state(self, state):
55
+ if state is not None:
56
+ raise ValueError('RandomPolicy has no adaptation state')
57
+
58
+ def act(self, observation: Any, info: Mapping[str, Any] | None = None) -> np.ndarray:
59
+ del observation, info
60
+ if self.action_dim is None:
61
+ raise RuntimeError("RandomPolicy action_dim was not set by the runner")
62
+ return self.rng.uniform(-1.0, 1.0, size=self.action_dim)
63
+
64
+
65
+ class CallablePolicy(Policy):
66
+ """Wrap a plain callable while supporting one- or two-argument callables."""
67
+
68
+ def __init__(self, fn: Callable[..., Any], policy_id: str | None = None):
69
+ self.fn = fn
70
+ self.policy_id = policy_id or getattr(fn, "__name__", "callable")
71
+
72
+ def act(self, observation: Any, info: Mapping[str, Any] | None = None) -> Any:
73
+ try:
74
+ parameters = inspect.signature(self.fn).parameters
75
+ if len(parameters) >= 2:
76
+ return self.fn(observation, info)
77
+ except (TypeError, ValueError):
78
+ pass
79
+ return self.fn(observation)
@@ -0,0 +1,58 @@
1
+ """Native RGB rendering with explicit model lifetime and visual-only groups."""
2
+ import mujoco
3
+
4
+
5
+ class NativeRGBRenderer:
6
+ def __init__(self, width=640, height=480, include_collision=False):
7
+ self.width, self.height = width, height
8
+ self.include_collision = include_collision
9
+ self._model = None
10
+ self._renderer = None
11
+ self._option = mujoco.MjvOption()
12
+ self._option.geomgroup[0] = 0
13
+ self._option.geomgroup[1] = 1
14
+ self._option.geomgroup[2] = 0
15
+ if include_collision:
16
+ self._option.geomgroup[0] = 1
17
+
18
+ def render(self, sim, camera):
19
+ """Render robosuite ``MjSim`` and native MuJoCo task objects."""
20
+ model_wrapper = getattr(sim, "model", sim)
21
+ data_wrapper = getattr(sim, "data", sim)
22
+ model = getattr(model_wrapper, "_model", model_wrapper)
23
+ data = getattr(data_wrapper, "_data", data_wrapper)
24
+ # ``LegacyTaskAdapter`` exposes native tasks through _NativeSimView,
25
+ # whose model/data wrappers are deliberately compatible with
26
+ # robosuite. Check the explicit marker as well as the direct raw
27
+ # model case; otherwise the visual-only group filter hides native
28
+ # scenes that keep their only geometry in collision group 0.
29
+ native_mujoco = bool(getattr(sim, "native_mujoco", False)) or model_wrapper is model
30
+ # Robosuite tasks use group 1 for their visual geometry and keep
31
+ # collision-only geoms in group 0. Imported native scenes generally
32
+ # put their only geometry in group 0, so hiding it yields a blank UI.
33
+ self._option.geomgroup[0] = int(native_mujoco or self.include_collision)
34
+ if model is not self._model:
35
+ self.close()
36
+ model.vis.global_.offwidth = max(model.vis.global_.offwidth, self.width)
37
+ model.vis.global_.offheight = max(model.vis.global_.offheight, self.height)
38
+ self._renderer = mujoco.Renderer(model, height=self.height, width=self.width)
39
+ self._model = model
40
+ camera_arg = camera
41
+ if camera == '__free__':
42
+ free = mujoco.MjvCamera()
43
+ free.type = mujoco.mjtCamera.mjCAMERA_FREE
44
+ free.lookat[:] = model.stat.center
45
+ free.distance = max(0.5, 2.2 * float(model.stat.extent))
46
+ free.azimuth, free.elevation = 38, -25
47
+ camera_arg = free
48
+ elif isinstance(camera, str) and mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_CAMERA, camera) < 0:
49
+ raise ValueError(f'Unknown camera: {camera}')
50
+ self._renderer.update_scene(data, camera=camera_arg, scene_option=self._option)
51
+ # Native Renderer already returns top-down RGB (no [::-1]).
52
+ return self._renderer.render()
53
+
54
+ def close(self):
55
+ if self._renderer is not None:
56
+ self._renderer.close()
57
+ self._renderer = None
58
+ self._model = None
@@ -0,0 +1,31 @@
1
+ """Default robot metadata registrations.
2
+
3
+ Calling ``register_default_robots`` is optional. It is deliberately separate
4
+ from imports of robosuite so registry tooling also works in a lightweight
5
+ development environment.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .core.registry import get_robot_registry
11
+ from .robots.adapters import RobosuiteRobotAdapter
12
+ from .robots.agilex_adapter import AgileXAdapter
13
+
14
+
15
+ ROBOSUITE_NAMES = (
16
+ "Panda", "UR5e", "XArm7", "Kinova3", "IIWA", "Sawyer", "Jaco", "Tiago",
17
+ "SpotWithArm", "Baxter", "GR1ArmsOnly", "PandaOmron",
18
+ )
19
+
20
+
21
+ def register_default_robots(*, replace: bool = False) -> None:
22
+ from integrations.rpent_embodiments import register
23
+ register()
24
+ registry = get_robot_registry()
25
+ for name in ROBOSUITE_NAMES:
26
+ key = name.lower()
27
+ if key not in {item[0] for item in registry.list()} or replace:
28
+ registry.register(key, RobosuiteRobotAdapter(name), replace=replace)
29
+ for robot_id in ("piperx", "dual_piperx", "mobile_dual_piperx", "legacy_piperx", "aloha", "aloha_mobile"):
30
+ if robot_id not in {item[0] for item in registry.list()} or replace:
31
+ registry.register(robot_id, AgileXAdapter(robot_id), replace=replace)
@@ -0,0 +1,33 @@
1
+ """Robot embodiment specifications and adapters.
2
+
3
+ The default PiPER-X family is downloaded and generated by
4
+ ``scripts/build_agilex_assets.py``. Generic model-backed legacy adapters remain
5
+ for explicit external assets; the built-in registry uses AgileXAdapter.
6
+ """
7
+
8
+ from .base import RobotAdapter, RobotSpec
9
+ from .agilex_adapter import AgileXAdapter
10
+ from .adapters import AlohaAdapter, PiperXAdapter, RobosuiteRobotAdapter
11
+ from .catalog import ALL_ROBOT_IDS, SINGLE_ARM_ROBOT_IDS, normalize_robot_id
12
+ from .resolution import (RobosuiteResolution, resolve_robosuite_robot,
13
+ register_robosuite_robot, registered_robot_ids)
14
+ from .sensors import MountedCamera, SensorProfile, register_sensor_profile
15
+
16
+ __all__ = [
17
+ "AgileXAdapter",
18
+ "AlohaAdapter",
19
+ "PiperXAdapter",
20
+ "RobotAdapter",
21
+ "RobotSpec",
22
+ "RobosuiteRobotAdapter",
23
+ "ALL_ROBOT_IDS",
24
+ "SINGLE_ARM_ROBOT_IDS",
25
+ "normalize_robot_id",
26
+ "RobosuiteResolution",
27
+ "resolve_robosuite_robot",
28
+ "register_robosuite_robot",
29
+ "registered_robot_ids",
30
+ "SensorProfile",
31
+ "MountedCamera",
32
+ "register_sensor_profile",
33
+ ]