| from __future__ import annotations |
|
|
| import math |
| from typing import Any |
|
|
| from dovla_cil.tasks.schema import RelationSpec, TaskSpec |
|
|
| SUPPORTED_PREDICATES = { |
| "inside", |
| "near", |
| "next_to", |
| "left_of", |
| "right_of", |
| "behind", |
| "in_front_of", |
| "lifted", |
| "opened", |
| "closed", |
| "grasped", |
| } |
|
|
|
|
| def evaluate_predicate(predicate: RelationSpec, symbolic_state: dict[str, Any]) -> bool: |
| """Evaluate one symbolic relation over a lightweight object state. |
| |
| Supported state shapes are intentionally permissive for simulator adapters. Object state can |
| live either at `state["objects"][object_id]` or directly at `state[object_id]`. Explicit |
| relations can also be provided as `state["relations"][relation_name] = [[arg0, arg1], ...]`. |
| """ |
|
|
| relation = predicate.name |
| args = predicate.args |
| if relation not in SUPPORTED_PREDICATES: |
| raise KeyError(f"Unsupported predicate: {relation}") |
| if _explicit_relation(symbolic_state, relation, args): |
| return True |
|
|
| if relation == "inside": |
| _expect_arity(predicate, 2) |
| obj_state = _object_state(symbolic_state, args[0]) |
| return obj_state.get("inside") == args[1] or obj_state.get("container") == args[1] |
|
|
| if relation in {"near", "next_to"}: |
| _expect_arity(predicate, 2) |
| threshold = float(symbolic_state.get("near_threshold", 0.5)) |
| return _distance(symbolic_state, args[0], args[1]) <= threshold |
|
|
| if relation in {"left_of", "right_of", "behind", "in_front_of"}: |
| _expect_arity(predicate, 2) |
| obj_pos = _position(symbolic_state, args[0]) |
| ref_pos = _position(symbolic_state, args[1]) |
| if relation == "left_of": |
| return obj_pos[0] < ref_pos[0] |
| if relation == "right_of": |
| return obj_pos[0] > ref_pos[0] |
| if relation == "behind": |
| return obj_pos[1] > ref_pos[1] |
| return obj_pos[1] < ref_pos[1] |
|
|
| if relation == "lifted": |
| _expect_arity(predicate, 1) |
| obj_state = _object_state(symbolic_state, args[0]) |
| if "lifted" in obj_state: |
| return bool(obj_state["lifted"]) |
| return _position(symbolic_state, args[0])[2] > float(symbolic_state.get("lifted_z", 0.1)) |
|
|
| if relation == "opened": |
| _expect_arity(predicate, 1) |
| return _is_open(_object_state(symbolic_state, args[0])) |
|
|
| if relation == "closed": |
| _expect_arity(predicate, 1) |
| state = _object_state(symbolic_state, args[0]) |
| if "closed" in state: |
| return bool(state["closed"]) |
| return not _is_open(state) |
|
|
| if relation == "grasped": |
| _expect_arity(predicate, 1) |
| return bool(_object_state(symbolic_state, args[0]).get("grasped", False)) |
|
|
| raise AssertionError(f"Unhandled predicate: {relation}") |
|
|
|
|
| def evaluate_task_success(task: TaskSpec, symbolic_state: dict[str, Any]) -> bool: |
| return all( |
| evaluate_predicate(predicate, symbolic_state) for predicate in task.success_predicates |
| ) |
|
|
|
|
| def near_target(position: float, target_position: float, tolerance: float) -> bool: |
| return abs(float(position) - float(target_position)) <= float(tolerance) |
|
|
|
|
| def moved_toward_target( |
| start_position: float, end_position: float, target_position: float, *, min_delta: float = 1e-6 |
| ) -> bool: |
| start_distance = abs(float(target_position) - float(start_position)) |
| end_distance = abs(float(target_position) - float(end_position)) |
| return end_distance < start_distance - min_delta |
|
|
|
|
| def _expect_arity(predicate: RelationSpec, arity: int) -> None: |
| if len(predicate.args) != arity: |
| raise ValueError( |
| f"Predicate {predicate.name!r} expects {arity} args, got {len(predicate.args)}" |
| ) |
|
|
|
|
| def _object_state(symbolic_state: dict[str, Any], object_id: str) -> dict[str, Any]: |
| objects = symbolic_state.get("objects", {}) |
| if isinstance(objects, dict) and object_id in objects: |
| value = objects[object_id] |
| elif object_id in symbolic_state: |
| value = symbolic_state[object_id] |
| else: |
| raise KeyError(f"Object {object_id!r} is missing from symbolic state") |
| if not isinstance(value, dict): |
| raise KeyError(f"Object {object_id!r} is missing from symbolic state") |
| return value |
|
|
|
|
| def _position(symbolic_state: dict[str, Any], object_id: str) -> tuple[float, float, float]: |
| state = _object_state(symbolic_state, object_id) |
| raw = state.get("position", state.get("pose", state.get("xyz"))) |
| if raw is None: |
| raw = [state.get("x", 0.0), state.get("y", 0.0), state.get("z", 0.0)] |
| if isinstance(raw, dict): |
| return ( |
| float(raw.get("x", 0.0)), |
| float(raw.get("y", 0.0)), |
| float(raw.get("z", 0.0)), |
| ) |
| if isinstance(raw, list | tuple): |
| values = [float(value) for value in raw] |
| while len(values) < 3: |
| values.append(0.0) |
| return values[0], values[1], values[2] |
| raise ValueError(f"Object {object_id!r} position must be a list, tuple, or dict") |
|
|
|
|
| def _distance(symbolic_state: dict[str, Any], left: str, right: str) -> float: |
| left_pos = _position(symbolic_state, left) |
| right_pos = _position(symbolic_state, right) |
| return math.sqrt(sum((left_pos[index] - right_pos[index]) ** 2 for index in range(3))) |
|
|
|
|
| def _is_open(state: dict[str, Any]) -> bool: |
| if "opened" in state: |
| return bool(state["opened"]) |
| if "open" in state: |
| return bool(state["open"]) |
| if state.get("joint_state") == "open": |
| return True |
| if "openness" in state: |
| return float(state["openness"]) > 0.5 |
| return False |
|
|
|
|
| def _explicit_relation(symbolic_state: dict[str, Any], relation: str, args: list[str]) -> bool: |
| relations = symbolic_state.get("relations", {}) |
| if not isinstance(relations, dict): |
| return False |
| candidates = relations.get(relation, []) |
| if isinstance(candidates, dict): |
| candidates = candidates.values() |
| normalized_args = tuple(args) |
| for candidate in candidates: |
| if tuple(candidate) == normalized_args: |
| return True |
| return False |
|
|