Skip to content

Add order-does-not-matter composite tasks#677

Open
peterd-NV wants to merge 20 commits into
mainfrom
peterd/composite_task_v03
Open

Add order-does-not-matter composite tasks#677
peterd-NV wants to merge 20 commits into
mainfrom
peterd/composite_task_v03

Conversation

@peterd-NV
Copy link
Copy Markdown
Collaborator

@peterd-NV peterd-NV commented May 13, 2026

Summary

This PR refactors composite tasks to create two classes:

  1. A new CompositeTaskBase class that enables "order-does-not-matter" subtask completion. Mostly copied over from existing SequentialTaskBase.
  2. A refactored SequentialTaskBase that is dervied from CompositeTaskBase for "order-does-matter" tasks.

Tests are updated as follows:

  1. Test cases for both TaskBase classes are updated accordingly.
  2. A new runtime test for CompositeTaskBase is added.

Other updates include:

  1. Added support for "don't cares" when specifying the desired_subtask_success_state by setting subtask value to None.

API usage of composite tasks remain the same as before.

Copy link
Copy Markdown
Contributor

@isaaclab-review-bot isaaclab-review-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Isaac Lab Review Bot — PR #677

Title: Draft: Add order-does-not-matter composite tasks
Author: @peterd-NV
Files Changed: 5 (composite_task_base.py, sequential_task_base.py, tests)


Summary

This PR introduces a CompositeTaskBase class that generalizes composite task handling where subtask completion order does not matter. The existing SequentialTaskBase is refactored to inherit from this new base class, preserving its sequential ordering semantics through method overrides.


📦 Latest Update (b0e2bb0)

Changes since last review (5cedddeb0e2bb0):

  1. Skippable None state option for desired_subtask_success_state:

    • Type annotation changed from list[bool] | None to list[bool | None] | None
    • None entries in the list are now treated as "don't care" wildcards
    • Added assertion to validate entries are True, False, or None
    • Updated success logic: positions with None are skipped when checking the desired pattern
    • Applied consistently to both CompositeTaskBase and SequentialTaskBase
  2. Bug fix: Changed if desired_subtask_success_state: to if desired_subtask_success_state is not None:

    • This correctly handles empty lists and prevents falsy list edge cases
  3. New tests for the None wildcard feature:

    • test_composite_desired_subtask_success_state_with_none in composite task tests
    • test_sequential_desired_subtask_success_state_with_none in sequential task tests
    • Tests verify don't-care semantics, pattern matching, and edge cases (all-None)
  4. Merge from main + lint fixes


📋 Previous Review Summary (5ceddde)

The previous commits introduced:

  • CompositeTaskBase class with order-independent subtask completion
  • SequentialTaskBase refactored to inherit from composite base
  • Comprehensive tests for both task types
  • Type annotation fixes (configclassAny return types)
  • Environment randomization config updates

✅ Assessment

The new changes are clean and well-implemented:

  • Feature value: The None wildcard in desired_subtask_success_state enables flexible task definitions where only some subtask final states matter — useful for tasks with optional steps
  • Correctness: The truthiness fix (is not None) is important for proper handling of empty desired states
  • Test coverage: New tests adequately cover the wildcard semantics for both composite and sequential tasks
  • Consistency: Changes applied uniformly across both task base classes

LGTM 👍

@peterd-NV peterd-NV changed the title Peterd/composite task v03 Draft: Add order-does-not-matter composite tasks May 14, 2026
@peterd-NV peterd-NV changed the title Draft: Add order-does-not-matter composite tasks Add order-does-not-matter composite tasks May 19, 2026
Copy link
Copy Markdown
Contributor

@isaaclab-review-bot isaaclab-review-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Isaac Lab Review Bot — PR #677

Title: Draft: Add order-does-not-matter composite tasks
Author: @peterd-NV
Files Changed: 5 (composite_task_base.py, sequential_task_base.py, tests)
Latest Commit: 9db9e8a


Summary

This PR introduces a CompositeTaskBase class that generalizes composite task handling where subtask completion order does not matter. The existing SequentialTaskBase is refactored to inherit from this new base class, preserving its sequential ordering semantics through method overrides.


🆕 Latest Changes (cleanup commit)

Since the previous review:

  1. Cleanup commit (9db9e8a): Code formatting and organization improvements

