Source code for embodichain.lab.sim.atomic_actions.policies
# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------
"""Motion-generation and bounded-recovery policies for atomic actions."""
from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING, Literal
if TYPE_CHECKING:
import torch
from embodichain.lab.sim.motion.motion_generator import MotionGenOptions
from embodichain.lab.sim.motion.planners import PlanOptions
[docs]
class DynamicCollisionMode(str, Enum):
"""Policy for consuming a live dynamic collision world.
This mode controls scene-snapshot obstacle binding and collision-world
revision recovery. It does not enable or disable a planner's configured
static-world or self-collision checks.
"""
OFF = "off"
"""Ignore scene-snapshot collision entities and their revisions."""
AUTO = "auto"
"""Use live collision entities when the selected motion strategy supports them."""
REQUIRED = "required"
"""Require live collision entities and a compatible motion planner."""
[docs]
@dataclass(frozen=True, slots=True)
class MotionPolicy:
"""Immutable motion-generation policy for one action invocation.
The policy is a runtime value object rather than application configuration.
``plan_opts`` is copied on construction so a caller-owned planner config
cannot change an invocation after it has been created.
"""
strategy: Literal["motion_gen", "ik_interp"] = "ik_interp"
"""Motion strategy: ``motion_gen`` or ``ik_interp``."""
sample_count: int = 50
"""Requested trajectory sample count when the backend does not preserve samples."""
dynamic_collision_mode: DynamicCollisionMode = DynamicCollisionMode.AUTO
"""How this invocation consumes live scene-snapshot collision entities."""
plan_opts: PlanOptions | None = None
"""Optional typed planner-specific options."""
def __post_init__(self) -> None:
valid_strategies = {"motion_gen", "ik_interp"}
if self.strategy not in valid_strategies:
raise ValueError(
f"strategy must be one of {sorted(valid_strategies)}, "
f"got {self.strategy!r}."
)
if self.sample_count < 2:
raise ValueError("sample_count must be at least 2.")
mode = self.dynamic_collision_mode
if isinstance(mode, str):
try:
mode = DynamicCollisionMode(mode)
except ValueError as exc:
raise ValueError(
"dynamic_collision_mode must be one of "
f"{[item.value for item in DynamicCollisionMode]}, got {mode!r}."
) from exc
elif not isinstance(mode, DynamicCollisionMode):
raise TypeError(
"dynamic_collision_mode must be a DynamicCollisionMode or string."
)
object.__setattr__(self, "dynamic_collision_mode", mode)
object.__setattr__(self, "plan_opts", deepcopy(self.plan_opts))
[docs]
def to_motion_gen_options(
self,
*,
start_qpos: "torch.Tensor",
control_part: str,
sample_count: int | None = None,
interpolation_dt: float | None = None,
cartesian_linear: bool = False,
) -> "MotionGenOptions":
"""Translate this atomic policy into motion-generator options.
Args:
start_qpos: Observed controlled-joint start positions.
control_part: Bound robot control-part name.
sample_count: Optional segment-local sample-count override.
interpolation_dt: Explicit waypoint interval used only by
deterministic interpolation.
cartesian_linear: Whether every supplied Cartesian keyframe is a
required linear-path sample rather than a sparse endpoint.
Returns:
Independently owned options for :class:`MotionGenerator`.
"""
from embodichain.lab.sim.motion.motion_generator import (
MotionGenOptions,
)
return MotionGenOptions(
strategy=self.strategy,
sample_count=self.sample_count if sample_count is None else sample_count,
start_qpos=start_qpos,
control_part=control_part,
plan_opts=self.plan_opts,
is_interpolate=True,
interpolation_dt=interpolation_dt,
is_linear=cartesian_linear,
preserve_cartesian_samples=cartesian_linear,
)
[docs]
@dataclass(frozen=True, slots=True)
class RecoveryPolicy:
"""Bounded local recovery policy used by the execution runtime."""
max_replans: int = 3
"""Maximum replans within one action attempt."""
max_action_retries: int = 2
"""Maximum whole-action retries after planning, execution, or effect failure."""
goal_translation_threshold: float = 0.02
"""Dynamic-goal translation threshold in metres."""
goal_rotation_threshold: float = 0.0872664626
"""Dynamic-goal rotation threshold in radians (five degrees by default)."""
action_timeout: float = 30.0
"""Maximum time for one action attempt, including terminal effect verification."""
def __post_init__(self) -> None:
if self.max_replans < 0:
raise ValueError("max_replans must be non-negative.")
if self.max_action_retries < 0:
raise ValueError("max_action_retries must be non-negative.")
threshold_fields = (
"goal_translation_threshold",
"goal_rotation_threshold",
"action_timeout",
)
for name in threshold_fields:
if getattr(self, name) <= 0.0:
raise ValueError(f"{name} must be greater than zero.")
__all__ = ["DynamicCollisionMode", "MotionPolicy", "RecoveryPolicy"]