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,181 @@
1
+ {
2
+ "sources": {
3
+ "arm": {
4
+ "repository": "https://github.com/agilexrobotics/agx_arm_urdf",
5
+ "revision": "983788b58d7eae76511177f768d85877534917fe",
6
+ "license": "MIT"
7
+ },
8
+ "mobile": {
9
+ "repository": "https://github.com/agilexrobotics/mobile_aloha_sim",
10
+ "revision": "f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a",
11
+ "license": "package.xml declares BSD; no top-level LICENSE in pinned source",
12
+ "use": "Tracer chassis and support frame only; no legacy arms or grippers"
13
+ }
14
+ },
15
+ "files": {
16
+ "arm/LICENSE": {
17
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/LICENSE",
18
+ "sha256": "247944c63de1573653722ba784f96123da267105b7b48577e0c8826a24b809f6"
19
+ },
20
+ "arm/agx_gripper/meshes/gripper_base.stl": {
21
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/agx_gripper/meshes/gripper_base.stl",
22
+ "sha256": "0b839dc0bcb7d94e3422c74f6396b54a0441ba4836448ab5586e7f372d1bdbf3"
23
+ },
24
+ "arm/agx_gripper/meshes/gripper_link1.stl": {
25
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/agx_gripper/meshes/gripper_link1.stl",
26
+ "sha256": "475a7d8db056524d7659fadf5a2c307d62791047b6214d733dfc7846ed1c613d"
27
+ },
28
+ "arm/agx_gripper/meshes/gripper_link2.stl": {
29
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/agx_gripper/meshes/gripper_link2.stl",
30
+ "sha256": "3f88d534a85e28a33ef7d2061adacbd1d25214aa9325f9e464aad0f9e4b74606"
31
+ },
32
+ "arm/agx_gripper/meshes/dae/gripper_link1.dae": {
33
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/agx_gripper/meshes/dae/gripper_link1.dae",
34
+ "sha256": "b4aaf0e024d09eb0454e70e0c5092638edcf7da7c253d1effdeeaec46f76427b"
35
+ },
36
+ "arm/agx_gripper/meshes/dae/gripper_link2.dae": {
37
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/agx_gripper/meshes/dae/gripper_link2.dae",
38
+ "sha256": "0da1350898b2787b76b84698253aeb1efd2ddb6360b2fa2ae03ecff389883af8"
39
+ },
40
+ "arm/agx_gripper/meshes/dae/gripper_base.dae": {
41
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/agx_gripper/meshes/dae/gripper_base.dae",
42
+ "sha256": "aaea10ec06709d6a3343dd3378553eee3df40b43ee276df10c5b68ac7f61bae2"
43
+ },
44
+ "arm/piper_x/urdf/piper_x_description.urdf": {
45
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/urdf/piper_x_description.urdf",
46
+ "sha256": "8786fa7d01c81358b446e8d5121ae2b8e7a9fc770513373f270f29a9d5a3a2f1"
47
+ },
48
+ "arm/piper_x/urdf/piper_x_with_gripper_description.xacro": {
49
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/urdf/piper_x_with_gripper_description.xacro",
50
+ "sha256": "88561fe45a3f4e639c7a233b13574d88d7707b545110cb30b983e360cdb57420"
51
+ },
52
+ "arm/piper_x/meshes/flange.stl": {
53
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/meshes/flange.stl",
54
+ "sha256": "f9ce752f93a6598858a779e3d8755a09e466b588efcffc3a1c3953773753a22b"
55
+ },
56
+ "arm/piper_x/meshes/link3.stl": {
57
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/meshes/link3.stl",
58
+ "sha256": "c51994fa4ca6a0577cfc65fe5a666ad47ad6af8dc98fbe81b71c4a7fe8ad09a5"
59
+ },
60
+ "arm/piper_x/meshes/base_link.stl": {
61
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/meshes/base_link.stl",
62
+ "sha256": "b732cf8a1f8435f51e3ec860f11189c3ec63744d3df008b7d3e9b6505dfc483d"
63
+ },
64
+ "arm/piper_x/meshes/link2.stl": {
65
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/meshes/link2.stl",
66
+ "sha256": "0316b881cd0bae6c9f8f794fabdee7fbf041f83f2f75980d34b67555fe8a3561"
67
+ },
68
+ "arm/piper_x/meshes/link1.stl": {
69
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/meshes/link1.stl",
70
+ "sha256": "60c53c4d5d20c5b663cc4ced2f5e322d2a857baa6e79cef04a33b5f9f7c1e985"
71
+ },
72
+ "arm/piper_x/meshes/link5.stl": {
73
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/meshes/link5.stl",
74
+ "sha256": "fb7ab27270e6b5fa0b2e6df9fa37722062f4ff397cd4f89883d23551bedd0801"
75
+ },
76
+ "arm/piper_x/meshes/link4.stl": {
77
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/meshes/link4.stl",
78
+ "sha256": "10b69bde5305c53fbb4a6dc4e910e8604d54d250a969e6cda8128d8244989831"
79
+ },
80
+ "arm/piper_x/meshes/link6.stl": {
81
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/meshes/link6.stl",
82
+ "sha256": "8c391520fd963fe4f9b46afcde82ceeab2a93b73c01fc5debe039fe632f597e6"
83
+ },
84
+ "arm/piper_x/meshes/dae/link4.dae": {
85
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/meshes/dae/link4.dae",
86
+ "sha256": "cda6aab6dfefb13d5409a691d7486c4253737f114cca9ada67da36a10fb08123"
87
+ },
88
+ "arm/piper_x/meshes/dae/link5.dae": {
89
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/meshes/dae/link5.dae",
90
+ "sha256": "8c2a2da46dfe9d13fc8841a990694c77106c27be292fac4fc42a86cd78a2dee1"
91
+ },
92
+ "arm/piper_x/meshes/dae/link6.dae": {
93
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/meshes/dae/link6.dae",
94
+ "sha256": "7e6076e2574f042f5067dd431e96486249cffbc8ab292fb66cb68848ad5bab9a"
95
+ },
96
+ "arm/piper_x/meshes/dae/link2.dae": {
97
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/meshes/dae/link2.dae",
98
+ "sha256": "05ad1975346a330129dbce02131405dba6f51d74d0e5f3a70ec154ef219d9163"
99
+ },
100
+ "arm/piper_x/meshes/dae/base_link.dae": {
101
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/meshes/dae/base_link.dae",
102
+ "sha256": "706008b1e425227d42fb490febb6a54a2a294a39911b8cf4e9ca4b8c61eb42bd"
103
+ },
104
+ "arm/piper_x/meshes/dae/link3.dae": {
105
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/meshes/dae/link3.dae",
106
+ "sha256": "001b0b97dd514a9f8e6b321e12a8005d17198eb8b60ec443b9bd67251a3790ce"
107
+ },
108
+ "arm/piper_x/meshes/dae/link1.dae": {
109
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/meshes/dae/link1.dae",
110
+ "sha256": "6448c874d7106738d20cdd044e0e88f86262935f0bc3bdc072c5025bcf47ecfd"
111
+ },
112
+ "arm/piper_x/meshes/dae/flange.dae": {
113
+ "url": "https://raw.githubusercontent.com/agilexrobotics/agx_arm_urdf/983788b58d7eae76511177f768d85877534917fe/piper_x/meshes/dae/flange.dae",
114
+ "sha256": "421c3e20c6109df3c96c5ce18ff0b2609b6604135ff790e4bfc1d9a281b0701a"
115
+ },
116
+ "mobile/aloha_new_description/urdf/aloha_new.urdf": {
117
+ "url": "https://raw.githubusercontent.com/agilexrobotics/mobile_aloha_sim/f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a/aloha_new_description/urdf/aloha_new.urdf",
118
+ "sha256": "d4a115493af6a67ef969cfb8686b54f2970225b617ec6dd99cf682c0c4c7b6e9"
119
+ },
120
+ "mobile/aloha_new_description/package.xml": {
121
+ "url": "https://raw.githubusercontent.com/agilexrobotics/mobile_aloha_sim/f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a/aloha_new_description/package.xml",
122
+ "sha256": "8b7aa5c8b34c8854ad4e033132ff1f8a406f5bb8c32260a8e14b337cf570dba4"
123
+ },
124
+ "mobile/aloha_new_description/meshes/base_link.STL": {
125
+ "url": "https://raw.githubusercontent.com/agilexrobotics/mobile_aloha_sim/f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a/aloha_new_description/meshes/base_link.STL",
126
+ "sha256": "fd9c5c5816513cae313b6bee70f95474faadd136eb3ea19c68ffa451dfcd962a"
127
+ },
128
+ "mobile/aloha_new_description/meshes/link1.STL": {
129
+ "url": "https://raw.githubusercontent.com/agilexrobotics/mobile_aloha_sim/f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a/aloha_new_description/meshes/link1.STL",
130
+ "sha256": "2288b210ac2a4a0a6150cbe024870e6d1eee21dea320d5d6d19d17eb3099b8ea"
131
+ },
132
+ "mobile/aloha_new_description/meshes/link2.STL": {
133
+ "url": "https://raw.githubusercontent.com/agilexrobotics/mobile_aloha_sim/f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a/aloha_new_description/meshes/link2.STL",
134
+ "sha256": "09f46d7c93dccf8073491fb4ef6fb89dbe5ec9b5e82ad2c383b96a50dd6fe81f"
135
+ },
136
+ "mobile/aloha_new_description/meshes/link3.STL": {
137
+ "url": "https://raw.githubusercontent.com/agilexrobotics/mobile_aloha_sim/f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a/aloha_new_description/meshes/link3.STL",
138
+ "sha256": "80077a3cb2fd4bf4a7f65920c8f3e2f3a590c6059146dc08731a536244114210"
139
+ },
140
+ "mobile/aloha_new_description/meshes/link4.STL": {
141
+ "url": "https://raw.githubusercontent.com/agilexrobotics/mobile_aloha_sim/f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a/aloha_new_description/meshes/link4.STL",
142
+ "sha256": "e9963ce55b0796a61e4f9eb3445d81f4d40b67dcfd43eafcf2619c637f341cfe"
143
+ },
144
+ "mobile/aloha_new_description/meshes/link5.STL": {
145
+ "url": "https://raw.githubusercontent.com/agilexrobotics/mobile_aloha_sim/f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a/aloha_new_description/meshes/link5.STL",
146
+ "sha256": "ab9877b96fe2b69622011049735f0fd244b1e30ada2dc65840364c07a5154323"
147
+ },
148
+ "mobile/aloha_new_description/meshes/piper_slave_meshes/link6.STL": {
149
+ "url": "https://raw.githubusercontent.com/agilexrobotics/mobile_aloha_sim/f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a/aloha_new_description/meshes/piper_slave_meshes/link6.STL",
150
+ "sha256": "227f63feb64d17c397c76ebb96a8720476345a3bba36e0d6d7c9d938db8ce677"
151
+ },
152
+ "mobile/aloha_new_description/meshes/tracer_base_link_no_wheel.dae": {
153
+ "url": "https://raw.githubusercontent.com/agilexrobotics/mobile_aloha_sim/f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a/aloha_new_description/meshes/tracer_base_link_no_wheel.dae",
154
+ "sha256": "d3a1ee3b34dbc418e888d68148d04fccda589e00d5100fbd3cbc2f17154e0a08"
155
+ },
156
+ "mobile/aloha_new_description/meshes/tracer_wheel.dae": {
157
+ "url": "https://raw.githubusercontent.com/agilexrobotics/mobile_aloha_sim/f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a/aloha_new_description/meshes/tracer_wheel.dae",
158
+ "sha256": "42784402bca05e9f406f4e08d8ea1cc7ff57db9fee0e0b0b557670d0ee1d10c1"
159
+ },
160
+ "mobile/aloha_new_description/meshes/castor_joint.dae": {
161
+ "url": "https://raw.githubusercontent.com/agilexrobotics/mobile_aloha_sim/f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a/aloha_new_description/meshes/castor_joint.dae",
162
+ "sha256": "901cad2da533c8fd0f3e3ce25a4d5b5d1c689d5f852640cd7dab041dc647d629"
163
+ },
164
+ "mobile/aloha_new_description/meshes/castor.dae": {
165
+ "url": "https://raw.githubusercontent.com/agilexrobotics/mobile_aloha_sim/f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a/aloha_new_description/meshes/castor.dae",
166
+ "sha256": "0a61bca6839f37ebebd89021474821abfd908d2b50867f221e3504db4d84a06a"
167
+ },
168
+ "mobile/aloha_new_description/meshes/body.dae": {
169
+ "url": "https://raw.githubusercontent.com/agilexrobotics/mobile_aloha_sim/f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a/aloha_new_description/meshes/body.dae",
170
+ "sha256": "2bad2557aad93a83adad913da6ee30f32d4414602f9f7be138c377a0543be659"
171
+ },
172
+ "mobile/aloha_new_description/meshes/piper_slave_meshes/link7.STL": {
173
+ "url": "https://raw.githubusercontent.com/agilexrobotics/mobile_aloha_sim/f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a/aloha_new_description/meshes/piper_slave_meshes/link7.STL",
174
+ "sha256": "63d89c3e9d5aba6c230f21a1e8ad5724f3818a1dba7e3493c58de4bee8d44cac"
175
+ },
176
+ "mobile/aloha_new_description/meshes/piper_slave_meshes/link8.STL": {
177
+ "url": "https://raw.githubusercontent.com/agilexrobotics/mobile_aloha_sim/f799ae0192c0c2bbd502ec4d9bbec8c2633acd1a/aloha_new_description/meshes/piper_slave_meshes/link8.STL",
178
+ "sha256": "8ff0fa31925c2339f8c64171e47c91410b671530d6613e33472b710cc4968ece"
179
+ }
180
+ }
181
+ }
scripts/pygame_demo.py ADDED
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env python3
2
+ """Manual replay viewer for the shipped fixed Instance/Episode task bank.
3
+
4
+ This script never samples a task. It loads the candidate bank, validates
5
+ the manifest and replays the saved initial state of
6
+ one of its 48 Instances and 720 Episodes through ``ReplayEnv``.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ from pathlib import Path
13
+ import sys
14
+
15
+ import numpy as np
16
+
17
+ ROOT = Path(__file__).resolve().parents[1]
18
+ if str(ROOT) not in sys.path:
19
+ sys.path.insert(0, str(ROOT))
20
+
21
+ DEFAULT_BANK = ROOT / "react_manip" / "suites" / "piperx_candidates_v1" / "bank.json"
22
+
23
+
24
+ class KeyboardPoseControl:
25
+ """Manual-only physical speed scaling and persistent stop targets."""
26
+
27
+ def __init__(self, robot, dt, fast_speed=1.0):
28
+ self.robot, self.dt = robot, float(dt)
29
+ self.fast_speed = float(fast_speed)
30
+ if not np.isfinite(self.fast_speed) or not 0 < self.fast_speed <= 2:
31
+ raise ValueError("fast speed must be in (0, 2] m/s")
32
+ self.previous = {arm: np.zeros(6) for arm in robot.arms}
33
+ for arm in robot.arms:
34
+ c = robot.part_controllers[arm]
35
+ if c.input_type != 'delta' or c.impedance_mode != 'fixed':
36
+ raise ValueError('Keyboard control requires fixed-impedance delta OSC_POSE')
37
+ c.update(force=True)
38
+ c.reset_goal(goal_update_mode='desired')
39
+ c.goal_pos, c.goal_ori = self.current_pose(c)
40
+
41
+ @staticmethod
42
+ def current_pose(c):
43
+ if c.input_ref_frame == 'world':
44
+ return c.ref_pos.copy(), c.ref_ori_mat.copy()
45
+ return c.world_to_origin_frame(c.ref_pos), c.goal_origin_to_eef_pose()[:3, :3]
46
+
47
+ def stop(self):
48
+ for arm in self.robot.arms:
49
+ c = self.robot.part_controllers[arm]
50
+ c.update(force=True)
51
+ c.goal_pos, c.goal_ori = self.current_pose(c)
52
+ self.previous[arm].fill(0)
53
+
54
+ def action(self, action_dim, layout, active, direction, grippers, fast=False):
55
+ from scipy.spatial.transform import Rotation
56
+ from react_manip.robots.arm_actions import arm_action
57
+ direction = np.asarray(direction, dtype=float).copy()
58
+ # Normalize diagonal input to preserve the requested translation speed.
59
+ for part in (slice(0, 3), slice(3, 6)):
60
+ direction[part] /= max(1., np.linalg.norm(direction[part]))
61
+ c = self.robot.part_controllers[active]
62
+ c.update(force=True)
63
+ position, orientation = self.current_pose(c)
64
+ previous = self.previous[active]
65
+ released = (direction[:3] == 0) & (previous[:3] != 0)
66
+ c.goal_pos[released] = position[released]
67
+ if not np.any(direction[3:]) and np.any(previous[3:]):
68
+ c.goal_ori = orientation.copy()
69
+ # Bound target lead when blocked by contact or a workspace limit.
70
+ moving = direction[:3] != 0
71
+ limit = .10 if fast else .02
72
+ c.goal_pos[moving] = position[moving] + np.clip(c.goal_pos[moving] - position[moving], -limit, limit)
73
+ if np.any(direction[3:]):
74
+ rotation = Rotation.from_matrix(c.goal_ori @ orientation.T).as_rotvec()
75
+ rotation *= min(1., .3 / max(np.linalg.norm(rotation), 1e-12))
76
+ c.goal_ori = Rotation.from_rotvec(rotation).as_matrix() @ orientation
77
+ delta = direction * np.array([self.fast_speed if fast else .06] * 3 + [1.2 if fast else .3] * 3) * self.dt
78
+ output_mid = (c.output_max + c.output_min) / 2
79
+ output_half = (c.output_max - c.output_min) / 2
80
+ pose = (delta - output_mid) / output_half
81
+ pose = pose * (c.input_max - c.input_min) / 2 + (c.input_max + c.input_min) / 2
82
+ self.previous[active] = direction.copy()
83
+ return arm_action(action_dim, layout, active, pose, grippers)
84
+
85
+
86
+ def _records(bank_path):
87
+ """Load and flatten the validated bank in stable task/instance order."""
88
+ from react_manip.instances.candidate_bank import load_bank
89
+
90
+ bank, manifest_path, manifest, choices = load_bank(bank_path)
91
+ groups = []
92
+ for task_id in sorted(choices):
93
+ for instance, episodes in sorted(choices[task_id],
94
+ key=lambda item: item[0]["task_instance_id"]):
95
+ groups.append((instance, episodes))
96
+ return bank, manifest_path, manifest, groups
97
+
98
+
99
+ def _select(groups, instance_id=None, episode_id=None, task_id=None):
100
+ """Resolve a single Instance/Episode, rejecting ambiguous combinations."""
101
+ if task_id is not None:
102
+ groups = [item for item in groups if item[0]["task_id"] == task_id]
103
+ if not groups:
104
+ raise ValueError("no Instances match the requested task")
105
+
106
+ if instance_id is not None:
107
+ groups = [item for item in groups if item[0]["task_instance_id"] == instance_id]
108
+ if not groups:
109
+ raise ValueError(f"unknown task Instance {instance_id!r}")
110
+ if episode_id is not None:
111
+ matches = [(instance, episodes) for instance, episodes in groups
112
+ if any(ep["episode_id"] == episode_id for ep in episodes)]
113
+ if not matches:
114
+ raise ValueError(f"unknown Episode {episode_id!r}")
115
+ if instance_id is not None and len(matches) != 1:
116
+ raise ValueError(f"Episode {episode_id!r} does not belong to Instance {instance_id!r}")
117
+ groups = matches
118
+
119
+ instance, episodes = groups[0]
120
+ if episode_id is None:
121
+ episode_index = 0
122
+ else:
123
+ episode_index = next(i for i, ep in enumerate(episodes)
124
+ if ep["episode_id"] == episode_id)
125
+ return instance, episodes, episode_index
126
+
127
+
128
+ def _print_instances(groups, task_id=None):
129
+ rows = [(instance, episodes) for instance, episodes in groups
130
+ if task_id is None or instance["task_id"] == task_id]
131
+ for index, (instance, episodes) in enumerate(rows, 1):
132
+ print(f"{index:02d}. {instance['task_instance_id']} "
133
+ f"task={instance['task_id']} episodes={len(episodes)}")
134
+ print(f"{len(rows)} instance(s), {sum(len(es) for _, es in rows)} episode(s)")
135
+
136
+
137
+ def _print_episodes(instance, episodes):
138
+ for index, episode in enumerate(episodes, 1):
139
+ print(f"{index:02d}. {episode['episode_id']}")
140
+ print(f"Instance {instance['task_instance_id']}: {len(episodes)} episode(s)")
141
+
142
+
143
+ def _parser():
144
+ parser = argparse.ArgumentParser(description=__doc__)
145
+ parser.add_argument('--bank', type=Path, default=DEFAULT_BANK,
146
+ help='candidate bank JSON (default: shipped piperx bank)')
147
+ parser.add_argument('--task', help='restrict listing/selection to one task')
148
+ parser.add_argument('--instance', help='Task Instance ID, for example tilte_maze_001')
149
+ parser.add_argument('--episode', help='Episode ID, for example tilte_maze_001_e000')
150
+ parser.add_argument('--list-instances', action='store_true',
151
+ help='print all Instances and exit without opening pygame')
152
+ parser.add_argument('--list-episodes', action='store_true',
153
+ help='print Episodes for --instance and exit without opening pygame')
154
+ parser.add_argument('--hz', type=float, default=20,
155
+ help='display pacing; task physics timing stays unchanged')
156
+ parser.add_argument('--camera', help='initial camera name')
157
+ parser.add_argument('--fast-speed', type=float, default=1.0,
158
+ help='fast target speed in m/s, (0, 2]; default 1.0')
159
+ return parser
160
+
161
+
162
+ def main(argv=None):
163
+ parser = _parser()
164
+ args = parser.parse_args(argv)
165
+ if args.hz <= 0 or not np.isfinite(args.hz):
166
+ parser.error('hz must be positive')
167
+ if not np.isfinite(args.fast_speed) or not 0 < args.fast_speed <= 2:
168
+ parser.error('fast-speed must be in (0, 2] m/s')
169
+ if args.list_episodes and args.instance is None:
170
+ parser.error('--list-episodes requires --instance')
171
+ if not args.bank.is_file():
172
+ parser.error(f'bank file does not exist: {args.bank}')
173
+
174
+ try:
175
+ bank, manifest_path, manifest, groups = _records(args.bank)
176
+ if args.task is not None:
177
+ groups = [item for item in groups if item[0]['task_id'] == args.task]
178
+ if args.list_instances:
179
+ _print_instances(groups, args.task)
180
+ return 0
181
+ if args.list_episodes:
182
+ instance, episodes, _ = _select(groups, args.instance, args.episode, args.task)
183
+ _print_episodes(instance, episodes)
184
+ return 0
185
+ instance, episodes, episode_index = _select(
186
+ groups, args.instance, args.episode, args.task)
187
+ except (OSError, TypeError, ValueError, KeyError, ImportError, json.JSONDecodeError) as exc:
188
+ parser.error(str(exc))
189
+
190
+ # Import pygame and the simulator only after list-only commands have run.
191
+ try:
192
+ import pygame
193
+ except ImportError:
194
+ parser.error('install dependencies: python -m pip install -r requirements.txt')
195
+ from react_manip.instances.runtime import ReplayEnv
196
+ from react_manip.rendering import NativeRGBRenderer
197
+ from react_manip.cameras import display_camera_names
198
+
199
+ env = renderer = None
200
+ try:
201
+ pygame.init()
202
+ screen = pygame.display.set_mode((1120, 720))
203
+ pygame.display.set_caption('ReAct-Manip fixed task bank')
204
+ font = pygame.font.Font(None, 24)
205
+ clock = pygame.time.Clock()
206
+ renderer = NativeRGBRenderer()
207
+ keys = set()
208
+ active = 0
209
+ camera_index = 0
210
+ fast_latched = False
211
+ raw = arms = layout = grippers = manual = cameras = None
212
+ done, info = False, {}
213
+ group_index = next(i for i, (item, _) in enumerate(groups)
214
+ if item["task_instance_id"] == instance["task_instance_id"])
215
+
216
+ def load_episode(new_group_index, new_episode_index):
217
+ """Close/recreate on Instance changes; bind/reset on Episode changes."""
218
+ nonlocal env, instance, episodes, episode_index, group_index
219
+ nonlocal raw, arms, layout, grippers, manual, done, info, cameras
220
+ nonlocal active, camera_index, fast_latched
221
+ new_instance, new_episodes = groups[new_group_index]
222
+ instance_changed = (env is None or
223
+ instance["task_instance_id"] != new_instance["task_instance_id"])
224
+ instance, episodes = new_instance, new_episodes
225
+ episode_index = new_episode_index % len(episodes)
226
+ group_index = new_group_index
227
+ if instance_changed:
228
+ if env is not None:
229
+ env.close()
230
+ env = ReplayEnv(instance, cameras=True)
231
+ env.bind(episodes[episode_index])
232
+ reset_result = env.reset()
233
+ info = reset_result[1] if isinstance(reset_result, tuple) and len(reset_result) == 2 else {}
234
+ raw = getattr(env, 'env', env)
235
+ robot = raw.robots[0]
236
+ arms = tuple(robot.arms)
237
+ layout = robot.composite_controller._action_split_indexes
238
+ grippers = {arm: -1.0 for arm in arms}
239
+ manual = KeyboardPoseControl(robot, raw.control_timestep, args.fast_speed)
240
+ fast_latched = False
241
+ done = False
242
+ active = 0
243
+ keys.clear()
244
+ cameras = list(display_camera_names(raw, include_legacy=True))
245
+ if not cameras:
246
+ cameras = ['__free__']
247
+ camera_index = cameras.index(args.camera) if args.camera in cameras else 0
248
+
249
+ def reset_episode():
250
+ load_episode(group_index, episode_index)
251
+
252
+ load_episode(group_index, episode_index)
253
+ running = True
254
+ while running:
255
+ for event in pygame.event.get():
256
+ if event.type == pygame.QUIT:
257
+ running = False
258
+ elif event.type == pygame.KEYDOWN:
259
+ if event.key == pygame.K_ESCAPE:
260
+ running = False
261
+ elif event.key == pygame.K_BACKSPACE:
262
+ reset_episode()
263
+ elif event.key == pygame.K_n:
264
+ load_episode(group_index, episode_index + 1)
265
+ elif event.key == pygame.K_p:
266
+ load_episode(group_index, episode_index - 1)
267
+ elif event.key == pygame.K_i:
268
+ load_episode((group_index + 1) % len(groups), 0)
269
+ elif event.key == pygame.K_o:
270
+ load_episode((group_index - 1) % len(groups), 0)
271
+ elif event.key == pygame.K_TAB:
272
+ manual.stop()
273
+ fast_latched = False
274
+ active = (active + 1) % len(arms)
275
+ keys.clear()
276
+ elif event.key == pygame.K_f:
277
+ fast_latched = not fast_latched
278
+ elif event.key in (pygame.K_EQUALS, pygame.K_KP_PLUS, pygame.K_MINUS, pygame.K_KP_MINUS):
279
+ sign = 1 if event.key in (pygame.K_EQUALS, pygame.K_KP_PLUS) else -1
280
+ manual.fast_speed = float(np.clip(round(manual.fast_speed + sign * .05, 2), .05, 2.))
281
+ elif event.key == pygame.K_SPACE:
282
+ arm = arms[active]
283
+ grippers[arm] = -grippers[arm]
284
+ elif pygame.K_1 <= event.key <= pygame.K_9:
285
+ index = event.key - pygame.K_1
286
+ if index < len(cameras):
287
+ camera_index = index
288
+ elif event.key in (pygame.K_LEFTBRACKET, pygame.K_RIGHTBRACKET):
289
+ camera_index = (camera_index + (1 if event.key == pygame.K_RIGHTBRACKET else -1)) % len(cameras)
290
+ else:
291
+ keys.add(event.key)
292
+ elif event.type == pygame.WINDOWFOCUSLOST:
293
+ fast_latched = False
294
+ keys.clear()
295
+ manual.stop()
296
+ elif event.type == pygame.KEYUP:
297
+ keys.discard(event.key)
298
+ if not running:
299
+ break
300
+
301
+ def axis(positive, negative):
302
+ return float((positive in keys) - (negative in keys))
303
+
304
+ target = np.array([
305
+ axis(pygame.K_RIGHT, pygame.K_LEFT), axis(pygame.K_UP, pygame.K_DOWN),
306
+ axis(pygame.K_PAGEUP, pygame.K_PAGEDOWN), axis(pygame.K_d, pygame.K_a),
307
+ axis(pygame.K_w, pygame.K_s), axis(pygame.K_e, pygame.K_q)])
308
+ fast = fast_latched or bool(keys & {pygame.K_LSHIFT, pygame.K_RSHIFT})
309
+ if not done:
310
+ action = manual.action(env.action_dim, layout, arms[active], target, grippers, fast=fast)
311
+ _, reward, terminated, truncated, info = env.step(action)
312
+ done = bool(terminated or truncated)
313
+
314
+ frame = renderer.render(raw.sim, cameras[camera_index])
315
+ screen.fill((20, 24, 30))
316
+ screen.blit(pygame.surfarray.make_surface(frame.transpose(1, 0, 2)), (0, 0))
317
+ score = next((info[k] for k in ('task_score', 'total_score', 'score', 'S') if k in info), '--')
318
+ lines = [
319
+ f"Task: {instance['task_id']}",
320
+ f"Instance: {instance['task_instance_id']} ({group_index + 1}/{len(groups)})",
321
+ f"Episode: {episodes[episode_index]['episode_id']} ({episode_index + 1}/{len(episodes)})",
322
+ f'Robot: {instance["robot_id"]} Arm: {arms[active]}',
323
+ f'Camera: {cameras[camera_index]}', f'Score: {score}',
324
+ f"Mode: {'FAST' if fast else 'FINE'} Target: {manual.fast_speed if fast else .06:.2f} m/s",
325
+ 'Arrows / PgUp PgDn: translation', 'A D / W S / Q E: rotation',
326
+ 'F: toggle fast; Shift: hold fast', f'+/-: fast target {manual.fast_speed:.2f} m/s',
327
+ 'Space: gripper Tab: switch arm', '1-9 / [ ]: cameras',
328
+ 'N/P: next/previous Episode I/O: next/previous Instance',
329
+ 'Backspace: reset Esc: quit',
330
+ 'Episode ended. N/P or Backspace to continue.' if done else 'Manual control',
331
+ ]
332
+ for index, line in enumerate(lines):
333
+ screen.blit(font.render(line, True, (235, 235, 235)), (650, 20 + index * 29))
334
+ prompt = str(info.get('language_prompt', ''))
335
+ words = prompt.split()
336
+ line, rows = '', []
337
+ for word in words:
338
+ candidate = (line + ' ' + word).strip()
339
+ if font.size(candidate)[0] > 1080 and line:
340
+ rows.append(line)
341
+ line = word
342
+ else:
343
+ line = candidate
344
+ if line:
345
+ rows.append(line)
346
+ for index, line in enumerate(rows[:6]):
347
+ screen.blit(font.render(line, True, (235, 235, 235)), (20, 510 + index * 28))
348
+ pygame.display.flip()
349
+ clock.tick(args.hz)
350
+ return 0
351
+ finally:
352
+ if renderer is not None:
353
+ renderer.close()
354
+ if env is not None:
355
+ env.close()
356
+ pygame.quit()
357
+
358
+
359
+ if __name__ == '__main__':
360
+ raise SystemExit(main())