The core changes from previous commits remain:

  • Skippable None state option for desired_subtask_success_state - entries can now be True, False, or None (wildcard/"don't care")
  • Bug fix: if desired_subtask_success_state:if desired_subtask_success_state is not None: for correct handling of empty/falsy lists
  • Comprehensive tests for wildcard semantics in both composite and sequential task classes

Architecture Highlights

CompositeTaskBase (new)

  • Order-independent subtask completion tracking via _subtask_success_state
  • Success requires all subtasks to have succeeded at some point AND match desired_subtask_success_state (if provided)
  • None entries in desired state are wildcards — skipped during pattern matching
  • Shared utility methods: add_suffix_configclass_transform, remove_configclass_transform
  • Metrics, events, and termination configs automatically combined from subtasks

SequentialTaskBase (refactored)

  • Now inherits from CompositeTaskBase
  • Overrides composite_task_success_func to enforce sequential ordering via _current_subtask_idx
  • Overrides reset_subtask_success_state to reset the index in addition to success state
  • Maintains backward compatibility with existing sequential task semantics

Test Coverage

test_composite_task_base.py (new)

  • Config transformation utilities (add_suffix, remove)
  • None wildcard semantics for desired subtask success state

test_composite_open_door.py (new)

  • Order-independence verification (forward and reverse completion order)
  • Multi-environment correctness
  • Reset/termination behavior

test_sequential_task_base.py (updated)

  • Sequential ordering enforcement (subtask N+1 blocked until N succeeds)
  • Success state latching behavior
  • desired_subtask_success_state pattern matching including None wildcards
  • Reset clearing both state and index

✅ Assessment

Strengths:

  • Clean refactoring that extracts common composite task logic
  • None wildcard feature enables flexible task definitions with optional steps
  • Good separation of concerns between base and sequential classes
  • Comprehensive test coverage for both new and existing functionality

Minor observations (non-blocking):

  1. The compute_metric_from_recording method accesses recorded_metric_data[0].shape[1] — this assumes all episodes have consistent shape, which should be fine but could add a defensive check
  2. Consider adding docstrings to the new test classes (_MockSuccessFunc, _MockSubtask, _MockEnv) for clarity

Overall this is a well-structured addition that properly generalizes the task composition system.

LGTM 👍


Note: This is a draft PR — when ready for final review, please mark it ready for review.


📝 Update (fa29499)

Commit: fa29499a — "fix stale comment"

Changes since 9db9e8a:

  1. Performance optimization in composite_task_base.py:

    • Loop restructured to evaluate subtask success functions once per subtask, then distribute results across envs
    • Previously: nested loop called success func for each (env, subtask) pair — O(envs × subtasks) function calls
    • Now: outer loop over subtasks, call success func once (returns results for all envs), inner loop distributes — O(subtasks) function calls
    • This is a meaningful performance improvement for multi-env training
  2. Comment cleanup in composite_task_base.py:

    • Fixed stale docstring: "sequential task" → "composite task" in get_metrics()
  3. Parallel optimization in sequential_task_base.py:

    • Similar batching optimization applied to the sequential variant
    • Added smart subtask filtering: only evaluates success functions for subtasks that are currently active across any env (sorted(set(env._current_subtask_idx)))
    • Falls back to full evaluation when desired_subtask_success_state requires checking all subtasks

Assessment: Good cleanup commit. The loop restructuring is a sensible optimization that reduces redundant subtask function evaluations from O(N×M) to O(N) where N=subtasks, M=envs. The stale comment fix addresses the terminology inconsistency.

Still LGTM 👍

@peterd-NV peterd-NV marked this pull request as ready for review May 19, 2026 15:38
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 19, 2026

Greptile Summary

This PR introduces CompositeTaskBase, an order-independent counterpart to the existing SequentialTaskBase, and refactors SequentialTaskBase to inherit from it. The split cleanly separates the "order-doesn't-matter" latch logic from the state-machine logic that enforces sequential completion. Support for None entries in desired_subtask_success_state is also added as "don't-care" positions.

  • New CompositeTaskBase: all subtask success functions are evaluated every step; env._subtask_success_state[env_idx][i] is latched immediately when subtask i currently succeeds; overall success (when desired_subtask_success_state=None) requires all latched states to be True.
  • Refactored SequentialTaskBase: overrides composite_task_success_func with a state-machine that advances _current_subtask_idx in order, and overrides reset_subtask_success_state to also clear _current_subtask_idx; static-method dispatch via self.method_name resolves correctly to the subclass implementation at config-build time.
  • Tests: new unit tests cover ordering, latching, None-entry desired states, and multi-env reset; a new runtime integration test validates order-independence end-to-end.

