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,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,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)
|
react_manip/rendering.py
ADDED
|
@@ -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
|
+
]
|