kitchenbench 0.3.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.
- kitchenbench/CLAUDE.md +60 -0
- kitchenbench/__init__.py +91 -0
- kitchenbench/distributions.py +123 -0
- kitchenbench/embodiment.py +138 -0
- kitchenbench/instances.py +87 -0
- kitchenbench/policies.py +96 -0
- kitchenbench/py.typed +0 -0
- kitchenbench/scoring.py +36 -0
- kitchenbench/specs.py +714 -0
- kitchenbench/tasks.py +169 -0
- kitchenbench-0.3.0.dist-info/METADATA +273 -0
- kitchenbench-0.3.0.dist-info/RECORD +15 -0
- kitchenbench-0.3.0.dist-info/WHEEL +4 -0
- kitchenbench-0.3.0.dist-info/entry_points.txt +19 -0
- kitchenbench-0.3.0.dist-info/licenses/LICENSE +21 -0
kitchenbench/CLAUDE.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# `kitchenbench` package — module map
|
|
2
|
+
|
|
3
|
+
A Inspect Robots plugin. Importing the package registers all 10 tasks, the mock
|
|
4
|
+
`kitchen` embodiment, and the mock policies with the Inspect Robots registry; entry
|
|
5
|
+
points (in `pyproject.toml`) make them discoverable without importing.
|
|
6
|
+
|
|
7
|
+
## Modules
|
|
8
|
+
|
|
9
|
+
| Module | Responsibility |
|
|
10
|
+
|--------|----------------|
|
|
11
|
+
| `distributions.py` | Pure-numpy `Distribution` types — `Uniform`, `Categorical` (samples **by index** to preserve dtype), `Normal`, `Constant`. Each `sample(rng)` returns a **builtin** scalar (JSON-native); `describe()` renders methodology notation. |
|
|
12
|
+
| `instances.py` | `TaskInstance` (stochastic setup of named distributions + goal), `Realization` (one sampled environment), `Validation` (per-expert ratings + accept rule), and the `K_REALIZATIONS`/`K_INSTANCES`/`K_EXPERTS` constants. `TaskInstance.realize(seed)` samples; `setup_spec()` describes. |
|
|
13
|
+
| `specs.py` | `TaskSpec` + `SPECS` — the **single source of truth**: 10 tasks, each with exactly `K_INSTANCES` (5) distribution-based `TaskInstance`s. |
|
|
14
|
+
| `tasks.py` | `build_scenes` (one `Scene` per instance), `make_task` (`Epochs(count=5, reducer="mean")`), `realize_scene(scene, seed)` (the run-time seam that recovers + realizes an instance), and the 10 `@task` factories. `TASK_FACTORIES` maps key → factory. |
|
|
15
|
+
| `scoring.py` | `task_success()` — success iff `termination_reason == "success"` (mock/sim privileged signal, or a real embodiment reporting operator-confirmed success) **or** an affirmative recorded operator verdict. |
|
|
16
|
+
| `embodiment.py` | `KitchenEmbodiment` — dependency-free abstract bimanual mock. Models *progress toward the scene goal* (not physics), like Inspect Robots's `CubePick`. Action space is `(8,)` = two arms × `[dx,dy,dz,gripper]`. On reset of a KitchenBench scene (marked `metadata["benchmark"] == "kitchenbench"`) it calls `realize_scene` so the observed instruction reflects the per-epoch realization (a real embodiment would also arrange the sampled setup); the instruction is carried on every step observation since real VLA policies re-condition on it each `act()`. |
|
|
17
|
+
| `policies.py` | `ScriptedKitchenPolicy` (reads privileged `goal_dir`, succeeds), `RandomKitchenPolicy`, `NoopKitchenPolicy`. All emit `ActionChunk`s. |
|
|
18
|
+
| `__init__.py` | Re-exports the public surface: factories, mock, policies, specs, distributions, `TaskInstance`/`Realization`/`Validation`, `realize_scene`, and the `K_*` constants (fenced by `__all__`). |
|
|
19
|
+
|
|
20
|
+
## Key invariants
|
|
21
|
+
|
|
22
|
+
- The entry-point name, the `@task(name=...)`, and the returned `Task.name` are
|
|
23
|
+
**all identical** (`kitchenbench/<key>`). Keep them in sync.
|
|
24
|
+
- The mock's hidden unit `goal_dir` + alignment threshold (0.99) is what makes the
|
|
25
|
+
scripted oracle succeed and random/no-op fail **deterministically** — don't
|
|
26
|
+
loosen the threshold or random policies may start passing and break tests.
|
|
27
|
+
- Compatibility is exact action-dim equality: the mock (dim 8) pairs with the
|
|
28
|
+
mock scripted policy (dim 8); a real YAM arm (higher DoF) pairs with a real VLA
|
|
29
|
+
— there is no cross-pairing. Tasks carry no action space, so they run on both.
|
|
30
|
+
|
|
31
|
+
## Methodology mapping (do not break)
|
|
32
|
+
|
|
33
|
+
- **task instance → `Scene`**, **realization → epoch**, `Epochs(count=5,
|
|
34
|
+
reducer="mean")` → per-scene reduced `task_success` = **P̂[Yᵢ=1]**.
|
|
35
|
+
- `metadata["task_success"]` at the eval level is the *mean of P̂ over instances* —
|
|
36
|
+
a **convenience aggregate, not** a methodology quantity (the methodology sorts P̂
|
|
37
|
+
into quantiles → pTQ / automation-halvings; out of scope here).
|
|
38
|
+
- Scene `metadata` is **strictly JSON-native** (distributions stored as
|
|
39
|
+
`setup_spec()` strings; the live `TaskInstance` is recovered by looking up
|
|
40
|
+
`metadata["instance_id"]` within `SPEC_BY_KEY[task].instances`, never stored —
|
|
41
|
+
id-based so replays of old logs fail loudly instead of silently realizing a
|
|
42
|
+
reordered instance). `metadata["benchmark"] == "kitchenbench"` is the marker
|
|
43
|
+
the mock embodiment keys on to decide a scene is realize-able.
|
|
44
|
+
- `Categorical.sample` samples **by index** (never `rng.choice(values)`, which
|
|
45
|
+
coerces numeric tuples to strings). `derive_seed` comes from `inspect_robots.rollout`
|
|
46
|
+
(not re-exported at the top level).
|
|
47
|
+
|
|
48
|
+
## Adding a task
|
|
49
|
+
|
|
50
|
+
1. Author 5 `TaskInstance`s (distribution-based) and a `TaskSpec(...)`; append to
|
|
51
|
+
`SPECS` in `specs.py`. Every `{placeholder}` in a goal must be in
|
|
52
|
+
`language_vars` (and in `setup`).
|
|
53
|
+
2. Add an `@task("kitchenbench/<key>")` factory in `tasks.py`, add it to
|
|
54
|
+
`TASK_FACTORIES` + the `__init__` re-export.
|
|
55
|
+
3. Add the entry point to `pyproject.toml` under
|
|
56
|
+
`[project.entry-points."inspect_robots.tasks"]`.
|
|
57
|
+
4. Add the task key to KitchenBench's entry in WorldEvals' `catalog.py`.
|
|
58
|
+
|
|
59
|
+
`tests/test_specs.py` / `test_realize_all.py` parametrize over `SPECS` and realize
|
|
60
|
+
all 50 instances, so a new spec is exercised automatically — keep coverage at 100%.
|
kitchenbench/__init__.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""KitchenBench — a bimanual kitchen-manipulation benchmark for VLA models.
|
|
2
|
+
|
|
3
|
+
Built on `Inspect Robots <https://github.com/robocurve/inspect-robots>`_; the first member of
|
|
4
|
+
`WorldEvals <https://github.com/robocurve/worldevals>`_. Importing this package
|
|
5
|
+
registers all 10 tasks with the Inspect Robots registry (via the ``@task``
|
|
6
|
+
decorator). The mock :class:`~kitchenbench.embodiment.KitchenEmbodiment` and the
|
|
7
|
+
mock policies are not registered on import — Inspect Robots resolves them through
|
|
8
|
+
this package's entry points when it is installed (as it does the tasks, so no
|
|
9
|
+
import is needed for ``inspect-robots list``).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from kitchenbench.distributions import (
|
|
15
|
+
Categorical,
|
|
16
|
+
Constant,
|
|
17
|
+
Distribution,
|
|
18
|
+
Normal,
|
|
19
|
+
Uniform,
|
|
20
|
+
)
|
|
21
|
+
from kitchenbench.embodiment import KitchenEmbodiment
|
|
22
|
+
from kitchenbench.instances import (
|
|
23
|
+
K_EXPERTS,
|
|
24
|
+
K_INSTANCES,
|
|
25
|
+
K_REALIZATIONS,
|
|
26
|
+
Realization,
|
|
27
|
+
TaskInstance,
|
|
28
|
+
Validation,
|
|
29
|
+
)
|
|
30
|
+
from kitchenbench.policies import (
|
|
31
|
+
NoopKitchenPolicy,
|
|
32
|
+
RandomKitchenPolicy,
|
|
33
|
+
ScriptedKitchenPolicy,
|
|
34
|
+
)
|
|
35
|
+
from kitchenbench.scoring import task_success
|
|
36
|
+
from kitchenbench.specs import SPEC_BY_KEY, SPECS, TaskSpec
|
|
37
|
+
from kitchenbench.tasks import (
|
|
38
|
+
TASK_FACTORIES,
|
|
39
|
+
build_scenes,
|
|
40
|
+
fold_cloth,
|
|
41
|
+
handoff,
|
|
42
|
+
make_task,
|
|
43
|
+
open_container,
|
|
44
|
+
place_cutlery,
|
|
45
|
+
place_in_rack,
|
|
46
|
+
pour_pasta,
|
|
47
|
+
realize_scene,
|
|
48
|
+
scoop_pasta,
|
|
49
|
+
seal_container,
|
|
50
|
+
sort_cutlery,
|
|
51
|
+
stack,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
__version__ = "0.3.0"
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"K_EXPERTS",
|
|
58
|
+
"K_INSTANCES",
|
|
59
|
+
"K_REALIZATIONS",
|
|
60
|
+
"SPECS",
|
|
61
|
+
"SPEC_BY_KEY",
|
|
62
|
+
"TASK_FACTORIES",
|
|
63
|
+
"Categorical",
|
|
64
|
+
"Constant",
|
|
65
|
+
"Distribution",
|
|
66
|
+
"KitchenEmbodiment",
|
|
67
|
+
"NoopKitchenPolicy",
|
|
68
|
+
"Normal",
|
|
69
|
+
"RandomKitchenPolicy",
|
|
70
|
+
"Realization",
|
|
71
|
+
"ScriptedKitchenPolicy",
|
|
72
|
+
"TaskInstance",
|
|
73
|
+
"TaskSpec",
|
|
74
|
+
"Uniform",
|
|
75
|
+
"Validation",
|
|
76
|
+
"__version__",
|
|
77
|
+
"build_scenes",
|
|
78
|
+
"fold_cloth",
|
|
79
|
+
"handoff",
|
|
80
|
+
"make_task",
|
|
81
|
+
"open_container",
|
|
82
|
+
"place_cutlery",
|
|
83
|
+
"place_in_rack",
|
|
84
|
+
"pour_pasta",
|
|
85
|
+
"realize_scene",
|
|
86
|
+
"scoop_pasta",
|
|
87
|
+
"seal_container",
|
|
88
|
+
"sort_cutlery",
|
|
89
|
+
"stack",
|
|
90
|
+
"task_success",
|
|
91
|
+
]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Probability distributions for KitchenBench task-instance setups.
|
|
2
|
+
|
|
3
|
+
A task instance (see :mod:`kitchenbench.instances`) specifies a *stochastic*
|
|
4
|
+
environment: each setup variable is a :class:`Distribution`. A *realization*
|
|
5
|
+
samples every variable to produce one concrete environment. These types are pure
|
|
6
|
+
NumPy with no Inspect Robots dependency.
|
|
7
|
+
|
|
8
|
+
Distributions mirror the methodology's notation
|
|
9
|
+
(:doc:`reference/physical-automation-methodology-docs`):
|
|
10
|
+
|
|
11
|
+
- :class:`Uniform` — ``Uniform[a, b]`` (continuous)
|
|
12
|
+
- :class:`Categorical` — ``Categorical({…})`` (uniform or weighted over a finite set)
|
|
13
|
+
- :class:`Normal` — ``N(μ, σ²)`` (Gaussian; a 2-D jitter is two of these)
|
|
14
|
+
- :class:`Constant` — a fixed (non-random) part of a setup
|
|
15
|
+
|
|
16
|
+
Every ``sample`` returns a **builtin** ``float``/``int``/``str`` (never a NumPy
|
|
17
|
+
scalar) so realizations are JSON-native and mypy-strict clean.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Protocol, runtime_checkable
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
Scalar = float | int | str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@runtime_checkable
|
|
31
|
+
class Distribution(Protocol):
|
|
32
|
+
"""A sampleable, self-describing setup variable."""
|
|
33
|
+
|
|
34
|
+
def sample(self, rng: np.random.Generator) -> Scalar: ...
|
|
35
|
+
|
|
36
|
+
def describe(self) -> str: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class Uniform:
|
|
41
|
+
"""Continuous uniform on ``[low, high]``.
|
|
42
|
+
|
|
43
|
+
Sampling follows numpy's half-open convention (``high`` itself has probability
|
|
44
|
+
zero); ``describe()`` keeps the methodology's closed-interval notation.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
low: float
|
|
48
|
+
high: float
|
|
49
|
+
|
|
50
|
+
def sample(self, rng: np.random.Generator) -> float:
|
|
51
|
+
return float(rng.uniform(self.low, self.high))
|
|
52
|
+
|
|
53
|
+
def describe(self) -> str:
|
|
54
|
+
return f"Uniform[{_num(self.low)}, {_num(self.high)}]"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class Categorical:
|
|
59
|
+
"""Uniform (or weighted) choice over a finite set of values.
|
|
60
|
+
|
|
61
|
+
Samples **by index** so the value's type is preserved — ``rng.choice`` over a
|
|
62
|
+
numeric/mixed tuple would coerce to a string array.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
values: tuple[Scalar, ...]
|
|
66
|
+
weights: tuple[float, ...] | None = None
|
|
67
|
+
|
|
68
|
+
def __post_init__(self) -> None:
|
|
69
|
+
if not self.values:
|
|
70
|
+
raise ValueError("Categorical needs at least one value")
|
|
71
|
+
if self.weights is not None:
|
|
72
|
+
if len(self.weights) != len(self.values):
|
|
73
|
+
raise ValueError("weights must match values in length")
|
|
74
|
+
if any(w < 0 for w in self.weights):
|
|
75
|
+
raise ValueError("weights must be non-negative")
|
|
76
|
+
if sum(self.weights) <= 0:
|
|
77
|
+
raise ValueError("weights must sum to a positive value")
|
|
78
|
+
|
|
79
|
+
def sample(self, rng: np.random.Generator) -> Scalar:
|
|
80
|
+
probs = None if self.weights is None else np.asarray(self.weights, dtype=np.float64)
|
|
81
|
+
if probs is not None:
|
|
82
|
+
probs = probs / probs.sum()
|
|
83
|
+
idx = int(rng.choice(len(self.values), p=probs))
|
|
84
|
+
return self.values[idx]
|
|
85
|
+
|
|
86
|
+
def describe(self) -> str:
|
|
87
|
+
body = ", ".join(_num(v) if isinstance(v, int | float) else str(v) for v in self.values)
|
|
88
|
+
if self.weights is None:
|
|
89
|
+
return f"Categorical({{{body}}})"
|
|
90
|
+
wbody = ", ".join(_num(w) for w in self.weights)
|
|
91
|
+
return f"Categorical({{{body}}}; weights=[{wbody}])"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class Normal:
|
|
96
|
+
"""Gaussian ``N(mean, std²)``."""
|
|
97
|
+
|
|
98
|
+
mean: float
|
|
99
|
+
std: float
|
|
100
|
+
|
|
101
|
+
def sample(self, rng: np.random.Generator) -> float:
|
|
102
|
+
return float(rng.normal(self.mean, self.std))
|
|
103
|
+
|
|
104
|
+
def describe(self) -> str:
|
|
105
|
+
return f"N({_num(self.mean)}, {_num(self.std)}²)"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class Constant:
|
|
110
|
+
"""A fixed, non-random setup value."""
|
|
111
|
+
|
|
112
|
+
value: Scalar
|
|
113
|
+
|
|
114
|
+
def sample(self, rng: np.random.Generator) -> Scalar:
|
|
115
|
+
return self.value
|
|
116
|
+
|
|
117
|
+
def describe(self) -> str:
|
|
118
|
+
return repr(self.value)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _num(x: float) -> str:
|
|
122
|
+
"""Render a number without a trailing ``.0`` for whole values."""
|
|
123
|
+
return str(int(x)) if isinstance(x, float) and x.is_integer() else str(x)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""KitchenEmbodiment — a dependency-free abstract bimanual mock kitchen.
|
|
2
|
+
|
|
3
|
+
It does **not** simulate physics; like Inspect Robots's ``CubePick`` it models *progress
|
|
4
|
+
toward the scene goal* so the whole pipeline (scenes → chunked rollout → score →
|
|
5
|
+
log) runs in CI with no hardware. The value of KitchenBench is the task
|
|
6
|
+
definitions; this mock exists to exercise them and to serve as the template for a
|
|
7
|
+
real YAM-arms embodiment.
|
|
8
|
+
|
|
9
|
+
Each reset draws a hidden unit "goal direction" in the 6-D dual-arm command
|
|
10
|
+
subspace. Progress advances only when a normalized action aligns with that
|
|
11
|
+
direction (cosine ≥ ``align_threshold``) — so the scripted oracle (which reads the
|
|
12
|
+
privileged ``goal_dir``) succeeds, while random/no-op policies do not.
|
|
13
|
+
|
|
14
|
+
The action space is bimanual on purpose: ``(8,)`` = two arms x ``[dx, dy, dz,
|
|
15
|
+
gripper]``. A real YAM embodiment is a higher-DoF analog (compatibility checking
|
|
16
|
+
matches exact dims, so the real arm pairs with a real VLA, not with this mock).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from inspect_robots import (
|
|
23
|
+
Action,
|
|
24
|
+
ActionSemantics,
|
|
25
|
+
Box,
|
|
26
|
+
CameraSpec,
|
|
27
|
+
EmbodimentInfo,
|
|
28
|
+
Observation,
|
|
29
|
+
ObservationSpace,
|
|
30
|
+
Scene,
|
|
31
|
+
StepResult,
|
|
32
|
+
)
|
|
33
|
+
from inspect_robots.embodiment import PRIVILEGED_SUCCESS, RENDERABLE, SEEDABLE
|
|
34
|
+
|
|
35
|
+
from kitchenbench.tasks import realize_scene
|
|
36
|
+
|
|
37
|
+
_IMG = 24
|
|
38
|
+
|
|
39
|
+
_ACTION_SPACE = Box(
|
|
40
|
+
shape=(8,),
|
|
41
|
+
low=np.full(8, -1.0),
|
|
42
|
+
high=np.full(8, 1.0),
|
|
43
|
+
semantics=ActionSemantics(control_mode="eef_delta_pos", gripper="continuous", frame="world"),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class KitchenEmbodiment:
|
|
48
|
+
"""Abstract bimanual mock kitchen world."""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
step_size: float = 0.25,
|
|
54
|
+
align_threshold: float = 0.99,
|
|
55
|
+
goal_threshold: float = 1.0,
|
|
56
|
+
):
|
|
57
|
+
self.step_size = step_size
|
|
58
|
+
self.align_threshold = align_threshold
|
|
59
|
+
self.goal_threshold = goal_threshold
|
|
60
|
+
self.num_steps = 0
|
|
61
|
+
|
|
62
|
+
self._progress = 0.0
|
|
63
|
+
self._goal = np.zeros(6, dtype=np.float64)
|
|
64
|
+
self._last = np.zeros(8, dtype=np.float64)
|
|
65
|
+
self._instruction: str | None = None
|
|
66
|
+
self._rng = np.random.RandomState(0)
|
|
67
|
+
|
|
68
|
+
self.info = EmbodimentInfo(
|
|
69
|
+
name="kitchen",
|
|
70
|
+
action_space=_ACTION_SPACE,
|
|
71
|
+
observation_space=ObservationSpace(
|
|
72
|
+
cameras=(CameraSpec(name="overhead", height=_IMG, width=_IMG, channels=3),),
|
|
73
|
+
state_keys=frozenset({"progress", "goal_dir", "left_eef", "right_eef"}),
|
|
74
|
+
),
|
|
75
|
+
control_hz=10.0,
|
|
76
|
+
is_simulated=True,
|
|
77
|
+
capabilities=frozenset({SEEDABLE, RENDERABLE, PRIVILEGED_SUCCESS}),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def reset(self, scene: Scene, *, seed: int | None = None) -> Observation:
|
|
81
|
+
self._rng = np.random.RandomState(seed if seed is not None else 0)
|
|
82
|
+
goal = self._rng.normal(size=6)
|
|
83
|
+
self._goal = goal / (np.linalg.norm(goal) or 1.0)
|
|
84
|
+
self._progress = 0.0
|
|
85
|
+
self._last = np.zeros(8, dtype=np.float64)
|
|
86
|
+
self.num_steps = 0
|
|
87
|
+
# For a KitchenBench scene (the "benchmark" marker keeps scenes from other
|
|
88
|
+
# plugins out), realize the task instance for this seed so the observed
|
|
89
|
+
# instruction reflects the per-epoch realization (a real embodiment /
|
|
90
|
+
# operator would also arrange the sampled setup). The realization rng is
|
|
91
|
+
# independent of the goal_dir rng above. Bare scenes fall back unchanged.
|
|
92
|
+
# The instruction is carried on every subsequent observation — real VLA
|
|
93
|
+
# policies re-condition on it at each act() call.
|
|
94
|
+
self._instruction = scene.instruction
|
|
95
|
+
if scene.metadata.get("benchmark") == "kitchenbench":
|
|
96
|
+
self._instruction = realize_scene(scene, seed).instruction
|
|
97
|
+
return self._observe()
|
|
98
|
+
|
|
99
|
+
def step(self, action: Action) -> StepResult:
|
|
100
|
+
self.num_steps += 1
|
|
101
|
+
data = np.clip(np.asarray(action.data, dtype=np.float64), -1.0, 1.0)
|
|
102
|
+
self._last = data
|
|
103
|
+
arm = data[:6]
|
|
104
|
+
norm = float(np.linalg.norm(arm))
|
|
105
|
+
if norm > 0.0:
|
|
106
|
+
cosine = float(np.dot(arm / norm, self._goal))
|
|
107
|
+
if cosine >= self.align_threshold:
|
|
108
|
+
self._progress = min(self.goal_threshold, self._progress + self.step_size)
|
|
109
|
+
success = self._progress >= self.goal_threshold
|
|
110
|
+
return StepResult(
|
|
111
|
+
observation=self._observe(),
|
|
112
|
+
reward=self._progress - 1.0,
|
|
113
|
+
terminated=success,
|
|
114
|
+
termination_reason="success" if success else None,
|
|
115
|
+
truncated=False,
|
|
116
|
+
info={"success": success, "progress": self._progress},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def close(self) -> None:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
def _observe(self) -> Observation:
|
|
123
|
+
return Observation(
|
|
124
|
+
images={"overhead": self._render()},
|
|
125
|
+
state={
|
|
126
|
+
"progress": np.array([self._progress], dtype=np.float64),
|
|
127
|
+
"goal_dir": self._goal.copy(),
|
|
128
|
+
"left_eef": self._last[:3].copy(),
|
|
129
|
+
"right_eef": self._last[3:6].copy(),
|
|
130
|
+
},
|
|
131
|
+
instruction=self._instruction,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def _render(self) -> np.ndarray:
|
|
135
|
+
img = np.zeros((_IMG, _IMG, 3), dtype=np.uint8)
|
|
136
|
+
filled = round(self._progress * _IMG)
|
|
137
|
+
img[_IMG - filled :, :, 1] = 200 # a green progress bar rising from the bottom
|
|
138
|
+
return img
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Task instances — the unit of the physical-automation methodology.
|
|
2
|
+
|
|
3
|
+
A **task instance** is a self-contained scenario: a *stochastic environment
|
|
4
|
+
setup* (named :class:`~kitchenbench.distributions.Distribution` variables) plus a
|
|
5
|
+
*goal* (a natural-language success criterion that may reference sampled
|
|
6
|
+
variables). A **realization** samples every variable to produce one concrete
|
|
7
|
+
environment; the methodology runs ``K_REALIZATIONS`` realizations of each of
|
|
8
|
+
``K_INSTANCES`` instances per task and estimates the instance success probability
|
|
9
|
+
as the mean binary outcome.
|
|
10
|
+
|
|
11
|
+
This module is pure (no Inspect Robots import); :mod:`kitchenbench.tasks` maps instances
|
|
12
|
+
onto Inspect Robots ``Scene``/``Epochs``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
from kitchenbench.distributions import Distribution, Scalar
|
|
23
|
+
|
|
24
|
+
#: Methodology recommendations (see the reference docs).
|
|
25
|
+
K_REALIZATIONS = 5 # independent rollouts per instance -> success probability
|
|
26
|
+
K_INSTANCES = 5 # validated instances per task
|
|
27
|
+
K_EXPERTS = 3 # validators per instance
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Validation:
|
|
32
|
+
"""Per-expert validation ratings carried with an instance.
|
|
33
|
+
|
|
34
|
+
Empty until the human commissioning pipeline is run; :attr:`validated` follows
|
|
35
|
+
the methodology accept rule (``K_EXPERTS`` ratings, all >= 4 on both axes).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
representativeness: tuple[int, ...] = ()
|
|
39
|
+
quality: tuple[int, ...] = ()
|
|
40
|
+
difficulty: tuple[int, ...] = () # optional ("nice to have" in the methodology)
|
|
41
|
+
source: str = "opus-draft" # provenance: "opus-draft" (AI) vs "human"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def validated(self) -> bool:
|
|
45
|
+
return (
|
|
46
|
+
len(self.representativeness) >= K_EXPERTS
|
|
47
|
+
and len(self.quality) >= K_EXPERTS
|
|
48
|
+
and all(r >= 4 for r in self.representativeness)
|
|
49
|
+
and all(q >= 4 for q in self.quality)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class Realization:
|
|
55
|
+
"""One concrete environment sampled from a :class:`TaskInstance`."""
|
|
56
|
+
|
|
57
|
+
seed: int
|
|
58
|
+
values: dict[str, Scalar]
|
|
59
|
+
instruction: str
|
|
60
|
+
setup_lines: tuple[str, ...]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class TaskInstance:
|
|
65
|
+
"""A stochastic scenario: named distributions + a goal."""
|
|
66
|
+
|
|
67
|
+
instance_id: str
|
|
68
|
+
goal: str # template; ``{var}`` placeholders must be in ``language_vars``
|
|
69
|
+
setup: dict[str, Distribution]
|
|
70
|
+
target_kind: str
|
|
71
|
+
language_vars: tuple[str, ...] = ()
|
|
72
|
+
static: dict[str, Any] = field(default_factory=dict)
|
|
73
|
+
validation: Validation = field(default_factory=Validation)
|
|
74
|
+
|
|
75
|
+
def realize(self, seed: int) -> Realization:
|
|
76
|
+
"""Sample every setup variable (sorted-key order) for one concrete environment."""
|
|
77
|
+
rng = np.random.default_rng(seed)
|
|
78
|
+
values: dict[str, Scalar] = {key: self.setup[key].sample(rng) for key in sorted(self.setup)}
|
|
79
|
+
instruction = self.goal.format(**{k: values[k] for k in self.language_vars})
|
|
80
|
+
setup_lines = tuple(f"{key} = {values[key]}" for key in sorted(self.setup))
|
|
81
|
+
return Realization(
|
|
82
|
+
seed=seed, values=values, instruction=instruction, setup_lines=setup_lines
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def setup_spec(self) -> dict[str, str]:
|
|
86
|
+
"""A JSON-native description of the setup (``{var: distribution.describe()}``)."""
|
|
87
|
+
return {key: self.setup[key].describe() for key in sorted(self.setup)}
|
kitchenbench/policies.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Mock policies for the KitchenEmbodiment.
|
|
2
|
+
|
|
3
|
+
- :class:`ScriptedKitchenPolicy` — reads the privileged ``goal_dir`` and drives
|
|
4
|
+
straight to success (the CI oracle / template for a real VLA policy).
|
|
5
|
+
- :class:`RandomKitchenPolicy` — random actions; effectively never aligns, so it
|
|
6
|
+
fails. Deterministic given the construction seed.
|
|
7
|
+
- :class:`NoopKitchenPolicy` — zero actions; never moves.
|
|
8
|
+
|
|
9
|
+
All emit :class:`~inspect_robots.ActionChunk`\\ s with ``H > 1`` to exercise open-loop
|
|
10
|
+
chunk execution.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
from inspect_robots import (
|
|
17
|
+
Action,
|
|
18
|
+
ActionChunk,
|
|
19
|
+
ActionSemantics,
|
|
20
|
+
Box,
|
|
21
|
+
Observation,
|
|
22
|
+
ObservationSpace,
|
|
23
|
+
PolicyConfig,
|
|
24
|
+
PolicyInfo,
|
|
25
|
+
Scene,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_ACTION_SPACE = Box(
|
|
29
|
+
shape=(8,),
|
|
30
|
+
semantics=ActionSemantics(control_mode="eef_delta_pos", gripper="continuous", frame="world"),
|
|
31
|
+
)
|
|
32
|
+
# The scripted oracle needs the privileged goal direction.
|
|
33
|
+
_SCRIPTED_OBS = ObservationSpace(state_keys=frozenset({"goal_dir"}))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ScriptedKitchenPolicy:
|
|
37
|
+
"""Drive both arms along the privileged ``goal_dir`` (deterministic success)."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, *, chunk_size: int = 4):
|
|
40
|
+
self.chunk_size = chunk_size
|
|
41
|
+
self.num_inferences = 0
|
|
42
|
+
self.info = PolicyInfo(
|
|
43
|
+
name="kitchen_scripted", action_space=_ACTION_SPACE, observation_space=_SCRIPTED_OBS
|
|
44
|
+
)
|
|
45
|
+
self.config = PolicyConfig(action_horizon=chunk_size)
|
|
46
|
+
|
|
47
|
+
def reset(self, scene: Scene) -> None:
|
|
48
|
+
self.num_inferences = 0
|
|
49
|
+
|
|
50
|
+
def act(self, observation: Observation) -> ActionChunk:
|
|
51
|
+
self.num_inferences += 1
|
|
52
|
+
goal = np.asarray(observation.state["goal_dir"], dtype=np.float64)
|
|
53
|
+
data = np.concatenate([goal, np.array([1.0, 1.0])]) # arms aligned; grippers closed
|
|
54
|
+
return ActionChunk(actions=[Action(data=data.copy()) for _ in range(self.chunk_size)])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RandomKitchenPolicy:
|
|
58
|
+
"""Emit random actions. Deterministic given the construction seed."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, *, chunk_size: int = 4, seed: int = 0):
|
|
61
|
+
self.chunk_size = chunk_size
|
|
62
|
+
self.num_inferences = 0
|
|
63
|
+
self._base_seed = seed
|
|
64
|
+
self._reset_count = 0
|
|
65
|
+
self._rng = np.random.RandomState(seed)
|
|
66
|
+
self.info = PolicyInfo(name="kitchen_random", action_space=_ACTION_SPACE)
|
|
67
|
+
self.config = PolicyConfig(action_horizon=chunk_size)
|
|
68
|
+
|
|
69
|
+
def reset(self, scene: Scene) -> None:
|
|
70
|
+
self._rng = np.random.RandomState(self._base_seed + self._reset_count)
|
|
71
|
+
self._reset_count += 1
|
|
72
|
+
self.num_inferences = 0
|
|
73
|
+
|
|
74
|
+
def act(self, observation: Observation) -> ActionChunk:
|
|
75
|
+
self.num_inferences += 1
|
|
76
|
+
actions = [
|
|
77
|
+
Action(data=self._rng.uniform(-1.0, 1.0, size=8)) for _ in range(self.chunk_size)
|
|
78
|
+
]
|
|
79
|
+
return ActionChunk(actions=actions)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class NoopKitchenPolicy:
|
|
83
|
+
"""Emit zero actions; never moves."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, *, chunk_size: int = 1):
|
|
86
|
+
self.chunk_size = chunk_size
|
|
87
|
+
self.num_inferences = 0
|
|
88
|
+
self.info = PolicyInfo(name="kitchen_noop", action_space=_ACTION_SPACE)
|
|
89
|
+
self.config = PolicyConfig(action_horizon=chunk_size)
|
|
90
|
+
|
|
91
|
+
def reset(self, scene: Scene) -> None:
|
|
92
|
+
self.num_inferences = 0
|
|
93
|
+
|
|
94
|
+
def act(self, observation: Observation) -> ActionChunk:
|
|
95
|
+
self.num_inferences += 1
|
|
96
|
+
return ActionChunk(actions=[Action(data=np.zeros(8)) for _ in range(self.chunk_size)])
|
kitchenbench/py.typed
ADDED
|
File without changes
|
kitchenbench/scoring.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""KitchenBench scoring.
|
|
2
|
+
|
|
3
|
+
``task_success`` works both in the dependency-free mock (which reports privileged
|
|
4
|
+
success via the termination reason) and on real hardware: there is no success
|
|
5
|
+
oracle on a real kitchen, so the real embodiment turns the operator's
|
|
6
|
+
confirmation into either a ``"success"`` termination reason or a recorded
|
|
7
|
+
operator verdict — ``task_success`` accepts either.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from inspect_robots import Score, Scorer, Target, TrialRecord
|
|
15
|
+
|
|
16
|
+
# Affirmative operator verdicts (case-insensitive), for real-world runs. Mirrors
|
|
17
|
+
# inspect_robots.scorer._OPERATOR_SUCCESS (private there) — keep the two in sync.
|
|
18
|
+
_AFFIRMATIVE = frozenset({"success", "pass", "yes", "y", "1", "true"})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class _TaskSuccess:
|
|
23
|
+
name: str = "task_success"
|
|
24
|
+
|
|
25
|
+
def __call__(self, record: TrialRecord, target: Target | None) -> Score:
|
|
26
|
+
if record.termination_reason == "success":
|
|
27
|
+
return Score(value=True, explanation="terminated with success")
|
|
28
|
+
verdict = record.operator_judgement
|
|
29
|
+
if verdict is not None and verdict.strip().lower() in _AFFIRMATIVE:
|
|
30
|
+
return Score(value=True, explanation=f"operator verdict: {verdict!r}")
|
|
31
|
+
return Score(value=False, explanation="no success signal")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def task_success() -> Scorer:
|
|
35
|
+
"""Success iff the trial terminated successfully or an operator confirmed it."""
|
|
36
|
+
return _TaskSuccess()
|