diff --git a/isaaclab_arena/environments/__init__.py b/isaaclab_arena/environments/__init__.py deleted file mode 100644 index fee3a6a9f..000000000 --- a/isaaclab_arena/environments/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md). -# All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 diff --git a/isaaclab_arena/environments/arena_env_graph_spec.py b/isaaclab_arena/environments/arena_env_graph_spec.py new file mode 100644 index 000000000..1dbf5f592 --- /dev/null +++ b/isaaclab_arena/environments/arena_env_graph_spec.py @@ -0,0 +1,223 @@ +# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +import yaml +from dataclasses import dataclass, field +from enum import Enum +from pathlib import Path +from typing import Any + +from isaaclab_arena.assets.object_base import ObjectType +from isaaclab_arena.environments.utils import ( + as_dict, + assert_env_graph_references_exist, + assert_env_graph_universal_ids, + optional_dict, + optional_enum, + optional_str, + parse_list, + required_enum, + required_number_sequence, + required_str, +) + + +class ArenaEnvGraphNodeType(Enum): + EMBODIMENT = "embodiment" + BACKGROUND = "background" + OBJECT = "object" + OBJECT_REFERENCE = "objectReference" + LIGHTING = "lighting" + + +class ArenaEnvGraphSpatialConstraintType(Enum): + IS_ANCHOR = "is_anchor" + NEXT_TO = "next_to" + ON = "on" + AT_POSE = "at_pose" # through set_initial_pose() + AT_POSITION = "at_position" # through object relation solver: AtPosition + POSITION_LIMITS = "position_limits" + RANDOM_AROUND_SOLUTION = "random_around_solution" + ROTATE_AROUND_SOLUTION = "rotate_around_solution" + # TODO(xinjieyao, 2026-05-21): Support "in" in solver + IN = "in" + + +@dataclass +class ArenaEnvGraphNodeSpec: + """Node in an environment graph. + + Could be an object, an object reference, an embodiment, a background, etc. + """ + + id: str + name: str # Name registered in the asset registry + type: ArenaEnvGraphNodeType + parent: str | None = None # Optional, only need for object references + prim_path: str | None = None # Optional, only need for object references + object_type: ObjectType | None = None # Optional, only need for type=object + params: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class ArenaEnvGraphSpatialConstraintSpec: + """Spatial constraint edge in an environment graph state spec. + + It defines a relation between two nodes. + """ + + id: str + type: ArenaEnvGraphSpatialConstraintType + parent: str + child: str | None = None # Optional, e.g. is_anchor constraint does not have a child + params: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class ArenaEnvGraphTaskConstraintSpec: + """Task-dependent constraint edge in an environment graph state spec.""" + + id: str + type: str + parent: str + child: str | None = None # Optional, could be a robot keeps gripper open or closed, or a single object + params: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class ArenaEnvGraphStateSpec: + """Snapshot of the environment state in the graph. + + Could be an initial, intermediate, or final state. + """ + + id: str + name: str + spatial_constraints: list[ArenaEnvGraphSpatialConstraintSpec] = field(default_factory=list) + task_constraints: list[ArenaEnvGraphTaskConstraintSpec] = field(default_factory=list) + + +@dataclass +class ArenaEnvGraphTaskSpec: + """Task entry in an environment graph.""" + + id: str + name: str + type: str # Task class name, could be a custom task class or a built-in task class + initial_state_spec_id: str + success_state_spec_id: str + task_args: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class ArenaEnvGraphSpec: + """Typed representation of an environment graph YAML file. + It defines the nodes, tasks, and state specs of the environment graph. + """ + + env_name: str + nodes: list[ArenaEnvGraphNodeSpec] = field(default_factory=list) + tasks: list[ArenaEnvGraphTaskSpec] = field(default_factory=list) + state_specs: list[ArenaEnvGraphStateSpec] = field(default_factory=list) + + @classmethod + def from_yaml(cls, path: str | Path) -> "ArenaEnvGraphSpec": + with Path(path).open("r", encoding="utf-8") as f: + return cls.from_dict(yaml.safe_load(f)) + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "ArenaEnvGraphSpec": + data = as_dict(data, "Env graph spec") + nodes = parse_list(data, "nodes", _parse_node) + tasks = parse_list(data, "tasks", _parse_task) + state_specs = parse_list(data, "state_specs", _parse_state_spec) + + assert_env_graph_universal_ids(nodes, tasks, state_specs) + assert_env_graph_references_exist(nodes, tasks, state_specs) + + return cls( + env_name=required_str(data, "env_name"), + nodes=nodes, + tasks=tasks, + state_specs=state_specs, + ) + + @property + def nodes_by_id(self) -> dict[str, ArenaEnvGraphNodeSpec]: + return {node.id: node for node in self.nodes} + + @property + def tasks_by_id(self) -> dict[str, ArenaEnvGraphTaskSpec]: + return {task.id: task for task in self.tasks} + + @property + def state_specs_by_id(self) -> dict[str, ArenaEnvGraphStateSpec]: + return {state_spec.id: state_spec for state_spec in self.state_specs} + + +def _parse_node(data: Any) -> ArenaEnvGraphNodeSpec: + data = as_dict(data, "Node spec") + return ArenaEnvGraphNodeSpec( + id=required_str(data, "id"), + name=required_str(data, "name"), + type=required_enum(data, "type", ArenaEnvGraphNodeType), + parent=optional_str(data, "parent"), + prim_path=optional_str(data, "prim_path"), + object_type=optional_enum(data, "object_type", ObjectType), + params=optional_dict(data, "params"), + ) + + +def _parse_spatial_constraint(data: Any) -> ArenaEnvGraphSpatialConstraintSpec: + data = as_dict(data, "Spatial constraint spec") + constraint_type = required_enum(data, "type", ArenaEnvGraphSpatialConstraintType) + params = optional_dict(data, "params") + if constraint_type == ArenaEnvGraphSpatialConstraintType.AT_POSE: + params["position_xyz"] = required_number_sequence(params, "position_xyz", 3) + params["rotation_xyzw"] = required_number_sequence(params, "rotation_xyzw", 4) + + return ArenaEnvGraphSpatialConstraintSpec( + id=required_str(data, "id"), + type=constraint_type, + parent=required_str(data, "parent"), + child=optional_str(data, "child"), + params=params, + ) + + +def _parse_task_constraint(data: Any) -> ArenaEnvGraphTaskConstraintSpec: + data = as_dict(data, "Task constraint spec") + return ArenaEnvGraphTaskConstraintSpec( + id=required_str(data, "id"), + type=required_str(data, "type"), + parent=optional_str(data, "parent"), + child=optional_str(data, "child"), + params=optional_dict(data, "params"), + ) + + +def _parse_state_spec(data: Any) -> ArenaEnvGraphStateSpec: + data = as_dict(data, "State spec") + assert "edges" not in data, "State spec must define spatial_constraints and task_constraints directly" + return ArenaEnvGraphStateSpec( + id=required_str(data, "id"), + name=required_str(data, "name"), + spatial_constraints=parse_list(data, "spatial_constraints", _parse_spatial_constraint), + task_constraints=parse_list(data, "task_constraints", _parse_task_constraint), + ) + + +def _parse_task(data: Any) -> ArenaEnvGraphTaskSpec: + data = as_dict(data, "Task spec") + for old_key in ("state_specs", "initial_state_spec", "success_state_spec"): + assert old_key not in data, "Task spec must use initial_state_spec_id and success_state_spec_id" + return ArenaEnvGraphTaskSpec( + id=required_str(data, "id"), + name=required_str(data, "name"), + type=required_str(data, "type"), + initial_state_spec_id=required_str(data, "initial_state_spec_id"), + success_state_spec_id=required_str(data, "success_state_spec_id"), + task_args=optional_dict(data, "task_args"), + ) diff --git a/isaaclab_arena/environments/utils.py b/isaaclab_arena/environments/utils.py new file mode 100644 index 000000000..6942b345b --- /dev/null +++ b/isaaclab_arena/environments/utils.py @@ -0,0 +1,130 @@ +# Copyright (c) 2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +from collections.abc import Callable +from enum import Enum +from numbers import Real +from typing import Any + + +def as_dict(data: Any, spec_name: str) -> dict[str, Any]: + assert isinstance(data, dict), f"{spec_name} must be a dict, got {type(data).__name__}" + return data + + +def parse_list(data: dict[str, Any], key: str, parser: Callable[[Any], Any]) -> list[Any]: + values = data.get(key, []) + assert isinstance(values, list), f"Field '{key}' must be a list" + return [parser(value) for value in values] + + +def required_str(data: dict[str, Any], key: str) -> str: + value = data.get(key) + assert isinstance(value, str) and value, f"Missing required string field '{key}'" + return value + + +def optional_str(data: dict[str, Any], key: str) -> str | None: + value = data.get(key) + assert value is None or isinstance(value, str), f"Optional field '{key}' must be a string when set" + return value + + +def optional_dict(data: dict[str, Any], key: str) -> dict[str, Any]: + value = data.get(key, {}) + assert value is None or isinstance(value, dict), f"Optional field '{key}' must be a dict when set" + return dict(value or {}) + + +def required_number_sequence(data: dict[str, Any], key: str, length: int) -> tuple[float, ...]: + value = data.get(key) + assert isinstance(value, (list, tuple)), f"Missing required numeric sequence field '{key}'" + assert len(value) == length, f"Field '{key}' must contain {length} numbers" + assert all( + isinstance(item, Real) and not isinstance(item, bool) for item in value + ), f"Field '{key}' must contain only numbers" + return tuple(float(item) for item in value) + + +def required_enum(data: dict[str, Any], key: str, enum_type: type[Enum]) -> Enum: + value = data.get(key) + assert value is not None, f"Missing required field '{key}'" + parsed = parse_enum(value, key, enum_type) + assert parsed is not None + return parsed + + +def optional_enum(data: dict[str, Any], key: str, enum_type: type[Enum]) -> Enum | None: + return parse_enum(data.get(key), key, enum_type) + + +def parse_enum(value: Any, key: str, enum_type: type[Enum]) -> Enum | None: + if value is None or isinstance(value, enum_type): + return value + assert isinstance(value, str), f"Field '{key}' must be a string when set" + try: + return enum_type(value) + except ValueError: + valid_values = [enum_value.value for enum_value in enum_type] + raise AssertionError(f"Unknown {key} '{value}'. Expected one of {valid_values}") from None + + +def assert_env_graph_universal_ids(nodes: list[Any], tasks: list[Any], state_specs: list[Any]) -> None: + id_locations: dict[str, list[str]] = {} + for node in nodes: + _add_id_location(id_locations, node.id, f"node '{node.id}'") + for task in tasks: + _add_id_location(id_locations, task.id, f"task '{task.id}'") + for state_spec in state_specs: + _add_id_location(id_locations, state_spec.id, f"state spec '{state_spec.id}'") + for constraint in state_spec.spatial_constraints: + _add_id_location(id_locations, constraint.id, f"spatial constraint '{constraint.id}'") + for constraint in state_spec.task_constraints: + _add_id_location(id_locations, constraint.id, f"task constraint '{constraint.id}'") + + duplicates = {spec_id: locations for spec_id, locations in id_locations.items() if len(locations) > 1} + assert not duplicates, f"Duplicate env graph ids found: {duplicates}" + + +def assert_env_graph_references_exist(nodes: list[Any], tasks: list[Any], state_specs: list[Any]) -> None: + node_ids = {node.id for node in nodes} + state_spec_ids = {state_spec.id for state_spec in state_specs} + + for node in nodes: + if node.parent is not None: + assert node.parent in node_ids, f"Node '{node.id}' references unknown parent '{node.parent}'" + + for task in tasks: + for label, state_spec_id in ( + ("initial_state_spec_id", task.initial_state_spec_id), + ("success_state_spec_id", task.success_state_spec_id), + ): + assert ( + state_spec_id in state_spec_ids + ), f"Task '{task.id}' references unknown state spec '{state_spec_id}' for '{label}'" + + for state_spec in state_specs: + for constraint in state_spec.spatial_constraints: + assert ( + constraint.parent in node_ids + ), f"Constraint '{constraint.id}' references unknown parent node '{constraint.parent}'" + if constraint.child is not None: + assert ( + constraint.child in node_ids + ), f"Constraint '{constraint.id}' references unknown child node '{constraint.child}'" + + for constraint in state_spec.task_constraints: + if constraint.parent is not None: + assert ( + constraint.parent in node_ids + ), f"Constraint '{constraint.id}' references unknown parent node '{constraint.parent}'" + if constraint.child is not None: + assert ( + constraint.child in node_ids + ), f"Constraint '{constraint.id}' references unknown child node '{constraint.child}'" + + +def _add_id_location(id_locations: dict[str, list[str]], spec_id: str, location: str) -> None: + id_locations.setdefault(spec_id, []).append(location) diff --git a/isaaclab_arena/tests/test_arena_env_graph_spec.py b/isaaclab_arena/tests/test_arena_env_graph_spec.py new file mode 100644 index 000000000..76f0570f0 --- /dev/null +++ b/isaaclab_arena/tests/test_arena_env_graph_spec.py @@ -0,0 +1,216 @@ +# Copyright (c) 2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +from pathlib import Path + +from isaaclab_arena.assets.object_base import ObjectType +from isaaclab_arena.environments.arena_env_graph_spec import ( + ArenaEnvGraphNodeType, + ArenaEnvGraphSpatialConstraintType, + ArenaEnvGraphSpec, + ArenaEnvGraphStateSpec, +) + +TEST_DATA_DIR = Path(__file__).parent / "test_data" + + +def test_arena_env_graph_spec_loads_pick_and_place_yaml(): + spec = ArenaEnvGraphSpec.from_yaml(TEST_DATA_DIR / "pick_and_place_maple_table_env_graph.yaml") + + assert spec.env_name == "pick_and_place_maple_table_default" + assert len(spec.nodes) == 5 + assert len(spec.tasks) == 1 + assert len(spec.state_specs) == 2 + + table = spec.nodes_by_id["maple_table_robolab_table"] + assert table.type == ArenaEnvGraphNodeType.OBJECT_REFERENCE + assert table.parent == "maple_table_robolab" + assert table.prim_path == "{ENV_REGEX_NS}/maple_table_robolab/table" + assert table.object_type == ObjectType.RIGID + + task = spec.tasks_by_id["pick_and_place_0"] + assert task.initial_state_spec_id == "state_spec_0" + assert task.success_state_spec_id == "state_spec_1" + assert task.task_args["object"] == "rubiks_cube_hot3d_robolab" + assert task.task_args["destination"] == "bowl_ycb_robolab" + + initial_state = spec.state_specs_by_id["state_spec_0"] + assert isinstance(initial_state, ArenaEnvGraphStateSpec) + assert len(initial_state.spatial_constraints) == 5 + assert len(initial_state.task_constraints) == 1 + + cube_limits = initial_state.spatial_constraints[2] + assert cube_limits.type == ArenaEnvGraphSpatialConstraintType.POSITION_LIMITS + assert cube_limits.parent == "rubiks_cube_hot3d_robolab" + assert cube_limits.params == {"x_min": 0.55, "x_max": 0.70, "y_min": -0.40, "y_max": -0.10} + + final_state = spec.state_specs_by_id["state_spec_1"] + in_constraint = final_state.spatial_constraints[3] + assert in_constraint.type == ArenaEnvGraphSpatialConstraintType.IN + assert in_constraint.parent == "bowl_ycb_robolab" + assert in_constraint.child == "rubiks_cube_hot3d_robolab" + + +def test_arena_env_graph_spec_parses_optional_task_constraints_and_at_pose(): + data = _minimal_env_graph_data() + data["state_specs"][0]["spatial_constraints"] = [_at_pose_constraint()] + del data["state_specs"][0]["task_constraints"] + + spec = ArenaEnvGraphSpec.from_dict(data) + state_spec = spec.state_specs_by_id["state_0"] + fixed_pose = state_spec.spatial_constraints[0] + + assert state_spec.task_constraints == [] + assert fixed_pose.type == ArenaEnvGraphSpatialConstraintType.AT_POSE + assert fixed_pose.parent == "cube" + assert fixed_pose.params["position_xyz"] == (0.1, 0.2, 0.3) + assert fixed_pose.params["rotation_xyzw"] == (0.0, 0.0, 0.0, 1.0) + + +def test_arena_env_graph_spec_rejects_invalid_data(): + cases = [ + ( + "duplicate node id", + lambda data: data["nodes"].append({"id": "table", "name": "duplicate_table", "type": "objectReference"}), + "Duplicate env graph ids", + ), + ( + "duplicate id across spec types", + lambda data: data["tasks"][0].__setitem__("id", "table"), + "Duplicate env graph ids", + ), + ( + "duplicate constraint id", + lambda data: data["state_specs"][0]["task_constraints"][0].__setitem__("id", "table_is_anchor"), + "Duplicate env graph ids", + ), + ( + "missing task state reference", + lambda data: data["tasks"][0].__setitem__("initial_state_spec_id", "missing_state"), + "unknown state spec 'missing_state'", + ), + ( + "missing required task state spec id", + lambda data: data["tasks"][0].pop("success_state_spec_id"), + "Missing required string field 'success_state_spec_id'", + ), + ( + "old task state map", + lambda data: data["tasks"][0].__setitem__("state_specs", {"initial": "state_0", "final": "state_0"}), + "must use initial_state_spec_id and success_state_spec_id", + ), + ( + "old task state keys", + _add_old_task_state_keys, + "must use initial_state_spec_id and success_state_spec_id", + ), + ( + "missing constraint node reference", + lambda data: data["state_specs"][0]["task_constraints"][0].__setitem__("child", "missing_cube"), + "unknown child node 'missing_cube'", + ), + ( + "missing spatial parent", + lambda data: data["state_specs"][0]["spatial_constraints"][0].pop("parent"), + "Missing required string field 'parent'", + ), + ( + "old state edges wrapper", + _move_state_constraints_under_edges, + "must define spatial_constraints and task_constraints directly", + ), + ( + "missing node parent reference", + lambda data: data["nodes"][1].__setitem__("parent", "missing_background"), + "unknown parent 'missing_background'", + ), + ( + "unknown object type", + lambda data: data["nodes"][1].__setitem__("object_type", "unknown"), + "Unknown object_type 'unknown'", + ), + ( + "unknown node type", + lambda data: data["nodes"][0].__setitem__("type", "unknown"), + "Unknown type 'unknown'", + ), + ( + "unknown spatial constraint type", + lambda data: data["state_specs"][0]["spatial_constraints"][0].__setitem__("type", "unknown"), + "Unknown type 'unknown'", + ), + ( + "invalid at_pose position", + lambda data: data["state_specs"][0]["spatial_constraints"].append( + _at_pose_constraint(position_xyz=[0.1, 0.2]) + ), + "Field 'position_xyz' must contain 3 numbers", + ), + ] + + for label, mutate, error_match in cases: + data = _minimal_env_graph_data() + mutate(data) + + try: + ArenaEnvGraphSpec.from_dict(data) + except AssertionError as exc: + assert error_match in str(exc), label + else: + raise AssertionError(f"{label}: expected AssertionError") + + +def _minimal_env_graph_data(): + return { + "env_name": "minimal_env_graph", + "nodes": [ + {"id": "robot", "name": "robot", "type": "embodiment"}, + {"id": "table", "name": "table", "type": "objectReference"}, + {"id": "cube", "name": "cube", "type": "object"}, + ], + "tasks": [{ + "id": "task_0", + "name": "task_0", + "type": "pick_and_place", + "initial_state_spec_id": "state_0", + "success_state_spec_id": "state_0", + }], + "state_specs": [{ + "id": "state_0", + "name": "state_0", + "spatial_constraints": [{"id": "table_is_anchor", "type": "is_anchor", "parent": "table"}], + "task_constraints": [{ + "id": "robot_reach_cube", + "type": "reach", + "parent": "robot", + "child": "cube", + }], + }], + } + + +def _at_pose_constraint(position_xyz=None, rotation_xyzw=None): + return { + "id": "cube_fixed_pose", + "type": "at_pose", + "parent": "cube", + "params": { + "position_xyz": [0.1, 0.2, 0.3] if position_xyz is None else position_xyz, + "rotation_xyzw": [0.0, 0.0, 0.0, 1.0] if rotation_xyzw is None else rotation_xyzw, + }, + } + + +def _add_old_task_state_keys(data): + data["tasks"][0]["initial_state_spec"] = "state_0" + data["tasks"][0]["success_state_spec"] = "state_0" + + +def _move_state_constraints_under_edges(data): + state_spec = data["state_specs"][0] + state_spec["edges"] = { + "spatial_constraints": state_spec.pop("spatial_constraints"), + "task_constraints": state_spec.pop("task_constraints"), + } diff --git a/isaaclab_arena/tests/test_data/pick_and_place_maple_table_env_graph.yaml b/isaaclab_arena/tests/test_data/pick_and_place_maple_table_env_graph.yaml new file mode 100644 index 000000000..537b42e1c --- /dev/null +++ b/isaaclab_arena/tests/test_data/pick_and_place_maple_table_env_graph.yaml @@ -0,0 +1,116 @@ +# Copyright (c) 2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +env_name: pick_and_place_maple_table_default + +nodes: + - id: droid_abs_joint_pos + name: droid_abs_joint_pos + type: embodiment + + - id: maple_table_robolab + name: maple_table_robolab + type: background + + - id: maple_table_robolab_table + name: table + type: objectReference + parent: maple_table_robolab + prim_path: "{ENV_REGEX_NS}/maple_table_robolab/table" + object_type: rigid + + - id: rubiks_cube_hot3d_robolab + name: rubiks_cube_hot3d_robolab + type: object + + - id: bowl_ycb_robolab + name: bowl_ycb_robolab + type: object + +tasks: + - id: pick_and_place_0 + name: pick_and_place_0 + type: pick_and_place + initial_state_spec_id: state_spec_0 + success_state_spec_id: state_spec_1 + task_args: + object: rubiks_cube_hot3d_robolab + destination: bowl_ycb_robolab + background: maple_table_robolab + episode_length_s: 20.0 + +state_specs: + - id: state_spec_0 + name: state_spec_0 + spatial_constraints: + - id: state_spec_0_maple_table_robolab_table_is_anchor + type: is_anchor + parent: maple_table_robolab_table + + - id: state_spec_0_rubiks_cube_hot3d_robolab_on_maple_table_robolab_table + type: "on" + parent: maple_table_robolab_table + child: rubiks_cube_hot3d_robolab + + - id: state_spec_0_rubiks_cube_hot3d_robolab_position_limits + type: position_limits + parent: rubiks_cube_hot3d_robolab + params: + x_min: 0.55 + x_max: 0.70 + y_min: -0.40 + y_max: -0.10 + + - id: state_spec_0_bowl_ycb_robolab_on_maple_table_robolab_table + type: "on" + parent: maple_table_robolab_table + child: bowl_ycb_robolab + + - id: state_spec_0_bowl_ycb_robolab_position_limits + type: position_limits + parent: bowl_ycb_robolab + params: + x_min: 0.55 + x_max: 0.70 + y_min: -0.40 + y_max: -0.10 + + task_constraints: + - id: state_spec_0_droid_reach_rubiks_cube_hot3d_robolab + type: "reach" + parent: droid_abs_joint_pos + child: rubiks_cube_hot3d_robolab + + - id: state_spec_1 + name: state_spec_1 + spatial_constraints: + - id: state_spec_1_maple_table_robolab_table_is_anchor + type: is_anchor + parent: maple_table_robolab_table + + - id: state_spec_1_bowl_ycb_robolab_on_maple_table_robolab_table + type: "on" + parent: maple_table_robolab_table + child: bowl_ycb_robolab + + - id: state_spec_1_bowl_ycb_robolab_position_limits + type: position_limits + parent: bowl_ycb_robolab + params: + x_min: 0.55 + x_max: 0.70 + y_min: -0.40 + y_max: -0.10 + + - id: state_spec_1_rubiks_cube_hot3d_robolab_in_bowl_ycb_robolab + type: "in" + parent: bowl_ycb_robolab + child: rubiks_cube_hot3d_robolab + + task_constraints: + - id: state_spec_1_droid_reach_bowl_ycb_robolab + type: "reach" + parent: droid_abs_joint_pos + child: bowl_ycb_robolab