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,242 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Manual task-default debugger; this does not replay or score the fixed bank."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
13
|
+
if str(ROOT) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(ROOT))
|
|
15
|
+
|
|
16
|
+
from scripts.pygame_demo import KeyboardPoseControl
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parser():
|
|
20
|
+
from react_manip.instances.records import SUPPORTED_TASKS
|
|
21
|
+
|
|
22
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
23
|
+
parser.add_argument('--task', choices=SUPPORTED_TASKS,
|
|
24
|
+
default='laptop_into_drawer')
|
|
25
|
+
parser.add_argument('--robot', default=None,
|
|
26
|
+
help='robot override; defaults to the task-configured embodiment')
|
|
27
|
+
parser.add_argument('--seed', type=int, default=0,
|
|
28
|
+
help='nonnegative task-default construction seed')
|
|
29
|
+
parser.add_argument('--scenario', type=Path,
|
|
30
|
+
help='resolved Scenario JSON for the selected task')
|
|
31
|
+
parser.add_argument('--hz', type=float, default=20,
|
|
32
|
+
help='display pacing; task physics timing stays unchanged')
|
|
33
|
+
parser.add_argument('--camera', help='initial camera name')
|
|
34
|
+
parser.add_argument('--fast-speed', type=float, default=1.0,
|
|
35
|
+
help='fast target speed in m/s, (0, 2]; default 1.0')
|
|
36
|
+
return parser
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main(argv=None):
|
|
40
|
+
parser = _parser()
|
|
41
|
+
args = parser.parse_args(argv)
|
|
42
|
+
if args.seed < 0:
|
|
43
|
+
parser.error('seed must be nonnegative')
|
|
44
|
+
if args.hz <= 0 or not np.isfinite(args.hz):
|
|
45
|
+
parser.error('hz must be positive')
|
|
46
|
+
if not np.isfinite(args.fast_speed) or not 0 < args.fast_speed <= 2:
|
|
47
|
+
parser.error('fast-speed must be in (0, 2] m/s')
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
import pygame
|
|
51
|
+
except ImportError:
|
|
52
|
+
parser.error('install dependencies: python -m pip install -r requirements.txt')
|
|
53
|
+
|
|
54
|
+
from react_manip.builtin import register_builtin_tasks
|
|
55
|
+
from react_manip.cameras import display_camera_names
|
|
56
|
+
from react_manip.core.compatibility import resolve_task_robot
|
|
57
|
+
from react_manip.core.registry import get_task_registry
|
|
58
|
+
from react_manip.core.scenarios import Scenario, sample_scenario
|
|
59
|
+
from react_manip.rendering import NativeRGBRenderer
|
|
60
|
+
from react_manip.robot_registry import register_default_robots
|
|
61
|
+
|
|
62
|
+
register_default_robots()
|
|
63
|
+
register_builtin_tasks()
|
|
64
|
+
definition = get_task_registry().get(args.task)
|
|
65
|
+
try:
|
|
66
|
+
args.robot = resolve_task_robot(definition.spec, args.robot)
|
|
67
|
+
except ValueError as exc:
|
|
68
|
+
parser.error(str(exc))
|
|
69
|
+
|
|
70
|
+
if args.scenario:
|
|
71
|
+
scenario = Scenario.load(args.scenario)
|
|
72
|
+
elif args.task == 'button_indicator_matching':
|
|
73
|
+
profile = ROOT / 'configs' / args.task / 'profile_default.json'
|
|
74
|
+
scenario = sample_scenario(json.loads(profile.read_text()), seed=args.seed)
|
|
75
|
+
else:
|
|
76
|
+
scenario = None
|
|
77
|
+
if scenario is not None and scenario.task_id != args.task:
|
|
78
|
+
parser.error('scenario task does not match --task')
|
|
79
|
+
|
|
80
|
+
seed = scenario.seed if scenario is not None else args.seed
|
|
81
|
+
env = renderer = None
|
|
82
|
+
try:
|
|
83
|
+
kwargs = dict(seed=seed, robot_id=args.robot,
|
|
84
|
+
has_offscreen_renderer=False)
|
|
85
|
+
if scenario is not None:
|
|
86
|
+
kwargs['scenario'] = scenario
|
|
87
|
+
env = definition.factory(**kwargs)
|
|
88
|
+
|
|
89
|
+
pygame.init()
|
|
90
|
+
screen = pygame.display.set_mode((1120, 720))
|
|
91
|
+
pygame.display.set_caption('ReAct-Manip task debug: ' + args.task)
|
|
92
|
+
font = pygame.font.Font(None, 24)
|
|
93
|
+
clock = pygame.time.Clock()
|
|
94
|
+
renderer = NativeRGBRenderer()
|
|
95
|
+
keys = set()
|
|
96
|
+
active = 0
|
|
97
|
+
camera_index = 0
|
|
98
|
+
fast_latched = False
|
|
99
|
+
raw = arms = layout = grippers = manual = cameras = None
|
|
100
|
+
done, info = False, {}
|
|
101
|
+
|
|
102
|
+
def reset():
|
|
103
|
+
nonlocal raw, arms, layout, grippers, manual, done, info
|
|
104
|
+
nonlocal cameras, active, camera_index, fast_latched
|
|
105
|
+
reset_result = env.reset()
|
|
106
|
+
info = (reset_result[1] if isinstance(reset_result, tuple)
|
|
107
|
+
and len(reset_result) == 2 else {})
|
|
108
|
+
raw = getattr(env, 'env', env)
|
|
109
|
+
robot = raw.robots[0]
|
|
110
|
+
arms = tuple(robot.arms)
|
|
111
|
+
layout = robot.composite_controller._action_split_indexes
|
|
112
|
+
grippers = {arm: -1.0 for arm in arms}
|
|
113
|
+
manual = KeyboardPoseControl(robot, raw.control_timestep,
|
|
114
|
+
args.fast_speed)
|
|
115
|
+
fast_latched = False
|
|
116
|
+
done = False
|
|
117
|
+
active = 0
|
|
118
|
+
keys.clear()
|
|
119
|
+
cameras = list(display_camera_names(raw, include_legacy=True))
|
|
120
|
+
if not cameras:
|
|
121
|
+
cameras = ['__free__']
|
|
122
|
+
camera_index = (cameras.index(args.camera)
|
|
123
|
+
if args.camera in cameras else 0)
|
|
124
|
+
|
|
125
|
+
reset()
|
|
126
|
+
running = True
|
|
127
|
+
while running:
|
|
128
|
+
for event in pygame.event.get():
|
|
129
|
+
if event.type == pygame.QUIT:
|
|
130
|
+
running = False
|
|
131
|
+
elif event.type == pygame.KEYDOWN:
|
|
132
|
+
if event.key == pygame.K_ESCAPE:
|
|
133
|
+
running = False
|
|
134
|
+
elif event.key == pygame.K_BACKSPACE:
|
|
135
|
+
reset()
|
|
136
|
+
elif event.key == pygame.K_TAB:
|
|
137
|
+
manual.stop()
|
|
138
|
+
fast_latched = False
|
|
139
|
+
active = (active + 1) % len(arms)
|
|
140
|
+
keys.clear()
|
|
141
|
+
elif event.key == pygame.K_f:
|
|
142
|
+
fast_latched = not fast_latched
|
|
143
|
+
elif event.key in (pygame.K_EQUALS, pygame.K_KP_PLUS,
|
|
144
|
+
pygame.K_MINUS, pygame.K_KP_MINUS):
|
|
145
|
+
sign = (1 if event.key in
|
|
146
|
+
(pygame.K_EQUALS, pygame.K_KP_PLUS) else -1)
|
|
147
|
+
manual.fast_speed = float(np.clip(
|
|
148
|
+
round(manual.fast_speed + sign * .05, 2), .05, 2.))
|
|
149
|
+
elif event.key == pygame.K_SPACE:
|
|
150
|
+
arm = arms[active]
|
|
151
|
+
grippers[arm] = -grippers[arm]
|
|
152
|
+
elif pygame.K_1 <= event.key <= pygame.K_9:
|
|
153
|
+
index = event.key - pygame.K_1
|
|
154
|
+
if index < len(cameras):
|
|
155
|
+
camera_index = index
|
|
156
|
+
elif event.key in (pygame.K_LEFTBRACKET,
|
|
157
|
+
pygame.K_RIGHTBRACKET):
|
|
158
|
+
step = 1 if event.key == pygame.K_RIGHTBRACKET else -1
|
|
159
|
+
camera_index = (camera_index + step) % len(cameras)
|
|
160
|
+
else:
|
|
161
|
+
keys.add(event.key)
|
|
162
|
+
elif event.type == pygame.WINDOWFOCUSLOST:
|
|
163
|
+
fast_latched = False
|
|
164
|
+
keys.clear()
|
|
165
|
+
manual.stop()
|
|
166
|
+
elif event.type == pygame.KEYUP:
|
|
167
|
+
keys.discard(event.key)
|
|
168
|
+
if not running:
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
def axis(positive, negative):
|
|
172
|
+
return float((positive in keys) - (negative in keys))
|
|
173
|
+
|
|
174
|
+
target = np.array([
|
|
175
|
+
axis(pygame.K_RIGHT, pygame.K_LEFT),
|
|
176
|
+
axis(pygame.K_UP, pygame.K_DOWN),
|
|
177
|
+
axis(pygame.K_PAGEUP, pygame.K_PAGEDOWN),
|
|
178
|
+
axis(pygame.K_d, pygame.K_a),
|
|
179
|
+
axis(pygame.K_w, pygame.K_s),
|
|
180
|
+
axis(pygame.K_e, pygame.K_q),
|
|
181
|
+
])
|
|
182
|
+
fast = fast_latched or bool(
|
|
183
|
+
keys & {pygame.K_LSHIFT, pygame.K_RSHIFT})
|
|
184
|
+
if not done:
|
|
185
|
+
action = manual.action(env.action_dim, layout, arms[active],
|
|
186
|
+
target, grippers, fast=fast)
|
|
187
|
+
_, _, terminated, truncated, info = env.step(action)
|
|
188
|
+
done = bool(terminated or truncated)
|
|
189
|
+
|
|
190
|
+
frame = renderer.render(raw.sim, cameras[camera_index])
|
|
191
|
+
screen.fill((20, 24, 30))
|
|
192
|
+
screen.blit(pygame.surfarray.make_surface(
|
|
193
|
+
frame.transpose(1, 0, 2)), (0, 0))
|
|
194
|
+
score = next((info[key] for key in
|
|
195
|
+
('task_score', 'total_score', 'score', 'S')
|
|
196
|
+
if key in info), '--')
|
|
197
|
+
lines = [
|
|
198
|
+
f'Task debug: {args.task}',
|
|
199
|
+
f'Robot: {args.robot} Seed: {seed} Arm: {arms[active]}',
|
|
200
|
+
f'Camera: {cameras[camera_index]}', f'Score: {score}',
|
|
201
|
+
f"Mode: {'FAST' if fast else 'FINE'} "
|
|
202
|
+
f"Target: {manual.fast_speed if fast else .06:.2f} m/s",
|
|
203
|
+
'Arrows / PgUp PgDn: translation',
|
|
204
|
+
'A D / W S / Q E: rotation',
|
|
205
|
+
'F: toggle fast; Shift: hold fast',
|
|
206
|
+
f'+/-: fast target {manual.fast_speed:.2f} m/s',
|
|
207
|
+
'Space: gripper Tab: switch arm',
|
|
208
|
+
'1-9 / [ ]: cameras',
|
|
209
|
+
'Backspace: reset Esc: quit',
|
|
210
|
+
('Episode ended. Backspace to reset.'
|
|
211
|
+
if done else 'Unscored task-default debug'),
|
|
212
|
+
]
|
|
213
|
+
for index, line in enumerate(lines):
|
|
214
|
+
screen.blit(font.render(line, True, (235, 235, 235)),
|
|
215
|
+
(650, 20 + index * 32))
|
|
216
|
+
prompt = str(info.get('language_prompt', ''))
|
|
217
|
+
line, rows = '', []
|
|
218
|
+
for word in prompt.split():
|
|
219
|
+
candidate = (line + ' ' + word).strip()
|
|
220
|
+
if font.size(candidate)[0] > 1080 and line:
|
|
221
|
+
rows.append(line)
|
|
222
|
+
line = word
|
|
223
|
+
else:
|
|
224
|
+
line = candidate
|
|
225
|
+
if line:
|
|
226
|
+
rows.append(line)
|
|
227
|
+
for index, line in enumerate(rows[:6]):
|
|
228
|
+
screen.blit(font.render(line, True, (235, 235, 235)),
|
|
229
|
+
(20, 510 + index * 28))
|
|
230
|
+
pygame.display.flip()
|
|
231
|
+
clock.tick(args.hz)
|
|
232
|
+
return 0
|
|
233
|
+
finally:
|
|
234
|
+
if renderer is not None:
|
|
235
|
+
renderer.close()
|
|
236
|
+
if env is not None:
|
|
237
|
+
env.close()
|
|
238
|
+
pygame.quit()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == '__main__':
|
|
242
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Rebuild saved initial states from existing resolved questions on current robots.
|
|
3
|
+
|
|
4
|
+
This preserves compatible question parameters, scenes, and instruction plans.
|
|
5
|
+
It does not copy old simulation vectors or certify robot solvability.
|
|
6
|
+
"""
|
|
7
|
+
import argparse
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
14
|
+
sys.path.insert(0, str(ROOT))
|
|
15
|
+
from react_manip.core.compatibility import resolve_task_robot
|
|
16
|
+
from react_manip.core.scenarios import Scenario
|
|
17
|
+
from react_manip.instances.records import digest, seal, save_record, validate_parameters
|
|
18
|
+
from react_manip.instances.runtime import (
|
|
19
|
+
_construct, capture_state, contract_for, runtime_identity, spec_for, ReplayEnv,
|
|
20
|
+
)
|
|
21
|
+
from react_manip.instances.candidate_bank import checked_path, load_bank, write_json
|
|
22
|
+
from react_manip.instances.variation import TASKS as VARIED_TASKS, sample_variation, episode_scenario
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_varied_episode(task, scenario, robot, source, index, report):
|
|
26
|
+
"""Resolve sizes/physics before compilation, retaining accepted stored values."""
|
|
27
|
+
episode = deepcopy(source)
|
|
28
|
+
variation = (episode['variation'] if 'variation' in episode else
|
|
29
|
+
sample_variation(task, scenario.parameters, scenario.seed, index))
|
|
30
|
+
resolved = episode_scenario(scenario, variation)
|
|
31
|
+
if task == 'leveling_board1':
|
|
32
|
+
from tasks.leveling_board1.feasibility import certify_dimensions
|
|
33
|
+
certificate = certify_dimensions(resolved.parameters)
|
|
34
|
+
report.setdefault('leveling_certificates', []).append(dict(
|
|
35
|
+
episode_id=episode['episode_id'], variation_digest=digest(variation), certificate=certificate))
|
|
36
|
+
if 'variation' not in source:
|
|
37
|
+
from tasks.leveling_board1.instances import sample_episode_instance
|
|
38
|
+
episode['scene'] = sample_episode_instance(
|
|
39
|
+
resolved.parameters, source['provenance']['seed'])
|
|
40
|
+
env = _construct(task, resolved, robot, episode=episode.get('scene'))
|
|
41
|
+
try:
|
|
42
|
+
env.reset()
|
|
43
|
+
episode.update(variation=variation, runtime=runtime_identity(env, task),
|
|
44
|
+
initial_state=capture_state(env))
|
|
45
|
+
return episode
|
|
46
|
+
finally:
|
|
47
|
+
env.close()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def read_checked(root, ref):
|
|
51
|
+
value = json.loads(checked_path(root, ref['path']).read_text())
|
|
52
|
+
actual = digest({k: v for k, v in value.items() if k != 'content_digest'})
|
|
53
|
+
if actual != ref['digest'] or actual != value['content_digest']:
|
|
54
|
+
raise ValueError(f"Corrupt source record: {ref['path']}")
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def bind_scene(env, episode):
|
|
59
|
+
if 'scene' in episode:
|
|
60
|
+
env.env.episode_instance = deepcopy(episode['scene'])
|
|
61
|
+
if episode['task_id'] == 'phone_lean':
|
|
62
|
+
env.env.episode_seed = episode['scene']['episode_seed']
|
|
63
|
+
env.env.layout_seed = env.env.episode_seed
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def rebuild(source, out, *, include_tasks=None, suite_id=None, min_lighting_scale=None):
|
|
67
|
+
source = source.resolve()
|
|
68
|
+
bank = json.loads(source.read_text())
|
|
69
|
+
if bank['content_digest'] != digest({k: v for k, v in bank.items() if k != 'content_digest'}):
|
|
70
|
+
raise ValueError('Corrupt source bank')
|
|
71
|
+
manifest = read_checked(source.parent, dict(path=bank['manifest'], digest=bank['manifest_digest']))
|
|
72
|
+
out.mkdir(parents=True, exist_ok=False)
|
|
73
|
+
groups = []
|
|
74
|
+
report = dict(source_bank_digest=bank['content_digest'], instances=0, episodes=0,
|
|
75
|
+
scope='Fresh resolved Episode models and reset states; not robot solvability certification',
|
|
76
|
+
preserved='Base Task parameters, Task/Episode IDs, round plans and protocol; four feedback tasks receive explicit Episode variations, retained on later rebuilds',
|
|
77
|
+
verification='Every episode restored exactly and advanced one zero-action step',
|
|
78
|
+
robots={}, cameras={}, language_samples=[], parameter_migrations=[])
|
|
79
|
+
for group in manifest['groups']:
|
|
80
|
+
old = read_checked(source.parent, group['task_instance'])
|
|
81
|
+
task = old['task_id']
|
|
82
|
+
if include_tasks is not None and task not in include_tasks:continue
|
|
83
|
+
spec, contract = spec_for(task), contract_for(task)
|
|
84
|
+
robot = resolve_task_robot(spec)
|
|
85
|
+
scenario = Scenario(**{k: deepcopy(v) for k, v in old['scenario'].items() if k != 'digest'})
|
|
86
|
+
if scenario.digest != old['scenario']['digest']:
|
|
87
|
+
raise ValueError('Corrupt source scenario')
|
|
88
|
+
if min_lighting_scale is not None:
|
|
89
|
+
lighting = scenario.parameters.get('lighting')
|
|
90
|
+
if isinstance(lighting, dict) and 'intensity_scale' in lighting:
|
|
91
|
+
old_scale = float(lighting['intensity_scale'])
|
|
92
|
+
if old_scale < min_lighting_scale:
|
|
93
|
+
lighting['intensity_scale'] = float(min_lighting_scale)
|
|
94
|
+
report.setdefault('lighting_migrations', []).append(dict(
|
|
95
|
+
task_instance_id=old['task_instance_id'], old_scale=old_scale,
|
|
96
|
+
new_scale=float(min_lighting_scale)))
|
|
97
|
+
if task in VARIED_TASKS:
|
|
98
|
+
scenario.metadata['episode_variation_version'] = 1
|
|
99
|
+
source_episodes = [read_checked(source.parent, ref) for ref in group['episodes']]
|
|
100
|
+
# Episode scenes for these tasks carry a digest of the Task parameters.
|
|
101
|
+
# Raising the lighting floor changes those parameters, so refresh the
|
|
102
|
+
# scene reference while preserving the sampled pose/layout itself.
|
|
103
|
+
if task in ('rope_hang', 'laptop_into_drawer'):
|
|
104
|
+
for episode in source_episodes:
|
|
105
|
+
scene = episode.get('scene')
|
|
106
|
+
if isinstance(scene, dict) and 'task_digest' in scene:
|
|
107
|
+
scene['task_digest'] = digest(scenario.parameters)
|
|
108
|
+
validate_parameters(scenario.parameters, contract)
|
|
109
|
+
env = _construct(task, scenario, robot)
|
|
110
|
+
try:
|
|
111
|
+
env.reset()
|
|
112
|
+
fields = {k: deepcopy(v) for k, v in old.items()
|
|
113
|
+
if k not in ('schema_version', 'kind', 'content_digest')}
|
|
114
|
+
fields.update(scenario=scenario.to_dict(), robot_id=robot, task_spec_digest=digest(spec.to_dict()),
|
|
115
|
+
contract_digest=digest(contract), runtime=runtime_identity(env, task))
|
|
116
|
+
instance = seal('task_instance', **fields)
|
|
117
|
+
save_record(instance, out / group['task_instance']['path'])
|
|
118
|
+
new_group = dict(task_instance=dict(path=group['task_instance']['path'],
|
|
119
|
+
digest=instance['content_digest']), episodes=[])
|
|
120
|
+
episodes = []
|
|
121
|
+
for episode_index, (ref, source_episode) in enumerate(zip(group['episodes'], source_episodes)):
|
|
122
|
+
episode = deepcopy(source_episode)
|
|
123
|
+
if episode['task_instance_digest'] != old['content_digest']:
|
|
124
|
+
raise ValueError('Source episode belongs to a different instance')
|
|
125
|
+
if task in VARIED_TASKS:
|
|
126
|
+
episode = build_varied_episode(task, scenario, robot, episode, episode_index, report)
|
|
127
|
+
fields = {k: deepcopy(v) for k,v in episode.items()
|
|
128
|
+
if k not in ('schema_version','kind','content_digest')}
|
|
129
|
+
fields['task_instance_digest'] = instance['content_digest']
|
|
130
|
+
record = seal('episode_instance', **fields)
|
|
131
|
+
save_record(record, out / ref['path'])
|
|
132
|
+
new_group['episodes'].append(dict(path=ref['path'], digest=record['content_digest']))
|
|
133
|
+
episodes.append(record)
|
|
134
|
+
print(f" {record['episode_id']}: resolved Episode model saved", flush=True)
|
|
135
|
+
continue
|
|
136
|
+
bind_scene(env, episode)
|
|
137
|
+
env.reset()
|
|
138
|
+
fields = {k: deepcopy(v) for k, v in episode.items()
|
|
139
|
+
if k not in ('schema_version', 'kind', 'content_digest')}
|
|
140
|
+
fields.update(task_instance_digest=instance['content_digest'], initial_state=capture_state(env))
|
|
141
|
+
record = seal('episode_instance', **fields)
|
|
142
|
+
save_record(record, out / ref['path'])
|
|
143
|
+
new_group['episodes'].append(dict(path=ref['path'], digest=record['content_digest']))
|
|
144
|
+
episodes.append(record)
|
|
145
|
+
finally:
|
|
146
|
+
env.close()
|
|
147
|
+
replay = ReplayEnv(instance, cameras=False)
|
|
148
|
+
try:
|
|
149
|
+
import numpy as np
|
|
150
|
+
for episode_index, episode in enumerate(episodes):
|
|
151
|
+
replay.bind(episode)
|
|
152
|
+
_, reset_info = replay.reset()
|
|
153
|
+
if episode_index == 0:
|
|
154
|
+
report['language_samples'].append(dict(task_instance_id=instance['task_instance_id'],
|
|
155
|
+
task_id=task, episode_id=episode['episode_id'],
|
|
156
|
+
prompt=reset_info['language_prompt_structured']))
|
|
157
|
+
if capture_state(replay) != episode['initial_state']:
|
|
158
|
+
raise ValueError('Replay initial state is not exact')
|
|
159
|
+
info = replay.step(np.zeros(replay.action_dim))[-1]
|
|
160
|
+
assert info['language_prompt_structured'] == replay._wrapped.get_language_prompt()
|
|
161
|
+
assert info['language_prompt'] == info['language_prompt_structured']['text']
|
|
162
|
+
finally:
|
|
163
|
+
replay.close()
|
|
164
|
+
groups.append(new_group)
|
|
165
|
+
report['instances'] += 1
|
|
166
|
+
report['episodes'] += len(episodes)
|
|
167
|
+
report['robots'][task] = robot
|
|
168
|
+
report['cameras'][task] = instance['runtime']['cameras']
|
|
169
|
+
print(f"{report['instances']}/{len(manifest['groups'])} {instance['task_instance_id']}: {robot}, {len(episodes)} exact replays", flush=True)
|
|
170
|
+
result = seal('manifest', suite_id=suite_id or manifest['suite_id'], protocol=manifest['protocol'], groups=groups)
|
|
171
|
+
save_record(result, out / 'candidates.json')
|
|
172
|
+
bank = {k: deepcopy(v) for k, v in bank.items() if k != 'content_digest'}
|
|
173
|
+
bank.update(manifest='candidates.json', manifest_digest=result['content_digest'])
|
|
174
|
+
bank['content_digest'] = digest(bank)
|
|
175
|
+
write_json(out / 'bank.json', bank)
|
|
176
|
+
load_bank(out / 'bank.json')
|
|
177
|
+
report['bank_digest'] = bank['content_digest']
|
|
178
|
+
report['included_tasks']=sorted(report['robots'])
|
|
179
|
+
report['excluded_tasks']=sorted(set(read_checked(source.parent,g['task_instance'])['task_id'] for g in manifest['groups'])-set(report['robots']))
|
|
180
|
+
write_json(out / 'generation.json', report)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == '__main__':
|
|
184
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
185
|
+
parser.add_argument('--source', type=Path, default=ROOT / 'react_manip/suites/piperx_candidates_v1/bank.json')
|
|
186
|
+
parser.add_argument('--out', type=Path, required=True, help='New, nonexistent output directory')
|
|
187
|
+
parser.add_argument('--min-lighting-scale', type=float,
|
|
188
|
+
help='Raise saved scenario lighting intensity_scale values below this floor before rebuilding')
|
|
189
|
+
args = parser.parse_args()
|
|
190
|
+
if args.min_lighting_scale is not None and not 0 < args.min_lighting_scale <= 2:
|
|
191
|
+
parser.error('--min-lighting-scale must be in (0, 2]')
|
|
192
|
+
rebuild(args.source, args.out, min_lighting_scale=args.min_lighting_scale)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Remove one task and its referenced records from a sealed candidate bank."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
12
|
+
sys.path.insert(0, str(ROOT))
|
|
13
|
+
|
|
14
|
+
from react_manip.instances.candidate_bank import checked_path, write_json
|
|
15
|
+
from react_manip.instances.records import digest, seal
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
IDENTITY_FIELDS = ("task_id", "task_instance_id", "episode_id")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _checked_record(path: Path) -> dict:
|
|
22
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
23
|
+
expected = digest({key: item for key, item in value.items() if key != "content_digest"})
|
|
24
|
+
if value.get("content_digest") != expected:
|
|
25
|
+
raise ValueError(f"content digest mismatch: {path}")
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _belongs_to_task(value: object, task_id: str) -> bool:
|
|
30
|
+
if not isinstance(value, dict):
|
|
31
|
+
return False
|
|
32
|
+
prefix = task_id + "_"
|
|
33
|
+
return any(
|
|
34
|
+
value.get(field) == task_id
|
|
35
|
+
or (isinstance(value.get(field), str) and value[field].startswith(prefix))
|
|
36
|
+
for field in IDENTITY_FIELDS
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def remove_task(bank_path: Path, task_id: str) -> None:
|
|
41
|
+
bank_path = bank_path.resolve()
|
|
42
|
+
bank = _checked_record(bank_path)
|
|
43
|
+
manifest_path = checked_path(bank_path.parent, bank["manifest"])
|
|
44
|
+
manifest = _checked_record(manifest_path)
|
|
45
|
+
prefix = f"instances/{task_id}_"
|
|
46
|
+
removed = [
|
|
47
|
+
group for group in manifest["groups"]
|
|
48
|
+
if group["task_instance"]["path"].startswith(prefix)
|
|
49
|
+
]
|
|
50
|
+
if not removed:
|
|
51
|
+
raise ValueError(f"task is not present in the bank: {task_id}")
|
|
52
|
+
if any(len(group["episodes"]) != 15 for group in removed):
|
|
53
|
+
raise ValueError("removed task contains a nonstandard Episode count")
|
|
54
|
+
|
|
55
|
+
kept = [group for group in manifest["groups"] if group not in removed]
|
|
56
|
+
result = seal(
|
|
57
|
+
"manifest",
|
|
58
|
+
suite_id=manifest["suite_id"],
|
|
59
|
+
protocol=manifest["protocol"],
|
|
60
|
+
groups=kept,
|
|
61
|
+
)
|
|
62
|
+
write_json(manifest_path, result)
|
|
63
|
+
|
|
64
|
+
bank_fields = {key: value for key, value in bank.items() if key != "content_digest"}
|
|
65
|
+
bank_fields["manifest_digest"] = result["content_digest"]
|
|
66
|
+
bank_fields["content_digest"] = digest(bank_fields)
|
|
67
|
+
write_json(bank_path, bank_fields)
|
|
68
|
+
|
|
69
|
+
generation_path = bank_path.parent / "generation.json"
|
|
70
|
+
if generation_path.exists():
|
|
71
|
+
generation = json.loads(generation_path.read_text(encoding="utf-8"))
|
|
72
|
+
for field in ("robots", "cameras"):
|
|
73
|
+
if isinstance(generation.get(field), dict):
|
|
74
|
+
generation[field].pop(task_id, None)
|
|
75
|
+
for field, value in list(generation.items()):
|
|
76
|
+
if isinstance(value, list):
|
|
77
|
+
generation[field] = [
|
|
78
|
+
item for item in value if not _belongs_to_task(item, task_id)
|
|
79
|
+
]
|
|
80
|
+
generation["instances"] = len(kept)
|
|
81
|
+
generation["episodes"] = sum(len(group["episodes"]) for group in kept)
|
|
82
|
+
generation["included_tasks"] = sorted(generation.get("robots", {}))
|
|
83
|
+
generation["bank_digest"] = bank_fields["content_digest"]
|
|
84
|
+
for obsolete in ("phone_certificates", "resampled_scenes", "variation_rejections"):
|
|
85
|
+
if generation.get(obsolete) == []:
|
|
86
|
+
generation.pop(obsolete)
|
|
87
|
+
write_json(generation_path, generation)
|
|
88
|
+
|
|
89
|
+
for group in removed:
|
|
90
|
+
for reference in [group["task_instance"], *group["episodes"]]:
|
|
91
|
+
checked_path(bank_path.parent, reference["path"]).unlink(missing_ok=True)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def main() -> None:
|
|
95
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
96
|
+
parser.add_argument("--bank", type=Path, required=True)
|
|
97
|
+
parser.add_argument("--task", required=True)
|
|
98
|
+
args = parser.parse_args()
|
|
99
|
+
remove_task(args.bank, args.task)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
main()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Select all three instances and their Episodes for one task from the main bank."""
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
8
|
+
sys.path.insert(0, str(ROOT))
|
|
9
|
+
from react_manip.instances.candidate_bank import (
|
|
10
|
+
ALGORITHM, copy_group, load_bank, write_json,
|
|
11
|
+
)
|
|
12
|
+
from react_manip.instances.records import digest, save_record, seal
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def select_task(path, *, task, seed, out):
|
|
16
|
+
bank, source, manifest, choices = load_bank(path)
|
|
17
|
+
if task not in choices:
|
|
18
|
+
raise ValueError(f'Unknown task {task!r}; available: {", ".join(sorted(choices))}')
|
|
19
|
+
if type(seed) is not int or seed < 0:
|
|
20
|
+
raise ValueError('draw seed must be a nonnegative integer')
|
|
21
|
+
selected_groups = []
|
|
22
|
+
records = []
|
|
23
|
+
for instance, episodes in choices[task]:
|
|
24
|
+
group = next(g for g in manifest['groups']
|
|
25
|
+
if g['task_instance']['digest'] == instance['content_digest'])
|
|
26
|
+
selected_groups.append(group)
|
|
27
|
+
records.append(dict(task_id=task, task_instance_id=instance['task_instance_id'],
|
|
28
|
+
task_instance_digest=instance['content_digest'],
|
|
29
|
+
episode_digests=[e['content_digest'] for e in episodes]))
|
|
30
|
+
out = Path(out)
|
|
31
|
+
out.mkdir(parents=True, exist_ok=False)
|
|
32
|
+
for group in selected_groups:
|
|
33
|
+
copy_group(group, source.parent, out)
|
|
34
|
+
selected = seal('manifest', suite_id=out.name, protocol=manifest['protocol'], groups=selected_groups)
|
|
35
|
+
save_record(selected, out / 'manifest.json')
|
|
36
|
+
provenance = dict(
|
|
37
|
+
schema_version='cl-manip.draw.v1', algorithm=ALGORITHM, draw_seed=seed,
|
|
38
|
+
draws_per_task=3,
|
|
39
|
+
bank_digest=bank['content_digest'], candidate_manifest_digest=manifest['content_digest'],
|
|
40
|
+
selected_manifest_digest=selected['content_digest'],
|
|
41
|
+
selected=records,
|
|
42
|
+
)
|
|
43
|
+
provenance['content_digest'] = digest(provenance)
|
|
44
|
+
write_json(out / 'selection.json', provenance)
|
|
45
|
+
return out / 'manifest.json'
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main():
|
|
49
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
50
|
+
parser.add_argument('--candidate-bank', type=Path,
|
|
51
|
+
default=ROOT / 'react_manip/suites/piperx_candidates_v1/bank.json')
|
|
52
|
+
parser.add_argument('--task', required=True)
|
|
53
|
+
parser.add_argument('--draw-seed', type=int, required=True)
|
|
54
|
+
parser.add_argument('--out', type=Path, required=True, help='New selection directory containing all three instances / 45 episodes')
|
|
55
|
+
args = parser.parse_args()
|
|
56
|
+
try:
|
|
57
|
+
result = select_task(args.candidate_bank, task=args.task, seed=args.draw_seed, out=args.out)
|
|
58
|
+
except (ValueError, TypeError, OSError) as exc:
|
|
59
|
+
parser.error(str(exc))
|
|
60
|
+
print(result)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == '__main__':
|
|
64
|
+
main()
|