1124 lines
42 KiB
Python
1124 lines
42 KiB
Python
|
|
"""Reproducible retargeting contracts and H1 baseline implementations.
|
||
|
|
|
||
|
|
The historical retargeting scripts returned method-specific tuples and debug
|
||
|
|
dictionaries. Formal trajectory experiments need every method to expose the
|
||
|
|
same success/failure semantics and solver diagnostics. This module provides
|
||
|
|
that contract without changing :mod:`core.sew_mapper2`.
|
||
|
|
|
||
|
|
The two Cartesian baselines intentionally share one :class:`PoseTargetBuilder`
|
||
|
|
so their comparison changes only the inverse-kinematics method. The builder
|
||
|
|
maps *changes* in the master shoulder-relative end-effector pose about frozen
|
||
|
|
reference configurations. This makes the reference pose exactly attainable
|
||
|
|
on the slave while keeping the mapping explicit and reproducible.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from enum import Enum
|
||
|
|
from time import perf_counter
|
||
|
|
from typing import Optional, Protocol, Sequence, runtime_checkable
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
import pinocchio as pin
|
||
|
|
|
||
|
|
from .model_contract import (
|
||
|
|
MASTER_FRAMES,
|
||
|
|
MASTER_JOINT_NAMES,
|
||
|
|
SLAVE_FRAMES,
|
||
|
|
SLAVE_JOINT_NAMES,
|
||
|
|
TeleoperationModels,
|
||
|
|
finite_joint_limits,
|
||
|
|
joint_q_indices,
|
||
|
|
load_models,
|
||
|
|
require_frame,
|
||
|
|
safe_configuration,
|
||
|
|
)
|
||
|
|
from .sew_mapper2 import BallJointConfig, SEWMapper
|
||
|
|
|
||
|
|
|
||
|
|
def _readonly_array(value: np.ndarray, shape: tuple[int, ...], name: str) -> np.ndarray:
|
||
|
|
array = np.asarray(value, dtype=float).copy()
|
||
|
|
if array.shape != shape:
|
||
|
|
raise ValueError(f"{name} must have shape {shape}, got {array.shape}")
|
||
|
|
if not np.all(np.isfinite(array)):
|
||
|
|
raise ValueError(f"{name} must contain only finite values")
|
||
|
|
array.setflags(write=False)
|
||
|
|
return array
|
||
|
|
|
||
|
|
|
||
|
|
def _pose_error(
|
||
|
|
current_position: np.ndarray,
|
||
|
|
current_rotation: np.ndarray,
|
||
|
|
target: "PoseTarget",
|
||
|
|
) -> tuple[np.ndarray, np.ndarray]:
|
||
|
|
"""Return position and world-axis orientation errors."""
|
||
|
|
position_error = target.position - current_position
|
||
|
|
orientation_error = pin.log3(target.rotation @ current_rotation.T)
|
||
|
|
return position_error, orientation_error
|
||
|
|
|
||
|
|
|
||
|
|
class RetargetingFailure(str, Enum):
|
||
|
|
"""Stable trajectory-level failure categories used by H1."""
|
||
|
|
|
||
|
|
NONE = "none"
|
||
|
|
INVALID_INPUT = "invalid_input"
|
||
|
|
MASTER_LIMIT_VIOLATION = "master_limit_violation"
|
||
|
|
DEGENERATE_GEOMETRY = "degenerate_geometry"
|
||
|
|
JOINT_LIMIT_VIOLATION = "joint_limit_violation"
|
||
|
|
TASK_TOLERANCE_EXCEEDED = "task_tolerance_exceeded"
|
||
|
|
SOLVER_NOT_CONVERGED = "solver_not_converged"
|
||
|
|
NUMERICAL_FAILURE = "numerical_failure"
|
||
|
|
|
||
|
|
|
||
|
|
class RetargetingSolverStatus(str, Enum):
|
||
|
|
"""Method-independent solver termination status."""
|
||
|
|
|
||
|
|
CLOSED_FORM = "closed_form"
|
||
|
|
CONVERGED = "converged"
|
||
|
|
MAX_ITERATIONS = "max_iterations"
|
||
|
|
INVALID_INPUT = "invalid_input"
|
||
|
|
FAILED = "failed"
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass(frozen=True)
|
||
|
|
class PoseTarget:
|
||
|
|
"""Slave end-effector target, expressed in the slave model world frame."""
|
||
|
|
|
||
|
|
position: np.ndarray
|
||
|
|
rotation: np.ndarray
|
||
|
|
valid: bool = True
|
||
|
|
smooth: bool = True
|
||
|
|
events: tuple[str, ...] = ()
|
||
|
|
|
||
|
|
def __post_init__(self) -> None:
|
||
|
|
object.__setattr__(
|
||
|
|
self, "position", _readonly_array(self.position, (3,), "position")
|
||
|
|
)
|
||
|
|
object.__setattr__(
|
||
|
|
self, "rotation", _readonly_array(self.rotation, (3, 3), "rotation")
|
||
|
|
)
|
||
|
|
if not np.allclose(self.rotation.T @ self.rotation, np.eye(3), atol=1e-8):
|
||
|
|
raise ValueError("rotation must be orthonormal")
|
||
|
|
if np.linalg.det(self.rotation) <= 0.0:
|
||
|
|
raise ValueError("rotation must be a proper rotation")
|
||
|
|
object.__setattr__(
|
||
|
|
self, "events", tuple(str(event) for event in self.events)
|
||
|
|
)
|
||
|
|
if self.smooth and not self.valid:
|
||
|
|
raise ValueError("an invalid target cannot be smooth")
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass(frozen=True)
|
||
|
|
class RetargetingDiagnostics:
|
||
|
|
"""Diagnostics recorded once per retargeting sample."""
|
||
|
|
|
||
|
|
status: RetargetingSolverStatus
|
||
|
|
iterations: int
|
||
|
|
runtime_s: float
|
||
|
|
cost: float
|
||
|
|
position_error_m: float
|
||
|
|
orientation_error_rad: float
|
||
|
|
active_limit_indices: tuple[int, ...] = ()
|
||
|
|
clipped: bool = False
|
||
|
|
message: str = ""
|
||
|
|
|
||
|
|
def __post_init__(self) -> None:
|
||
|
|
if self.iterations < 0:
|
||
|
|
raise ValueError("iterations must be non-negative")
|
||
|
|
numeric = (
|
||
|
|
self.runtime_s,
|
||
|
|
self.cost,
|
||
|
|
self.position_error_m,
|
||
|
|
self.orientation_error_rad,
|
||
|
|
)
|
||
|
|
if any(not np.isfinite(item) or item < 0.0 for item in numeric):
|
||
|
|
raise ValueError("diagnostic scalars must be finite and non-negative")
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass(frozen=True)
|
||
|
|
class RetargetingResult:
|
||
|
|
"""Uniform output returned by every retargeting method."""
|
||
|
|
|
||
|
|
method: str
|
||
|
|
q_slave: np.ndarray
|
||
|
|
success: bool
|
||
|
|
smooth: bool
|
||
|
|
failure: RetargetingFailure
|
||
|
|
diagnostics: RetargetingDiagnostics
|
||
|
|
target: Optional[PoseTarget] = None
|
||
|
|
events: tuple[str, ...] = ()
|
||
|
|
|
||
|
|
def __post_init__(self) -> None:
|
||
|
|
q_slave = np.asarray(self.q_slave, dtype=float).copy()
|
||
|
|
if q_slave.ndim != 1:
|
||
|
|
raise ValueError("q_slave must be a one-dimensional configuration")
|
||
|
|
if not np.all(np.isfinite(q_slave)):
|
||
|
|
raise ValueError("q_slave must contain only finite values")
|
||
|
|
q_slave.setflags(write=False)
|
||
|
|
object.__setattr__(self, "q_slave", q_slave)
|
||
|
|
if not self.method:
|
||
|
|
raise ValueError("method must be non-empty")
|
||
|
|
if self.success != (self.failure is RetargetingFailure.NONE):
|
||
|
|
raise ValueError("success must be equivalent to failure == NONE")
|
||
|
|
if self.smooth and not self.success:
|
||
|
|
raise ValueError("a failed result cannot be smooth")
|
||
|
|
|
||
|
|
|
||
|
|
@runtime_checkable
|
||
|
|
class Retargeter(Protocol):
|
||
|
|
"""Structural interface consumed by a future trajectory experiment runner."""
|
||
|
|
|
||
|
|
name: str
|
||
|
|
|
||
|
|
def retarget(
|
||
|
|
self,
|
||
|
|
q_master: np.ndarray,
|
||
|
|
q_slave_seed: Optional[np.ndarray] = None,
|
||
|
|
) -> RetargetingResult:
|
||
|
|
...
|
||
|
|
|
||
|
|
|
||
|
|
class PoseTargetProvider(Protocol):
|
||
|
|
"""Target policy shared by Cartesian retargeting baselines."""
|
||
|
|
|
||
|
|
q_slave_reference: np.ndarray
|
||
|
|
|
||
|
|
def build(self, q_master: np.ndarray) -> PoseTarget:
|
||
|
|
...
|
||
|
|
|
||
|
|
|
||
|
|
class PoseTargetBuilder:
|
||
|
|
"""Build one frozen, cross-embodiment Cartesian target convention."""
|
||
|
|
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
master_model: pin.Model,
|
||
|
|
slave_model: pin.Model,
|
||
|
|
*,
|
||
|
|
master_joint_names: Sequence[str],
|
||
|
|
slave_joint_names: Sequence[str],
|
||
|
|
master_shoulder_frame: str,
|
||
|
|
master_ee_frame: str,
|
||
|
|
slave_shoulder_frame: str,
|
||
|
|
slave_ee_frame: str,
|
||
|
|
q_master_reference: Optional[np.ndarray] = None,
|
||
|
|
q_slave_reference: Optional[np.ndarray] = None,
|
||
|
|
translation_scale: Optional[float] = None,
|
||
|
|
) -> None:
|
||
|
|
self.master_model = master_model
|
||
|
|
self.slave_model = slave_model
|
||
|
|
self.master_data = master_model.createData()
|
||
|
|
self.slave_data = slave_model.createData()
|
||
|
|
self.master_q_indices = joint_q_indices(master_model, master_joint_names)
|
||
|
|
self.slave_q_indices = joint_q_indices(slave_model, slave_joint_names)
|
||
|
|
self.master_shoulder_id = require_frame(master_model, master_shoulder_frame)
|
||
|
|
self.master_ee_id = require_frame(master_model, master_ee_frame)
|
||
|
|
self.slave_shoulder_id = require_frame(slave_model, slave_shoulder_frame)
|
||
|
|
self.slave_ee_id = require_frame(slave_model, slave_ee_frame)
|
||
|
|
|
||
|
|
q_m_ref = (
|
||
|
|
safe_configuration(master_model, master_joint_names)
|
||
|
|
if q_master_reference is None
|
||
|
|
else np.asarray(q_master_reference, dtype=float)
|
||
|
|
)
|
||
|
|
q_s_ref = (
|
||
|
|
safe_configuration(slave_model, slave_joint_names)
|
||
|
|
if q_slave_reference is None
|
||
|
|
else np.asarray(q_slave_reference, dtype=float)
|
||
|
|
)
|
||
|
|
self.q_master_reference = _readonly_array(
|
||
|
|
q_m_ref, (master_model.nq,), "q_master_reference"
|
||
|
|
)
|
||
|
|
self.q_slave_reference = _readonly_array(
|
||
|
|
q_s_ref, (slave_model.nq,), "q_slave_reference"
|
||
|
|
)
|
||
|
|
|
||
|
|
master_reference = self._master_pose(self.q_master_reference)
|
||
|
|
slave_reference = self._slave_pose(self.q_slave_reference)
|
||
|
|
self._master_relative_reference = (
|
||
|
|
master_reference[0] - master_reference[2]
|
||
|
|
)
|
||
|
|
self._slave_position_reference = slave_reference[0]
|
||
|
|
self._orientation_alignment = (
|
||
|
|
slave_reference[1] @ master_reference[1].T
|
||
|
|
)
|
||
|
|
self._position_axis_alignment = (
|
||
|
|
slave_reference[3] @ master_reference[3].T
|
||
|
|
)
|
||
|
|
|
||
|
|
master_reach = float(np.linalg.norm(self._master_relative_reference))
|
||
|
|
slave_reach = float(
|
||
|
|
np.linalg.norm(slave_reference[0] - slave_reference[2])
|
||
|
|
)
|
||
|
|
if master_reach <= 1e-9 or slave_reach <= 1e-9:
|
||
|
|
raise ValueError("reference shoulder-to-EE distances must be non-zero")
|
||
|
|
scale = slave_reach / master_reach if translation_scale is None else float(
|
||
|
|
translation_scale
|
||
|
|
)
|
||
|
|
if not np.isfinite(scale) or scale <= 0.0:
|
||
|
|
raise ValueError("translation_scale must be finite and positive")
|
||
|
|
self.translation_scale = scale
|
||
|
|
|
||
|
|
def _master_pose(
|
||
|
|
self, q: np.ndarray
|
||
|
|
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||
|
|
pin.forwardKinematics(self.master_model, self.master_data, q)
|
||
|
|
pin.updateFramePlacements(self.master_model, self.master_data)
|
||
|
|
ee = self.master_data.oMf[self.master_ee_id]
|
||
|
|
shoulder = self.master_data.oMf[self.master_shoulder_id]
|
||
|
|
return (
|
||
|
|
ee.translation.copy(),
|
||
|
|
ee.rotation.copy(),
|
||
|
|
shoulder.translation.copy(),
|
||
|
|
shoulder.rotation.copy(),
|
||
|
|
)
|
||
|
|
|
||
|
|
def _slave_pose(
|
||
|
|
self, q: np.ndarray
|
||
|
|
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||
|
|
pin.forwardKinematics(self.slave_model, self.slave_data, q)
|
||
|
|
pin.updateFramePlacements(self.slave_model, self.slave_data)
|
||
|
|
ee = self.slave_data.oMf[self.slave_ee_id]
|
||
|
|
shoulder = self.slave_data.oMf[self.slave_shoulder_id]
|
||
|
|
return (
|
||
|
|
ee.translation.copy(),
|
||
|
|
ee.rotation.copy(),
|
||
|
|
shoulder.translation.copy(),
|
||
|
|
shoulder.rotation.copy(),
|
||
|
|
)
|
||
|
|
|
||
|
|
def build(self, q_master: np.ndarray) -> PoseTarget:
|
||
|
|
q_master = _readonly_array(
|
||
|
|
q_master, (self.master_model.nq,), "q_master"
|
||
|
|
)
|
||
|
|
position, rotation, shoulder_position, _ = self._master_pose(q_master)
|
||
|
|
relative = position - shoulder_position
|
||
|
|
relative_delta = relative - self._master_relative_reference
|
||
|
|
target_position = self._slave_position_reference + (
|
||
|
|
self.translation_scale
|
||
|
|
* self._position_axis_alignment
|
||
|
|
@ relative_delta
|
||
|
|
)
|
||
|
|
target_rotation = self._orientation_alignment @ rotation
|
||
|
|
return PoseTarget(target_position, target_rotation)
|
||
|
|
|
||
|
|
|
||
|
|
class SEWFeasiblePoseTargetBuilder:
|
||
|
|
"""Expose the SEW mapper's feasible wrist target to comparator IK methods.
|
||
|
|
|
||
|
|
This builder supports the attribution block in which bounded DLS and
|
||
|
|
task-priority IK receive exactly the same wrist position/orientation target
|
||
|
|
as the proposed SEW recovery. It deliberately does not expose the SEW
|
||
|
|
elbow target to either comparator.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
mapper: SEWMapper,
|
||
|
|
q_slave_reference: Optional[np.ndarray] = None,
|
||
|
|
) -> None:
|
||
|
|
self.mapper = mapper
|
||
|
|
reference = (
|
||
|
|
safe_configuration(
|
||
|
|
mapper.s_model,
|
||
|
|
tuple(
|
||
|
|
mapper.s_model.names[joint_id]
|
||
|
|
for joint_id in mapper.s_joint_ids
|
||
|
|
),
|
||
|
|
)
|
||
|
|
if q_slave_reference is None
|
||
|
|
else np.asarray(q_slave_reference, dtype=float)
|
||
|
|
)
|
||
|
|
self.q_slave_reference = _readonly_array(
|
||
|
|
reference, (mapper.s_model.nq,), "q_slave_reference"
|
||
|
|
)
|
||
|
|
|
||
|
|
def build(self, q_master: np.ndarray) -> PoseTarget:
|
||
|
|
q_master = np.asarray(q_master, dtype=float)
|
||
|
|
if q_master.shape == (self.mapper.m_model.nq,):
|
||
|
|
q_master7 = q_master[self.mapper.m_qidx7]
|
||
|
|
elif q_master.shape == (7,):
|
||
|
|
q_master7 = q_master
|
||
|
|
else:
|
||
|
|
raise ValueError(
|
||
|
|
"q_master must be a full master configuration or a 7-vector"
|
||
|
|
)
|
||
|
|
target = self.mapper._target_from_master(q_master7)
|
||
|
|
nonsmooth_events = {
|
||
|
|
"reach_clipped_lower",
|
||
|
|
"reach_clipped_upper",
|
||
|
|
"reference_axis_fallback",
|
||
|
|
"master_shoulder_wrist_degenerate",
|
||
|
|
"master_arm_plane_degenerate",
|
||
|
|
}
|
||
|
|
events = tuple(str(event) for event in target["events"])
|
||
|
|
valid = bool(target["hard_geometry_valid"])
|
||
|
|
smooth = bool(
|
||
|
|
valid and not any(event in nonsmooth_events for event in events)
|
||
|
|
)
|
||
|
|
return PoseTarget(
|
||
|
|
target["pW_s_ref"],
|
||
|
|
target["RmEE"],
|
||
|
|
valid=valid,
|
||
|
|
smooth=smooth,
|
||
|
|
events=events,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class _RetargetingBase:
|
||
|
|
"""Shared validation and kinematic diagnostics."""
|
||
|
|
|
||
|
|
name = "retargeting_base"
|
||
|
|
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
models: TeleoperationModels,
|
||
|
|
target_builder: PoseTargetProvider,
|
||
|
|
*,
|
||
|
|
master_joint_names: Sequence[str] = MASTER_JOINT_NAMES,
|
||
|
|
slave_joint_names: Sequence[str] = SLAVE_JOINT_NAMES,
|
||
|
|
slave_ee_frame: str = SLAVE_FRAMES["ee"],
|
||
|
|
joint_limit_margin: float = 1e-4,
|
||
|
|
) -> None:
|
||
|
|
self.master_model = models.master
|
||
|
|
self.slave_model = models.slave
|
||
|
|
self.target_builder = target_builder
|
||
|
|
self.master_q_indices = joint_q_indices(
|
||
|
|
self.master_model, master_joint_names
|
||
|
|
)
|
||
|
|
self.slave_q_indices = joint_q_indices(self.slave_model, slave_joint_names)
|
||
|
|
self.master_lower, self.master_upper = finite_joint_limits(
|
||
|
|
self.master_model, master_joint_names
|
||
|
|
)
|
||
|
|
self.slave_lower, self.slave_upper = finite_joint_limits(
|
||
|
|
self.slave_model, slave_joint_names
|
||
|
|
)
|
||
|
|
self.slave_ee_id = require_frame(self.slave_model, slave_ee_frame)
|
||
|
|
self.slave_data = self.slave_model.createData()
|
||
|
|
self.joint_limit_margin = float(joint_limit_margin)
|
||
|
|
if not np.isfinite(self.joint_limit_margin) or self.joint_limit_margin < 0.0:
|
||
|
|
raise ValueError("joint_limit_margin must be finite and non-negative")
|
||
|
|
|
||
|
|
def _master_configuration(
|
||
|
|
self, q_master: np.ndarray
|
||
|
|
) -> tuple[Optional[np.ndarray], Optional[RetargetingFailure]]:
|
||
|
|
q_input = np.asarray(q_master, dtype=float)
|
||
|
|
if q_input.shape == (self.master_model.nq,):
|
||
|
|
q = q_input.copy()
|
||
|
|
elif q_input.shape == (len(self.master_q_indices),):
|
||
|
|
q = pin.neutral(self.master_model)
|
||
|
|
q[self.master_q_indices] = q_input
|
||
|
|
else:
|
||
|
|
return None, RetargetingFailure.INVALID_INPUT
|
||
|
|
if not np.all(np.isfinite(q)):
|
||
|
|
return None, RetargetingFailure.INVALID_INPUT
|
||
|
|
q7 = q[self.master_q_indices]
|
||
|
|
if np.any(q7 < self.master_lower) or np.any(q7 > self.master_upper):
|
||
|
|
return None, RetargetingFailure.MASTER_LIMIT_VIOLATION
|
||
|
|
return q, None
|
||
|
|
|
||
|
|
def _slave_seed(self, seed: Optional[np.ndarray]) -> Optional[np.ndarray]:
|
||
|
|
if seed is None:
|
||
|
|
return np.asarray(
|
||
|
|
self.target_builder.q_slave_reference, dtype=float
|
||
|
|
).copy()
|
||
|
|
seed_array = np.asarray(seed, dtype=float)
|
||
|
|
if seed_array.shape == (self.slave_model.nq,):
|
||
|
|
q = seed_array.copy()
|
||
|
|
elif seed_array.shape == (len(self.slave_q_indices),):
|
||
|
|
q = pin.neutral(self.slave_model)
|
||
|
|
q[self.slave_q_indices] = seed_array
|
||
|
|
else:
|
||
|
|
return None
|
||
|
|
if not np.all(np.isfinite(q)):
|
||
|
|
return None
|
||
|
|
q7 = q[self.slave_q_indices]
|
||
|
|
if np.any(q7 < self.slave_lower) or np.any(q7 > self.slave_upper):
|
||
|
|
return None
|
||
|
|
return q
|
||
|
|
|
||
|
|
def _kinematics(
|
||
|
|
self, q_slave: np.ndarray, target: PoseTarget
|
||
|
|
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||
|
|
pin.forwardKinematics(self.slave_model, self.slave_data, q_slave)
|
||
|
|
pin.updateFramePlacements(self.slave_model, self.slave_data)
|
||
|
|
placement = self.slave_data.oMf[self.slave_ee_id]
|
||
|
|
position_error, orientation_error = _pose_error(
|
||
|
|
placement.translation, placement.rotation, target
|
||
|
|
)
|
||
|
|
jacobian = pin.computeFrameJacobian(
|
||
|
|
self.slave_model,
|
||
|
|
self.slave_data,
|
||
|
|
q_slave,
|
||
|
|
self.slave_ee_id,
|
||
|
|
pin.ReferenceFrame.LOCAL_WORLD_ALIGNED,
|
||
|
|
)
|
||
|
|
return position_error, orientation_error, jacobian[:, self.slave_q_indices]
|
||
|
|
|
||
|
|
def _active_limits(self, q_slave: np.ndarray) -> tuple[int, ...]:
|
||
|
|
q7 = q_slave[self.slave_q_indices]
|
||
|
|
clearance = np.minimum(q7 - self.slave_lower, self.slave_upper - q7)
|
||
|
|
return tuple(
|
||
|
|
int(index)
|
||
|
|
for index in np.flatnonzero(clearance <= self.joint_limit_margin)
|
||
|
|
)
|
||
|
|
|
||
|
|
def _invalid_result(
|
||
|
|
self,
|
||
|
|
failure: RetargetingFailure,
|
||
|
|
started_at: float,
|
||
|
|
message: str,
|
||
|
|
) -> RetargetingResult:
|
||
|
|
q = np.asarray(self.target_builder.q_slave_reference, dtype=float)
|
||
|
|
diagnostics = RetargetingDiagnostics(
|
||
|
|
status=RetargetingSolverStatus.INVALID_INPUT,
|
||
|
|
iterations=0,
|
||
|
|
runtime_s=max(0.0, perf_counter() - started_at),
|
||
|
|
cost=0.0,
|
||
|
|
position_error_m=0.0,
|
||
|
|
orientation_error_rad=0.0,
|
||
|
|
message=message,
|
||
|
|
)
|
||
|
|
return RetargetingResult(
|
||
|
|
method=self.name,
|
||
|
|
q_slave=q,
|
||
|
|
success=False,
|
||
|
|
smooth=False,
|
||
|
|
failure=failure,
|
||
|
|
diagnostics=diagnostics,
|
||
|
|
events=(failure.value,),
|
||
|
|
)
|
||
|
|
|
||
|
|
def _invalid_target_result(
|
||
|
|
self,
|
||
|
|
target: PoseTarget,
|
||
|
|
started_at: float,
|
||
|
|
) -> RetargetingResult:
|
||
|
|
diagnostics = RetargetingDiagnostics(
|
||
|
|
status=RetargetingSolverStatus.FAILED,
|
||
|
|
iterations=0,
|
||
|
|
runtime_s=max(0.0, perf_counter() - started_at),
|
||
|
|
cost=0.0,
|
||
|
|
position_error_m=0.0,
|
||
|
|
orientation_error_rad=0.0,
|
||
|
|
message="invalid Cartesian target",
|
||
|
|
)
|
||
|
|
events = tuple(
|
||
|
|
dict.fromkeys(
|
||
|
|
(*target.events, RetargetingFailure.DEGENERATE_GEOMETRY.value)
|
||
|
|
)
|
||
|
|
)
|
||
|
|
return RetargetingResult(
|
||
|
|
method=self.name,
|
||
|
|
q_slave=self.target_builder.q_slave_reference,
|
||
|
|
success=False,
|
||
|
|
smooth=False,
|
||
|
|
failure=RetargetingFailure.DEGENERATE_GEOMETRY,
|
||
|
|
diagnostics=diagnostics,
|
||
|
|
target=target,
|
||
|
|
events=events,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class ScaledJointSpaceRetargeter(_RetargetingBase):
|
||
|
|
"""Anthropometrically scaled joint-range mapping baseline."""
|
||
|
|
|
||
|
|
name = "scaled_joint_space"
|
||
|
|
|
||
|
|
def retarget(
|
||
|
|
self,
|
||
|
|
q_master: np.ndarray,
|
||
|
|
q_slave_seed: Optional[np.ndarray] = None,
|
||
|
|
) -> RetargetingResult:
|
||
|
|
del q_slave_seed # Closed-form baseline has no branch state.
|
||
|
|
started_at = perf_counter()
|
||
|
|
q_m, failure = self._master_configuration(q_master)
|
||
|
|
if failure is not None:
|
||
|
|
return self._invalid_result(failure, started_at, failure.value)
|
||
|
|
|
||
|
|
target = self.target_builder.build(q_m)
|
||
|
|
if not target.valid:
|
||
|
|
return self._invalid_target_result(target, started_at)
|
||
|
|
normalized = (
|
||
|
|
q_m[self.master_q_indices] - self.master_lower
|
||
|
|
) / (self.master_upper - self.master_lower)
|
||
|
|
q_slave = pin.neutral(self.slave_model)
|
||
|
|
q_slave[self.slave_q_indices] = self.slave_lower + normalized * (
|
||
|
|
self.slave_upper - self.slave_lower
|
||
|
|
)
|
||
|
|
position_error, orientation_error, _ = self._kinematics(q_slave, target)
|
||
|
|
active = self._active_limits(q_slave)
|
||
|
|
position_norm = float(np.linalg.norm(position_error))
|
||
|
|
orientation_norm = float(np.linalg.norm(orientation_error))
|
||
|
|
diagnostics = RetargetingDiagnostics(
|
||
|
|
status=RetargetingSolverStatus.CLOSED_FORM,
|
||
|
|
iterations=0,
|
||
|
|
runtime_s=perf_counter() - started_at,
|
||
|
|
cost=0.5 * (position_norm**2 + orientation_norm**2),
|
||
|
|
position_error_m=position_norm,
|
||
|
|
orientation_error_rad=orientation_norm,
|
||
|
|
active_limit_indices=active,
|
||
|
|
)
|
||
|
|
return RetargetingResult(
|
||
|
|
method=self.name,
|
||
|
|
q_slave=q_slave,
|
||
|
|
success=True,
|
||
|
|
smooth=bool(target.smooth and not active),
|
||
|
|
failure=RetargetingFailure.NONE,
|
||
|
|
diagnostics=diagnostics,
|
||
|
|
target=target,
|
||
|
|
events=(
|
||
|
|
*target.events,
|
||
|
|
*(("joint_limit_active",) if active else ()),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class BoundedDLSIKRetargeter(_RetargetingBase):
|
||
|
|
"""Bounded iterative damped-least-squares Cartesian IK baseline."""
|
||
|
|
|
||
|
|
name = "bounded_dls_ik"
|
||
|
|
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
models: TeleoperationModels,
|
||
|
|
target_builder: PoseTargetProvider,
|
||
|
|
*,
|
||
|
|
damping: float = 2e-3,
|
||
|
|
orientation_weight_m: float = 0.20,
|
||
|
|
max_iterations: int = 160,
|
||
|
|
step_limit_rad: float = 0.20,
|
||
|
|
position_tolerance_m: float = 2e-4,
|
||
|
|
orientation_tolerance_rad: float = 2e-3,
|
||
|
|
**kwargs,
|
||
|
|
) -> None:
|
||
|
|
super().__init__(models, target_builder, **kwargs)
|
||
|
|
self.damping = float(damping)
|
||
|
|
self.orientation_weight_m = float(orientation_weight_m)
|
||
|
|
self.max_iterations = int(max_iterations)
|
||
|
|
self.step_limit_rad = float(step_limit_rad)
|
||
|
|
self.position_tolerance_m = float(position_tolerance_m)
|
||
|
|
self.orientation_tolerance_rad = float(orientation_tolerance_rad)
|
||
|
|
positive = (
|
||
|
|
self.damping,
|
||
|
|
self.orientation_weight_m,
|
||
|
|
self.step_limit_rad,
|
||
|
|
self.position_tolerance_m,
|
||
|
|
self.orientation_tolerance_rad,
|
||
|
|
)
|
||
|
|
if any(not np.isfinite(value) or value <= 0.0 for value in positive):
|
||
|
|
raise ValueError("DLS tuning parameters must be finite and positive")
|
||
|
|
if self.max_iterations <= 0:
|
||
|
|
raise ValueError("max_iterations must be positive")
|
||
|
|
|
||
|
|
def _weighted_cost(
|
||
|
|
self, q_slave: np.ndarray, target: PoseTarget
|
||
|
|
) -> tuple[float, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||
|
|
ep, eo, jacobian = self._kinematics(q_slave, target)
|
||
|
|
error = np.concatenate((ep, self.orientation_weight_m * eo))
|
||
|
|
weighted_jacobian = jacobian.copy()
|
||
|
|
weighted_jacobian[3:, :] *= self.orientation_weight_m
|
||
|
|
return 0.5 * float(error @ error), error, ep, eo, weighted_jacobian
|
||
|
|
|
||
|
|
def retarget(
|
||
|
|
self,
|
||
|
|
q_master: np.ndarray,
|
||
|
|
q_slave_seed: Optional[np.ndarray] = None,
|
||
|
|
) -> RetargetingResult:
|
||
|
|
started_at = perf_counter()
|
||
|
|
q_m, failure = self._master_configuration(q_master)
|
||
|
|
if failure is not None:
|
||
|
|
return self._invalid_result(failure, started_at, failure.value)
|
||
|
|
q_slave = self._slave_seed(q_slave_seed)
|
||
|
|
if q_slave is None:
|
||
|
|
return self._invalid_result(
|
||
|
|
RetargetingFailure.INVALID_INPUT,
|
||
|
|
started_at,
|
||
|
|
"invalid q_slave_seed",
|
||
|
|
)
|
||
|
|
target = self.target_builder.build(q_m)
|
||
|
|
if not target.valid:
|
||
|
|
return self._invalid_target_result(target, started_at)
|
||
|
|
|
||
|
|
status = RetargetingSolverStatus.MAX_ITERATIONS
|
||
|
|
iterations = 0
|
||
|
|
numerical_failure = False
|
||
|
|
for iterations in range(1, self.max_iterations + 1):
|
||
|
|
cost, error, ep, eo, jacobian = self._weighted_cost(q_slave, target)
|
||
|
|
if (
|
||
|
|
np.linalg.norm(ep) <= self.position_tolerance_m
|
||
|
|
and np.linalg.norm(eo) <= self.orientation_tolerance_rad
|
||
|
|
):
|
||
|
|
status = RetargetingSolverStatus.CONVERGED
|
||
|
|
break
|
||
|
|
try:
|
||
|
|
normal = jacobian @ jacobian.T + (
|
||
|
|
self.damping**2
|
||
|
|
) * np.eye(6)
|
||
|
|
step = jacobian.T @ np.linalg.solve(normal, error)
|
||
|
|
except np.linalg.LinAlgError:
|
||
|
|
numerical_failure = True
|
||
|
|
status = RetargetingSolverStatus.FAILED
|
||
|
|
break
|
||
|
|
step_norm = float(np.linalg.norm(step))
|
||
|
|
if not np.isfinite(step_norm):
|
||
|
|
numerical_failure = True
|
||
|
|
status = RetargetingSolverStatus.FAILED
|
||
|
|
break
|
||
|
|
if step_norm > self.step_limit_rad:
|
||
|
|
step *= self.step_limit_rad / step_norm
|
||
|
|
|
||
|
|
q7 = q_slave[self.slave_q_indices]
|
||
|
|
accepted = False
|
||
|
|
for scale in (1.0, 0.5, 0.25, 0.125, 0.0625):
|
||
|
|
candidate = q_slave.copy()
|
||
|
|
candidate[self.slave_q_indices] = np.clip(
|
||
|
|
q7 + scale * step, self.slave_lower, self.slave_upper
|
||
|
|
)
|
||
|
|
candidate_cost = self._weighted_cost(candidate, target)[0]
|
||
|
|
if candidate_cost < cost:
|
||
|
|
q_slave = candidate
|
||
|
|
accepted = True
|
||
|
|
break
|
||
|
|
if not accepted:
|
||
|
|
# A deterministic small step lets the active set change while
|
||
|
|
# still bounding the solver near a stationary point.
|
||
|
|
candidate = q_slave.copy()
|
||
|
|
candidate[self.slave_q_indices] = np.clip(
|
||
|
|
q7 + 0.01 * step, self.slave_lower, self.slave_upper
|
||
|
|
)
|
||
|
|
if self._weighted_cost(candidate, target)[0] < cost:
|
||
|
|
q_slave = candidate
|
||
|
|
else:
|
||
|
|
status = RetargetingSolverStatus.FAILED
|
||
|
|
break
|
||
|
|
|
||
|
|
cost, _, ep, eo, _ = self._weighted_cost(q_slave, target)
|
||
|
|
position_norm = float(np.linalg.norm(ep))
|
||
|
|
orientation_norm = float(np.linalg.norm(eo))
|
||
|
|
converged = bool(
|
||
|
|
position_norm <= self.position_tolerance_m
|
||
|
|
and orientation_norm <= self.orientation_tolerance_rad
|
||
|
|
)
|
||
|
|
if converged:
|
||
|
|
status = RetargetingSolverStatus.CONVERGED
|
||
|
|
failure = RetargetingFailure.NONE
|
||
|
|
elif numerical_failure:
|
||
|
|
failure = RetargetingFailure.NUMERICAL_FAILURE
|
||
|
|
elif iterations >= self.max_iterations:
|
||
|
|
failure = RetargetingFailure.SOLVER_NOT_CONVERGED
|
||
|
|
else:
|
||
|
|
failure = RetargetingFailure.TASK_TOLERANCE_EXCEEDED
|
||
|
|
active = self._active_limits(q_slave)
|
||
|
|
diagnostics = RetargetingDiagnostics(
|
||
|
|
status=status,
|
||
|
|
iterations=iterations,
|
||
|
|
runtime_s=perf_counter() - started_at,
|
||
|
|
cost=cost,
|
||
|
|
position_error_m=position_norm,
|
||
|
|
orientation_error_rad=orientation_norm,
|
||
|
|
active_limit_indices=active,
|
||
|
|
clipped=bool(active),
|
||
|
|
)
|
||
|
|
events: list[str] = list(target.events)
|
||
|
|
if failure is not RetargetingFailure.NONE:
|
||
|
|
events.append(failure.value)
|
||
|
|
if active:
|
||
|
|
events.append("joint_limit_active")
|
||
|
|
return RetargetingResult(
|
||
|
|
method=self.name,
|
||
|
|
q_slave=q_slave,
|
||
|
|
success=converged,
|
||
|
|
smooth=bool(converged and target.smooth and not active),
|
||
|
|
failure=failure,
|
||
|
|
diagnostics=diagnostics,
|
||
|
|
target=target,
|
||
|
|
events=tuple(events),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TaskPriorityIKRetargeter(BoundedDLSIKRetargeter):
|
||
|
|
"""Position-first IK with orientation, centering, and manipulability tasks."""
|
||
|
|
|
||
|
|
name = "task_priority_ik"
|
||
|
|
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
models: TeleoperationModels,
|
||
|
|
target_builder: PoseTargetProvider,
|
||
|
|
*,
|
||
|
|
joint_centering_gain: float = 0.05,
|
||
|
|
manipulability_gain: float = 0.005,
|
||
|
|
manipulability_fd_step_rad: float = 1e-4,
|
||
|
|
manipulability_regularization: float = 1e-8,
|
||
|
|
**kwargs,
|
||
|
|
) -> None:
|
||
|
|
super().__init__(models, target_builder, **kwargs)
|
||
|
|
self.joint_centering_gain = float(joint_centering_gain)
|
||
|
|
self.manipulability_gain = float(manipulability_gain)
|
||
|
|
self.manipulability_fd_step_rad = float(manipulability_fd_step_rad)
|
||
|
|
self.manipulability_regularization = float(
|
||
|
|
manipulability_regularization
|
||
|
|
)
|
||
|
|
if (
|
||
|
|
not np.isfinite(self.joint_centering_gain)
|
||
|
|
or self.joint_centering_gain < 0.0
|
||
|
|
):
|
||
|
|
raise ValueError(
|
||
|
|
"joint_centering_gain must be finite and non-negative"
|
||
|
|
)
|
||
|
|
nonnegative = (
|
||
|
|
self.manipulability_gain,
|
||
|
|
self.manipulability_fd_step_rad,
|
||
|
|
self.manipulability_regularization,
|
||
|
|
)
|
||
|
|
if any(not np.isfinite(value) or value < 0.0 for value in nonnegative):
|
||
|
|
raise ValueError(
|
||
|
|
"manipulability settings must be finite and non-negative"
|
||
|
|
)
|
||
|
|
if self.manipulability_gain > 0.0 and (
|
||
|
|
self.manipulability_fd_step_rad <= 0.0
|
||
|
|
or self.manipulability_regularization <= 0.0
|
||
|
|
):
|
||
|
|
raise ValueError(
|
||
|
|
"enabled manipulability optimization needs positive FD and "
|
||
|
|
"regularization values"
|
||
|
|
)
|
||
|
|
|
||
|
|
def _damped_pseudoinverse(self, matrix: np.ndarray) -> np.ndarray:
|
||
|
|
rows = matrix.shape[0]
|
||
|
|
return matrix.T @ np.linalg.solve(
|
||
|
|
matrix @ matrix.T + (self.damping**2) * np.eye(rows),
|
||
|
|
np.eye(rows),
|
||
|
|
)
|
||
|
|
|
||
|
|
def _log_manipulability(self, q_slave: np.ndarray) -> float:
|
||
|
|
"""Regularized log-volume of the six-dimensional velocity ellipsoid."""
|
||
|
|
pin.forwardKinematics(self.slave_model, self.slave_data, q_slave)
|
||
|
|
pin.updateFramePlacements(self.slave_model, self.slave_data)
|
||
|
|
jacobian = pin.computeFrameJacobian(
|
||
|
|
self.slave_model,
|
||
|
|
self.slave_data,
|
||
|
|
q_slave,
|
||
|
|
self.slave_ee_id,
|
||
|
|
pin.ReferenceFrame.LOCAL_WORLD_ALIGNED,
|
||
|
|
)[:, self.slave_q_indices]
|
||
|
|
sign, log_determinant = np.linalg.slogdet(
|
||
|
|
jacobian @ jacobian.T
|
||
|
|
+ self.manipulability_regularization * np.eye(6)
|
||
|
|
)
|
||
|
|
return 0.5 * float(log_determinant) if sign > 0.0 else -np.inf
|
||
|
|
|
||
|
|
def _manipulability_gradient(self, q_slave: np.ndarray) -> np.ndarray:
|
||
|
|
if self.manipulability_gain == 0.0:
|
||
|
|
return np.zeros(7, dtype=float)
|
||
|
|
gradient = np.zeros(7, dtype=float)
|
||
|
|
q7 = q_slave[self.slave_q_indices]
|
||
|
|
step = self.manipulability_fd_step_rad
|
||
|
|
for joint in range(7):
|
||
|
|
plus = q_slave.copy()
|
||
|
|
minus = q_slave.copy()
|
||
|
|
plus7 = q7.copy()
|
||
|
|
minus7 = q7.copy()
|
||
|
|
plus7[joint] = min(self.slave_upper[joint], plus7[joint] + step)
|
||
|
|
minus7[joint] = max(self.slave_lower[joint], minus7[joint] - step)
|
||
|
|
denominator = plus7[joint] - minus7[joint]
|
||
|
|
if denominator <= 0.0:
|
||
|
|
continue
|
||
|
|
plus[self.slave_q_indices] = plus7
|
||
|
|
minus[self.slave_q_indices] = minus7
|
||
|
|
gradient[joint] = (
|
||
|
|
self._log_manipulability(plus)
|
||
|
|
- self._log_manipulability(minus)
|
||
|
|
) / denominator
|
||
|
|
return gradient
|
||
|
|
|
||
|
|
def retarget(
|
||
|
|
self,
|
||
|
|
q_master: np.ndarray,
|
||
|
|
q_slave_seed: Optional[np.ndarray] = None,
|
||
|
|
) -> RetargetingResult:
|
||
|
|
started_at = perf_counter()
|
||
|
|
q_m, failure = self._master_configuration(q_master)
|
||
|
|
if failure is not None:
|
||
|
|
return self._invalid_result(failure, started_at, failure.value)
|
||
|
|
q_slave = self._slave_seed(q_slave_seed)
|
||
|
|
if q_slave is None:
|
||
|
|
return self._invalid_result(
|
||
|
|
RetargetingFailure.INVALID_INPUT,
|
||
|
|
started_at,
|
||
|
|
"invalid q_slave_seed",
|
||
|
|
)
|
||
|
|
target = self.target_builder.build(q_m)
|
||
|
|
if not target.valid:
|
||
|
|
return self._invalid_target_result(target, started_at)
|
||
|
|
center = 0.5 * (self.slave_lower + self.slave_upper)
|
||
|
|
|
||
|
|
status = RetargetingSolverStatus.MAX_ITERATIONS
|
||
|
|
numerical_failure = False
|
||
|
|
iterations = 0
|
||
|
|
for iterations in range(1, self.max_iterations + 1):
|
||
|
|
cost, _, ep, eo, jacobian = self._weighted_cost(q_slave, target)
|
||
|
|
if (
|
||
|
|
np.linalg.norm(ep) <= self.position_tolerance_m
|
||
|
|
and np.linalg.norm(eo) <= self.orientation_tolerance_rad
|
||
|
|
):
|
||
|
|
status = RetargetingSolverStatus.CONVERGED
|
||
|
|
break
|
||
|
|
try:
|
||
|
|
jp = jacobian[:3, :]
|
||
|
|
jo = jacobian[3:, :] / self.orientation_weight_m
|
||
|
|
jp_inverse = self._damped_pseudoinverse(jp)
|
||
|
|
dq_position = jp_inverse @ ep
|
||
|
|
null_position = np.eye(7) - jp_inverse @ jp
|
||
|
|
|
||
|
|
jo_null = jo @ null_position
|
||
|
|
jo_null_inverse = self._damped_pseudoinverse(jo_null)
|
||
|
|
dq_orientation = jo_null_inverse @ (eo - jo @ dq_position)
|
||
|
|
|
||
|
|
full_jacobian = np.vstack((jp, jo))
|
||
|
|
full_inverse = self._damped_pseudoinverse(full_jacobian)
|
||
|
|
null_full = np.eye(7) - full_inverse @ full_jacobian
|
||
|
|
q7 = q_slave[self.slave_q_indices]
|
||
|
|
dq_center = (
|
||
|
|
self.joint_centering_gain * null_full @ (center - q7)
|
||
|
|
)
|
||
|
|
dq_manipulability = (
|
||
|
|
self.manipulability_gain
|
||
|
|
* null_full
|
||
|
|
@ self._manipulability_gradient(q_slave)
|
||
|
|
)
|
||
|
|
step = (
|
||
|
|
dq_position
|
||
|
|
+ dq_orientation
|
||
|
|
+ dq_center
|
||
|
|
+ dq_manipulability
|
||
|
|
)
|
||
|
|
except np.linalg.LinAlgError:
|
||
|
|
numerical_failure = True
|
||
|
|
status = RetargetingSolverStatus.FAILED
|
||
|
|
break
|
||
|
|
step_norm = float(np.linalg.norm(step))
|
||
|
|
if not np.isfinite(step_norm):
|
||
|
|
numerical_failure = True
|
||
|
|
status = RetargetingSolverStatus.FAILED
|
||
|
|
break
|
||
|
|
if step_norm > self.step_limit_rad:
|
||
|
|
step *= self.step_limit_rad / step_norm
|
||
|
|
|
||
|
|
q7 = q_slave[self.slave_q_indices]
|
||
|
|
accepted = False
|
||
|
|
for scale in (1.0, 0.5, 0.25, 0.125, 0.0625):
|
||
|
|
candidate = q_slave.copy()
|
||
|
|
candidate[self.slave_q_indices] = np.clip(
|
||
|
|
q7 + scale * step, self.slave_lower, self.slave_upper
|
||
|
|
)
|
||
|
|
if self._weighted_cost(candidate, target)[0] < cost:
|
||
|
|
q_slave = candidate
|
||
|
|
accepted = True
|
||
|
|
break
|
||
|
|
if not accepted:
|
||
|
|
status = RetargetingSolverStatus.FAILED
|
||
|
|
break
|
||
|
|
|
||
|
|
cost, _, ep, eo, _ = self._weighted_cost(q_slave, target)
|
||
|
|
position_norm = float(np.linalg.norm(ep))
|
||
|
|
orientation_norm = float(np.linalg.norm(eo))
|
||
|
|
converged = bool(
|
||
|
|
position_norm <= self.position_tolerance_m
|
||
|
|
and orientation_norm <= self.orientation_tolerance_rad
|
||
|
|
)
|
||
|
|
if converged:
|
||
|
|
status = RetargetingSolverStatus.CONVERGED
|
||
|
|
failure = RetargetingFailure.NONE
|
||
|
|
elif numerical_failure:
|
||
|
|
failure = RetargetingFailure.NUMERICAL_FAILURE
|
||
|
|
elif iterations >= self.max_iterations:
|
||
|
|
failure = RetargetingFailure.SOLVER_NOT_CONVERGED
|
||
|
|
else:
|
||
|
|
failure = RetargetingFailure.TASK_TOLERANCE_EXCEEDED
|
||
|
|
active = self._active_limits(q_slave)
|
||
|
|
diagnostics = RetargetingDiagnostics(
|
||
|
|
status=status,
|
||
|
|
iterations=iterations,
|
||
|
|
runtime_s=perf_counter() - started_at,
|
||
|
|
cost=cost,
|
||
|
|
position_error_m=position_norm,
|
||
|
|
orientation_error_rad=orientation_norm,
|
||
|
|
active_limit_indices=active,
|
||
|
|
clipped=bool(active),
|
||
|
|
message="secondary_objective=log_manipulability",
|
||
|
|
)
|
||
|
|
events: list[str] = list(target.events)
|
||
|
|
if failure is not RetargetingFailure.NONE:
|
||
|
|
events.append(failure.value)
|
||
|
|
if active:
|
||
|
|
events.append("joint_limit_active")
|
||
|
|
return RetargetingResult(
|
||
|
|
method=self.name,
|
||
|
|
q_slave=q_slave,
|
||
|
|
success=converged,
|
||
|
|
smooth=bool(converged and target.smooth and not active),
|
||
|
|
failure=failure,
|
||
|
|
diagnostics=diagnostics,
|
||
|
|
target=target,
|
||
|
|
events=tuple(events),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class SEWRetargeterAdapter:
|
||
|
|
"""Expose the existing bounded SEW mapper through :class:`Retargeter`."""
|
||
|
|
|
||
|
|
name = "sew"
|
||
|
|
|
||
|
|
def __init__(self, mapper) -> None:
|
||
|
|
self.mapper = mapper
|
||
|
|
|
||
|
|
def retarget(
|
||
|
|
self,
|
||
|
|
q_master: np.ndarray,
|
||
|
|
q_slave_seed: Optional[np.ndarray] = None,
|
||
|
|
) -> RetargetingResult:
|
||
|
|
started_at = perf_counter()
|
||
|
|
try:
|
||
|
|
q_slave, debug = self.mapper.retarget(
|
||
|
|
q_master, q_s_init=q_slave_seed
|
||
|
|
)
|
||
|
|
except (ValueError, FloatingPointError) as error:
|
||
|
|
diagnostics = RetargetingDiagnostics(
|
||
|
|
status=RetargetingSolverStatus.INVALID_INPUT,
|
||
|
|
iterations=0,
|
||
|
|
runtime_s=perf_counter() - started_at,
|
||
|
|
cost=0.0,
|
||
|
|
position_error_m=0.0,
|
||
|
|
orientation_error_rad=0.0,
|
||
|
|
message=str(error),
|
||
|
|
)
|
||
|
|
return RetargetingResult(
|
||
|
|
method=self.name,
|
||
|
|
q_slave=pin.neutral(self.mapper.s_model),
|
||
|
|
success=False,
|
||
|
|
smooth=False,
|
||
|
|
failure=RetargetingFailure.INVALID_INPUT,
|
||
|
|
diagnostics=diagnostics,
|
||
|
|
events=(RetargetingFailure.INVALID_INPUT.value,),
|
||
|
|
)
|
||
|
|
events = tuple(str(event) for event in debug.get("events", ()))
|
||
|
|
if debug.get("success", False):
|
||
|
|
failure = RetargetingFailure.NONE
|
||
|
|
elif "invalid_master_geometry" in events:
|
||
|
|
failure = RetargetingFailure.DEGENERATE_GEOMETRY
|
||
|
|
elif "joint_limit_violation" in events:
|
||
|
|
failure = RetargetingFailure.JOINT_LIMIT_VIOLATION
|
||
|
|
elif "task_tolerance_exceeded" in events:
|
||
|
|
failure = RetargetingFailure.TASK_TOLERANCE_EXCEEDED
|
||
|
|
elif "bounded_solver_not_converged" in events:
|
||
|
|
failure = RetargetingFailure.SOLVER_NOT_CONVERGED
|
||
|
|
else:
|
||
|
|
failure = RetargetingFailure.NUMERICAL_FAILURE
|
||
|
|
diagnostics = RetargetingDiagnostics(
|
||
|
|
status=(
|
||
|
|
RetargetingSolverStatus.CONVERGED
|
||
|
|
if failure is RetargetingFailure.NONE
|
||
|
|
else RetargetingSolverStatus.FAILED
|
||
|
|
),
|
||
|
|
iterations=int(debug.get("solver_nfev", 0)),
|
||
|
|
runtime_s=perf_counter() - started_at,
|
||
|
|
cost=max(0.0, float(debug.get("solver_cost", 0.0))),
|
||
|
|
position_error_m=max(0.0, float(debug.get("position_error", 0.0))),
|
||
|
|
orientation_error_rad=max(
|
||
|
|
0.0, float(debug.get("orientation_error", 0.0))
|
||
|
|
),
|
||
|
|
active_limit_indices=tuple(
|
||
|
|
int(index)
|
||
|
|
for index in debug.get("joint_limit_active_indices", ())
|
||
|
|
),
|
||
|
|
clipped=bool(debug.get("reach_clipped", False)),
|
||
|
|
)
|
||
|
|
return RetargetingResult(
|
||
|
|
method=self.name,
|
||
|
|
q_slave=q_slave,
|
||
|
|
success=failure is RetargetingFailure.NONE,
|
||
|
|
smooth=bool(debug.get("smooth", False)),
|
||
|
|
failure=failure,
|
||
|
|
diagnostics=diagnostics,
|
||
|
|
events=events,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def build_canonical_baselines(
|
||
|
|
models: Optional[TeleoperationModels] = None,
|
||
|
|
) -> dict[str, Retargeter]:
|
||
|
|
"""Build the three frozen H1 baselines for the canonical URDF pair."""
|
||
|
|
canonical_models = load_models() if models is None else models
|
||
|
|
target_builder = PoseTargetBuilder(
|
||
|
|
canonical_models.master,
|
||
|
|
canonical_models.slave,
|
||
|
|
master_joint_names=MASTER_JOINT_NAMES,
|
||
|
|
slave_joint_names=SLAVE_JOINT_NAMES,
|
||
|
|
master_shoulder_frame=MASTER_FRAMES["shoulder"],
|
||
|
|
master_ee_frame=MASTER_FRAMES["ee"],
|
||
|
|
slave_shoulder_frame=SLAVE_FRAMES["shoulder"],
|
||
|
|
slave_ee_frame=SLAVE_FRAMES["ee"],
|
||
|
|
)
|
||
|
|
baselines: tuple[Retargeter, ...] = (
|
||
|
|
ScaledJointSpaceRetargeter(canonical_models, target_builder),
|
||
|
|
BoundedDLSIKRetargeter(canonical_models, target_builder),
|
||
|
|
TaskPriorityIKRetargeter(canonical_models, target_builder),
|
||
|
|
)
|
||
|
|
return {baseline.name: baseline for baseline in baselines}
|
||
|
|
|
||
|
|
|
||
|
|
def build_canonical_sew_target_baselines(
|
||
|
|
models: Optional[TeleoperationModels] = None,
|
||
|
|
) -> tuple[dict[str, Retargeter], SEWRetargeterAdapter]:
|
||
|
|
"""Build the H1 common-target attribution baselines and proposed adapter."""
|
||
|
|
canonical_models = load_models() if models is None else models
|
||
|
|
mapper = SEWMapper(
|
||
|
|
master_model=canonical_models.master,
|
||
|
|
slave_model=canonical_models.slave,
|
||
|
|
m_shoulder=MASTER_FRAMES["shoulder"],
|
||
|
|
m_elbow=MASTER_FRAMES["elbow"],
|
||
|
|
m_wrist=MASTER_FRAMES["wrist"],
|
||
|
|
m_ee=MASTER_FRAMES["ee"],
|
||
|
|
s_shoulder=SLAVE_FRAMES["shoulder"],
|
||
|
|
s_elbow=SLAVE_FRAMES["elbow"],
|
||
|
|
s_wrist=SLAVE_FRAMES["wrist"],
|
||
|
|
s_ee=SLAVE_FRAMES["wrist"],
|
||
|
|
master_joint_names=MASTER_JOINT_NAMES,
|
||
|
|
slave_joint_names=SLAVE_JOINT_NAMES,
|
||
|
|
slave_shoulder_cfg=BallJointConfig(
|
||
|
|
axis_order="yxy",
|
||
|
|
joint_names=SLAVE_JOINT_NAMES[:3],
|
||
|
|
signs=(-1.0, 1.0, -1.0),
|
||
|
|
),
|
||
|
|
slave_wrist_cfg=BallJointConfig(
|
||
|
|
axis_order="yzx",
|
||
|
|
joint_names=SLAVE_JOINT_NAMES[4:],
|
||
|
|
signs=(-1.0, 1.0, 1.0),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
target_builder = SEWFeasiblePoseTargetBuilder(mapper)
|
||
|
|
baselines: tuple[Retargeter, ...] = (
|
||
|
|
ScaledJointSpaceRetargeter(
|
||
|
|
canonical_models,
|
||
|
|
target_builder,
|
||
|
|
slave_ee_frame=SLAVE_FRAMES["wrist"],
|
||
|
|
),
|
||
|
|
BoundedDLSIKRetargeter(
|
||
|
|
canonical_models,
|
||
|
|
target_builder,
|
||
|
|
slave_ee_frame=SLAVE_FRAMES["wrist"],
|
||
|
|
),
|
||
|
|
TaskPriorityIKRetargeter(
|
||
|
|
canonical_models,
|
||
|
|
target_builder,
|
||
|
|
slave_ee_frame=SLAVE_FRAMES["wrist"],
|
||
|
|
),
|
||
|
|
)
|
||
|
|
return (
|
||
|
|
{baseline.name: baseline for baseline in baselines},
|
||
|
|
SEWRetargeterAdapter(mapper),
|
||
|
|
)
|