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,196 @@
|
|
|
1
|
+
"""Step-level tracking for fixed-horizon, sustained-success evaluation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
import math
|
|
7
|
+
from typing import Any, Iterable, Mapping
|
|
8
|
+
|
|
9
|
+
from .specs import SuccessRule
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _failure_flags(failure: Any) -> tuple[bool, bool]:
|
|
13
|
+
if failure is None or failure is False:
|
|
14
|
+
return False, False
|
|
15
|
+
if isinstance(failure, Mapping):
|
|
16
|
+
return True, bool(failure.get("recoverable", False))
|
|
17
|
+
return True, False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class SustainedSuccessTracker:
|
|
22
|
+
"""Collect continuous-state and discrete-event success signals."""
|
|
23
|
+
|
|
24
|
+
control_hz: float = 20.0
|
|
25
|
+
steps: int = 0
|
|
26
|
+
state_steps: int = 0
|
|
27
|
+
success_steps: int = 0
|
|
28
|
+
longest_streak_steps: int = 0
|
|
29
|
+
current_streak_steps: int = 0
|
|
30
|
+
completed_events: int = 0
|
|
31
|
+
failure_count: int = 0
|
|
32
|
+
recoverable_failure_count: int = 0
|
|
33
|
+
recovery_count: int = 0
|
|
34
|
+
time_to_first_failure_sec: float | None = None
|
|
35
|
+
recovery_latency_sec: list[float] = field(default_factory=list)
|
|
36
|
+
max_scalars: dict[str, float] = field(default_factory=dict)
|
|
37
|
+
sum_scalars: dict[str, float] = field(default_factory=dict)
|
|
38
|
+
count_scalars: dict[str, int] = field(default_factory=dict)
|
|
39
|
+
last_scalars: dict[str, float] = field(default_factory=dict)
|
|
40
|
+
_failure_started_at: float | None = field(default=None, init=False, repr=False)
|
|
41
|
+
_last_failure: bool = field(default=False, init=False, repr=False)
|
|
42
|
+
_last_info: Mapping[str, Any] = field(default_factory=dict, init=False, repr=False)
|
|
43
|
+
|
|
44
|
+
def update(
|
|
45
|
+
self,
|
|
46
|
+
success_state: bool | None,
|
|
47
|
+
*,
|
|
48
|
+
events: Iterable[Any] = (),
|
|
49
|
+
failure: Any = None,
|
|
50
|
+
dt: float | None = None,
|
|
51
|
+
info: Mapping[str, Any] | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Add one control tick to the trace.
|
|
54
|
+
|
|
55
|
+
``success_state=None`` means the task did not expose a continuous
|
|
56
|
+
predicate for this tick. It is conservatively counted as a non-success
|
|
57
|
+
tick, preventing missing signals from inflating occupancy.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
dt = float(dt if dt is not None else 1.0 / self.control_hz)
|
|
61
|
+
if dt <= 0:
|
|
62
|
+
raise ValueError("dt must be positive")
|
|
63
|
+
self.steps += 1
|
|
64
|
+
self.state_steps += 1
|
|
65
|
+
if success_state is True:
|
|
66
|
+
self.success_steps += 1
|
|
67
|
+
self.current_streak_steps += 1
|
|
68
|
+
self.longest_streak_steps = max(self.longest_streak_steps, self.current_streak_steps)
|
|
69
|
+
elif success_state is False:
|
|
70
|
+
self.current_streak_steps = 0
|
|
71
|
+
else:
|
|
72
|
+
# An unknown predicate cannot be part of a continuous success
|
|
73
|
+
# streak and is conservatively treated as a failed tick.
|
|
74
|
+
self.current_streak_steps = 0
|
|
75
|
+
|
|
76
|
+
event_list = list(events)
|
|
77
|
+
self.completed_events += len(event_list)
|
|
78
|
+
failed, recoverable = _failure_flags(failure)
|
|
79
|
+
now = (self.steps - 1) * dt
|
|
80
|
+
if failed and not self._last_failure:
|
|
81
|
+
self.failure_count += 1
|
|
82
|
+
if recoverable:
|
|
83
|
+
self.recoverable_failure_count += 1
|
|
84
|
+
self._failure_started_at = now
|
|
85
|
+
if self.time_to_first_failure_sec is None:
|
|
86
|
+
self.time_to_first_failure_sec = now
|
|
87
|
+
elif self._last_failure and self._failure_started_at is not None:
|
|
88
|
+
self.recovery_count += 1
|
|
89
|
+
self.recovery_latency_sec.append(max(0.0, now - self._failure_started_at))
|
|
90
|
+
self._failure_started_at = None
|
|
91
|
+
self._last_failure = failed
|
|
92
|
+
self._last_info = info or {}
|
|
93
|
+
# Optional, task-independent safety and real-time signals. Tasks may
|
|
94
|
+
# emit any of these fields without changing the tracker schema.
|
|
95
|
+
for key in (
|
|
96
|
+
"peak_force_N",
|
|
97
|
+
"impulse_Ns",
|
|
98
|
+
"joint_limit_violation",
|
|
99
|
+
"inference_latency_ms",
|
|
100
|
+
"action_age_ms",
|
|
101
|
+
# Optional task scoring signals. Tasks may expose one or more of
|
|
102
|
+
# these without creating a task-specific runner branch.
|
|
103
|
+
"total_score",
|
|
104
|
+
"circle_hold_score",
|
|
105
|
+
"hole_drop_score",
|
|
106
|
+
"time_score",
|
|
107
|
+
"distance_score",
|
|
108
|
+
"attempt_score",
|
|
109
|
+
"distance_travelled_m",
|
|
110
|
+
"distance_fraction",
|
|
111
|
+
"lane_score",
|
|
112
|
+
"max_lane_deviation_m",
|
|
113
|
+
"speed_m_s",
|
|
114
|
+
"insertion_depth_m",
|
|
115
|
+
"max_insertion_depth_m",
|
|
116
|
+
"current_circle_hold_sec",
|
|
117
|
+
"max_circle_hold_sec",
|
|
118
|
+
):
|
|
119
|
+
value = (info or {}).get(key)
|
|
120
|
+
if isinstance(value, (int, float)) and math.isfinite(float(value)):
|
|
121
|
+
scalar = float(value)
|
|
122
|
+
self.max_scalars[key] = max(self.max_scalars.get(key, float("-inf")), scalar)
|
|
123
|
+
self.sum_scalars[key] = self.sum_scalars.get(key, 0.0) + scalar
|
|
124
|
+
self.count_scalars[key] = self.count_scalars.get(key, 0) + 1
|
|
125
|
+
self.last_scalars[key] = scalar
|
|
126
|
+
|
|
127
|
+
def record_failure(self, *, recoverable: bool = False, at_sec: float | None = None) -> None:
|
|
128
|
+
"""Record an out-of-band failure without adding a control tick."""
|
|
129
|
+
|
|
130
|
+
if not self._last_failure:
|
|
131
|
+
self.failure_count += 1
|
|
132
|
+
if recoverable:
|
|
133
|
+
self.recoverable_failure_count += 1
|
|
134
|
+
if self.time_to_first_failure_sec is None:
|
|
135
|
+
self.time_to_first_failure_sec = (
|
|
136
|
+
float(at_sec) if at_sec is not None else self.steps / self.control_hz
|
|
137
|
+
)
|
|
138
|
+
self._last_failure = True
|
|
139
|
+
|
|
140
|
+
def summarize(self, rule: SuccessRule, *, terminal_success: bool = False) -> dict[str, Any]:
|
|
141
|
+
occupancy = self.success_steps / self.state_steps if self.state_steps else 0.0
|
|
142
|
+
longest = self.longest_streak_steps / self.control_hz
|
|
143
|
+
passed = (
|
|
144
|
+
occupancy >= rule.min_occupancy
|
|
145
|
+
and longest >= rule.min_longest_streak_sec
|
|
146
|
+
and self.completed_events >= rule.min_events
|
|
147
|
+
and (rule.max_failures is None or self.failure_count <= rule.max_failures)
|
|
148
|
+
and (not rule.require_terminal_success or terminal_success)
|
|
149
|
+
)
|
|
150
|
+
metrics = {
|
|
151
|
+
"steps": self.steps,
|
|
152
|
+
"state_steps": self.state_steps,
|
|
153
|
+
"sim_time_sec": self.steps / self.control_hz,
|
|
154
|
+
"success_steps": self.success_steps,
|
|
155
|
+
"success_occupancy": round(occupancy, 6),
|
|
156
|
+
"longest_success_streak_sec": round(longest, 6),
|
|
157
|
+
"completed_events": self.completed_events,
|
|
158
|
+
"event_rate_hz": round(self.completed_events / (self.steps / self.control_hz), 6)
|
|
159
|
+
if self.steps
|
|
160
|
+
else 0.0,
|
|
161
|
+
"failure_count": self.failure_count,
|
|
162
|
+
"recoverable_failure_count": self.recoverable_failure_count,
|
|
163
|
+
"recovery_count": self.recovery_count,
|
|
164
|
+
"time_to_first_failure_sec": self.time_to_first_failure_sec,
|
|
165
|
+
"mean_recovery_latency_sec": round(sum(self.recovery_latency_sec) / len(self.recovery_latency_sec), 6)
|
|
166
|
+
if self.recovery_latency_sec
|
|
167
|
+
else None,
|
|
168
|
+
"terminal_success": bool(terminal_success),
|
|
169
|
+
"passed": bool(passed),
|
|
170
|
+
}
|
|
171
|
+
for key, value in self.max_scalars.items():
|
|
172
|
+
metrics[f"max_{key}"] = round(value, 6)
|
|
173
|
+
if "max_peak_force_N" in metrics:
|
|
174
|
+
metrics["peak_force_N"] = metrics["max_peak_force_N"]
|
|
175
|
+
for key in (
|
|
176
|
+
"circle_hold_score", "hole_drop_score", "attempt_score",
|
|
177
|
+
"distance_score", "distance_travelled_m", "distance_fraction",
|
|
178
|
+
"max_lane_deviation_m", "speed_m_s", "insertion_depth_m",
|
|
179
|
+
"max_insertion_depth_m", "max_circle_hold_sec",
|
|
180
|
+
):
|
|
181
|
+
max_key = f"max_{key}"
|
|
182
|
+
if max_key in metrics:
|
|
183
|
+
# Scores and a longest-duration measure are monotonic within
|
|
184
|
+
# a task episode, so their maximum is the final audit value.
|
|
185
|
+
metrics[key] = metrics[max_key]
|
|
186
|
+
for key in ("time_score",):
|
|
187
|
+
if key in self.last_scalars:
|
|
188
|
+
metrics[key] = round(self.last_scalars[key], 6)
|
|
189
|
+
# Lane and total scores may decrease after a vehicle leaves its lane.
|
|
190
|
+
for key in ("lane_score", "total_score", "attempt_score"):
|
|
191
|
+
if key in self.last_scalars:
|
|
192
|
+
metrics[key] = round(self.last_scalars[key], 6)
|
|
193
|
+
for key, value in self.sum_scalars.items():
|
|
194
|
+
count = self.count_scalars[key]
|
|
195
|
+
metrics[f"mean_{key}"] = round(value / count, 6) if count else None
|
|
196
|
+
return metrics
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Policy-observation filtering for privileged task state.
|
|
2
|
+
|
|
3
|
+
Geometry truth is exposed through :class:`GeometryFeatureAdapter`, not through
|
|
4
|
+
the observation passed to a normal policy. Legacy robosuite tasks can still
|
|
5
|
+
produce named object sensors, so the benchmark runner enforces the task's
|
|
6
|
+
visibility contract at its final policy boundary.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Mapping
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def policy_observation(observation: Any, task_spec: Any, env: Any = None) -> Any:
|
|
15
|
+
"""Return the non-privileged view that may be passed to a policy.
|
|
16
|
+
|
|
17
|
+
Tasks list any legacy raw keys in ``privileged_observation_keys``. The
|
|
18
|
+
robosuite aggregate ``object-state`` is always privileged when geometry is
|
|
19
|
+
declared internal. Non-mapping observations are returned unchanged so
|
|
20
|
+
native state-vector tasks keep their existing API.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
if not isinstance(observation, Mapping):
|
|
24
|
+
return observation
|
|
25
|
+
contract = getattr(task_spec, "observation", None)
|
|
26
|
+
if contract is None or getattr(contract, "geometry_view", "internal") != "internal":
|
|
27
|
+
return observation
|
|
28
|
+
if "geometry" in tuple(getattr(contract, "default_policy_view", ())):
|
|
29
|
+
return observation
|
|
30
|
+
|
|
31
|
+
task_env = getattr(env, "env", env)
|
|
32
|
+
hidden = set(getattr(task_env, "privileged_observation_keys", ()))
|
|
33
|
+
hidden.add("object-state")
|
|
34
|
+
if not hidden.intersection(observation):
|
|
35
|
+
return observation
|
|
36
|
+
|
|
37
|
+
filtered = observation.copy()
|
|
38
|
+
for key in hidden:
|
|
39
|
+
filtered.pop(key, None)
|
|
40
|
+
return filtered
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def policy_info(info: Mapping[str, Any], policy: Any, env: Any) -> Mapping[str, Any]:
|
|
44
|
+
"""Attach geometry truth only for an explicitly privileged oracle.
|
|
45
|
+
|
|
46
|
+
A regular policy sees the original public ``info`` mapping. Oracle code
|
|
47
|
+
must opt in with ``requires_geometry_truth = True``; this makes privileged
|
|
48
|
+
use visible in code review and prevents accidental model leakage.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
if not bool(getattr(policy, "requires_geometry_truth", False)):
|
|
52
|
+
raw = getattr(env, 'env', env)
|
|
53
|
+
hidden = set(getattr(raw, 'privileged_info_keys', ()))
|
|
54
|
+
return {k:v for k,v in info.items() if k not in hidden} if hidden else info
|
|
55
|
+
from .geometry import GeometryFeatureAdapter
|
|
56
|
+
|
|
57
|
+
enriched = dict(info)
|
|
58
|
+
enriched["geometry_ground_truth"] = GeometryFeatureAdapter.for_task(env).extract()
|
|
59
|
+
return enriched
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Protocols shared by tasks, policies, and the fixed-horizon runner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import math
|
|
7
|
+
from typing import Any, Mapping, Protocol, Sequence, runtime_checkable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class StepResult:
|
|
12
|
+
"""Normalized result of either Gymnasium (5-tuple) or robosuite (4-tuple)."""
|
|
13
|
+
|
|
14
|
+
observation: Any
|
|
15
|
+
reward: float
|
|
16
|
+
terminated: bool
|
|
17
|
+
truncated: bool
|
|
18
|
+
info: Mapping[str, Any]
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def done(self) -> bool:
|
|
22
|
+
return self.terminated or self.truncated
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@runtime_checkable
|
|
26
|
+
class BenchmarkTask(Protocol):
|
|
27
|
+
"""Minimum environment interface expected by :class:`BenchmarkRunner`."""
|
|
28
|
+
|
|
29
|
+
action_dim: int
|
|
30
|
+
|
|
31
|
+
def reset(self, *args: Any, **kwargs: Any) -> Any:
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
def step(self, action: Any) -> Any:
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
def close(self) -> None:
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@runtime_checkable
|
|
42
|
+
class Policy(Protocol):
|
|
43
|
+
"""Policy interface; model-specific dependencies stay outside the runner."""
|
|
44
|
+
|
|
45
|
+
def reset(self, *, seed: int | None = None, task_spec: Any = None) -> None:
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def act(self, observation: Any, info: Mapping[str, Any] | None = None) -> Any:
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def normalize_reset(value: Any) -> tuple[Any, Mapping[str, Any]]:
|
|
53
|
+
"""Normalize old robosuite and Gymnasium reset return conventions."""
|
|
54
|
+
|
|
55
|
+
if isinstance(value, tuple) and len(value) == 2 and isinstance(value[1], Mapping):
|
|
56
|
+
return value[0], value[1]
|
|
57
|
+
return value, {}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def normalize_step(value: Sequence[Any]) -> StepResult:
|
|
61
|
+
"""Normalize a four- or five-element environment step result."""
|
|
62
|
+
|
|
63
|
+
if len(value) == 5:
|
|
64
|
+
obs, reward, terminated, truncated, info = value
|
|
65
|
+
reward = float(reward)
|
|
66
|
+
if not math.isfinite(reward):
|
|
67
|
+
raise ValueError(f"environment returned non-finite reward: {reward}")
|
|
68
|
+
return StepResult(obs, reward, bool(terminated), bool(truncated), info or {})
|
|
69
|
+
if len(value) == 4:
|
|
70
|
+
obs, reward, done, info = value
|
|
71
|
+
reward = float(reward)
|
|
72
|
+
if not math.isfinite(reward):
|
|
73
|
+
raise ValueError(f"environment returned non-finite reward: {reward}")
|
|
74
|
+
truncated = bool(done and (info or {}).get('TimeLimit.truncated', False))
|
|
75
|
+
return StepResult(obs, reward, bool(done) and not truncated, truncated, info or {})
|
|
76
|
+
raise ValueError("env.step(action) must return a 4- or 5-element tuple")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class AdaptivePolicy(Policy, Protocol):
|
|
80
|
+
"""Optional contract for task-selected practice/exam protocols.
|
|
81
|
+
|
|
82
|
+
begin_episode clears transient episode state and sets trial RNG, preserving
|
|
83
|
+
learned adaptation throughout practice AND exams. Snapshot hooks are only
|
|
84
|
+
required by the historical frozen protocol, not online evaluation.
|
|
85
|
+
end_episode receives filtered final feedback, including horizon timeouts.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def begin_episode(self, *, phase: str, episode_index: int, seed: int, task_spec: Any) -> None: ...
|
|
89
|
+
def end_episode(self, **feedback: Any) -> None: ...
|
|
90
|
+
def get_adaptation_state(self) -> Any: ...
|
|
91
|
+
def set_adaptation_state(self, state: Any) -> None: ...
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Task and robot registries used for additive extension."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
from .specs import TaskSpec
|
|
9
|
+
|
|
10
|
+
Factory = Callable[..., Any]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class TaskDefinition:
|
|
15
|
+
spec: TaskSpec
|
|
16
|
+
factory: Factory
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TaskRegistry:
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self._items: dict[str, TaskDefinition] = {}
|
|
22
|
+
|
|
23
|
+
def register(self, spec: TaskSpec | dict[str, Any], factory: Factory, *, replace: bool = False) -> TaskDefinition:
|
|
24
|
+
spec = spec if isinstance(spec, TaskSpec) else TaskSpec.from_mapping(spec)
|
|
25
|
+
if spec.task_id in self._items and not replace:
|
|
26
|
+
raise KeyError(f"task already registered: {spec.task_id}")
|
|
27
|
+
definition = TaskDefinition(spec, factory)
|
|
28
|
+
self._items[spec.task_id] = definition
|
|
29
|
+
return definition
|
|
30
|
+
|
|
31
|
+
def get(self, task_id: str) -> TaskDefinition:
|
|
32
|
+
try:
|
|
33
|
+
return self._items[task_id]
|
|
34
|
+
except KeyError as exc:
|
|
35
|
+
known = ", ".join(sorted(self._items)) or "<empty>"
|
|
36
|
+
raise KeyError(f"unknown task {task_id!r}; registered tasks: {known}") from exc
|
|
37
|
+
|
|
38
|
+
def list(self) -> tuple[TaskDefinition, ...]:
|
|
39
|
+
return tuple(self._items[key] for key in sorted(self._items))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RobotRegistry:
|
|
43
|
+
def __init__(self) -> None:
|
|
44
|
+
self._items: dict[str, Any] = {}
|
|
45
|
+
|
|
46
|
+
def register(self, robot_id: str, adapter_or_factory: Any, *, replace: bool = False) -> Any:
|
|
47
|
+
robot_id = str(robot_id).lower()
|
|
48
|
+
if robot_id in self._items and not replace:
|
|
49
|
+
raise KeyError(f"robot already registered: {robot_id}")
|
|
50
|
+
self._items[robot_id] = adapter_or_factory
|
|
51
|
+
return adapter_or_factory
|
|
52
|
+
|
|
53
|
+
def get(self, robot_id: str) -> Any:
|
|
54
|
+
robot_id = str(robot_id).lower()
|
|
55
|
+
if robot_id in ("aloha", "aloha_mobile"):
|
|
56
|
+
from ..robots.catalog import normalize_robot_id
|
|
57
|
+
robot_id = normalize_robot_id(robot_id)
|
|
58
|
+
try:
|
|
59
|
+
return self._items[robot_id]
|
|
60
|
+
except KeyError as exc:
|
|
61
|
+
known = ", ".join(sorted(self._items)) or "<empty>"
|
|
62
|
+
raise KeyError(f"unknown robot {robot_id!r}; registered robots: {known}") from exc
|
|
63
|
+
|
|
64
|
+
def list(self) -> tuple[tuple[str, Any], ...]:
|
|
65
|
+
return tuple((key, self._items[key]) for key in sorted(self._items))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
_TASKS = TaskRegistry()
|
|
69
|
+
_ROBOTS = RobotRegistry()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_task_registry() -> TaskRegistry:
|
|
73
|
+
return _TASKS
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_robot_registry() -> RobotRegistry:
|
|
77
|
+
return _ROBOTS
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def register_task(spec: TaskSpec | dict[str, Any], factory: Factory | None = None, **kwargs: Any):
|
|
81
|
+
"""Register directly or use as ``@register_task(spec)`` decorator."""
|
|
82
|
+
|
|
83
|
+
if factory is not None:
|
|
84
|
+
return _TASKS.register(spec, factory, **kwargs)
|
|
85
|
+
|
|
86
|
+
def decorator(fn: Factory) -> Factory:
|
|
87
|
+
_TASKS.register(spec, fn, **kwargs)
|
|
88
|
+
return fn
|
|
89
|
+
|
|
90
|
+
return decorator
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def register_robot(robot_id: str, adapter_or_factory: Any, **kwargs: Any) -> Any:
|
|
94
|
+
return _ROBOTS.register(robot_id, adapter_or_factory, **kwargs)
|