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