Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ success probability **P̂[Yᵢ=1]**. Instances are AI-authored drafts
(strict), `pytest --cov` at **100% coverage**. Pre-commit runs ruff+mypy on
commit and the coverage gate on push (via `uv run`). CI (Linux+macOS ×
py3.11/3.12) and the 100% gate are **required, blocking PR checks**.
Every public module, class, and function needs a docstring, enforced by Ruff
D1; state the contract instead of restating the symbol name.
- **Authoring imports come from the top-level `inspect_robots` package** (its public
API, stable since v0.3.0): `from inspect_robots import Task, Scene, Target, task,
ActionChunk, ...`. Don't import from `inspect_robots.<submodule>` unless a symbol
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,9 @@ uv run pytest --cov # 100% coverage required
uv run ruff check . && uv run mypy
```

Every public module, class, and function needs a docstring, enforced by Ruff D1;
state the contract instead of restating the symbol name.

## Citation

If you use KitchenBench in your research, please cite it:
Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ target-version = "py310"
src = ["src", "tests"]

[tool.ruff.lint]
select = ["E", "F", "W", "I", "UP", "B", "C4", "SIM", "RUF"]
select = ["E", "F", "W", "I", "UP", "B", "C4", "SIM", "RUF", "D1"]
ignore = ["D105", "D107"]

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["D1"]

[tool.ruff.lint.isort]
known-first-party = ["kitchenbench"]
Expand Down
16 changes: 14 additions & 2 deletions src/kitchenbench/distributions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@
class Distribution(Protocol):
"""A sampleable, self-describing setup variable."""

def sample(self, rng: np.random.Generator) -> Scalar: ...
def sample(self, rng: np.random.Generator) -> Scalar:
"""Draw one JSON-native scalar from the distribution."""
...

def describe(self) -> str: ...
def describe(self) -> str:
"""Render the distribution in physical-automation methodology notation."""
...


@dataclass(frozen=True)
Expand All @@ -48,9 +52,11 @@ class Uniform:
high: float

def sample(self, rng: np.random.Generator) -> float:
"""Draw a builtin float using NumPy's half-open interval convention."""
return float(rng.uniform(self.low, self.high))

def describe(self) -> str:
"""Render the closed-interval notation used by the methodology."""
return f"Uniform[{_num(self.low)}, {_num(self.high)}]"


Expand All @@ -77,13 +83,15 @@ def __post_init__(self) -> None:
raise ValueError("weights must sum to a positive value")

def sample(self, rng: np.random.Generator) -> Scalar:
"""Draw by index so mixed-category scalar types are preserved."""
probs = None if self.weights is None else np.asarray(self.weights, dtype=np.float64)
if probs is not None:
probs = probs / probs.sum()
idx = int(rng.choice(len(self.values), p=probs))
return self.values[idx]

def describe(self) -> str:
"""Render the finite support and optional weights in methodology notation."""
body = ", ".join(_num(v) if isinstance(v, int | float) else str(v) for v in self.values)
if self.weights is None:
return f"Categorical({{{body}}})"
Expand All @@ -99,9 +107,11 @@ class Normal:
std: float

def sample(self, rng: np.random.Generator) -> float:
"""Draw a builtin float from the configured Gaussian."""
return float(rng.normal(self.mean, self.std))

def describe(self) -> str:
"""Render mean and variance notation from the configured standard deviation."""
return f"N({_num(self.mean)}, {_num(self.std)}²)"


Expand All @@ -112,9 +122,11 @@ class Constant:
value: Scalar

def sample(self, rng: np.random.Generator) -> Scalar:
"""Return the fixed value without consuming randomness."""
return self.value

def describe(self) -> str:
"""Render the fixed value as a Python literal."""
return repr(self.value)


Expand Down
3 changes: 3 additions & 0 deletions src/kitchenbench/embodiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def __init__(
)

def reset(self, scene: Scene, *, seed: int | None = None) -> Observation:
"""Seed the hidden goal and expose the scene's per-epoch instruction."""
self._rng = np.random.RandomState(seed if seed is not None else 0)
goal = self._rng.normal(size=6)
self._goal = goal / (np.linalg.norm(goal) or 1.0)
Expand All @@ -112,6 +113,7 @@ def reset(self, scene: Scene, *, seed: int | None = None) -> Observation:
return self._observe()

def step(self, action: Action) -> StepResult:
"""Advance progress when the dual-arm command aligns with the hidden goal."""
self.num_steps += 1
data = np.clip(np.asarray(action.data, dtype=np.float64), -1.0, 1.0)
self._last = data
Expand All @@ -132,6 +134,7 @@ def step(self, action: Action) -> StepResult:
)

def close(self) -> None:
"""Release no resources because the mock has no external state."""
return None

def _observe(self) -> Observation:
Expand Down
1 change: 1 addition & 0 deletions src/kitchenbench/instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class Validation:

@property
def validated(self) -> bool:
"""Whether the ratings meet the methodology's expert count and accept threshold."""
return (
len(self.representativeness) >= K_EXPERTS
and len(self.quality) >= K_EXPERTS
Expand Down
6 changes: 6 additions & 0 deletions src/kitchenbench/policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ def __init__(self, *, chunk_size: int = 4):
self.config = PolicyConfig(action_horizon=chunk_size)

def reset(self, scene: Scene) -> None:
"""Clear inference accounting for a new scene."""
self.num_inferences = 0

def act(self, observation: Observation) -> ActionChunk:
"""Use the privileged goal direction to emit one aligned action chunk."""
self.num_inferences += 1
goal = np.asarray(observation.state["goal_dir"], dtype=np.float64)
data = np.concatenate([goal, np.array([1.0, 1.0])]) # arms aligned; grippers closed
Expand All @@ -67,11 +69,13 @@ def __init__(self, *, chunk_size: int = 4, seed: int = 0):
self.config = PolicyConfig(action_horizon=chunk_size)

def reset(self, scene: Scene) -> None:
"""Start the next scene's deterministic per-reset random stream."""
self._rng = np.random.RandomState(self._base_seed + self._reset_count)
self._reset_count += 1
self.num_inferences = 0

def act(self, observation: Observation) -> ActionChunk:
"""Draw one open-loop chunk from the current scene's random stream."""
self.num_inferences += 1
actions = [
Action(data=self._rng.uniform(-1.0, 1.0, size=8)) for _ in range(self.chunk_size)
Expand All @@ -89,8 +93,10 @@ def __init__(self, *, chunk_size: int = 1):
self.config = PolicyConfig(action_horizon=chunk_size)

def reset(self, scene: Scene) -> None:
"""Clear inference accounting for a new scene."""
self.num_inferences = 0

def act(self, observation: Observation) -> ActionChunk:
"""Emit a zero-filled chunk with the configured horizon."""
self.num_inferences += 1
return ActionChunk(actions=[Action(data=np.zeros(8)) for _ in range(self.chunk_size)])
1 change: 1 addition & 0 deletions src/kitchenbench/sim/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class SceneBlueprint:
values: Mapping[str, Scalar]

def object_names(self) -> tuple[str, ...]:
"""Return the object names in spawn order."""
return tuple(obj.name for obj in self.objects)


Expand Down
2 changes: 2 additions & 0 deletions src/kitchenbench/sim/success.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ def opening_fraction(self, name: str) -> float:

@dataclass(frozen=True)
class Verdict:
"""Result of a quantitative check, paired with a human-readable reason."""

success: bool
explanation: str

Expand Down
10 changes: 10 additions & 0 deletions src/kitchenbench/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,51 +112,61 @@ def make_task(spec: TaskSpec) -> Task:

@task("kitchenbench/place_cutlery")
def place_cutlery() -> Task:
"""Build the registered cutlery placement task from its stochastic instances."""
return make_task(SPEC_BY_KEY["place_cutlery"])


@task("kitchenbench/stack")
def stack() -> Task:
"""Build the registered dishware stacking task from its stochastic instances."""
return make_task(SPEC_BY_KEY["stack"])


@task("kitchenbench/place_in_rack")
def place_in_rack() -> Task:
"""Build the registered rack insertion task from its stochastic instances."""
return make_task(SPEC_BY_KEY["place_in_rack"])


@task("kitchenbench/pour_pasta")
def pour_pasta() -> Task:
"""Build the registered granular pouring task from its stochastic instances."""
return make_task(SPEC_BY_KEY["pour_pasta"])


@task("kitchenbench/open_container")
def open_container() -> Task:
"""Build the registered container opening task from its stochastic instances."""
return make_task(SPEC_BY_KEY["open_container"])


@task("kitchenbench/fold_cloth")
def fold_cloth() -> Task:
"""Build the registered cloth folding task from its stochastic instances."""
return make_task(SPEC_BY_KEY["fold_cloth"])


@task("kitchenbench/seal_container")
def seal_container() -> Task:
"""Build the registered lid sealing task from its stochastic instances."""
return make_task(SPEC_BY_KEY["seal_container"])


@task("kitchenbench/handoff")
def handoff() -> Task:
"""Build the registered two-arm handoff task from its stochastic instances."""
return make_task(SPEC_BY_KEY["handoff"])


@task("kitchenbench/sort_cutlery")
def sort_cutlery() -> Task:
"""Build the registered cutlery sorting task from its stochastic instances."""
return make_task(SPEC_BY_KEY["sort_cutlery"])


@task("kitchenbench/scoop_pasta")
def scoop_pasta() -> Task:
"""Build the registered measured scooping task from its stochastic instances."""
return make_task(SPEC_BY_KEY["scoop_pasta"])


Expand Down
Loading