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/specs.py ADDED
@@ -0,0 +1,714 @@
1
+ """Declarative specifications for the KitchenBench tasks — the single source of truth.
2
+
3
+ Each :class:`TaskSpec` carries exactly ``K_INSTANCES`` (5) :class:`TaskInstance`\\ s
4
+ (see :mod:`kitchenbench.instances`): self-contained scenarios whose environment
5
+ setups are **distributions**. :mod:`kitchenbench.tasks` turns each instance into a
6
+ Inspect Robots ``Scene`` and runs ``K_REALIZATIONS`` (5) realizations per instance.
7
+
8
+ Instances are AI-authored drafts (``Validation(source="opus-draft")``) and are
9
+ **not yet human-validated** — the methodology requires K_EXPERTS=3 reviewers
10
+ (representativeness & quality >= 4) before the instances are trustworthy.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+
17
+ from kitchenbench.distributions import Categorical, Constant, Normal, Uniform
18
+ from kitchenbench.instances import TaskInstance
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class TaskSpec:
23
+ """One KitchenBench task and its (distribution-based) task instances."""
24
+
25
+ key: str
26
+ title: str
27
+ category: str
28
+ bimanual: bool
29
+ max_steps: int
30
+ instances: tuple[TaskInstance, ...]
31
+ version: str = "1"
32
+ description: str = ""
33
+
34
+
35
+ # Common reusable distributions.
36
+ def _jitter(sigma_cm: float) -> Normal:
37
+ """A 1-axis centroid jitter ~ N(0, sigma²) in cm."""
38
+ return Normal(0.0, sigma_cm)
39
+
40
+
41
+ _PLACE_CUTLERY = (
42
+ TaskInstance(
43
+ instance_id="place_cutlery/spoon-on-plate",
44
+ goal="place the {cutlery} on the {dishware}",
45
+ setup={
46
+ "cutlery": Categorical(("spoon", "fork", "knife")),
47
+ "dishware": Categorical(("plate", "bowl", "napkin")),
48
+ "cutlery_x_cm": Uniform(-20, -5),
49
+ "dishware_x_cm": Uniform(5, 20),
50
+ "jitter_y_cm": _jitter(2.0),
51
+ },
52
+ language_vars=("cutlery", "dishware"),
53
+ target_kind="place_on",
54
+ ),
55
+ TaskInstance(
56
+ instance_id="place_cutlery/from-drawer",
57
+ goal="place the {cutlery} on the plate",
58
+ setup={
59
+ "cutlery": Categorical(("spoon", "fork", "knife")),
60
+ "approach_angle_deg": Uniform(-30, 30),
61
+ "plate_x_cm": Normal(8.0, 2.0),
62
+ "plate_y_cm": _jitter(2.5),
63
+ },
64
+ language_vars=("cutlery",),
65
+ target_kind="place_on",
66
+ ),
67
+ TaskInstance(
68
+ instance_id="place_cutlery/cluttered-bench",
69
+ goal="place the fork on the {dishware}",
70
+ setup={
71
+ "dishware": Categorical(("plate", "bowl")),
72
+ "distractor_count": Categorical((1, 2, 3)),
73
+ "fork_x_cm": Uniform(-18, -8),
74
+ "fork_yaw_deg": Uniform(0, 90),
75
+ },
76
+ language_vars=("dishware",),
77
+ target_kind="place_on",
78
+ ),
79
+ TaskInstance(
80
+ instance_id="place_cutlery/napkin-soft",
81
+ goal="place the {cutlery} on the napkin",
82
+ setup={
83
+ "cutlery": Categorical(("spoon", "fork", "knife")),
84
+ "napkin_x_cm": Normal(10.0, 3.0),
85
+ "napkin_y_cm": _jitter(3.0),
86
+ "napkin_rotation_deg": Uniform(0, 45),
87
+ },
88
+ language_vars=("cutlery",),
89
+ target_kind="place_on",
90
+ ),
91
+ TaskInstance(
92
+ instance_id="place_cutlery/far-reach",
93
+ goal="place the knife on the {dishware}",
94
+ setup={
95
+ "dishware": Categorical(("plate", "bowl", "napkin")),
96
+ "dishware_x_cm": Uniform(20, 30),
97
+ "knife_x_cm": Uniform(-25, -15),
98
+ "jitter_y_cm": _jitter(2.0),
99
+ },
100
+ language_vars=("dishware",),
101
+ target_kind="place_on",
102
+ ),
103
+ )
104
+
105
+ _STACK = (
106
+ TaskInstance(
107
+ instance_id="stack/cups",
108
+ goal="stack the cups",
109
+ setup={
110
+ "count": Categorical((2, 3, 4)),
111
+ "spread_cm": Uniform(5, 15),
112
+ "jitter_x_cm": _jitter(2.0),
113
+ "jitter_y_cm": _jitter(2.0),
114
+ },
115
+ target_kind="stack",
116
+ ),
117
+ TaskInstance(
118
+ instance_id="stack/bowls",
119
+ goal="stack the bowls",
120
+ setup={
121
+ "count": Categorical((2, 3)),
122
+ "diameter_cm": Categorical((12, 15, 18)),
123
+ "spread_cm": Uniform(8, 18),
124
+ },
125
+ target_kind="stack",
126
+ ),
127
+ TaskInstance(
128
+ instance_id="stack/plates",
129
+ goal="stack the plates",
130
+ setup={
131
+ "count": Categorical((2, 3, 4, 5)),
132
+ "plate_x_cm": Normal(0.0, 3.0),
133
+ "plate_y_cm": _jitter(3.0),
134
+ },
135
+ target_kind="stack",
136
+ ),
137
+ TaskInstance(
138
+ instance_id="stack/mixed-sizes",
139
+ goal="stack the bowls",
140
+ setup={
141
+ "count": Categorical((3, 4)),
142
+ "size_order": Categorical(("largest_first", "shuffled")),
143
+ "spread_cm": Uniform(10, 20),
144
+ },
145
+ target_kind="stack",
146
+ ),
147
+ TaskInstance(
148
+ instance_id="stack/tight-spacing",
149
+ goal="stack the cups",
150
+ setup={
151
+ "count": Categorical((2, 3)),
152
+ "spread_cm": Uniform(3, 8),
153
+ "jitter_x_cm": _jitter(1.0),
154
+ },
155
+ target_kind="stack",
156
+ ),
157
+ )
158
+
159
+ _PLACE_IN_RACK = (
160
+ TaskInstance(
161
+ instance_id="place_in_rack/plate",
162
+ goal="place the {dishware} into the dish rack",
163
+ setup={
164
+ "dishware": Categorical(("plate", "bowl", "cup")),
165
+ "slot_index": Categorical((1, 2, 3, 4)),
166
+ "rack_x_cm": Normal(12.0, 2.0),
167
+ "dishware_yaw_deg": Uniform(0, 90),
168
+ },
169
+ language_vars=("dishware",),
170
+ target_kind="place_in",
171
+ ),
172
+ TaskInstance(
173
+ instance_id="place_in_rack/bowl-tilt",
174
+ goal="place the bowl into the dish rack",
175
+ setup={
176
+ "slot_index": Categorical((1, 2, 3)),
177
+ "approach_tilt_deg": Uniform(0, 25),
178
+ "rack_y_cm": _jitter(2.0),
179
+ },
180
+ target_kind="place_in",
181
+ ),
182
+ TaskInstance(
183
+ instance_id="place_in_rack/cup-narrow",
184
+ goal="place the cup into the dish rack",
185
+ setup={
186
+ "slot_index": Categorical((1, 2, 3, 4, 5)),
187
+ "slot_width_cm": Categorical((6, 8)),
188
+ "cup_x_cm": Uniform(-15, -5),
189
+ },
190
+ target_kind="place_in",
191
+ ),
192
+ TaskInstance(
193
+ instance_id="place_in_rack/rightmost-slot",
194
+ goal="place the {dishware} into the dish rack",
195
+ setup={
196
+ "dishware": Categorical(("plate", "bowl")),
197
+ "slot_index": Constant(4),
198
+ "rack_x_cm": Uniform(10, 18),
199
+ },
200
+ language_vars=("dishware",),
201
+ target_kind="place_in",
202
+ ),
203
+ TaskInstance(
204
+ instance_id="place_in_rack/wet-slippery",
205
+ goal="place the plate into the dish rack",
206
+ setup={
207
+ "slot_index": Categorical((1, 2, 3, 4)),
208
+ "surface_friction": Categorical(("low", "medium")),
209
+ "plate_x_cm": Normal(-10.0, 2.0),
210
+ },
211
+ target_kind="place_in",
212
+ ),
213
+ )
214
+
215
+ _POUR_PASTA = (
216
+ TaskInstance(
217
+ instance_id="pour_pasta/measuring-cup-to-bowl",
218
+ goal="pour the dry pasta into the {vessel}",
219
+ setup={
220
+ "vessel": Categorical(("bowl", "cup", "pot")),
221
+ "fill_g": Uniform(80, 200),
222
+ "vessel_x_cm": Normal(0.0, 3.0),
223
+ "vessel_y_cm": _jitter(3.0),
224
+ "pour_height_cm": Uniform(8, 15),
225
+ },
226
+ language_vars=("vessel",),
227
+ target_kind="pour_into",
228
+ static={"substance": "dry_pasta"},
229
+ ),
230
+ TaskInstance(
231
+ instance_id="pour_pasta/full-box",
232
+ goal="pour the dry pasta into the pot",
233
+ setup={
234
+ "fill_g": Uniform(300, 500),
235
+ "pour_angle_deg": Uniform(45, 80),
236
+ "pot_x_cm": Normal(5.0, 2.0),
237
+ },
238
+ target_kind="pour_into",
239
+ static={"substance": "dry_pasta"},
240
+ ),
241
+ TaskInstance(
242
+ instance_id="pour_pasta/narrow-cup",
243
+ goal="pour the dry pasta into the cup",
244
+ setup={
245
+ "fill_g": Uniform(40, 120),
246
+ "cup_diameter_cm": Categorical((6, 8)),
247
+ "pour_height_cm": Uniform(5, 10),
248
+ },
249
+ target_kind="pour_into",
250
+ static={"substance": "dry_pasta"},
251
+ ),
252
+ TaskInstance(
253
+ instance_id="pour_pasta/steady-and-pour",
254
+ goal="pour the dry pasta into the {vessel}",
255
+ setup={
256
+ "vessel": Categorical(("bowl", "pot")),
257
+ "fill_g": Uniform(150, 350),
258
+ "steadying_force_n": Uniform(2, 6),
259
+ "vessel_x_cm": _jitter(2.5),
260
+ },
261
+ language_vars=("vessel",),
262
+ target_kind="pour_into",
263
+ static={"substance": "dry_pasta"},
264
+ ),
265
+ TaskInstance(
266
+ instance_id="pour_pasta/long-spaghetti",
267
+ goal="pour the dry pasta into the pot",
268
+ setup={
269
+ "fill_g": Uniform(200, 400),
270
+ "strand_length_cm": Categorical((24, 26)),
271
+ "pot_y_cm": _jitter(3.0),
272
+ },
273
+ target_kind="pour_into",
274
+ static={"substance": "dry_pasta"},
275
+ ),
276
+ )
277
+
278
+ _OPEN_CONTAINER = (
279
+ TaskInstance(
280
+ instance_id="open_container/jar",
281
+ goal="open the {container}",
282
+ setup={
283
+ "container": Categorical(("jar", "bottle", "food container")),
284
+ "lid_torque_nm": Uniform(0.5, 2.5),
285
+ "container_x_cm": Normal(0.0, 2.0),
286
+ "container_yaw_deg": Uniform(0, 90),
287
+ },
288
+ language_vars=("container",),
289
+ target_kind="open",
290
+ ),
291
+ TaskInstance(
292
+ instance_id="open_container/bottle-cap",
293
+ goal="open the bottle",
294
+ setup={
295
+ "cap_turns": Categorical((1, 2, 3)),
296
+ "lid_torque_nm": Uniform(0.3, 1.5),
297
+ "bottle_x_cm": _jitter(2.0),
298
+ },
299
+ target_kind="open",
300
+ ),
301
+ TaskInstance(
302
+ instance_id="open_container/snap-lid",
303
+ goal="open the food container",
304
+ setup={
305
+ "clip_count": Categorical((2, 4)),
306
+ "pry_force_n": Uniform(5, 20),
307
+ "container_y_cm": _jitter(2.5),
308
+ },
309
+ target_kind="open",
310
+ ),
311
+ TaskInstance(
312
+ instance_id="open_container/stiff-jar",
313
+ goal="open the jar",
314
+ setup={
315
+ "lid_torque_nm": Uniform(2.0, 4.0),
316
+ "brace_force_n": Uniform(8, 20),
317
+ "jar_diameter_cm": Categorical((6, 8, 10)),
318
+ },
319
+ target_kind="open",
320
+ ),
321
+ TaskInstance(
322
+ instance_id="open_container/tilted",
323
+ goal="open the {container}",
324
+ setup={
325
+ "container": Categorical(("jar", "bottle")),
326
+ "tilt_deg": Uniform(0, 30),
327
+ "lid_torque_nm": Uniform(0.5, 2.0),
328
+ },
329
+ language_vars=("container",),
330
+ target_kind="open",
331
+ ),
332
+ )
333
+
334
+ _FOLD_CLOTH = (
335
+ TaskInstance(
336
+ instance_id="fold_cloth/dish-towel",
337
+ goal="fold the {cloth}",
338
+ setup={
339
+ "cloth": Categorical(("dish towel", "napkin", "cloth")),
340
+ "size_cm": Categorical((30, 40, 50)),
341
+ "initial_rotation_deg": Uniform(0, 90),
342
+ "slack": Uniform(0.0, 0.4),
343
+ },
344
+ language_vars=("cloth",),
345
+ target_kind="fold",
346
+ ),
347
+ TaskInstance(
348
+ instance_id="fold_cloth/napkin-half",
349
+ goal="fold the napkin",
350
+ setup={
351
+ "fold_count": Categorical((1, 2)),
352
+ "napkin_x_cm": Normal(0.0, 2.0),
353
+ "napkin_y_cm": _jitter(2.0),
354
+ },
355
+ target_kind="fold",
356
+ ),
357
+ TaskInstance(
358
+ instance_id="fold_cloth/large-cloth",
359
+ goal="fold the cloth",
360
+ setup={
361
+ "size_cm": Categorical((50, 60, 70)),
362
+ "slack": Uniform(0.2, 0.6),
363
+ "corner_offset_cm": _jitter(4.0),
364
+ },
365
+ target_kind="fold",
366
+ ),
367
+ TaskInstance(
368
+ instance_id="fold_cloth/crumpled-start",
369
+ goal="fold the {cloth}",
370
+ setup={
371
+ "cloth": Categorical(("dish towel", "cloth")),
372
+ "crumple_level": Categorical(("light", "moderate")),
373
+ "initial_rotation_deg": Uniform(0, 180),
374
+ },
375
+ language_vars=("cloth",),
376
+ target_kind="fold",
377
+ ),
378
+ TaskInstance(
379
+ instance_id="fold_cloth/thirds",
380
+ goal="fold the dish towel",
381
+ setup={
382
+ "fold_count": Constant(2),
383
+ "size_cm": Categorical((40, 50)),
384
+ "slack": Uniform(0.0, 0.3),
385
+ },
386
+ target_kind="fold",
387
+ ),
388
+ )
389
+
390
+ _SEAL_CONTAINER = (
391
+ TaskInstance(
392
+ instance_id="seal_container/food-container",
393
+ goal="seal the {container} with its lid",
394
+ setup={
395
+ "container": Categorical(("food container", "pot", "jar")),
396
+ "lid_offset_cm": _jitter(2.0),
397
+ "press_force_n": Uniform(5, 20),
398
+ "lid_yaw_deg": Uniform(0, 45),
399
+ },
400
+ language_vars=("container",),
401
+ target_kind="seal",
402
+ ),
403
+ TaskInstance(
404
+ instance_id="seal_container/twist-lid-jar",
405
+ goal="seal the jar with its lid",
406
+ setup={
407
+ "thread_turns": Categorical((1, 2, 3)),
408
+ "seat_torque_nm": Uniform(0.5, 2.0),
409
+ "jar_x_cm": Normal(0.0, 2.0),
410
+ },
411
+ target_kind="seal",
412
+ ),
413
+ TaskInstance(
414
+ instance_id="seal_container/snap-on-pot",
415
+ goal="seal the pot with its lid",
416
+ setup={
417
+ "clip_count": Categorical((2, 4)),
418
+ "press_force_n": Uniform(10, 30),
419
+ "pot_y_cm": _jitter(2.5),
420
+ },
421
+ target_kind="seal",
422
+ ),
423
+ TaskInstance(
424
+ instance_id="seal_container/misaligned-lid",
425
+ goal="seal the {container} with its lid",
426
+ setup={
427
+ "container": Categorical(("food container", "pot")),
428
+ "lid_offset_cm": Uniform(2, 5),
429
+ "lid_yaw_deg": Uniform(15, 60),
430
+ },
431
+ language_vars=("container",),
432
+ target_kind="seal",
433
+ ),
434
+ TaskInstance(
435
+ instance_id="seal_container/tight-fit",
436
+ goal="seal the food container with its lid",
437
+ setup={
438
+ "clearance_mm": Categorical((1, 2)),
439
+ "press_force_n": Uniform(15, 35),
440
+ "lid_offset_cm": _jitter(1.0),
441
+ },
442
+ target_kind="seal",
443
+ ),
444
+ )
445
+
446
+ _HANDOFF = (
447
+ TaskInstance(
448
+ instance_id="handoff/utensil",
449
+ goal="hand off the {item} from one arm to the other",
450
+ setup={
451
+ "item": Categorical(("utensil", "cup", "produce item")),
452
+ "pickup_x_cm": Uniform(-25, -10),
453
+ "handoff_height_cm": Uniform(15, 30),
454
+ "item_yaw_deg": Uniform(0, 90),
455
+ },
456
+ language_vars=("item",),
457
+ target_kind="handoff",
458
+ ),
459
+ TaskInstance(
460
+ instance_id="handoff/cup-upright",
461
+ goal="hand off the cup from one arm to the other",
462
+ setup={
463
+ "fill_level": Categorical(("empty", "half")),
464
+ "handoff_x_cm": Normal(0.0, 2.0),
465
+ "handoff_height_cm": Uniform(18, 28),
466
+ },
467
+ target_kind="handoff",
468
+ ),
469
+ TaskInstance(
470
+ instance_id="handoff/produce-delicate",
471
+ goal="hand off the produce item from one arm to the other",
472
+ setup={
473
+ "fragility": Categorical(("firm", "delicate")),
474
+ "grip_force_n": Uniform(2, 8),
475
+ "pickup_y_cm": _jitter(2.0),
476
+ },
477
+ target_kind="handoff",
478
+ ),
479
+ TaskInstance(
480
+ instance_id="handoff/long-tool",
481
+ goal="hand off the {item} from one arm to the other",
482
+ setup={
483
+ "item": Categorical(("utensil", "produce item")),
484
+ "length_cm": Categorical((20, 30, 40)),
485
+ "handoff_height_cm": Uniform(15, 25),
486
+ },
487
+ language_vars=("item",),
488
+ target_kind="handoff",
489
+ ),
490
+ TaskInstance(
491
+ instance_id="handoff/cross-body",
492
+ goal="hand off the cup from one arm to the other",
493
+ setup={
494
+ "pickup_x_cm": Uniform(10, 25),
495
+ "release_x_cm": Uniform(-25, -10),
496
+ "handoff_height_cm": Uniform(20, 30),
497
+ },
498
+ target_kind="handoff",
499
+ ),
500
+ )
501
+
502
+ _SORT_CUTLERY = (
503
+ TaskInstance(
504
+ instance_id="sort_cutlery/balanced-pile",
505
+ goal="sort the cutlery into the correct tray compartments",
506
+ setup={
507
+ "spoon_count": Categorical((2, 3)),
508
+ "fork_count": Categorical((2, 3)),
509
+ "knife_count": Categorical((2, 3)),
510
+ "pile_spread_cm": Uniform(8, 18),
511
+ },
512
+ target_kind="sort",
513
+ static={"categories": "spoon,fork,knife"},
514
+ ),
515
+ TaskInstance(
516
+ instance_id="sort_cutlery/spoon-heavy",
517
+ goal="sort the cutlery into the correct tray compartments",
518
+ setup={
519
+ "spoon_count": Categorical((4, 5, 6)),
520
+ "fork_count": Categorical((1, 2)),
521
+ "knife_count": Categorical((1, 2)),
522
+ },
523
+ target_kind="sort",
524
+ static={"categories": "spoon,fork,knife"},
525
+ ),
526
+ TaskInstance(
527
+ instance_id="sort_cutlery/overlapping",
528
+ goal="sort the cutlery into the correct tray compartments",
529
+ setup={
530
+ "total_count": Categorical((6, 8, 10)),
531
+ "overlap_level": Categorical(("low", "high")),
532
+ "pile_x_cm": Normal(0.0, 3.0),
533
+ },
534
+ target_kind="sort",
535
+ static={"categories": "spoon,fork,knife"},
536
+ ),
537
+ TaskInstance(
538
+ instance_id="sort_cutlery/tray-offset",
539
+ goal="sort the cutlery into the correct tray compartments",
540
+ setup={
541
+ "tray_x_cm": Uniform(10, 20),
542
+ "tray_yaw_deg": Uniform(0, 30),
543
+ "total_count": Categorical((6, 9)),
544
+ },
545
+ target_kind="sort",
546
+ static={"categories": "spoon,fork,knife"},
547
+ ),
548
+ TaskInstance(
549
+ instance_id="sort_cutlery/sparse",
550
+ goal="sort the cutlery into the correct tray compartments",
551
+ setup={
552
+ "spoon_count": Categorical((1, 2)),
553
+ "fork_count": Categorical((1, 2)),
554
+ "knife_count": Categorical((1, 2)),
555
+ "pile_spread_cm": Uniform(12, 24),
556
+ },
557
+ target_kind="sort",
558
+ static={"categories": "spoon,fork,knife"},
559
+ ),
560
+ )
561
+
562
+ _SCOOP_PASTA = (
563
+ TaskInstance(
564
+ instance_id="scoop_pasta/spoon-penne",
565
+ goal="scoop the {pasta} with the {tool} and transfer it to the container",
566
+ setup={
567
+ "pasta": Categorical(("penne", "rigatoni")),
568
+ "tool": Categorical(("spoon", "measuring cup")),
569
+ "fill_target_g": Uniform(30, 120),
570
+ "pile_x_cm": Normal(-8.0, 2.0),
571
+ "container_x_cm": Uniform(8, 18),
572
+ },
573
+ language_vars=("pasta", "tool"),
574
+ target_kind="scoop_transfer",
575
+ ),
576
+ TaskInstance(
577
+ instance_id="scoop_pasta/measuring-cup-level",
578
+ goal="scoop the penne with the measuring cup and transfer it to the container",
579
+ setup={
580
+ "fill_target_g": Uniform(80, 160),
581
+ "level_tolerance_g": Categorical((5, 10)),
582
+ "pile_y_cm": _jitter(2.0),
583
+ },
584
+ target_kind="scoop_transfer",
585
+ ),
586
+ TaskInstance(
587
+ instance_id="scoop_pasta/rigatoni-large",
588
+ goal="scoop the rigatoni with the {tool} and transfer it to the container",
589
+ setup={
590
+ "tool": Categorical(("spoon", "measuring cup")),
591
+ "fill_target_g": Uniform(40, 100),
592
+ "pile_depth_cm": Categorical((3, 5, 7)),
593
+ },
594
+ language_vars=("tool",),
595
+ target_kind="scoop_transfer",
596
+ ),
597
+ TaskInstance(
598
+ instance_id="scoop_pasta/shallow-pile",
599
+ goal="scoop the {pasta} with the spoon and transfer it to the container",
600
+ setup={
601
+ "pasta": Categorical(("penne", "rigatoni")),
602
+ "pile_depth_cm": Categorical((1, 2)),
603
+ "fill_target_g": Uniform(20, 60),
604
+ },
605
+ language_vars=("pasta",),
606
+ target_kind="scoop_transfer",
607
+ ),
608
+ TaskInstance(
609
+ instance_id="scoop_pasta/far-container",
610
+ goal="scoop the penne with the measuring cup and transfer it to the container",
611
+ setup={
612
+ "container_x_cm": Uniform(20, 32),
613
+ "fill_target_g": Uniform(60, 140),
614
+ "transfer_height_cm": Uniform(10, 20),
615
+ },
616
+ target_kind="scoop_transfer",
617
+ ),
618
+ )
619
+
620
+
621
+ SPECS: tuple[TaskSpec, ...] = (
622
+ TaskSpec(
623
+ key="place_cutlery",
624
+ title="Place cutlery on dishware",
625
+ category="pick_place",
626
+ bimanual=False,
627
+ max_steps=60,
628
+ instances=_PLACE_CUTLERY,
629
+ description="Pick a single piece of cutlery and place it on a target surface.",
630
+ ),
631
+ TaskSpec(
632
+ key="stack",
633
+ title="Stack dishware",
634
+ category="stacking",
635
+ bimanual=False,
636
+ max_steps=80,
637
+ instances=_STACK,
638
+ description="Stack multiple like items into a single neat stack.",
639
+ ),
640
+ TaskSpec(
641
+ key="place_in_rack",
642
+ title="Place dishware in the dish rack",
643
+ category="insertion",
644
+ bimanual=False,
645
+ max_steps=80,
646
+ instances=_PLACE_IN_RACK,
647
+ description="Drop a dish into the correct slot of a dish rack.",
648
+ ),
649
+ TaskSpec(
650
+ key="pour_pasta",
651
+ title="Pour dry pasta into a vessel",
652
+ category="granular",
653
+ bimanual=True,
654
+ max_steps=100,
655
+ instances=_POUR_PASTA,
656
+ description="Pour dry pasta into a receiving vessel; one arm steadies, the other pours.",
657
+ ),
658
+ TaskSpec(
659
+ key="open_container",
660
+ title="Open a container",
661
+ category="articulated",
662
+ bimanual=True,
663
+ max_steps=120,
664
+ instances=_OPEN_CONTAINER,
665
+ description="Remove or unscrew a lid — one arm braces while the other twists or pries.",
666
+ ),
667
+ TaskSpec(
668
+ key="fold_cloth",
669
+ title="Fold a cloth",
670
+ category="deformable",
671
+ bimanual=True,
672
+ max_steps=120,
673
+ instances=_FOLD_CLOTH,
674
+ description="Deformable manipulation: grasp opposite corners and manage slack.",
675
+ ),
676
+ TaskSpec(
677
+ key="seal_container",
678
+ title="Seal a container with its lid",
679
+ category="mating",
680
+ bimanual=True,
681
+ max_steps=120,
682
+ instances=_SEAL_CONTAINER,
683
+ description="Align and press-or-twist a matching lid onto a base while one arm holds it.",
684
+ ),
685
+ TaskSpec(
686
+ key="handoff",
687
+ title="Hand off an object between arms",
688
+ category="coordination",
689
+ bimanual=True,
690
+ max_steps=80,
691
+ instances=_HANDOFF,
692
+ description="A pure handover that a single arm cannot do — the must-use-both-arms anchor.",
693
+ ),
694
+ TaskSpec(
695
+ key="sort_cutlery",
696
+ title="Sort cutlery into a utensil tray",
697
+ category="classification",
698
+ bimanual=False,
699
+ max_steps=200,
700
+ instances=_SORT_CUTLERY,
701
+ description="Sort a mixed pile into spoon/fork/knife compartments — multi-instance.",
702
+ ),
703
+ TaskSpec(
704
+ key="scoop_pasta",
705
+ title="Scoop pasta with a tool and transfer it",
706
+ category="granular_tool",
707
+ bimanual=True,
708
+ max_steps=120,
709
+ instances=_SCOOP_PASTA,
710
+ description="Tool-mediated granular handling: manage fill level, then transfer.",
711
+ ),
712
+ )
713
+
714
+ SPEC_BY_KEY: dict[str, TaskSpec] = {spec.key: spec for spec in SPECS}