This guide provides a complete reference for defining subtasks in RoboLab, covering all supported formats, logical modes, and use cases.
- Overview
- Subtask Definitions
- The Subtask Dataclass
- Condition Formats
- Format 1: Single Callable (Atomic Subtask)
- Format 2: List of Callables
- Format 3: List of Tuples (Callable, Score)
- Format 4: Set of Callables
- Format 5: Set of Tuples (Callable, Score)
- Format 6: Dict with Single Callables
- Format 7: Dict with List of Callables
- Format 8: Dict with List of Tuples (Callable, Score)
- Format 9: Dict with Set of Callables
- Logical Modes
- Composite Functions
- Sequential Subtasks
- Scoring System
- Effective Operations & Difficulty Scoring
RoboLab uses a hierarchical state machine architecture to manage complex manipulation tasks involving multiple objects and sequential execution phases.
Subtasks are defined as a list of subtask groups, where each subtask group can handle parallel subtask checking and composite tasks such as "and"/"any"/"choose" type subtasks. Each subtask group must be complete until the next subtask state machine step.
Subtask(conditions, logical, score, K) ← Subtask dataclass
↓
SubtaskStateMachine ← Manages sequential subtasks
↓
ConditionalsStateMachine ← Manages parallel conditions
↓
Parallel execution with regression checking
The Subtask dataclass is the core building block for defining task conditions. Each subtask must be completed before moving to the next subtask.
from robolab.core.task.subtask import Subtask
@dataclass
class Subtask:
"""Self-documenting container for a group of parallel conditions."""
conditions: Union[Callable, list, set, dict]
score: float = 1.0
logical: Literal["all", "any", "choose"] = "all"
K: Optional[int] = None # Required when logical="choose"
name: str = "unnamed_subtask"The conditions parameter supports 8 different formats for convenience, but will all be unified at loading time:
Use Case: Single condition to check
from functools import partial
from robolab.core.task.subtask import Subtask
from robolab.core.task.conditionals import object_grabbed
Subtask(
conditions=partial(object_grabbed, object='banana'),
name="grab_banana"
)Internal Structure: {"conditions": [(func, 1.0)]}
Use Case: Multiple conditions as separate groups, each with equal score
Subtask(
conditions=[
partial(object_grabbed, object='banana'),
partial(object_grabbed, object='rubiks_cube')
],
logical="any", # Grab ANY one object
name="grab_any_object"
)Internal Structure: Each callable becomes its own group with equal scores
{
"group1": [(func1, 0.5)],
"group2": [(func2, 0.5)]
}Use Case: Multiple conditions with custom score weighting
Subtask(
conditions=[
(partial(object_grabbed, object='banana'), 0.3),
(partial(object_in_container, object='banana', container='bowl'), 0.7)
],
logical="all",
name="weighted_conditions"
)Internal Structure: Each tuple becomes its own group
{
"group1": [(func1, 1.0)], # Scores normalized within group
"group2": [(func2, 1.0)]
}Use Case: Same as list, but order doesn't matter semantically
Subtask(
conditions={
partial(object_grabbed, object='banana'),
partial(object_grabbed, object='rubiks_cube')
},
logical="any",
name="grab_any_set"
)Note: Converted to list internally (order may vary)
Use Case: Multiple conditions with scores, order-agnostic
Subtask(
conditions={
(partial(object_grabbed, object='banana'), 0.5),
(partial(object_grabbed, object='rubiks_cube'), 0.5)
},
logical="any",
name="weighted_set"
)Use Case: Explicit group names, single condition per group
Subtask(
conditions={
"banana": partial(object_grabbed, object='banana'),
"cube": partial(object_grabbed, object='rubiks_cube')
},
logical="all", # Both must be grabbed
name="grab_both_explicit"
)Internal Structure:
{
"banana": [(func1, 1.0)],
"cube": [(func2, 1.0)]
}Use Case: Multiple sequential conditions per group, equal scores
This is the most common format for complex multi-step tasks.
Subtask(
conditions={
"banana": [
partial(object_grabbed, object='banana'),
partial(object_above_bottom, object='banana', reference_object='bowl'),
partial(object_dropped, object='banana'),
partial(object_in_container, object='banana', container='bowl')
],
"cube": [
partial(object_grabbed, object='rubiks_cube'),
partial(object_above_bottom, object='rubiks_cube', reference_object='bowl'),
partial(object_dropped, object='rubiks_cube'),
partial(object_in_container, object='rubiks_cube', container='bowl')
]
},
logical="all", # Both banana AND cube must complete all steps
name="pick_and_place_both"
)Internal Structure: Each callable gets equal score within its group
{
"banana": [(func1, 0.25), (func2, 0.25), (func3, 0.25), (func4, 0.25)],
"cube": [(func1, 0.25), (func2, 0.25), (func3, 0.25), (func4, 0.25)]
}Key Feature: Within each group, conditions are checked sequentially with regression checking.
Use Case: Full control over groups, sequences, and scores
This is the most flexible format.
Subtask(
conditions={
"banana": [
(partial(object_grabbed, object='banana'), 0.1),
(partial(object_above_bottom, object='banana', reference_object='bowl'), 0.2),
(partial(object_dropped, object='banana'), 0.3),
(partial(object_in_container, object='banana', container='bowl'), 0.4)
],
"cube": [
(partial(object_grabbed, object='rubiks_cube'), 0.25),
(partial(object_above_bottom, object='rubiks_cube', reference_object='bowl'), 0.25),
(partial(object_dropped, object='rubiks_cube'), 0.25),
(partial(object_in_container, object='rubiks_cube', container='bowl'), 0.25)
]
},
logical="all",
name="fully_weighted"
)Note: Scores within each group are normalized to sum to 1.0.
Use Case: Order-agnostic sequential conditions per group
Subtask(
conditions={
"banana": {
partial(object_grabbed, object='banana'),
partial(object_in_container, object='banana', container='bowl')
},
"cube": {
partial(object_grabbed, object='rubiks_cube'),
partial(object_in_container, object='rubiks_cube', container='bowl')
}
},
logical="any", # Either banana OR cube completes
name="pick_and_place_any"
)Note: Converted to list internally.
The logical parameter determines when a subtask group is considered complete.
Semantics: All groups must complete all their conditions
Progress Tracking:
Completed: 0/2 groups, Score: 0.00
rubiks_cube: 0/4 conditions (0% complete)
banana: 0/4 conditions (0% complete)
Completed: 0/2 groups, Score: 0.375
rubiks_cube: 0/4 conditions (0% complete)
banana: 3/4 conditions (75% complete)
Completed: 1/2 groups, Score: 0.75
rubiks_cube: 2/4 conditions (50% complete)
banana: 4/4 conditions (100% complete) ✓
Completed: 2/2 groups, Score: 1.0 ✓
rubiks_cube: 4/4 conditions (100% complete) ✓
banana: 4/4 conditions (100% complete) ✓
Semantics: Success when any single group completes all its conditions
Progress Tracking:
Completed: 0/3 groups, Score: 0.00
red_block: 0/4 conditions (0% complete)
blue_block: 0/4 conditions (0% complete)
green_block: 0/4 conditions (0% complete)
Completed: 0/3 groups, Score: 0.50
red_block: 0/4 conditions (0% complete)
blue_block: 2/4 conditions (50% complete)
green_block: 0/4 conditions (0% complete)
Completed: 1/3 groups, Score: 1.0 ✓ (blue_block completed)
red_block: 0/4 conditions (0% complete)
blue_block: 4/4 conditions (100% complete) ✓
green_block: 1/4 conditions (25% complete)
Semantics: Success when exactly K groups complete all their conditions
Important: The K parameter is required when using logical="choose".
Progress Tracking:
Completed: 0/5 groups, Score: 0.30, Need K=2
banana_01: 0/4 conditions (0% complete)
banana_02: 3/4 conditions (75% complete)
banana_03: 1/4 conditions (25% complete)
banana_04: 0/4 conditions (0% complete)
banana_05: 2/4 conditions (50% complete)
Top 2: [0.75, 0.50] → avg = 0.625
Completed: 1/5 groups, Score: 0.75, Need K=2
banana_01: 0/4 conditions (0% complete)
banana_02: 4/4 conditions (100% complete) ✓
banana_03: 1/4 conditions (25% complete)
banana_04: 0/4 conditions (0% complete)
banana_05: 2/4 conditions (50% complete)
Top 2: [1.0, 0.50] → avg = 0.75
Completed: 2/5 groups, Score: 1.0 ✓ (K=2 reached!)
banana_01: 0/4 conditions (0% complete)
banana_02: 4/4 conditions (100% complete) ✓
banana_03: 1/4 conditions (25% complete)
banana_04: 0/4 conditions (0% complete)
banana_05: 4/4 conditions (100% complete) ✓
Top 2: [1.0, 1.0] → avg = 1.0
Composite functions automatically expand into multiple atomic subtasks, providing convenient shortcuts for common task patterns.
The pick_and_place() function is a composite that creates a complete pick-and-place sequence:
Signature:
@composite
def pick_and_place(
object: str | list[str],
container: str,
logical: Literal["all", "any", "choose"] = "all",
K: Optional[int] = None,
score: float = 1.0
) -> Subtask:Returns: A Subtask dataclass with the following structure:
{
"object1": [
(partial(object_grabbed, object='object1'), 0.25),
(partial(object_above_bottom, object='object1', reference_object='container'), 0.25),
(partial(object_dropped, object='object1'), 0.25),
(partial(object_in_container, object='object1', container='container'), 0.25)
],
"object2": [
# ... same sequence for object2
]
}Examples:
from robolab.core.task.conditionals import pick_and_place
# Example 1: Both objects must be placed
subtasks = [
pick_and_place(
object=["rubiks_cube", "banana"],
container="bowl",
logical="all",
score=1.0
)
]
# Example 2: Any one object is sufficient
subtasks = [
pick_and_place(
object=["red_block", "blue_block", "green_block"],
container="bin",
logical="any",
score=0.5
)
]
# Example 3: Exactly 2 out of 3 objects
subtasks = [
pick_and_place(
object=["banana_01", "banana_02", "banana_03"],
container="bowl",
logical="choose",
K=2,
score=1.0
)
]NOTE Do NOT use for termination conditions (use object_placed_in_container instead). Terminations check final state only, while subtasks track intermediate progress.
Multiple Subtask objects can be chained to create multi-stage tasks. Each stage must complete before the next begins.
subtasks = [
# Stage 1:
Subtask(...),
# Stage 2:
Subtask(...)
]subtasks = [
# Stage 1: Pick any one block, must be complete before starting the next one
pick_and_place(
object=["red_block", "blue_block"],
container="bowl",
logical="any",
score=0.3
),
# Stage 2: Both fruits must be placed.
pick_and_place(
object=["banana", "apple"],
container="bowl",
logical="all",
score=0.4
),
]Progress Tracking:
Overall Progress: 0/2 stages complete (0%)
Current Stage 1: logical='any', score=0.3
red_block: 2/4 (50%)
blue_block: 0/4 (0%)
Overall Progress: 1/2 stages complete (50%)
Current Stage 2: logical='all', score=0.4
banana: 0/4 (0%)
apple: 0/4 (0%)
Overall Progress: 2/2 stages complete (100%) ✓
Within Each Group: Condition scores are relative to each other, normalized to sum to 1.0
# Before normalization
conditions = {
"banana": [
(func1, 0.1),
(func2, 0.2),
(func3, 0.3)
]
}
# Total: 0.6
# After normalization (automatic)
conditions = {
"banana": [
(func1, 0.167), # 0.1 / 0.6
(func2, 0.333), # 0.2 / 0.6
(func3, 0.500) # 0.3 / 0.6
]
}
# Total: 1.0Across Subtasks: Subtasks are also relative to each other, which will be normalized to 1.
subtasks = [
Subtask(..., score=0.2), # 20% of total task
Subtask(..., score=0.5), # 50% of total task
Subtask(..., score=0.3) # 30% of total task
]
# These don't need to sum to 1.0, but they define relative weightsRunning RubiksCubeBananaTaskHomeOffice_0: 'put the cube and banana in the bowl'
19%|█████████████████▏ | 90/470 [00:13<01:09, 5.45it/s]
--------------------------------------------------
SUBTASK STATE
--------------------------------------------------
Current Object Progress: {'rubiks_cube': 0, 'banana': 1}
Objects and their current subtasks:
rubiks_cube (step 0/4):
0. ❌ object_grabbed(object=rubiks_cube) <-- CURRENT
1. ❌ object_above_bottom_surface(object=rubiks_cube, surface=bowl) (pending)
2. ❌ object_dropped(object=rubiks_cube) (pending)
3. ❌ object_in_container(object=rubiks_cube, container=bowl, tolerance=0.05) (pending)
banana (step 1/4):
0. ✅ object_grabbed(object=banana) (completed)
1. ❌ object_above_bottom_surface(object=banana, surface=bowl) <-- CURRENT
2. ❌ object_dropped(object=banana) (pending)
3. ❌ object_in_container(object=banana, container=bowl, tolerance=0.05) (pending)
==================================================
Subtask status: i: 90, status: {'status': <SubtaskStatusCode.OBJECT_GRABBED_SUCCESS: 120>, 'completed': 1, 'total': 8, 'info': "success: object_grabbed(object='banana');"}
30%|██████████████████████████▉ | 142/470 [00:23<01:11, 4.56it/s]
--------------------------------------------------
SUBTASK STATE
--------------------------------------------------
Current Object Progress: {'rubiks_cube': 0, 'banana': 2}
Objects and their current subtasks:
rubiks_cube (step 0/4):
0. ❌ object_grabbed(object=rubiks_cube) <-- CURRENT
1. ❌ object_above_bottom_surface(object=rubiks_cube, surface=bowl) (pending)
2. ❌ object_dropped(object=rubiks_cube) (pending)
3. ❌ object_in_container(object=rubiks_cube, container=bowl, tolerance=0.05) (pending)
banana (step 2/4):
0. ✅ object_grabbed(object=banana) (completed)
1. ✅ object_above_bottom_surface(object=banana, surface=bowl) (completed)
2. ❌ object_dropped(object=banana) <-- CURRENT
3. ❌ object_in_container(object=banana, container=bowl, tolerance=0.05) (pending)
==================================================
Subtask status: i: 142, status: {'status': <SubtaskStatusCode.OBJECT_ABOVE_BOTTOM_SURFACE_SUCCESS: 160>, 'completed': 2, 'total': 8, 'info': "success: object_above_bottom_surface(object='banana', surface='bowl');"}
39%|██████████████████████████████████▊ | 184/470 [00:32<01:02, 4.57it/s]
--------------------------------------------------
SUBTASK STATE
--------------------------------------------------
Current Object Progress: {'rubiks_cube': 0, 'banana': 3}
Objects and their current subtasks:
rubiks_cube (step 0/4):
0. ❌ object_grabbed(object=rubiks_cube) <-- CURRENT
1. ❌ object_above_bottom_surface(object=rubiks_cube, surface=bowl) (pending)
2. ❌ object_dropped(object=rubiks_cube) (pending)
3. ❌ object_in_container(object=rubiks_cube, container=bowl, tolerance=0.05) (pending)
banana (step 3/4):
0. ✅ object_grabbed(object=banana) (completed)
1. ✅ object_above_bottom_surface(object=banana, surface=bowl) (completed)
2. ✅ object_dropped(object=banana) (completed)
3. ❌ object_in_container(object=banana, container=bowl, tolerance=0.05) <-- CURRENT
==================================================
Subtask status: i: 184, status: {'status': <SubtaskStatusCode.OBJECT_DROPPED_SUCCESS: 140>, 'completed': 3, 'total': 8, 'info': "success: object_dropped(object='banana');"}
39%|███████████████████████████████████ | 185/470 [00:32<00:57, 4.92it/s]
--------------------------------------------------
SUBTASK STATE
--------------------------------------------------
Current Object Progress: {'rubiks_cube': 0, 'banana': 4}
Objects and their current subtasks:
rubiks_cube (step 0/4):
0. ❌ object_grabbed(object=rubiks_cube) <-- CURRENT
1. ❌ object_above_bottom_surface(object=rubiks_cube, surface=bowl) (pending)
2. ❌ object_dropped(object=rubiks_cube) (pending)
3. ❌ object_in_container(object=rubiks_cube, container=bowl, tolerance=0.05) (pending)
banana (step 4/4):
0. ✅ object_grabbed(object=banana) (completed)
1. ✅ object_above_bottom_surface(object=banana, surface=bowl) (completed)
2. ✅ object_dropped(object=banana) (completed)
3. ✅ object_in_container(object=banana, container=bowl, tolerance=0.05) (completed)
==================================================
Subtask status: i: 185, status: {'status': <SubtaskStatusCode.OBJECT_IN_CONTAINER_SUCCESS: 110>, 'completed': 4, 'total': 8, 'info': "success: object_in_container(object='banana', container='bowl', tolerance=0.05). All subtasks complete for banana.;"}
88%|██████████████████████████████████████████████████████████████████████████████▍ | 414/470 [01:11<00:10, 5.43it/s]
--------------------------------------------------
SUBTASK STATE
--------------------------------------------------
Current Object Progress: {'rubiks_cube': 1, 'banana': 4}
Objects and their current subtasks:
rubiks_cube (step 1/4):
0. ✅ object_grabbed(object=rubiks_cube) (completed)
1. ❌ object_above_bottom_surface(object=rubiks_cube, surface=bowl) <-- CURRENT
2. ❌ object_dropped(object=rubiks_cube) (pending)
3. ❌ object_in_container(object=rubiks_cube, container=bowl, tolerance=0.05) (pending)
banana (step 4/4):
0. ✅ object_grabbed(object=banana) (completed)
1. ✅ object_above_bottom_surface(object=banana, surface=bowl) (completed)
2. ✅ object_dropped(object=banana) (completed)
3. ✅ object_in_container(object=banana, container=bowl, tolerance=0.05) (completed)
==================================================
Subtask status: i: 414, status: {'status': <SubtaskStatusCode.OBJECT_GRABBED_SUCCESS: 120>, 'completed': 5, 'total': 8, 'info': "success: object_grabbed(object='rubiks_cube');"}
93%|██████████████████████████████████████████████████████████████████████████████████▉ | 438/470 [01:17<00:07, 4.56it/s]
--------------------------------------------------
SUBTASK STATE
--------------------------------------------------
Current Object Progress: {'rubiks_cube': 2, 'banana': 4}
Objects and their current subtasks:
rubiks_cube (step 2/4):
0. ✅ object_grabbed(object=rubiks_cube) (completed)
1. ✅ object_above_bottom_surface(object=rubiks_cube, surface=bowl) (completed)
2. ❌ object_dropped(object=rubiks_cube) <-- CURRENT
3. ❌ object_in_container(object=rubiks_cube, container=bowl, tolerance=0.05) (pending)
banana (step 4/4):
0. ✅ object_grabbed(object=banana) (completed)
1. ✅ object_above_bottom_surface(object=banana, surface=bowl) (completed)
2. ✅ object_dropped(object=banana) (completed)
3. ✅ object_in_container(object=banana, container=bowl, tolerance=0.05) (completed)
==================================================
Subtask status: i: 438, status: {'status': <SubtaskStatusCode.OBJECT_ABOVE_BOTTOM_SURFACE_SUCCESS: 160>, 'completed': 6, 'total': 8, 'info': "success: object_above_bottom_surface(object='rubiks_cube', surface='bowl');"}
97%|██████████████████████████████████████████████████████████████████████████████████████▎ | 456/470 [01:20<00:02, 4.73it/s]
--------------------------------------------------
SUBTASK STATE
--------------------------------------------------
Current Object Progress: {'rubiks_cube': 3, 'banana': 4}
Objects and their current subtasks:
rubiks_cube (step 3/4):
0. ✅ object_grabbed(object=rubiks_cube) (completed)
1. ✅ object_above_bottom_surface(object=rubiks_cube, surface=bowl) (completed)
2. ✅ object_dropped(object=rubiks_cube) (completed)
3. ❌ object_in_container(object=rubiks_cube, container=bowl, tolerance=0.05) <-- CURRENT
banana (step 4/4):
0. ✅ object_grabbed(object=banana) (completed)
1. ✅ object_above_bottom_surface(object=banana, surface=bowl) (completed)
2. ✅ object_dropped(object=banana) (completed)
3. ✅ object_in_container(object=banana, container=bowl, tolerance=0.05) (completed)
==================================================
Subtask status: i: 456, status: {'status': <SubtaskStatusCode.OBJECT_DROPPED_SUCCESS: 140>, 'completed': 7, 'total': 8, 'info': "success: object_dropped(object='rubiks_cube');"}
97%|██████████████████████████████████████████████████████████████████████████████████████▋ | 458/470 [01:21<00:02, 5.18it/s]
--------------------------------------------------
SUBTASK STATE
--------------------------------------------------
Current Object Progress: {'rubiks_cube': 4, 'banana': 4}
Objects and their current subtasks:
rubiks_cube (step 4/4):
0. ✅ object_grabbed(object=rubiks_cube) (completed)
1. ✅ object_above_bottom_surface(object=rubiks_cube, surface=bowl) (completed)
2. ✅ object_dropped(object=rubiks_cube) (completed)
3. ✅ object_in_container(object=rubiks_cube, container=bowl, tolerance=0.05) (completed)
banana (step 4/4):
0. ✅ object_grabbed(object=banana) (completed)
1. ✅ object_above_bottom_surface(object=banana, surface=bowl) (completed)
2. ✅ object_dropped(object=banana) (completed)
3. ✅ object_in_container(object=banana, container=bowl, tolerance=0.05) (completed)
==================================================
Subtask status: i: 458, status: {'status': <SubtaskStatusCode.OBJECT_IN_CONTAINER_SUCCESS: 110>, 'completed': 8, 'total': 8, 'info': "success: object_in_container(object='rubiks_cube', container='bowl', tolerance=0.05). All subtasks complete for rubiks_cube.;"}
Tasks are automatically assigned a difficulty score and difficulty label based on their subtask structure and attributes. This scoring is deterministic and computed from the task definition — no manual labeling is required.
The num_subtasks metric counts the number of distinct manipulation actions the robot must perform, accounting for the subtask's logical mode:
| Logical Mode | Subtask Count |
|---|---|
"all" |
Number of object groups (every group must complete) |
"any" |
1 (only one group needs to complete) |
"choose" |
K (exactly K groups must complete) |
The total is summed across all sequential stages in the task. For example, a task with two sequential stages — the first requiring "all" of 2 objects, the second "any" of 3 — has 2 + 1 = 3 subtasks.
See count_subtasks() in robolab/core/task/subtask_utils.py.
The difficulty score combines manipulation volume with skill complexity:
difficulty_score = num_subtasks + max(skill_weight)
where max(skill_weight) is the highest weight among the task's attributes. The skill weights are:
| Weight | Attributes |
|---|---|
| 0 | color, semantics, size, conjunction, vague |
| +1 | spatial |
| +2 | counting, sorting, stacking, affordance |
| +3 | reorientation |
Labels are assigned based on the score using fixed thresholds:
| Label | Score Range |
|---|---|
| simple | score <= 2 |
| moderate | score 3–4 |
| complex | score >= 5 |
| Task | Subtasks | Max Skill Weight | Score | Label |
|---|---|---|---|---|
RubiksCubeTask (1 pick-and-place, no special attrs) |
1 | 0 | 1 | simple |
BowlStackingLeftOnRightTask (1 subtask, spatial) |
1 | 1 (spatial) | 2 | simple |
Stack3RubiksCubeTask (2 subtasks, stacking) |
2 | 2 (stacking) | 4 | moderate |
BlockStackingSpecifiedOrderTask (3 subtasks, stacking+color) |
3 | 2 (stacking) | 5 | complex |
ReorientAllMugsTask (4 subtasks, reorientation) |
4 | 3 (reorientation) | 7 | complex |
The scoring constants and function live in robolab/core/task/subtask_utils.py:
SKILL_WEIGHTS— attribute-to-weight mappingDIFFICULTY_THRESHOLDS—(simple_max, moderate_max)tuplecompute_difficulty_score(num_subtasks, attributes)— returns(score, label)
The metadata pipeline (robolab/tasks/_utils/load_task_info.py) automatically populates num_subtasks, difficulty_score, and difficulty_label for each task. Summary statistics are available via:
python robolab/tasks/_utils/compute_task_statistics.py --difficulty
python robolab/tasks/_utils/compute_task_statistics.py --difficulty -v # full task list