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,135 @@
|
|
|
1
|
+
"""Built-in and custom robot embodiment adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
from .base import ModelBackedAdapter, RobotSpec
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RobosuiteRobotAdapter:
|
|
11
|
+
"""Metadata adapter for a robot already supported by robosuite."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, robot_name: str):
|
|
14
|
+
self.robot_name = robot_name
|
|
15
|
+
bimanual = robot_name in {"Baxter", "GR1ArmsOnly"}
|
|
16
|
+
self.spec = RobotSpec(
|
|
17
|
+
robot_id=robot_name.lower(),
|
|
18
|
+
arm_names=("left", "right") if bimanual else ("right",),
|
|
19
|
+
gripper_type="robosuite",
|
|
20
|
+
metadata={"robosuite_name": robot_name},
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def validate_assets(self) -> None:
|
|
24
|
+
# The robosuite installation performs the authoritative asset check.
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
def make(self, **kwargs: Any) -> str:
|
|
28
|
+
del kwargs
|
|
29
|
+
return self.robot_name
|
|
30
|
+
|
|
31
|
+
def load_mujoco_model(self) -> Any:
|
|
32
|
+
raise NotImplementedError("robosuite robots are loaded through suite.make")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PiperXAdapter(ModelBackedAdapter):
|
|
36
|
+
"""PiperX single-arm adapter.
|
|
37
|
+
|
|
38
|
+
``spec`` is configurable because Piper/PiperX hardware revisions and
|
|
39
|
+
grippers may differ. No default joint list is guessed here; the adapter
|
|
40
|
+
must be constructed with the exact MJCF and calibrated metadata.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
model_path: str | None = None,
|
|
46
|
+
*,
|
|
47
|
+
arm_dof: int | None = None,
|
|
48
|
+
action_dim: int | None = None,
|
|
49
|
+
eef_site: str = "piperx_gripper_site",
|
|
50
|
+
gripper_type: str = "parallel",
|
|
51
|
+
metadata: Mapping[str, Any] | None = None,
|
|
52
|
+
):
|
|
53
|
+
super().__init__(
|
|
54
|
+
RobotSpec(
|
|
55
|
+
robot_id="piperx",
|
|
56
|
+
arm_names=("right",),
|
|
57
|
+
arm_dof=() if arm_dof is None else (int(arm_dof),),
|
|
58
|
+
gripper_type=gripper_type,
|
|
59
|
+
action_dim=action_dim,
|
|
60
|
+
eef_sites=(eef_site,),
|
|
61
|
+
model_path=model_path,
|
|
62
|
+
capabilities=frozenset({"single_arm", "parallel_gripper", "custom_mjcf"}),
|
|
63
|
+
metadata=dict(metadata or {}),
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AlohaAdapter(ModelBackedAdapter):
|
|
69
|
+
"""Adapter for a split, fixed-base bimanual ALOHA embodiment.
|
|
70
|
+
|
|
71
|
+
The two arm model paths may point to one combined MJCF or two independent
|
|
72
|
+
arm MJCF files. A later loader can choose the representation without
|
|
73
|
+
changing task or policy APIs.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
model_path: str | None = None,
|
|
79
|
+
*,
|
|
80
|
+
left_model_path: str | None = None,
|
|
81
|
+
right_model_path: str | None = None,
|
|
82
|
+
arm_dof: int | None = None,
|
|
83
|
+
gripper_type: str = "parallel",
|
|
84
|
+
eef_sites: tuple[str, str] = ("aloha_left_gripper_site", "aloha_right_gripper_site"),
|
|
85
|
+
metadata: Mapping[str, Any] | None = None,
|
|
86
|
+
):
|
|
87
|
+
paths = {"left_model_path": left_model_path, "right_model_path": right_model_path}
|
|
88
|
+
super().__init__(
|
|
89
|
+
RobotSpec(
|
|
90
|
+
robot_id="aloha",
|
|
91
|
+
arm_names=("left", "right"),
|
|
92
|
+
arm_dof=() if arm_dof is None else (int(arm_dof), int(arm_dof)),
|
|
93
|
+
gripper_type=gripper_type,
|
|
94
|
+
eef_sites=eef_sites,
|
|
95
|
+
model_path=model_path,
|
|
96
|
+
capabilities=frozenset({"bimanual", "parallel_gripper", "custom_mjcf"}),
|
|
97
|
+
metadata={**paths, **dict(metadata or {})},
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def validate_assets(self) -> None:
|
|
102
|
+
if self.model_path is not None:
|
|
103
|
+
return super().validate_assets()
|
|
104
|
+
missing = []
|
|
105
|
+
for key in ("left_model_path", "right_model_path"):
|
|
106
|
+
value = self.spec.metadata.get(key)
|
|
107
|
+
if not value:
|
|
108
|
+
missing.append(key)
|
|
109
|
+
else:
|
|
110
|
+
from pathlib import Path
|
|
111
|
+
|
|
112
|
+
if not Path(str(value)).expanduser().is_file():
|
|
113
|
+
raise FileNotFoundError(f"robot model does not exist: {value}")
|
|
114
|
+
if missing:
|
|
115
|
+
raise FileNotFoundError(
|
|
116
|
+
"aloha requires model_path or both left_model_path/right_model_path; missing "
|
|
117
|
+
+ ", ".join(missing)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def load_mujoco_model(self) -> Any:
|
|
121
|
+
"""Load one combined model or both split arm models.
|
|
122
|
+
|
|
123
|
+
A split return value is a mapping so a higher-level bimanual scene
|
|
124
|
+
builder can place and namespace the two arms explicitly.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
if self.model_path is not None:
|
|
128
|
+
return super().load_mujoco_model()
|
|
129
|
+
self.validate_assets()
|
|
130
|
+
import mujoco
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"left": mujoco.MjModel.from_xml_path(str(self.spec.metadata["left_model_path"])),
|
|
134
|
+
"right": mujoco.MjModel.from_xml_path(str(self.spec.metadata["right_model_path"])),
|
|
135
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Metadata facade for the generated official PiPER-X and custom dual assemblies."""
|
|
2
|
+
from .base import RobotSpec
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AgileXAdapter:
|
|
6
|
+
def __init__(self, robot_id):
|
|
7
|
+
if robot_id not in ("piperx", "dual_piperx", "mobile_dual_piperx", "legacy_piperx", "aloha", "aloha_mobile"):
|
|
8
|
+
raise ValueError(robot_id)
|
|
9
|
+
arms = ("right", "left") if robot_id not in ("piperx", "legacy_piperx") else ("right",)
|
|
10
|
+
self.spec = RobotSpec(
|
|
11
|
+
robot_id=robot_id, arm_names=arms, arm_dof=(6,)*len(arms),
|
|
12
|
+
gripper_type="parallel", action_dim=7*len(arms), control_modes=("osc_pose",),
|
|
13
|
+
eef_sites=tuple(f"gripper0_{arm}_grip_site" for arm in arms),
|
|
14
|
+
capabilities=frozenset({"custom_mjcf", "parallel_gripper", "bimanual" if len(arms)==2 else "single_arm"}),
|
|
15
|
+
metadata={"manufacturer": "AgileX", "arm_model": "legacy follower" if robot_id in ("legacy_piperx", "aloha", "aloha_mobile") else "PiPER-X", "asset_version": "legacy-agilex-family.v1" if robot_id in ("legacy_piperx", "aloha", "aloha_mobile") else "piperx-family.v1", "assembly": "custom" if len(arms) == 2 else "single", "mobile_base": robot_id in ("mobile_dual_piperx", "aloha_mobile"), "base_command_dim": 2 if robot_id in ("mobile_dual_piperx", "aloha_mobile") else 0, "calibrated": False,
|
|
16
|
+
"action_layout": "right_pose,left_pose,right_gripper,left_gripper" if len(arms)==2 else "right_pose,right_gripper"},
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def validate_assets(self):
|
|
20
|
+
from .agilex_robosuite import validate_bundle
|
|
21
|
+
if self.spec.robot_id in ("legacy_piperx", "aloha", "aloha_mobile"):
|
|
22
|
+
from .legacy_agilex_robosuite import validate_bundle
|
|
23
|
+
validate_bundle()
|
|
24
|
+
|
|
25
|
+
def make(self, **kwargs):
|
|
26
|
+
from .agilex_robosuite import resolve_robot
|
|
27
|
+
return resolve_robot(self.spec.robot_id)
|
|
28
|
+
|
|
29
|
+
def load_mujoco_model(self):
|
|
30
|
+
from robosuite.models.robots import create_robot
|
|
31
|
+
from robosuite.models.grippers import gripper_factory
|
|
32
|
+
robot = create_robot(self.make())
|
|
33
|
+
for arm in robot.arms:
|
|
34
|
+
robot.add_gripper(gripper_factory(robot.default_gripper[arm], idn=f"0_{arm}"), robot.eef_name[arm])
|
|
35
|
+
return robot.get_model()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Legacy extractor for the archived mobile_aloha_sim follower model.
|
|
2
|
+
|
|
3
|
+
This prepares URDF input; it does not implement a runnable task or convert DAE
|
|
4
|
+
meshes. Source: agilexrobotics/mobile_aloha_sim, aloha_new.urdf.
|
|
5
|
+
"""
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
import xml.etree.ElementTree as ET
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def extract_followers(source: ET.Element, embodiment: str) -> ET.Element:
|
|
11
|
+
"""Keep front follower subtrees, excluding leaders, chassis and cameras.
|
|
12
|
+
|
|
13
|
+
Single PiPER is rooted at its base origin. Dual-arm mounting transforms are
|
|
14
|
+
preserved relative to the upstream body_Link frame (not the world frame).
|
|
15
|
+
This legacy model is not the official PiPER-X asset.
|
|
16
|
+
"""
|
|
17
|
+
if embodiment in ("piperx", "dual_piperx", "mobile_dual_piperx"):
|
|
18
|
+
raise ValueError("PiPER-X requires scripts/build_agilex_assets.py and official agx_arm_urdf assets")
|
|
19
|
+
selections = {"piper": ("fr",), "agilex_aloha": ("fl", "fr")}
|
|
20
|
+
if embodiment not in selections:
|
|
21
|
+
raise ValueError("embodiment must be piper or agilex_aloha")
|
|
22
|
+
links = {link.attrib["name"]: link for link in source.findall("link")}
|
|
23
|
+
joints = source.findall("joint")
|
|
24
|
+
output = ET.Element("robot", name=embodiment + "_followers")
|
|
25
|
+
ET.SubElement(output, "link", name="world")
|
|
26
|
+
for material in source.findall("material"):
|
|
27
|
+
output.append(deepcopy(material))
|
|
28
|
+
for prefix in selections[embodiment]:
|
|
29
|
+
base = prefix + "_base_link"
|
|
30
|
+
if base not in links:
|
|
31
|
+
raise ValueError(f"missing follower base: {base}")
|
|
32
|
+
selected = {base}
|
|
33
|
+
chosen = []
|
|
34
|
+
pending = list(joints)
|
|
35
|
+
while True:
|
|
36
|
+
additions = [j for j in pending if j.find("parent").get("link") in selected]
|
|
37
|
+
if not additions:
|
|
38
|
+
break
|
|
39
|
+
for joint in additions:
|
|
40
|
+
child = joint.find("child").get("link")
|
|
41
|
+
if child in selected or child not in links:
|
|
42
|
+
raise ValueError(f"invalid follower tree at {child}")
|
|
43
|
+
selected.add(child)
|
|
44
|
+
chosen.append(joint)
|
|
45
|
+
pending.remove(joint)
|
|
46
|
+
revolute = [j for j in chosen if j.get("type") == "revolute"]
|
|
47
|
+
fingers = [j for j in chosen if j.get("type") == "prismatic"]
|
|
48
|
+
if len(revolute) != 6 or len(fingers) != 2:
|
|
49
|
+
raise ValueError(f"{prefix}: expected six arm and two finger joints")
|
|
50
|
+
for joint in revolute + fingers:
|
|
51
|
+
limit = joint.find("limit")
|
|
52
|
+
if limit is None or not {"lower", "upper", "effort", "velocity"} <= set(limit.keys()):
|
|
53
|
+
raise ValueError(f"missing limits: {joint.get('name')}")
|
|
54
|
+
child = links[joint.find("child").get("link")]
|
|
55
|
+
if child.find("inertial") is None or child.find("collision") is None:
|
|
56
|
+
raise ValueError(f"missing dynamics/collision: {child.get('name')}")
|
|
57
|
+
mounts = [j for j in joints if j.find("child").get("link") == base]
|
|
58
|
+
if len(mounts) != 1 or mounts[0].get("type") != "fixed":
|
|
59
|
+
raise ValueError(f"expected fixed mount for {base}")
|
|
60
|
+
mount = deepcopy(mounts[0])
|
|
61
|
+
if mount.find("parent").get("link") != "body_Link":
|
|
62
|
+
raise ValueError("unexpected mounting frame; inspect upstream model")
|
|
63
|
+
mount.find("parent").set("link", "world")
|
|
64
|
+
if embodiment == "piper":
|
|
65
|
+
origin = mount.find("origin")
|
|
66
|
+
if origin is None:
|
|
67
|
+
origin = ET.SubElement(mount, "origin")
|
|
68
|
+
origin.set("xyz", "0 0 0")
|
|
69
|
+
origin.set("rpy", "0 0 0")
|
|
70
|
+
for name in sorted(selected):
|
|
71
|
+
output.append(deepcopy(links[name]))
|
|
72
|
+
output.append(mount)
|
|
73
|
+
output.extend(deepcopy(chosen))
|
|
74
|
+
return output
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Lazy robosuite integration of pinned official AgileX follower models."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import os
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def asset_dir():
|
|
8
|
+
override = os.environ.get("AGILEX_ASSET_DIR")
|
|
9
|
+
if override:
|
|
10
|
+
return Path(override)
|
|
11
|
+
root = Path(__file__).resolve().parents[2]
|
|
12
|
+
if (root / "pyproject.toml").is_file() and (root / "scripts/build_agilex_assets.py").is_file():
|
|
13
|
+
return root / "assets/robots/agilex"
|
|
14
|
+
return Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) / "react-manip/robots/agilex"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def validate_bundle():
|
|
18
|
+
"""Reject stale or modified generated bundles, including old grasp aids."""
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
import xml.etree.ElementTree as ET
|
|
22
|
+
directory = asset_dir()
|
|
23
|
+
manifest = directory / "provenance.json"
|
|
24
|
+
if not manifest.exists():
|
|
25
|
+
raise FileNotFoundError("Run react-manip-assets prepare to build PiPER-X assets")
|
|
26
|
+
data = json.loads(manifest.read_text())
|
|
27
|
+
if data.get("schema") != "piperx-family.v1":
|
|
28
|
+
raise ValueError("Legacy AgileX assets: rebuild with scripts/build_agilex_assets.py")
|
|
29
|
+
lock = Path(__file__).resolve().parents[2] / "scripts/piperx_assets.lock.json"
|
|
30
|
+
if data.get("source_lock_sha256") != hashlib.sha256(lock.read_bytes()).hexdigest():
|
|
31
|
+
raise ValueError("PiPER-X source lock changed; rebuild the asset bundle")
|
|
32
|
+
for name in ("piperx.xml", "dual_piperx.xml", "mobile_dual_piperx.xml", "gripper.xml"):
|
|
33
|
+
if name not in data.get("generated_sha256", {}):
|
|
34
|
+
raise ValueError(f"Incomplete PiPER-X manifest: {name}")
|
|
35
|
+
for name, expected in data["generated_sha256"].items():
|
|
36
|
+
path = directory / name
|
|
37
|
+
if not path.is_file() or hashlib.sha256(path.read_bytes()).hexdigest() != expected:
|
|
38
|
+
raise ValueError(f"Stale or modified PiPER-X asset {path}; rebuild the bundle")
|
|
39
|
+
gripper = ET.parse(directory / "gripper.xml")
|
|
40
|
+
for geom in gripper.iter("geom"):
|
|
41
|
+
if geom.get("name", "").endswith(("_pad", "_flat_pad")):
|
|
42
|
+
raise ValueError("Unexpected legacy grasp-helper geometry")
|
|
43
|
+
if "_collision" in geom.get("name", "") and (geom.get("contype", "1") == "0" or geom.get("conaffinity", "1") == "0"):
|
|
44
|
+
raise ValueError("Disabled physical gripper surface")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def controller_config(arms=("right",)):
|
|
48
|
+
return {"type": "BASIC", "body_parts": {arm: {
|
|
49
|
+
"type": "OSC_POSE", "input_max": 1, "input_min": -1,
|
|
50
|
+
"output_max": [0.015]*3 + [0.08]*3, "output_min": [-0.015]*3 + [-0.08]*3,
|
|
51
|
+
"kp": 60, "damping_ratio": 1.5, "impedance_mode": "fixed",
|
|
52
|
+
"input_type": "delta", "input_ref_frame": "world", "uncouple_pos_ori": True,
|
|
53
|
+
"goal_update_mode": "achieved", "interpolation": None,
|
|
54
|
+
"gripper": {"type": "GRIP"},
|
|
55
|
+
} for arm in arms}}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def register_agilex():
|
|
59
|
+
"""Register classes without importing robosuite during registry discovery."""
|
|
60
|
+
validate_bundle()
|
|
61
|
+
from robosuite.models.robots.robot_model import REGISTERED_ROBOTS
|
|
62
|
+
if "PiperX" in REGISTERED_ROBOTS:
|
|
63
|
+
return
|
|
64
|
+
from robosuite.models.robots.manipulators.manipulator_model import ManipulatorModel
|
|
65
|
+
from robosuite.models.grippers.gripper_model import GripperModel
|
|
66
|
+
from robosuite.models.grippers import GRIPPER_MAPPING
|
|
67
|
+
from robosuite.robots import ROBOT_CLASS_MAPPING, FixedBaseRobot
|
|
68
|
+
|
|
69
|
+
class PiperXGripper(GripperModel):
|
|
70
|
+
def __init__(self, idn=0):
|
|
71
|
+
super().__init__(str(asset_dir() / "gripper.xml"), idn=idn)
|
|
72
|
+
# Simulation tuning, independent of the checksum-locked geometry.
|
|
73
|
+
# The generated 1 N s/m damping lets a full-stroke position command
|
|
74
|
+
# overshoot the slide stops and interlock the opposing finger meshes.
|
|
75
|
+
# Dampen the physical joints, without clipping positions or removing
|
|
76
|
+
# contact surfaces. Tasks can still apply their validated settings.
|
|
77
|
+
for joint in self.worldbody.iter("joint"):
|
|
78
|
+
if joint.get("type") == "slide":
|
|
79
|
+
joint.set("damping", "8")
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def dof(self):
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def init_qpos(self):
|
|
87
|
+
return np.array([0.04, -0.04])
|
|
88
|
+
|
|
89
|
+
def format_action(self, action):
|
|
90
|
+
# +1 closes both opposing slides; -1 opens. The keyboard demo
|
|
91
|
+
# sends a persistent toggle state, so map that state directly to
|
|
92
|
+
# the normalized gripper target instead of accumulating only a
|
|
93
|
+
# small increment on every simulation step. The downstream
|
|
94
|
+
# position actuators still provide the physical smoothing.
|
|
95
|
+
self.current_action = np.clip(np.asarray(action, dtype=float), -1, 1)
|
|
96
|
+
return np.array([-self.current_action[0], self.current_action[0]])
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def _important_geoms(self):
|
|
100
|
+
# Every convex part participates in robosuite contact detection.
|
|
101
|
+
import json
|
|
102
|
+
names = json.loads((asset_dir() / "provenance.json").read_text())["gripper_collision_names"]
|
|
103
|
+
return {"left_finger": names["finger7"], "right_finger": names["finger8"],
|
|
104
|
+
"left_fingerpad": names["finger7"], "right_fingerpad": names["finger8"]}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class PiperX(ManipulatorModel):
|
|
108
|
+
arms = ["right"]
|
|
109
|
+
asset_name = "piperx.xml"
|
|
110
|
+
fixed_stand = dict(height=.80, half_depth=.09, half_width=.10)
|
|
111
|
+
|
|
112
|
+
def __init__(self, idn=0):
|
|
113
|
+
super().__init__(str(asset_dir() / self.asset_name), idn=idn)
|
|
114
|
+
if self.fixed_stand is not None:
|
|
115
|
+
from .mounts import install_fixed_stand
|
|
116
|
+
install_fixed_stand(self, **self.fixed_stand)
|
|
117
|
+
|
|
118
|
+
def add_gripper(self, gripper, arm_name=None):
|
|
119
|
+
super().add_gripper(gripper, arm_name)
|
|
120
|
+
# Install cameras on the embodiment before any task merges XML.
|
|
121
|
+
# Covers tasks that use robosuite's own ManipulationTask as well.
|
|
122
|
+
from ..cameras import ensure_robot_cameras
|
|
123
|
+
ensure_robot_cameras(self)
|
|
124
|
+
self.cameras = self.get_element_names(self.worldbody, 'camera')
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def default_base(self):
|
|
128
|
+
return "NullMount"
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def default_gripper(self):
|
|
132
|
+
return {arm: "PiperXGripper" for arm in self.arms}
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def default_controller_config(self):
|
|
136
|
+
return {arm: "osc_pose" for arm in self.arms}
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def init_qpos(self):
|
|
140
|
+
# Official PiPER-X joint frames differ from the old extracted arm.
|
|
141
|
+
# Start with the fingers pointing toward the tabletop, within the
|
|
142
|
+
# official limits and with margin to the wrist limit.
|
|
143
|
+
return np.tile([0, 1.2, -1.1, 1.2, 0, 0], len(self.arms))
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def base_xpos_offset(self):
|
|
147
|
+
return {"empty": (-0.45, 0, 0), "bins": (-0.45, 0, 0),
|
|
148
|
+
"table": lambda length: (-0.1-length/2, 0, 0)}
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def top_offset(self):
|
|
152
|
+
return np.array([0, 0, 0.7])
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def _horizontal_radius(self):
|
|
156
|
+
return 0.7
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def arm_type(self):
|
|
160
|
+
return "single" if len(self.arms) == 1 else "bimanual"
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def _eef_name(self):
|
|
164
|
+
return {arm: arm + "_hand" for arm in self.arms}
|
|
165
|
+
|
|
166
|
+
class DualPiperX(PiperX):
|
|
167
|
+
arms = ["right", "left"]
|
|
168
|
+
asset_name = "dual_piperx.xml"
|
|
169
|
+
# Imported dual-arm bases are at x=.23875, y=+/- .30.
|
|
170
|
+
# Support their mounting plane, not the empty asset origin.
|
|
171
|
+
fixed_stand = dict(height=.65, half_depth=.09, half_width=.375, center_xy=(.23875, 0.))
|
|
172
|
+
|
|
173
|
+
class MobileDualPiperX(DualPiperX):
|
|
174
|
+
asset_name = "mobile_dual_piperx.xml"
|
|
175
|
+
fixed_stand = None
|
|
176
|
+
|
|
177
|
+
# FixedBaseRobot is used only for the two arm controllers. The XML
|
|
178
|
+
# still contains the physical free root and ten wheel/caster joints;
|
|
179
|
+
# they must not enter robosuite's scalar arm-qpos reset/indexing.
|
|
180
|
+
@property
|
|
181
|
+
def joints(self):
|
|
182
|
+
return [name for name in super().joints if "mobile_" not in name]
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def actuators(self):
|
|
186
|
+
return [name for name in super().actuators if "mobile_" not in name]
|
|
187
|
+
|
|
188
|
+
GRIPPER_MAPPING["PiperXGripper"] = PiperXGripper
|
|
189
|
+
for cls in (PiperX, DualPiperX, MobileDualPiperX):
|
|
190
|
+
ROBOT_CLASS_MAPPING[cls.__name__] = FixedBaseRobot
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def resolve_robot(robot_id):
|
|
194
|
+
if robot_id in ('legacy_piperx', 'aloha', 'aloha_mobile'):
|
|
195
|
+
from .legacy_agilex_robosuite import resolve_robot as resolve_legacy
|
|
196
|
+
return resolve_legacy(robot_id)
|
|
197
|
+
if robot_id not in ("piperx", "dual_piperx", "mobile_dual_piperx"):
|
|
198
|
+
raise ValueError(f"not an AgileX robot: {robot_id}")
|
|
199
|
+
register_agilex()
|
|
200
|
+
if robot_id == "mobile_dual_piperx":
|
|
201
|
+
import xml.etree.ElementTree as ET
|
|
202
|
+
if ET.parse(asset_dir() / "mobile_dual_piperx.xml").find(".//freejoint[@name='mobile_free']") is None:
|
|
203
|
+
raise ValueError("Stale planar ALOHA assets: run python scripts/build_agilex_assets.py")
|
|
204
|
+
return "PiperX" if robot_id == "piperx" else ("MobileDualPiperX" if robot_id == "mobile_dual_piperx" else "DualPiperX")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def validate_compiled_grippers(env):
|
|
208
|
+
"""Task composition may tune materials, but cannot replace contact shapes."""
|
|
209
|
+
import json
|
|
210
|
+
raw = getattr(env, 'env', env)
|
|
211
|
+
robots = getattr(raw, 'robots', ())
|
|
212
|
+
if not any(getattr(robot, 'name', '') in ('PiperX','DualPiperX','MobileDualPiperX','AgileXPiper','AgileXAloha','AgileXMobileAloha') for robot in robots):
|
|
213
|
+
return
|
|
214
|
+
model = raw.sim.model
|
|
215
|
+
for robot in robots:
|
|
216
|
+
if getattr(robot, 'name', '') not in ('PiperX','DualPiperX','MobileDualPiperX','AgileXPiper','AgileXAloha','AgileXMobileAloha'):
|
|
217
|
+
continue
|
|
218
|
+
legacy = getattr(robot, 'name', '').startswith('AgileX')
|
|
219
|
+
directory = asset_dir()
|
|
220
|
+
if legacy:
|
|
221
|
+
from .legacy_agilex_robosuite import asset_dir as legacy_asset_dir
|
|
222
|
+
directory = legacy_asset_dir()
|
|
223
|
+
manifest = json.loads((directory / 'provenance.json').read_text())
|
|
224
|
+
for gripper in robot.gripper.values():
|
|
225
|
+
if type(gripper).__name__ != ('AgileXPiperGripper' if legacy else 'PiperXGripper'):
|
|
226
|
+
raise ValueError('PiPER-X must use the central PiperXGripper asset')
|
|
227
|
+
prefix = gripper.naming_prefix
|
|
228
|
+
expected = {prefix + name for parts in manifest['gripper_collision_names'].values() for name in parts}
|
|
229
|
+
finger_bodies = {model.body_name2id(prefix + name) for name in ('finger7', 'finger8')}
|
|
230
|
+
for geom_id in range(model.ngeom):
|
|
231
|
+
if model.geom_bodyid[geom_id] in finger_bodies and (model.geom_contype[geom_id] or model.geom_conaffinity[geom_id]):
|
|
232
|
+
if model.geom_id2name(geom_id) not in expected:
|
|
233
|
+
raise ValueError('Task added an unapproved PiPER-X finger collision shape')
|
|
234
|
+
for parts in manifest['gripper_collision_names'].values():
|
|
235
|
+
for name in parts:
|
|
236
|
+
c = model.geom_name2id(prefix + name)
|
|
237
|
+
v = model.geom_name2id(prefix + name.replace('_collision','_visual'))
|
|
238
|
+
if (model.geom_type[c] != model.geom_type[v]
|
|
239
|
+
or not np.allclose(model.geom_size[c], model.geom_size[v], atol=1e-10)
|
|
240
|
+
or model.geom_dataid[c] != model.geom_dataid[v]
|
|
241
|
+
or not np.allclose(model.geom_pos[c],model.geom_pos[v],atol=1e-10)
|
|
242
|
+
or not np.allclose(model.geom_quat[c],model.geom_quat[v],atol=1e-10)
|
|
243
|
+
or not model.geom_contype[c] or not model.geom_conaffinity[c]
|
|
244
|
+
or model.geom_contype[v] or model.geom_conaffinity[v]
|
|
245
|
+
or model.geom_margin[c] != 0):
|
|
246
|
+
raise ValueError(f'Task changed PiPER-X contact surface: {prefix + name}')
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Build multi-arm actions using controller-advertised slices, not 7D chunks."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def arm_action(action_dim, layout, active_arm, pose, grippers):
|
|
6
|
+
action = np.zeros(action_dim, dtype=np.float32)
|
|
7
|
+
start, end = layout[active_arm]
|
|
8
|
+
if end - start != 6:
|
|
9
|
+
raise ValueError("keyboard pose control requires six Cartesian inputs per arm")
|
|
10
|
+
action[start:end] = np.clip(np.asarray(pose), -1, 1)
|
|
11
|
+
for arm, value in grippers.items():
|
|
12
|
+
start, end = layout[arm + "_gripper"]
|
|
13
|
+
if end - start != 1:
|
|
14
|
+
raise ValueError("keyboard gripper control requires one input per gripper")
|
|
15
|
+
action[start:end] = np.clip(value, -1, 1)
|
|
16
|
+
return action
|
|
17
|
+
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Embodiment-neutral robot metadata and adapter contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Mapping, Protocol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class RobotSpec:
|
|
12
|
+
robot_id: str
|
|
13
|
+
arm_names: tuple[str, ...] = ("right",)
|
|
14
|
+
arm_dof: tuple[int, ...] = ()
|
|
15
|
+
gripper_type: str | None = None
|
|
16
|
+
control_modes: tuple[str, ...] = ("joint_position",)
|
|
17
|
+
action_dim: int | None = None
|
|
18
|
+
eef_sites: tuple[str, ...] = ()
|
|
19
|
+
model_path: str | None = None
|
|
20
|
+
capabilities: frozenset[str] = field(default_factory=frozenset)
|
|
21
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
def __post_init__(self) -> None:
|
|
24
|
+
if not self.robot_id or any(ch.isspace() for ch in self.robot_id):
|
|
25
|
+
raise ValueError("robot_id must be a non-empty, whitespace-free string")
|
|
26
|
+
if not self.arm_names and 'task_level_proxy' not in self.capabilities:
|
|
27
|
+
raise ValueError("arm_names must contain at least one arm")
|
|
28
|
+
if self.arm_dof and len(self.arm_dof) != len(self.arm_names):
|
|
29
|
+
raise ValueError("arm_dof must have one entry per arm")
|
|
30
|
+
if self.action_dim is not None and self.action_dim < 1:
|
|
31
|
+
raise ValueError("action_dim must be positive")
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def required_arms(self) -> int:
|
|
35
|
+
return len(self.arm_names)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_mapping(cls, value: Mapping[str, Any]) -> "RobotSpec":
|
|
39
|
+
arm_names = value.get("arm_names", ("right",))
|
|
40
|
+
if isinstance(arm_names, str):
|
|
41
|
+
arm_names = (arm_names,)
|
|
42
|
+
control_modes = value.get("control_modes", ("joint_position",))
|
|
43
|
+
if isinstance(control_modes, str):
|
|
44
|
+
control_modes = (control_modes,)
|
|
45
|
+
eef_sites = value.get("eef_sites", ())
|
|
46
|
+
if isinstance(eef_sites, str):
|
|
47
|
+
eef_sites = (eef_sites,)
|
|
48
|
+
arm_dof = value.get("arm_dof", ())
|
|
49
|
+
if isinstance(arm_dof, int):
|
|
50
|
+
arm_dof = (arm_dof,)
|
|
51
|
+
return cls(
|
|
52
|
+
robot_id=str(value["robot_id"]),
|
|
53
|
+
arm_names=tuple(arm_names),
|
|
54
|
+
arm_dof=tuple(int(v) for v in arm_dof),
|
|
55
|
+
gripper_type=value.get("gripper_type"),
|
|
56
|
+
control_modes=tuple(control_modes),
|
|
57
|
+
action_dim=None if value.get("action_dim") is None else int(value["action_dim"]),
|
|
58
|
+
eef_sites=tuple(eef_sites),
|
|
59
|
+
model_path=value.get("model_path"),
|
|
60
|
+
capabilities=frozenset(value.get("capabilities", ())),
|
|
61
|
+
metadata=dict(value.get("metadata", {})),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class RobotAdapter(Protocol):
|
|
66
|
+
"""Protocol implemented by a robot embodiment integration."""
|
|
67
|
+
|
|
68
|
+
spec: RobotSpec
|
|
69
|
+
|
|
70
|
+
def validate_assets(self) -> None:
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
def make(self, **kwargs: Any) -> Any:
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
def load_mujoco_model(self) -> Any:
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ModelBackedAdapter:
|
|
81
|
+
"""Common asset validation for custom MJCF-backed robots."""
|
|
82
|
+
|
|
83
|
+
spec: RobotSpec
|
|
84
|
+
|
|
85
|
+
def __init__(self, spec: RobotSpec):
|
|
86
|
+
self.spec = spec
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def model_path(self) -> Path | None:
|
|
90
|
+
return Path(self.spec.model_path).expanduser() if self.spec.model_path else None
|
|
91
|
+
|
|
92
|
+
def validate_assets(self) -> None:
|
|
93
|
+
path = self.model_path
|
|
94
|
+
if path is None:
|
|
95
|
+
raise FileNotFoundError(
|
|
96
|
+
f"{self.spec.robot_id} has no model_path; provide an MJCF asset before simulation"
|
|
97
|
+
)
|
|
98
|
+
if not path.is_file():
|
|
99
|
+
raise FileNotFoundError(f"robot model does not exist: {path}")
|
|
100
|
+
|
|
101
|
+
def load_mujoco_model(self) -> Any:
|
|
102
|
+
"""Load the configured MJCF for lightweight asset/control smoke tests."""
|
|
103
|
+
|
|
104
|
+
self.validate_assets()
|
|
105
|
+
import mujoco # imported only when a simulator is actually requested
|
|
106
|
+
|
|
107
|
+
return mujoco.MjModel.from_xml_path(str(self.model_path))
|
|
108
|
+
|
|
109
|
+
def make(self, **kwargs: Any) -> Any:
|
|
110
|
+
"""Return integration-specific robot object.
|
|
111
|
+
|
|
112
|
+
Subclasses should implement this once the exact robosuite/MuJoCo
|
|
113
|
+
model is available. Keeping the method explicit prevents a missing
|
|
114
|
+
asset from silently falling back to Panda.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
del kwargs
|
|
118
|
+
self.validate_assets()
|
|
119
|
+
raise NotImplementedError(f"{self.spec.robot_id} model loader is not configured")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Built-in ID snapshots plus live registry-based normalization.
|
|
2
|
+
|
|
3
|
+
Use registered_robot_ids() for extension-aware enumeration. Task compatibility
|
|
4
|
+
is separate from registration and may still narrow the available combinations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
ALL_ROBOT_IDS = ("panda", "sawyer", "piperx", "dual_piperx", "mobile_dual_piperx", "legacy_piperx", "aloha", "aloha_mobile")
|
|
8
|
+
SINGLE_ARM_ROBOT_IDS = ("panda", "sawyer", "piperx", "legacy_piperx")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def normalize_robot_id(robot_id: str) -> str:
|
|
12
|
+
from .resolution import registered_robot_ids
|
|
13
|
+
value = str(robot_id).strip().lower()
|
|
14
|
+
if value not in registered_robot_ids():
|
|
15
|
+
raise ValueError(
|
|
16
|
+
f"unknown robot {robot_id!r}; supported IDs: {', '.join(registered_robot_ids())}"
|
|
17
|
+
)
|
|
18
|
+
return value
|