react-manip 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. assets/materials/realistic/common/ceramic.png +0 -0
  2. assets/materials/realistic/common/cream-plaster.png +0 -0
  3. assets/materials/realistic/common/gray-felt.png +0 -0
  4. assets/materials/realistic/common/gray-plaster.png +0 -0
  5. assets/materials/realistic/common/gray-woodgrain.png +0 -0
  6. assets/materials/realistic/common/light-gray-floor-tile.png +0 -0
  7. assets/materials/realistic/common/light-gray-plaster.png +0 -0
  8. assets/materials/realistic/common/metal.png +0 -0
  9. assets/materials/realistic/common/oak-wood.png +0 -0
  10. assets/materials/realistic/common/steel-brushed.png +0 -0
  11. assets/materials/realistic/common/steel-scratched.png +0 -0
  12. assets/materials/realistic/common/varnished-wood-panels.png +0 -0
  13. assets/materials/realistic/common/walnut-wood.png +0 -0
  14. assets/materials/realistic/common/white-bricks.png +0 -0
  15. assets/materials/realistic/common/wood-tiles.png +0 -0
  16. assets/materials/realistic/tiles074/Tiles074_2K_Color.png +0 -0
  17. assets/materials/realistic/woodfloor049/WoodFloor049_2K_Color.png +0 -0
  18. configs/box_insertion/profile_default.json +204 -0
  19. react_manip/__init__.py +12 -0
  20. react_manip/asset_cli.py +18 -0
  21. react_manip/builtin.py +168 -0
  22. react_manip/cameras.py +301 -0
  23. react_manip/category_objects.py +248 -0
  24. react_manip/core/__init__.py +60 -0
  25. react_manip/core/aggregate.py +59 -0
  26. react_manip/core/compatibility.py +64 -0
  27. react_manip/core/contracts.py +42 -0
  28. react_manip/core/geometry.py +269 -0
  29. react_manip/core/language.py +116 -0
  30. react_manip/core/metrics.py +196 -0
  31. react_manip/core/observation.py +59 -0
  32. react_manip/core/protocol.py +91 -0
  33. react_manip/core/registry.py +94 -0
  34. react_manip/core/runner.py +498 -0
  35. react_manip/core/scenarios.py +110 -0
  36. react_manip/core/specs.py +203 -0
  37. react_manip/core/timeouts.py +44 -0
  38. react_manip/core/visual_randomization.py +261 -0
  39. react_manip/evaluation/__init__.py +4 -0
  40. react_manip/evaluation/metrics.py +17 -0
  41. react_manip/evaluation/protocols.py +326 -0
  42. react_manip/evaluation/viewer.py +240 -0
  43. react_manip/instances/__init__.py +6 -0
  44. react_manip/instances/candidate_bank.py +109 -0
  45. react_manip/instances/public.py +95 -0
  46. react_manip/instances/records.py +345 -0
  47. react_manip/instances/runtime.py +306 -0
  48. react_manip/instances/suite.py +231 -0
  49. react_manip/instances/trajectory.py +59 -0
  50. react_manip/instances/variation.py +118 -0
  51. react_manip/legacy.py +318 -0
  52. react_manip/policies/__init__.py +5 -0
  53. react_manip/policies/base.py +79 -0
  54. react_manip/rendering.py +58 -0
  55. react_manip/robot_registry.py +31 -0
  56. react_manip/robots/__init__.py +33 -0
  57. react_manip/robots/adapters.py +135 -0
  58. react_manip/robots/agilex_adapter.py +35 -0
  59. react_manip/robots/agilex_description.py +74 -0
  60. react_manip/robots/agilex_robosuite.py +246 -0
  61. react_manip/robots/arm_actions.py +17 -0
  62. react_manip/robots/base.py +119 -0
  63. react_manip/robots/catalog.py +18 -0
  64. react_manip/robots/legacy_agilex_robosuite.py +169 -0
  65. react_manip/robots/mobile_base.py +33 -0
  66. react_manip/robots/mounts.py +80 -0
  67. react_manip/robots/resolution.py +103 -0
  68. react_manip/robots/sawyer_robosuite.py +147 -0
  69. react_manip/robots/sensors.py +137 -0
  70. react_manip/utils/__init__.py +1 -0
  71. react_manip/visual_task.py +50 -0
  72. react_manip-0.1.0.dist-info/METADATA +446 -0
  73. react_manip-0.1.0.dist-info/RECORD +99 -0
  74. react_manip-0.1.0.dist-info/WHEEL +5 -0
  75. react_manip-0.1.0.dist-info/entry_points.txt +2 -0
  76. react_manip-0.1.0.dist-info/licenses/LICENSE +131 -0
  77. react_manip-0.1.0.dist-info/top_level.txt +5 -0
  78. scripts/add_phone_lean_to_bank.py +242 -0
  79. scripts/agilex_wheeled_builder.py +133 -0
  80. scripts/audit_task_solvability.py +150 -0
  81. scripts/build_agilex_assets.py +317 -0
  82. scripts/build_legacy_agilex_assets.py +244 -0
  83. scripts/evaluate_all_tasks.py +309 -0
  84. scripts/evaluate_policy.py +281 -0
  85. scripts/piperx_assets.lock.json +181 -0
  86. scripts/pygame_demo.py +360 -0
  87. scripts/pygame_task_debug.py +242 -0
  88. scripts/regenerate_instance_bank.py +192 -0
  89. scripts/remove_task_from_bank.py +103 -0
  90. scripts/select_task_instances.py +64 -0
  91. scripts/verify_curling_reachability.py +206 -0
  92. scripts/verify_piperx_tasks.py +81 -0
  93. tasks/box_insertion/__init__.py +3 -0
  94. tasks/box_insertion/assets/scene.xml +50 -0
  95. tasks/box_insertion/env.py +642 -0
  96. tasks/box_insertion/evaluation.json +16 -0
  97. tasks/box_insertion/instance_contract.json +321 -0
  98. tasks/box_insertion/scoring.py +23 -0
  99. tasks/box_insertion/task_spec.json +333 -0
@@ -0,0 +1,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()