Confidence Score: 5/5

Safe to merge; the core latch and state-machine logic is correct and well-covered by new tests.

The refactoring is structurally sound: static-method override dispatch works correctly, the sequential state machine is preserved, and the composite latch is implemented cleanly. The two observations are an edge-case semantic trap with all-None desired state and a pre-existing performance note about evaluating unreachable subtasks, neither of which causes incorrect task completion in normal usage.

composite_task_base.py — the all-None desired_subtask_success_state edge case is worth a follow-up guard or documentation note.

Important Files Changed

Filename Overview
isaaclab_arena/tasks/composite_task_base.py New base class implementing order-independent composite task logic; vacuous-truth edge case with all-None desired_subtask_success_state produces immediate success on step 0.
isaaclab_arena/tasks/sequential_task_base.py Correctly slimmed to a subclass of CompositeTaskBase; overrides composite_task_success_func and reset_subtask_success_state to maintain the state-machine ordering contract.
isaaclab_arena/tests/test_composite_task_base.py New unit tests for CompositeTaskBase; all-None desired-state test verifies vacuous-True but only after prior steps have populated env._subtask_success_state, so the step-0 edge case is not exercised.
isaaclab_arena/tests/test_sequential_task_base.py Comprehensive behavioural tests replacing the old config-transform-only tests; covers ordering, latching, desired-state with None entries, and multi-env reset.
isaaclab_arena/tests/test_composite_open_door.py New runtime integration tests for CompositeTaskBase; forward-order, reverse-order, multi-env, and reset-condition scenarios are all covered.

Class Diagram

%%{init: {'theme': 'neutral'}}%%
classDiagram
    class TaskBase {
        +get_scene_cfg()
        +get_events_cfg()
        +get_termination_cfg()
        +get_metrics()
        +get_mimic_env_cfg()
    }
    class CompositeTaskBase {
        +subtasks: list[TaskBase]
        +desired_subtask_success_state: list[bool|None]|None
        +composite_task_success_func(env, subtasks, desired)$
        +reset_subtask_success_state(env, env_ids, subtasks)$
        +get_scene_cfg()
        +get_events_cfg()
        +get_termination_cfg()
        +get_metrics()
        +get_mimic_env_cfg()
    }
    class SequentialTaskBase {
        +composite_task_success_func(env, subtasks, desired)$
        +reset_subtask_success_state(env, env_ids, subtasks)$
    }
    TaskBase <|-- CompositeTaskBase
    CompositeTaskBase <|-- SequentialTaskBase
Loading

Reviews (3): Last reviewed commit: "Merge branch 'main' of github.com:isaac-..." | Re-trigger Greptile

Comment thread isaaclab_arena/tasks/sequential_task_base.py
Comment on lines +291 to +292
def get_metrics(self) -> list[MetricBase]:
"Get metrics for the sequential task."
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Stale docstring — this method now lives in CompositeTaskBase, not the sequential task class.

Suggested change
def get_metrics(self) -> list[MetricBase]:
"Get metrics for the sequential task."
def get_metrics(self) -> list[MetricBase]:
"Get metrics for the composite task."

Copy link
Copy Markdown
Collaborator

@alexmillane alexmillane left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. A few minor comments.

Comment thread isaaclab_arena/tasks/composite_task_base.py
self,
subtasks: list[TaskBase],
episode_length_s: float | None = None,
desired_subtask_success_state: list[bool | None] | None = None,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The meaning of this argument is not immediately obvious to me. Are there cases where we'd like to build a composite task, but that we don't want some of the task to be successful?

Suggestion to add a short docstring.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a docstring to explain. It's an optional argument that can be used to specify the precise subtask success value at the end of the episode. When not provided, once a subtask flips True, it is deemed "completed" for the rest of the episode regardless of it's current success state is True or False at the end of the episode. This is desirable for cases when multiple subtask successes should not be true at the same time (e.g. bottle grasped, bottle in fridge)

