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,131 @@
1
+ # PolyForm Noncommercial License 1.0.0
2
+
3
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
4
+
5
+ ## Acceptance
6
+
7
+ In order to get any license under these terms, you must agree
8
+ to them as both strict obligations and conditions to all
9
+ your licenses.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a copyright license for the
14
+ software to do everything you might do with the software
15
+ that would otherwise infringe the licensor's copyright
16
+ in it for any permitted purpose. However, you may
17
+ only distribute the software according to [Distribution
18
+ License](#distribution-license) and make changes or new works
19
+ based on the software according to [Changes and New Works
20
+ License](#changes-and-new-works-license).
21
+
22
+ ## Distribution License
23
+
24
+ The licensor grants you an additional copyright license
25
+ to distribute copies of the software. Your license
26
+ to distribute covers distributing the software with
27
+ changes and new works permitted by [Changes and New Works
28
+ License](#changes-and-new-works-license).
29
+
30
+ ## Notices
31
+
32
+ You must ensure that anyone who gets a copy of any part of
33
+ the software from you also gets a copy of these terms or the
34
+ URL for them above, as well as copies of any plain-text lines
35
+ beginning with `Required Notice:` that the licensor provided
36
+ with the software. For example:
37
+
38
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
39
+
40
+ ## Changes and New Works License
41
+
42
+ The licensor grants you an additional copyright license to
43
+ make changes and new works based on the software for any
44
+ permitted purpose.
45
+
46
+ ## Patent License
47
+
48
+ The licensor grants you a patent license for the software that
49
+ covers patent claims the licensor can license, or becomes able
50
+ to license, that you would infringe by using the software.
51
+
52
+ ## Noncommercial Purposes
53
+
54
+ Any noncommercial purpose is a permitted purpose.
55
+
56
+ ## Personal Uses
57
+
58
+ Personal use for research, experiment, and testing for
59
+ the benefit of public knowledge, personal study, private
60
+ entertainment, hobby projects, amateur pursuits, or religious
61
+ observance, without any anticipated commercial application,
62
+ is use for a permitted purpose.
63
+
64
+ ## Noncommercial Organizations
65
+
66
+ Use by any charitable organization, educational institution,
67
+ public research organization, public safety or health
68
+ organization, environmental protection organization,
69
+ or government institution is use for a permitted purpose
70
+ regardless of the source of funding or obligations resulting
71
+ from the funding.
72
+
73
+ ## Fair Use
74
+
75
+ You may have "fair use" rights for the software under the
76
+ law. These terms do not limit them.
77
+
78
+ ## No Other Rights
79
+
80
+ These terms do not allow you to sublicense or transfer any of
81
+ your licenses to anyone else, or prevent the licensor from
82
+ granting licenses to anyone else. These terms do not imply
83
+ any other licenses.
84
+
85
+ ## Patent Defense
86
+
87
+ If you make any written claim that the software infringes or
88
+ contributes to infringement of any patent, your patent license
89
+ for the software granted under these terms ends immediately. If
90
+ your company makes such a claim, your patent license ends
91
+ immediately for work on behalf of your company.
92
+
93
+ ## Violations
94
+
95
+ The first time you are notified in writing that you have
96
+ violated any of these terms, or done anything with the software
97
+ not covered by your licenses, your licenses can nonetheless
98
+ continue if you come into full compliance with these terms,
99
+ and take practical steps to correct past violations, within
100
+ 32 days of receiving notice. Otherwise, all your licenses
101
+ end immediately.
102
+
103
+ ## No Liability
104
+
105
+ ***As far as the law allows, the software comes as is, without
106
+ any warranty or condition, and the licensor will not be liable
107
+ to you for any damages arising out of these terms or the use
108
+ or nature of the software, under any kind of legal claim.***
109
+
110
+ ## Definitions
111
+
112
+ The **licensor** is the individual or entity offering these
113
+ terms, and the **software** is the software the licensor makes
114
+ available under these terms.
115
+
116
+ **You** refers to the individual or entity agreeing to these
117
+ terms.
118
+
119
+ **Your company** is any legal entity, sole proprietorship,
120
+ or other kind of organization that you work for, plus all
121
+ organizations that have control over, are under the control of,
122
+ or are under common control with that organization. **Control**
123
+ means ownership of substantially all the assets of an entity,
124
+ or the power to direct its management and policies by vote,
125
+ contract, or otherwise. Control can be direct or indirect.
126
+
127
+ **Your licenses** are all the licenses granted to you for the
128
+ software under these terms.
129
+
130
+ **Use** means anything you do with the software requiring one
131
+ of your licenses.
@@ -0,0 +1,5 @@
1
+ assets
2
+ configs
3
+ react_manip
4
+ scripts
5
+ tasks
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env python3
2
+ """Add three config-only phone_lean candidates to an existing candidate bank."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from copy import deepcopy
7
+ import json
8
+ from pathlib import Path
9
+ import shutil
10
+ import sys
11
+
12
+ import numpy as np
13
+
14
+
15
+ ROOT = Path(__file__).resolve().parents[1]
16
+ sys.path.insert(0, str(ROOT))
17
+
18
+ from react_manip.core.compatibility import resolve_task_robot
19
+ from react_manip.core.scenarios import Scenario
20
+ from react_manip.instances.candidate_bank import load_bank, write_json
21
+ from react_manip.instances.records import VERSION, digest, load_suite, save_record, seal
22
+ from react_manip.instances.runtime import (
23
+ ReplayEnv,
24
+ _construct,
25
+ capture_state,
26
+ contract_for,
27
+ runtime_identity,
28
+ spec_for,
29
+ )
30
+ from tasks.phone_lean.instances import sample_episode_instance, sample_task_instance
31
+
32
+
33
+ TASK_ID = "phone_lean"
34
+ CANDIDATE_SEEDS = (0, 1, 2)
35
+ EPISODES_PER_CANDIDATE = 15
36
+ REQUIRED_SUPPORTS = frozenset({"bowl", "bottle"})
37
+
38
+
39
+ def _build_group(out: Path, index: int, task_seed: int, report: dict) -> dict:
40
+ spec = spec_for(TASK_ID)
41
+ contract = contract_for(TASK_ID)
42
+ robot = resolve_task_robot(spec)
43
+ task = sample_task_instance(task_seed)
44
+ if not REQUIRED_SUPPORTS.intersection(task["support_names"]):
45
+ raise ValueError(
46
+ f"task seed {task_seed} must contain at least one of {sorted(REQUIRED_SUPPORTS)}"
47
+ )
48
+
49
+ identity = f"{TASK_ID}_{index:03d}"
50
+ scenario = Scenario("1.0", TASK_ID, identity, task_seed, task)
51
+ base_scene = sample_episode_instance(task, 1_000_000)
52
+ scenario.metadata["initial_episode_scene"] = deepcopy(base_scene)
53
+ env = _construct(TASK_ID, scenario, robot, episode=base_scene)
54
+ try:
55
+ env.reset()
56
+ instance = seal(
57
+ "task_instance",
58
+ task_id=TASK_ID,
59
+ task_instance_id=identity,
60
+ task_spec_digest=digest(spec.to_dict()),
61
+ contract_digest=digest(contract),
62
+ robot_id=robot,
63
+ scenario=scenario.to_dict(),
64
+ runtime=runtime_identity(env, TASK_ID),
65
+ provenance={"sampler": VERSION, "seed": task_seed},
66
+ )
67
+ finally:
68
+ env.close()
69
+
70
+ instance_ref = f"instances/{identity}.json"
71
+ save_record(instance, out / instance_ref)
72
+ group = {
73
+ "task_instance": {
74
+ "path": instance_ref,
75
+ "digest": instance["content_digest"],
76
+ },
77
+ "episodes": [],
78
+ }
79
+ episodes = []
80
+ for episode_seed in range(EPISODES_PER_CANDIDATE):
81
+ scene = sample_episode_instance(task, episode_seed)
82
+ env = _construct(TASK_ID, scenario, robot, episode=scene)
83
+ try:
84
+ env.reset()
85
+ if runtime_identity(env, TASK_ID) != instance["runtime"]:
86
+ raise ValueError("Episode layout changed the frozen Task runtime identity")
87
+ episode_id = f"{identity}_e{episode_seed:03d}"
88
+ episode = seal(
89
+ "episode_instance",
90
+ task_id=TASK_ID,
91
+ task_instance_id=identity,
92
+ task_instance_digest=instance["content_digest"],
93
+ episode_id=episode_id,
94
+ initial_state=capture_state(env),
95
+ round_plan=None,
96
+ provenance={"sampler": VERSION, "seed": episode_seed},
97
+ scene=scene,
98
+ )
99
+ finally:
100
+ env.close()
101
+ episode_ref = f"episodes/{episode_id}.json"
102
+ save_record(episode, out / episode_ref)
103
+ group["episodes"].append(
104
+ {"path": episode_ref, "digest": episode["content_digest"]}
105
+ )
106
+ episodes.append(episode)
107
+
108
+ replay = ReplayEnv(instance, cameras=False)
109
+ try:
110
+ language_sample = None
111
+ for episode_index, episode in enumerate(episodes):
112
+ replay.bind(episode)
113
+ _, reset_info = replay.reset()
114
+ if capture_state(replay) != episode["initial_state"]:
115
+ raise ValueError(f"{episode['episode_id']}: replay state is not exact")
116
+ info = replay.step(np.zeros(replay.action_dim))[-1]
117
+ if info["language_prompt_structured"] != replay._wrapped.get_language_prompt():
118
+ raise ValueError(f"{episode['episode_id']}: language prompt changed after one step")
119
+ if episode_index == 0:
120
+ language_sample = {
121
+ "task_instance_id": identity,
122
+ "task_id": TASK_ID,
123
+ "episode_id": episode["episode_id"],
124
+ "prompt": reset_info["language_prompt_structured"],
125
+ }
126
+ finally:
127
+ replay.close()
128
+
129
+ report["candidates"].append(
130
+ {
131
+ "task_instance_id": identity,
132
+ "task_seed": task_seed,
133
+ "support_names": task["support_names"],
134
+ "small_support_names": task["small_support_names"],
135
+ "large_support_names": task["large_support_names"],
136
+ "base_episode_seed": base_scene["episode_seed"],
137
+ "episode_seeds": list(range(EPISODES_PER_CANDIDATE)),
138
+ "exact_replays": EPISODES_PER_CANDIDATE,
139
+ }
140
+ )
141
+ report["language_samples"].append(language_sample)
142
+ print(
143
+ f"{identity}: task_seed={task_seed}, supports={task['support_names']}, "
144
+ f"{EPISODES_PER_CANDIDATE} exact replays",
145
+ flush=True,
146
+ )
147
+ return group
148
+
149
+
150
+ def extend_bank(source: Path, out: Path) -> None:
151
+ source = source.resolve()
152
+ source_bank, source_manifest_path, source_manifest, by_task = load_bank(source)
153
+ if TASK_ID in by_task:
154
+ raise ValueError(f"{TASK_ID} already exists in the source bank")
155
+ if out.exists():
156
+ raise FileExistsError(f"output already exists: {out}")
157
+ shutil.copytree(source.parent, out)
158
+
159
+ extension = {
160
+ "schema_version": "phone-lean-bank-extension.v1",
161
+ "task_id": TASK_ID,
162
+ "candidate_seeds": list(CANDIDATE_SEEDS),
163
+ "episodes_per_candidate": EPISODES_PER_CANDIDATE,
164
+ "selection_rule": "Each fixed candidate seed must include bowl or bottle; no solvability screening or seed replacement.",
165
+ "verification": "Every generated Episode restored exactly and advanced one zero-action step.",
166
+ "candidates": [],
167
+ "language_samples": [],
168
+ }
169
+ new_groups = [
170
+ _build_group(out, index, seed, extension)
171
+ for index, seed in enumerate(CANDIDATE_SEEDS)
172
+ ]
173
+
174
+ _, source_groups = load_suite(source_manifest_path)
175
+ task_by_digest = {
176
+ instance["content_digest"]: instance["task_id"]
177
+ for instance, _ in source_groups
178
+ }
179
+ task_by_digest.update(
180
+ {group["task_instance"]["digest"]: TASK_ID for group in new_groups}
181
+ )
182
+ groups = [*source_manifest["groups"], *new_groups]
183
+ groups.sort(
184
+ key=lambda group: (
185
+ task_by_digest[group["task_instance"]["digest"]],
186
+ group["task_instance"]["path"],
187
+ )
188
+ )
189
+ manifest = seal(
190
+ "manifest",
191
+ suite_id=source_manifest["suite_id"],
192
+ protocol=source_manifest["protocol"],
193
+ groups=groups,
194
+ )
195
+ write_json(out / "candidates.json", manifest)
196
+
197
+ bank = deepcopy(source_bank)
198
+ bank["manifest_digest"] = manifest["content_digest"]
199
+ bank.pop("content_digest")
200
+ bank["content_digest"] = digest(bank)
201
+ write_json(out / "bank.json", bank)
202
+
203
+ generation_path = source.parent / "generation.json"
204
+ generation = json.loads(generation_path.read_text())
205
+ generation["instances"] = len(groups)
206
+ generation["episodes"] = sum(len(group["episodes"]) for group in groups)
207
+ generation["extended_from_bank_digest"] = source_bank["content_digest"]
208
+ generation.setdefault("extensions", []).append(extension)
209
+ generation.setdefault("robots", {})[TASK_ID] = resolve_task_robot(spec_for(TASK_ID))
210
+ first_instance = json.loads(
211
+ (out / new_groups[0]["task_instance"]["path"]).read_text()
212
+ )
213
+ generation.setdefault("cameras", {})[TASK_ID] = first_instance["runtime"]["cameras"]
214
+ generation.setdefault("language_samples", []).extend(extension["language_samples"])
215
+ generation["included_tasks"] = sorted({*generation.get("included_tasks", []), TASK_ID})
216
+ generation["excluded_tasks"] = []
217
+ generation["bank_digest"] = bank["content_digest"]
218
+ write_json(out / "generation.json", generation)
219
+
220
+ loaded_bank, _, loaded_manifest, loaded = load_bank(out / "bank.json")
221
+ if loaded_bank["content_digest"] != bank["content_digest"]:
222
+ raise ValueError("written bank digest changed during validation")
223
+ if len(loaded_manifest["groups"]) != len(source_manifest["groups"]) + 3:
224
+ raise ValueError("candidate manifest group count mismatch")
225
+ if len(loaded[TASK_ID]) != 3 or any(len(episodes) != 15 for _, episodes in loaded[TASK_ID]):
226
+ raise ValueError("phone_lean bank must contain three groups of fifteen Episodes")
227
+
228
+
229
+ def main() -> None:
230
+ parser = argparse.ArgumentParser(description=__doc__)
231
+ parser.add_argument(
232
+ "--source",
233
+ type=Path,
234
+ default=ROOT / "react_manip/suites/piperx_candidates_v1/bank.json",
235
+ )
236
+ parser.add_argument("--out", type=Path, required=True, help="new output directory")
237
+ args = parser.parse_args()
238
+ extend_bank(args.source, args.out)
239
+
240
+
241
+ if __name__ == "__main__":
242
+ main()
@@ -0,0 +1,133 @@
1
+ """Official aloha_new URDF chassis tree -> free-root MJCF.
2
+
3
+ CAD and inertias come from pinned upstream. The operator display bundled in the
4
+ mobile body CAD is intentionally omitted; a head camera is added by the outer
5
+ builder at the same upper-body station. Wheel servo/friction are explicitly
6
+ simulation tuning, not system identification. No planar propulsion actuators.
7
+ """
8
+ import xml.etree.ElementTree as ET
9
+ import numpy as np
10
+
11
+
12
+ def add_wheeled_base(root, base, source, fetch, destination, pose):
13
+ import trimesh
14
+ links = {x.get("name"): x for x in source.findall("link")}
15
+ joints = source.findall("joint")
16
+ ET.SubElement(base, "freejoint", name="mobile_free")
17
+ bounds = {}
18
+
19
+ def add(link_name, parent, origin):
20
+ link = links[link_name]
21
+ name = "mobile_" + link_name
22
+ body = ET.SubElement(parent, "body", name=name, **pose(origin))
23
+ if link_name in ("left_wheel_link", "right_wheel_link"):
24
+ xyz = np.fromstring(body.get("pos"), sep=" ")
25
+ xyz[2] -= 0.011
26
+ body.set("pos", " ".join(map(str, xyz)))
27
+ inertial = link.find("inertial")
28
+ # Upstream puts chassis inertia in an identity fixed child. Merge it
29
+ # onto the chassis body so MuJoCo never infers mass from mass-zero CAD.
30
+ if link_name == "base_link":
31
+ inertial = links["inertial_link"].find("inertial")
32
+ if inertial is not None:
33
+ inertia_origin = inertial.find("origin")
34
+ if inertia_origin is not None and not np.allclose(np.fromstring(inertia_origin.get("rpy", "0 0 0"), sep=" "), 0):
35
+ raise ValueError("rotated chassis inertia requires tensor conversion")
36
+ inertia = inertial.find("inertia")
37
+ ET.SubElement(body, "inertial", mass=inertial.find("mass").get("value"),
38
+ pos=inertial.find("origin").get("xyz", "0 0 0"),
39
+ fullinertia=" ".join(inertia.get(k) for k in ("ixx", "iyy", "izz", "ixy", "ixz", "iyz")))
40
+ for i, visual in enumerate(link.findall("visual")):
41
+ mesh = visual.find("geometry/mesh")
42
+ path = mesh.get("filename").removeprefix("package://")
43
+ raw = fetch(path)
44
+ converted = destination / (name + f"_{i}.obj")
45
+ scene = trimesh.load(str(raw), force="scene")
46
+ geometry = scene.to_geometry()
47
+ # The upstream mobile CAD stores the operator display and frame
48
+ # in one ``body_Link`` mesh. The benchmark replaces that display
49
+ # with a head camera, so clip only the elevated panel (z~=1.18--
50
+ # 1.34 m in this link frame) while preserving the chassis below.
51
+ # Applying the filter before exporting and collision decomposition
52
+ # keeps visual and physical geometry consistent.
53
+ if link_name == "body_Link":
54
+ geometry.update_faces(geometry.triangles_center[:, 2] < 1.15)
55
+ geometry.remove_unreferenced_vertices()
56
+ geometry.export(str(converted))
57
+ bounds[link_name] = geometry.bounds.tolist()
58
+ mesh_name = name + f"_mesh{i}"
59
+ ET.SubElement(root.find("asset"), "mesh", name=mesh_name, file=str(converted.resolve()),
60
+ scale=mesh.get("scale", "1 1 1"))
61
+ ET.SubElement(body, "geom", name=name + f"_vis{i}", type="mesh", mesh=mesh_name,
62
+ group="1", contype="0", conaffinity="0", mass="0", rgba="0.35 0.38 0.42 1",
63
+ **pose(visual.find("origin")))
64
+ # Convex hull of official collision shape for rigid chassis/frame.
65
+ # Wheels use analytic rolling surfaces fitted to the CAD bounds.
66
+ if link_name in ("left_wheel_link", "right_wheel_link"):
67
+ extent = geometry.extents
68
+ center = geometry.bounds.mean(axis=0)
69
+ radius = max(extent[0], extent[2]) / 2
70
+ ET.SubElement(body, "geom", name=name+"_tire", type="cylinder",
71
+ pos=" ".join(map(str, center)), quat="0.70710678 0.70710678 0 0",
72
+ size=f"{radius} {extent[1]/2}", friction="1.0 0.001 0.0001", condim="3", mass="0")
73
+ elif link_name.endswith("wheel_link") and link_name[:2] in ("fl", "fr", "rl", "rr"):
74
+ extent = geometry.extents
75
+ # Mesh is in wheel frame; local Z is its rotation axis.
76
+ ET.SubElement(body, "geom", name=name+"_roller", type="cylinder",
77
+ pos=" ".join(map(str, geometry.bounds.mean(axis=0))),
78
+ size=f"{max(extent[:2])/2} {extent[2]/2}", friction="0.8 0.001 0.0001", mass="0")
79
+ elif link_name == "body_Link":
80
+ # Separate CAD solids: a hull of the entire frame incorrectly
81
+ # fills its open spaces. Omit thin sheets and small fasteners.
82
+ pieces = geometry.split(only_watertight=False, repair=False)
83
+ for k, piece in enumerate(pieces):
84
+ if min(piece.extents) < .003 or max(piece.extents) < .04:
85
+ continue
86
+ # Rotated zero-thickness sheets can have nonzero XYZ
87
+ # extents; reject them before MuJoCo's 3D hull compiler.
88
+ centered = piece.vertices - piece.vertices.mean(axis=0)
89
+ if np.linalg.svd(centered, compute_uv=False)[-1] / np.sqrt(len(centered)) < .0005:
90
+ continue
91
+ hull = piece.convex_hull
92
+ path = destination / f"mobile_frame_collision_{k}.obj"
93
+ hull.export(str(path))
94
+ mesh_id = f"mobile_frame_collision_{k}"
95
+ ET.SubElement(root.find("asset"), "mesh", name=mesh_id, file=str(path.resolve()))
96
+ ET.SubElement(body, "geom", name=mesh_id, type="mesh", mesh=mesh_id,
97
+ mass="0", **pose(visual.find("origin")))
98
+ else:
99
+ for collision in link.findall("collision"):
100
+ ET.SubElement(body, "geom", name=name+f"_collision{i}", type="mesh", mesh=mesh_name,
101
+ mass="0", **pose(collision.find("origin")))
102
+ for joint in joints:
103
+ if joint.find("parent").get("link") != link_name:
104
+ continue
105
+ child_name = joint.find("child").get("link")
106
+ if child_name == "inertial_link":
107
+ continue
108
+ if child_name.startswith(("fl_base", "fr_base", "bl_base", "br_base")):
109
+ continue
110
+ child = add(child_name, body, joint.find("origin"))
111
+ if child_name.endswith("wheel_link") and child_name[:2] in ("fl", "fr", "rl", "rr"):
112
+ # Rollers sit inside chassis wheel wells; its convex hull has
113
+ # no wheel-well cutouts. Other wheel contacts remain enabled.
114
+ ET.SubElement(root.find("contact"), "exclude", body1="mobile_base_link", body2=child.get("name"))
115
+ if joint.get("type") != "fixed":
116
+ jname = "mobile_" + joint.get("name")
117
+ ET.SubElement(child, "joint", name=jname, type="hinge", axis=joint.find("axis").get("xyz"),
118
+ damping="0.02", armature="0.01")
119
+ if joint.get("name") in ("left_wheel", "right_wheel"):
120
+ ET.SubElement(root.find("actuator"), "velocity", name=jname+"_motor", joint=jname,
121
+ kv="40", ctrllimited="true", ctrlrange="-6 6", forcelimited="true", forcerange="-20 20")
122
+ ET.SubElement(root.find("contact"), "exclude", body1=name, body2=child.get("name"))
123
+ return body
124
+
125
+ for name in ("base_link", "body_Link"):
126
+ joint = next(j for j in joints if j.find("child").get("link") == name)
127
+ add(name, base, joint.find("origin"))
128
+ radius = (bounds["left_wheel_link"][1][2] - bounds["left_wheel_link"][0][2]) / 2
129
+ return {"wheel_radius": radius, "track_width": 0.34, "cad_bounds": bounds,
130
+ "source": "aloha_new.urdf", "drive": "free root, two velocity-actuated wheels; passive caster hinges",
131
+ "servo_calibrated": False, "drive_axle_z_correction_m": -0.011,
132
+ "wheel_servo": {"kv": 40, "torque_limit_nm": 20, "speed_limit_rad_s": 6},
133
+ "collision": "CAD chassis hull with wheel-well exclusions; frame component hulls (thickness under 3mm, span under 40mm and degenerate sheets omitted); CAD-fitted wheel cylinders"}
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env python3
2
+ """Bounded privileged witnesses on unchanged bank environments; no model score."""
3
+ import argparse, ast, importlib, importlib.util, json, sys, types, traceback, hashlib, subprocess
4
+ from pathlib import Path
5
+ import numpy as np
6
+ ROOT=Path(__file__).resolve().parents[1]
7
+ sys.path.insert(0,str(ROOT))
8
+ from react_manip.instances.candidate_bank import load_bank
9
+ from react_manip.instances.runtime import ReplayEnv, runtime_identity, _json
10
+
11
+ LEGACY_REF = "05025dc395aaf709685c50773600cf50f109529d"
12
+
13
+ def prepare_controllers(destination):
14
+ """Recover diagnostic algorithms, never historical environments or physics."""
15
+ if destination.exists(): return
16
+ names=subprocess.check_output(['git','ls-tree','-r','--name-only',LEGACY_REF],cwd=ROOT,text=True).splitlines()
17
+ tasks={p.parent.name for p in (ROOT/'tasks').glob('*/task_spec.json')}
18
+ for name in names:
19
+ if not ((name.startswith('tasks/') and name.split('/')[1] in tasks and '/operation/' in name and name.endswith('.py')) or name == 'tasks/car_drive2/demo_aloha.py'): continue
20
+ source=subprocess.check_output(['git','show',LEGACY_REF+':'+name],cwd=ROOT,text=True)
21
+ source=source.replace('benchmark.','react_manip.').replace('frontends.arm_actions','react_manip.robots.arm_actions').replace("(p/'benchmark')","(p/'react_manip')").replace('(p / "benchmark")','(p / "react_manip")')
22
+ source=source.replace('from react_manip.core.episode import EpisodeEvaluator\n','')
23
+ if name.endswith('/ring_toss1/operation/solve.py'):
24
+ source=source.replace('{"right": gripper, "left": -1.0}', '{arm: (gripper if arm == "right" else -1.0) for arm in env.robots[0].arms}')
25
+ source='\n'.join(line for line in source.splitlines() if not any(x in line for x in ('controller.output_max[:3] =','controller.output_min[:3] =','controller.action_scale =')))+'\n'
26
+ if name.endswith('/rope_hang/operation/rebalance.py'):
27
+ class KeepOriginalPhysics(ast.NodeTransformer):
28
+ def visit_Assign(self,node):
29
+ forbidden=('model.actuator_gainprm','model.actuator_biasprm','self._arm_controller.kp','self._arm_controller.kd')
30
+ return ast.copy_location(ast.Pass(),node) if any(x in ast.unparse(node.targets) for x in forbidden) else node
31
+ source=ast.unparse(KeepOriginalPhysics().visit(ast.parse(source)))+'\n'
32
+ target=destination/name;target.parent.mkdir(parents=True,exist_ok=True);target.write_text(source)
33
+
34
+ class Finished(BaseException): pass
35
+ class Session:
36
+ def __init__(self, env):
37
+ self.replay=env; self.task_id=env._instance['task_id']; self.env=self; self.actions=[]; self.info={}; self.observation={}; self.ended=False
38
+ def __getattr__(self,k): return getattr(self.replay.env,k)
39
+ def close(self): pass
40
+ def reset(self, **kwargs):
41
+ if self.actions: raise RuntimeError('Controller reset after rollout started')
42
+ self.observation,self.info=self.replay.reset()
43
+ return self.observation
44
+ def step(self,a):
45
+ if self.ended: raise Finished()
46
+ a=np.asarray(a,float)
47
+ self.actions.append(a.tolist())
48
+ self.observation,r,t,tr,self.info=self.replay.step(a)
49
+ self.ended=bool(t or tr)
50
+ if len(self.actions)%500==0: print('progress',len(self.actions),flush=True)
51
+ if self.ended: raise Finished()
52
+ return self.observation,r,self.ended,self.info
53
+
54
+ def load_module(name,path):
55
+ spec=importlib.util.spec_from_file_location(name,path)
56
+ module=importlib.util.module_from_spec(spec);sys.modules[name]=module;spec.loader.exec_module(module)
57
+ return module
58
+
59
+ def main():
60
+ p=argparse.ArgumentParser();p.add_argument('--task',required=True);p.add_argument('--candidate',type=int,default=0);p.add_argument('--replay-actions',type=Path);p.add_argument('--out',type=Path,required=True)
61
+ args=p.parse_args();out=args.out.resolve();out.mkdir(parents=True,exist_ok=False)
62
+ legacy=ROOT/'outputs/solvability_20261012/legacy'
63
+ prepare_controllers(legacy)
64
+ bank,_,_,groups=load_bank(ROOT/'react_manip/suites/piperx_candidates_v1/bank.json')
65
+ instance,episodes=groups[args.task][args.candidate];episode=episodes[0]
66
+ report=dict(task=args.task,robot=instance['robot_id'],bank_digest=bank['content_digest'],instance_digest=instance['content_digest'],episode_digest=episode['content_digest'],episode_id=episode['episode_id'],controller_source_commit=LEGACY_REF,scope='One saved Episode, bounded historical controller adaptation, privileged calibration; not full bank certification',status='started')
67
+ def save(): (out/'report.json').write_text(json.dumps(_json(report),ensure_ascii=False,indent=2))
68
+ save();env=None;s=None
69
+ try:
70
+ env=ReplayEnv(instance,cameras=False);env.bind(episode);obs,info=env.reset();s=Session(env);s.observation=obs;s.info=info
71
+ before=runtime_identity(env._wrapped,args.task)
72
+ prefix='tasks.'+args.task+'.operation'
73
+ importlib.import_module('tasks.'+args.task)
74
+ pkg=types.ModuleType(prefix);pkg.__path__=[str(legacy/'tasks'/args.task/'operation')];pkg.make_env=lambda **kw:s;sys.modules[prefix]=pkg
75
+ # Imported code is diagnostic only; construction is replaced by the strict bank environment.
76
+ def mod(n): return importlib.import_module(prefix+'.'+n)
77
+ t=args.task;controller=None
78
+ if args.replay_actions:
79
+ report['replayed_actions_from']=str(args.replay_actions)
80
+ for action in json.loads(args.replay_actions.read_text()): s.step(action)
81
+ raise Finished()
82
+ if t=='box_insertion':
83
+ controller=mod('density_controller').DensityAwareBoxInsertionController(robot_id=instance['robot_id'],direct_gripper_contact=True);controller.set_action_dim(env.action_dim)
84
+ elif t=='button_indicator_matching': controller=mod('demo_runtime').ButtonPressDemo(s,'aloha')
85
+ elif t=='pan_flip': controller=mod('controller').PanFlipController(s)
86
+ elif t=='pitch_pot': controller=mod('arm').PickThrowController(s,motion=mod('planning').motion_for_target(s))
87
+ elif t=='rail_ball_to_cup': controller=mod('arm').PickPlaceController(s)
88
+ elif t=='conveyor_grasp': controller=mod('solve').ConveyorSortingController(s)
89
+ elif t=='find_object_in_drawer2': controller=mod('retrieval').RetrievalController(s)
90
+ elif t=='tilte_maze': controller=mod('solve').JoystickMazeController(s)
91
+ elif t=='rope_hang': controller=mod('rebalance').RebalanceController(s)
92
+ elif t=='laptop_into_drawer': controller=mod('controller').PhysicsOraclePolicy()
93
+ assert before==runtime_identity(env._wrapped,t),'Controller modified model or controller configuration'
94
+ if controller is not None:
95
+ for i in range(int(s.horizon)):
96
+ state=np.r_[s.sim.data.qpos.copy(),s.sim.data.qvel.copy()]
97
+ if t in ('box_insertion','laptop_into_drawer'):
98
+ # Read native observations for privileged physical calibration.
99
+ observation=s.replay.env._get_observations(force_update=True)
100
+ if hasattr(s.replay.env,'_observation'): observation=s.replay.env._observation()
101
+ elif hasattr(s.replay.env,'_obs'): observation=s.replay.env._obs()
102
+ a=controller.act(observation,info=s.info)
103
+ if t=='laptop_into_drawer':
104
+ from react_manip.robots.arm_actions import arm_action
105
+ layout=s.robots[0].composite_controller._action_split_indexes
106
+ a=arm_action(env.action_dim,layout,'right',a[:6],{'right':a[6],'left':-1.0})
107
+ else: a=controller.act()
108
+ np.testing.assert_array_equal(state,np.r_[s.sim.data.qpos,s.sim.data.qvel])
109
+ s.step(a)
110
+ report['controller_phase']=str(getattr(controller,'phase','unknown'))
111
+ elif t in ('leveling_board1','ring_toss1','turntable_gamble1'):
112
+ mod('solve').run(out=out/'legacy_result',record=False)
113
+ elif t=='car_drive2':
114
+ m=load_module('audit_car',legacy/'tasks/car_drive2/demo_aloha.py');m.run(out=out/'legacy_result',record=False)
115
+ else: raise ValueError('No controller')
116
+ except Finished: pass
117
+ except Exception as exc:
118
+ report['error']=f'{type(exc).__name__}: {exc}';report['traceback']=traceback.format_exc()
119
+ finally:
120
+ if s is not None:
121
+ if 'controller' in locals() and controller is not None:
122
+ report['controller_phase']=str(getattr(controller,'phase',getattr(controller,'stage','unknown')))
123
+ report.update(steps=len(s.actions),final_info=_json(s.info),simulation_seconds=float(s.sim.data.time))
124
+ report['model_controller_unchanged']=before==runtime_identity(env._wrapped,args.task)
125
+ (out/'actions.json').write_text(json.dumps(s.actions))
126
+ if not report['model_controller_unchanged']:
127
+ after=runtime_identity(env._wrapped,args.task)
128
+ report['runtime_changed_fields']=[k for k in before if before[k]!=after[k]]
129
+ if args.task in ('button_indicator_matching','conveyor_grasp') and report['runtime_changed_fields']==['model_digest']:
130
+ report['model_change_note']='Task-owned dynamic model state: indicator colors/lights or conveyor object activation; source controller has no model writes. Not a static-model certificate.'
131
+ else: report.setdefault('error','Model/controller changed during calibration')
132
+ if env is not None: env.close()
133
+ success=s is not None and bool(s.info.get('terminal_success',False)) and not report.get('error')
134
+ report['status']='attempt_failed' if not report.get('error') else 'controller_incompatible'
135
+ if success:
136
+ fresh=ReplayEnv(instance,cameras=False)
137
+ try:
138
+ fresh.bind(episode);fresh.reset();ri={};ended=False
139
+ for a in s.actions:
140
+ _,_,te,tr,ri=fresh.step(a)
141
+ if te or tr: ended=True;break
142
+ report['independent_replay']={'terminal_success':bool(ri.get('terminal_success')), 'final_info':_json(ri),'ended':ended}
143
+ score_keys=('total_score','task_score','score')
144
+ score_equal=all(ri.get(k)==s.info.get(k) for k in score_keys)
145
+ report['independent_replay']['scores_equal']=score_equal
146
+ report['status']='success_replayed' if ri.get('terminal_success') and score_equal else 'replay_failed'
147
+ finally:fresh.close()
148
+ report['controller_sources_sha256']={str(f.relative_to(legacy)):hashlib.sha256(f.read_bytes()).hexdigest() for f in (legacy/'tasks'/args.task/'operation').glob('*.py')}
149
+ save();print(args.task,report['status'],report.get('steps'),report.get('error',''),flush=True)
150
+ if __name__=='__main__':main()