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,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())
|