This argument gives the user the flexibility to enforce when subtask successes "should" be true (e.g. bottle in fridge, fridge door closed).

Comment on lines +153 to +154
current_subtask_success_state[env_idx][subtask_idx] = True
env._subtask_success_state[env_idx][subtask_idx] = True
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between these two variables? I guess they must represent different things, but it's not clear from the naming. Suggestion to a) rename to make things clearer, and b) add a comment if "a)" is not possible.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think I see. One is if a sub-task has ever been successful and the other is if it is successful right now. Suggestion to rename :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You got it exactly right! 👍 _subtask_success_state tracks whether a subtask has ever succeeded (latched), while current_subtask_success_state is the live/instantaneous success status.

A rename to make that distinction clearer sounds reasonable — something like _subtask_ever_succeeded vs _subtask_currently_succeeding would be more self-documenting. Worth flagging to the author if you feel strongly about it!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the naming to "subtask_ever_succeeded" and "subtask_currently_succeeding" to be more clear.

Comment on lines +161 to +168
per_env_success = [
all(
env._subtask_success_state[env_idx][i] and current_subtask_success_state[env_idx][i] == desired
for i, desired in enumerate(desired_subtask_success_state)
if desired is not None
)
for env_idx in range(env.num_envs)
]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, and maybe I'm not Pythonic enough, I would break this nested list composition into for loops. It's quite hard to interpret what's happening here. I can't quite tell. I suspect it would be easier if this was formatted as a raw loop.

# Check for duplicate fields across subtask scene configs and warn if found
duplicates = check_configclass_field_duplicates(*(subtask.get_scene_cfg() for subtask in self.subtasks))
if duplicates:
import warnings
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to import this locally? Suggestion to move this to the top if not

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to the top

Comment on lines +304 to +310
for subtask in self.subtasks[1:]:
subtask_eef_names_set = set(subtask.get_mimic_env_cfg(arm_mode).subtask_configs.keys())
if subtask_eef_names_set != mimic_eef_names:
raise ValueError(
f"All subtasks must have the same Mimic eef_names.\nSubtask 0 has eef_names: {mimic_eef_names}, but"
f" subtask {self.subtasks.index(subtask)} has eef_names: {subtask_eef_names_set}."
)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion to move out to another function.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

setattr(mimic_env_cfg.datagen_config, key, value)

mimic_env_cfg.subtask_configs = self.combine_mimic_subtask_configs(arm_mode)
return mimic_env_cfg
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing about this class and (unfortunately, the framework), is that we have been inconsistent with the naming of functions with leading underscores. I feel that some of these functions should have leading underscores to indicate that they're internal functions... But admittedly we've not done that consistently, so up to you.

env,
subtasks: list[TaskBase],
desired_subtask_success_state: list[bool] | None,
desired_subtask_success_state: list[bool | None] | None,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion to add an expanded docstring to indicate the meaning of this variable.

Comment on lines +47 to +54
# Compute the success state for the subtasks
for subtask_idx in subtasks_to_evaluate:
subtask_success_func = subtasks[subtask_idx].get_termination_cfg().success.func
subtask_success_params = subtasks[subtask_idx].get_termination_cfg().success.params
results = subtask_success_func(env, **subtask_success_params)
for env_idx in range(env.num_envs):
if results[env_idx]:
current_subtask_success_state[env_idx][subtask_idx] = True
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a (near) duplicate of a loop used in the base class. Could we move this to a shared function?

Comment on lines +29 to +44
class CompositeOpenDoorTask(CompositeTaskBase):
def __init__(
self,
subtasks,
episode_length_s=None,
):
super().__init__(subtasks=subtasks, episode_length_s=episode_length_s)

def get_metrics(self):
return []

def get_prompt(self):
return ""

def get_mimic_env_cfg(self, embodiment_name: str):
return None
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to subclass CompositeTaskBase here?

We don't really add anything to the base class. This structure makes constructing a composite task quite verbose.

Can we not do CompositeTaskBase([subtask_1, subtask_2])?

Also with the current approach we can do

pnp_1  = PickAndPlaceTask(apple, bowl)
pnp_2 = PickAndPlaceTask(banana, bowl)

open_door_composite = OpenDoorCompositeTask([pnp_1, pnp_2])

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants