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 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%.
@@ -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)}
@@ -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
@@ -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()