feat: 优化双臂采摘QP稳健性
This commit is contained in:
@@ -100,6 +100,43 @@ def test_deployed_workspace_is_in_front_of_robot(config_name, node_names) -> Non
|
||||
assert parameters["workspace_max"] == [0.70, 0.10, 0.75]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"arm,single_config,dual_node,j3_reference_deg,j3_weight",
|
||||
[
|
||||
("left", "left_arm_rm75.yaml", "left_arm_teleop", 67.96, 1e-5),
|
||||
("right", "right_arm_rm75.yaml", "right_arm_teleop", -89.57, 1e-4),
|
||||
],
|
||||
)
|
||||
def test_qp_optimization_parameters_match_single_and_dual_configs(
|
||||
arm,
|
||||
single_config,
|
||||
dual_node,
|
||||
j3_reference_deg,
|
||||
j3_weight,
|
||||
) -> None:
|
||||
del arm
|
||||
with (CONFIG_DIR / single_config).open(encoding="utf-8") as stream:
|
||||
single = yaml.safe_load(stream)["single_arm_velocity_teleop"][
|
||||
"ros__parameters"
|
||||
]
|
||||
with (CONFIG_DIR / "dual_arm_rm75.yaml").open(encoding="utf-8") as stream:
|
||||
dual = yaml.safe_load(stream)[dual_node]["ros__parameters"]
|
||||
|
||||
expected = {
|
||||
"qp_j3_reference_deg": j3_reference_deg,
|
||||
"qp_j3_weight": j3_weight,
|
||||
"qp_j4_min_deg": 10.0,
|
||||
"qp_j4_warn_deg": 25.0,
|
||||
"qp_j4_weight": 1e-4,
|
||||
"qp_manipulability_sigma_stop": 0.01,
|
||||
"qp_manipulability_sigma_warn": 0.04,
|
||||
"qp_manipulability_weight": 1e-4,
|
||||
}
|
||||
for name, value in expected.items():
|
||||
assert single[name] == pytest.approx(value)
|
||||
assert dual[name] == pytest.approx(value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("existing", "expected_operation"),
|
||||
[(False, "create"), (True, "update")],
|
||||
|
||||
@@ -11,6 +11,7 @@ from xr_rm_teleop.single_arm_velocity_teleop import (
|
||||
SingleArmVelocityTeleop,
|
||||
_make_transform,
|
||||
_so3_exp,
|
||||
_so3_log,
|
||||
)
|
||||
|
||||
|
||||
@@ -610,7 +611,7 @@ def test_first_feedback_initializes_last_valid_target_without_solving() -> None:
|
||||
assert teleop._ik_solver.solve_calls == 0
|
||||
|
||||
|
||||
def test_qp_failure_returns_last_known_good_target() -> None:
|
||||
def test_qp_failure_returns_none_and_keeps_last_known_good_target() -> None:
|
||||
class FailingSolver:
|
||||
def solve(self, target):
|
||||
del target
|
||||
@@ -624,11 +625,11 @@ def test_qp_failure_returns_last_known_good_target() -> None:
|
||||
|
||||
target = teleop._solve_joint_target(np.eye(4))
|
||||
|
||||
assert target == pytest.approx([0.1] * 7)
|
||||
assert target is None
|
||||
assert teleop._last_valid_joint_target == pytest.approx([0.1] * 7)
|
||||
|
||||
|
||||
def test_qp_success_updates_last_known_good_target() -> None:
|
||||
def test_qp_success_waits_for_send_before_updating_last_known_good_target() -> None:
|
||||
class SuccessfulSolver:
|
||||
def solve(self, target):
|
||||
del target
|
||||
@@ -643,7 +644,54 @@ def test_qp_success_updates_last_known_good_target() -> None:
|
||||
target = teleop._solve_joint_target(np.eye(4))
|
||||
|
||||
assert target == pytest.approx([0.2] * 7)
|
||||
assert teleop._last_valid_joint_target == pytest.approx([0.2] * 7)
|
||||
assert teleop._last_valid_joint_target == pytest.approx([0.1] * 7)
|
||||
|
||||
|
||||
def test_target_filters_do_not_commit_candidate_state() -> None:
|
||||
teleop = object.__new__(SingleArmVelocityTeleop)
|
||||
teleop._filtered_target = [0.0, 0.0, 0.0]
|
||||
teleop._filtered_orientation_target = np.eye(3)
|
||||
teleop._target_filter_alpha = 0.5
|
||||
teleop._target_filter_alpha_fast = 0.5
|
||||
teleop._target_filter_fast_threshold_m = 1.0
|
||||
teleop._orientation_filter_alpha = 0.5
|
||||
|
||||
position = teleop._filter_target([0.2, 0.0, 0.0])
|
||||
orientation = teleop._filter_orientation_target(
|
||||
_so3_exp(np.asarray([0.0, 0.0, 0.2]))
|
||||
)
|
||||
|
||||
assert position == pytest.approx([0.1, 0.0, 0.0])
|
||||
assert teleop._filtered_target == pytest.approx([0.0, 0.0, 0.0])
|
||||
assert teleop._filtered_orientation_target == pytest.approx(np.eye(3))
|
||||
assert np.linalg.norm(_so3_log(orientation)) == pytest.approx(0.1)
|
||||
|
||||
|
||||
def test_failed_send_does_not_commit_cartesian_reference_state() -> None:
|
||||
teleop = object.__new__(SingleArmVelocityTeleop)
|
||||
teleop._last_valid_joint_target = [0.1] * 7
|
||||
teleop._filtered_target = [0.2, 0.0, 0.0]
|
||||
teleop._filtered_orientation_target = np.eye(3)
|
||||
teleop._last_sent_target = [0.2, 0.0, 0.0]
|
||||
teleop._last_sent_orientation = np.eye(3)
|
||||
teleop._last_command_time = FakeTime()
|
||||
teleop._send_joint_target = lambda joints: False
|
||||
|
||||
sent = teleop._send_and_commit_joint_target(
|
||||
[0.3] * 7,
|
||||
[0.3, 0.0, 0.0],
|
||||
_so3_exp(np.asarray([0.0, 0.0, 0.1])),
|
||||
[0.3, 0.0, 0.0],
|
||||
_so3_exp(np.asarray([0.0, 0.0, 0.1])),
|
||||
FakeTime(),
|
||||
)
|
||||
|
||||
assert not sent
|
||||
assert teleop._last_valid_joint_target == pytest.approx([0.1] * 7)
|
||||
assert teleop._filtered_target == pytest.approx([0.2, 0.0, 0.0])
|
||||
assert teleop._filtered_orientation_target == pytest.approx(np.eye(3))
|
||||
assert teleop._last_sent_target == pytest.approx([0.2, 0.0, 0.0])
|
||||
assert teleop._last_sent_orientation == pytest.approx(np.eye(3))
|
||||
|
||||
|
||||
def test_enter_active_control_initializes_se3_orientation_state() -> None:
|
||||
|
||||
@@ -6,6 +6,7 @@ from xml.etree import ElementTree
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from xr_rm_teleop import placo_ik_solver
|
||||
from xr_rm_teleop.placo_ik_solver import (
|
||||
QP_ORIENTATION_TOLERANCE_RAD,
|
||||
QP_POSITION_TOLERANCE_M,
|
||||
@@ -242,6 +243,7 @@ def test_qp_solve_rejects_position_error_above_two_millimeters() -> None:
|
||||
solver._frame_task = SimpleNamespace(T_a_b=None)
|
||||
solver._solver = SimpleNamespace(solve=lambda update: None)
|
||||
solver._validate_result = lambda result, previous: None
|
||||
solver._update_auxiliary_task_weights = lambda: None
|
||||
solver._target_errors = lambda: (2.1e-3, 0.0)
|
||||
|
||||
with pytest.raises(RuntimeError, match="QP did not converge after 30"):
|
||||
@@ -285,3 +287,130 @@ def test_qp_result_rejects_nan_position_and_velocity_violations() -> None:
|
||||
solver._validate_result(np.full(7, 2.0))
|
||||
with pytest.raises(ValueError, match="velocity"):
|
||||
solver._validate_result(np.full(7, 0.2))
|
||||
|
||||
|
||||
def test_lower_margin_activation_is_clamped_and_linear() -> None:
|
||||
activation = placo_ik_solver._lower_margin_activation
|
||||
|
||||
assert activation(0.05, 0.01, 0.04) == 0.0
|
||||
assert activation(0.025, 0.01, 0.04) == pytest.approx(0.5)
|
||||
assert activation(0.005, 0.01, 0.04) == 1.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"arm,joint_degrees,j3_reference_deg",
|
||||
[
|
||||
("left", ARM_CASES[0][1], 67.96),
|
||||
("right", ARM_CASES[1][1], -89.57),
|
||||
],
|
||||
)
|
||||
def test_solver_configures_auxiliary_qp_tasks(
|
||||
arm: str,
|
||||
joint_degrees: list[float],
|
||||
j3_reference_deg: float,
|
||||
) -> None:
|
||||
pytest.importorskip("placo")
|
||||
solver = PlacoIkSolver(
|
||||
str(DUAL_URDF_PATH),
|
||||
1.0 / 90.0,
|
||||
arm,
|
||||
j3_reference_deg=j3_reference_deg,
|
||||
j3_weight=1e-5,
|
||||
j4_min_deg=10.0,
|
||||
j4_warn_deg=25.0,
|
||||
j4_weight=1e-4,
|
||||
manipulability_sigma_stop=0.01,
|
||||
manipulability_sigma_warn=0.04,
|
||||
manipulability_weight=1e-4,
|
||||
)
|
||||
joints = np.radians(joint_degrees).tolist()
|
||||
solver.update_joint_state(joints)
|
||||
|
||||
assert solver._j3_task.get_joint(
|
||||
solver._joint_names[2]
|
||||
) == pytest.approx(math.radians(j3_reference_deg))
|
||||
assert np.asarray(solver._j4_constraint.A)[
|
||||
solver._q_offsets[3]
|
||||
] == pytest.approx(-1.0)
|
||||
assert np.asarray(solver._j4_constraint.b) == pytest.approx(
|
||||
[-math.radians(10.0)]
|
||||
)
|
||||
assert solver._j4_constraint.priority == "hard"
|
||||
jacobian = solver._active_tcp_jacobian()
|
||||
assert jacobian.shape == (6, 7)
|
||||
assert np.isfinite(jacobian).all()
|
||||
assert np.linalg.svd(jacobian, compute_uv=False)[-1] > 0.0
|
||||
|
||||
|
||||
def test_failed_qp_restores_internal_state_to_actual_feedback() -> None:
|
||||
solver, joints = _dual_placo_solver("left", ARM_CASES[0][1])
|
||||
current_pose = solver.update_joint_state(joints)
|
||||
unreachable = current_pose.copy()
|
||||
unreachable[2, 3] += 10.0
|
||||
|
||||
with pytest.raises((RuntimeError, ValueError)):
|
||||
solver.solve(unreachable)
|
||||
|
||||
assert solver._robot.state.q[solver._q_offsets] == pytest.approx(joints)
|
||||
|
||||
|
||||
def test_solver_rejects_non_positive_manipulability_threshold() -> None:
|
||||
pytest.importorskip("placo")
|
||||
|
||||
with pytest.raises(ValueError, match="manipulability thresholds"):
|
||||
PlacoIkSolver(
|
||||
str(DUAL_URDF_PATH),
|
||||
1.0 / 90.0,
|
||||
"left",
|
||||
manipulability_sigma_stop=0.0,
|
||||
manipulability_sigma_warn=0.04,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"q4_deg,sigma_min,expected_activation",
|
||||
[
|
||||
(25.0, 0.04, 0.0),
|
||||
(17.5, 0.025, 0.5),
|
||||
(10.0, 0.01, 1.0),
|
||||
],
|
||||
)
|
||||
def test_auxiliary_weights_activate_only_inside_warning_margins(
|
||||
q4_deg: float,
|
||||
sigma_min: float,
|
||||
expected_activation: float,
|
||||
) -> None:
|
||||
class TaskSpy:
|
||||
def __init__(self) -> None:
|
||||
self.calls = []
|
||||
|
||||
def configure(self, name, priority, weight) -> None:
|
||||
self.calls.append((name, priority, weight))
|
||||
|
||||
solver = object.__new__(PlacoIkSolver)
|
||||
solver._q_offsets = np.arange(7, 14)
|
||||
solver._robot = SimpleNamespace(
|
||||
state=SimpleNamespace(q=np.zeros(21))
|
||||
)
|
||||
solver._robot.state.q[solver._q_offsets[3]] = math.radians(q4_deg)
|
||||
solver._j4_task = TaskSpy()
|
||||
solver._j4_min = math.radians(10.0)
|
||||
solver._j4_warn = math.radians(25.0)
|
||||
solver._j4_weight = 1e-4
|
||||
solver._manipulability_task = TaskSpy()
|
||||
solver._manipulability_sigma_stop = 0.01
|
||||
solver._manipulability_sigma_warn = 0.04
|
||||
solver._manipulability_weight = 1e-4
|
||||
jacobian = np.zeros((6, 7))
|
||||
jacobian[:, :6] = np.diag([1.0] * 5 + [sigma_min])
|
||||
solver._active_tcp_jacobian = lambda: jacobian
|
||||
|
||||
solver._update_auxiliary_task_weights()
|
||||
|
||||
expected_weight = 1e-4 * expected_activation
|
||||
assert solver._j4_task.calls == [
|
||||
("j4_soft_buffer", "soft", pytest.approx(expected_weight))
|
||||
]
|
||||
assert solver._manipulability_task.calls == [
|
||||
("tcp_6d_manipulability", "soft", pytest.approx(expected_weight))
|
||||
]
|
||||
|
||||
@@ -31,6 +31,14 @@ QP_POSITION_TOLERANCE_M = 2e-3
|
||||
QP_ORIENTATION_TOLERANCE_RAD = 5e-3
|
||||
|
||||
|
||||
def _lower_margin_activation(value: float, stop: float, warn: float) -> float:
|
||||
if not all(np.isfinite(item) for item in (value, stop, warn)):
|
||||
raise ValueError("activation values must be finite")
|
||||
if stop >= warn:
|
||||
raise ValueError("activation stop must be smaller than warn")
|
||||
return float(np.clip((warn - value) / (warn - stop), 0.0, 1.0))
|
||||
|
||||
|
||||
def _validated_transform(transform: np.ndarray) -> np.ndarray:
|
||||
values = np.asarray(transform, dtype=float)
|
||||
if values.shape != (4, 4) or not np.isfinite(values).all():
|
||||
@@ -60,6 +68,15 @@ class PlacoIkSolver:
|
||||
urdf_path: str,
|
||||
dt: float,
|
||||
arm: str,
|
||||
*,
|
||||
j3_reference_deg: float | None = None,
|
||||
j3_weight: float = 1e-5,
|
||||
j4_min_deg: float | None = None,
|
||||
j4_warn_deg: float | None = None,
|
||||
j4_weight: float = 1e-4,
|
||||
manipulability_sigma_stop: float = 0.01,
|
||||
manipulability_sigma_warn: float = 0.04,
|
||||
manipulability_weight: float = 0.0,
|
||||
) -> None:
|
||||
if dt <= 0.0:
|
||||
raise ValueError("dt must be positive")
|
||||
@@ -134,12 +151,37 @@ class PlacoIkSolver:
|
||||
]
|
||||
)
|
||||
self._actual_joints: np.ndarray | None = None
|
||||
weights = (j3_weight, j4_weight, manipulability_weight)
|
||||
if not all(np.isfinite(value) and value >= 0.0 for value in weights):
|
||||
raise ValueError("QP auxiliary weights must be finite and non-negative")
|
||||
if j3_reference_deg is not None and not np.isfinite(j3_reference_deg):
|
||||
raise ValueError("J3 reference must be finite")
|
||||
if (j4_min_deg is None) != (j4_warn_deg is None):
|
||||
raise ValueError("J4 minimum and warning angles must be configured together")
|
||||
if j4_min_deg is not None:
|
||||
if not all(np.isfinite(value) for value in (j4_min_deg, j4_warn_deg)):
|
||||
raise ValueError("J4 angles must be finite")
|
||||
if j4_warn_deg <= j4_min_deg:
|
||||
raise ValueError("J4 warning angle must exceed its minimum")
|
||||
j4_limits_deg = np.degrees(self._joint_limits[3])
|
||||
if j4_min_deg < j4_limits_deg[0] or j4_warn_deg > j4_limits_deg[1]:
|
||||
raise ValueError("J4 safety angles must stay within URDF limits")
|
||||
if not (
|
||||
np.isfinite(manipulability_sigma_stop)
|
||||
and np.isfinite(manipulability_sigma_warn)
|
||||
and 0.0 < manipulability_sigma_stop
|
||||
< manipulability_sigma_warn
|
||||
):
|
||||
raise ValueError(
|
||||
"manipulability thresholds must satisfy 0 < stop < warn"
|
||||
)
|
||||
|
||||
self._solver = placo.KinematicsSolver(self._robot)
|
||||
self._solver.dt = dt
|
||||
self._solver.mask_fbase(True)
|
||||
for name in inactive_joint_names:
|
||||
self._solver.mask_dof(name)
|
||||
self._solver.enable_joint_limits(True)
|
||||
self._solver.enable_velocity_limits(True)
|
||||
self._frame_task = self._solver.add_relative_frame_task(
|
||||
self._base_frame,
|
||||
@@ -149,6 +191,53 @@ class PlacoIkSolver:
|
||||
self._frame_task.configure("rm75_relative_frame", "soft", 1.0)
|
||||
self._solver.add_kinetic_energy_regularization_task(1e-6)
|
||||
|
||||
self._j3_task = None
|
||||
if j3_reference_deg is not None and j3_weight > 0.0:
|
||||
self._j3_task = self._solver.add_joints_task()
|
||||
self._j3_task.set_joints(
|
||||
{self._joint_names[2]: np.deg2rad(j3_reference_deg)}
|
||||
)
|
||||
self._j3_task.configure("j3_reference", "soft", j3_weight)
|
||||
|
||||
self._j4_task = None
|
||||
self._j4_constraint = None
|
||||
self._j4_min = None
|
||||
self._j4_warn = None
|
||||
self._j4_weight = j4_weight
|
||||
if j4_min_deg is not None:
|
||||
self._j4_min = float(np.deg2rad(j4_min_deg))
|
||||
self._j4_warn = float(np.deg2rad(j4_warn_deg))
|
||||
self._j4_task = self._solver.add_joints_task()
|
||||
self._j4_task.set_joints(
|
||||
{self._joint_names[3]: self._j4_warn}
|
||||
)
|
||||
self._j4_task.configure("j4_soft_buffer", "soft", 0.0)
|
||||
matrix = np.zeros((1, self._robot.state.q.size))
|
||||
matrix[0, self._q_offsets[3]] = -1.0
|
||||
self._j4_constraint = (
|
||||
self._solver.add_joint_space_half_spaces_constraint(
|
||||
matrix,
|
||||
np.asarray([-self._j4_min]),
|
||||
)
|
||||
)
|
||||
self._j4_constraint.configure("j4_lower_bound", "hard")
|
||||
|
||||
self._manipulability_task = None
|
||||
self._manipulability_sigma_stop = manipulability_sigma_stop
|
||||
self._manipulability_sigma_warn = manipulability_sigma_warn
|
||||
self._manipulability_weight = manipulability_weight
|
||||
if manipulability_weight > 0.0:
|
||||
self._manipulability_task = self._solver.add_manipulability_task(
|
||||
self._tcp_frame,
|
||||
"both",
|
||||
1.0,
|
||||
)
|
||||
self._manipulability_task.configure(
|
||||
"tcp_6d_manipulability",
|
||||
"soft",
|
||||
0.0,
|
||||
)
|
||||
|
||||
@property
|
||||
def joint_names(self) -> list[str]:
|
||||
return list(self._joint_names)
|
||||
@@ -183,32 +272,64 @@ class PlacoIkSolver:
|
||||
float(orientation_task.error_norm()),
|
||||
)
|
||||
|
||||
def _active_tcp_jacobian(self) -> np.ndarray:
|
||||
jacobian = np.asarray(
|
||||
self._robot.frame_jacobian(
|
||||
self._tcp_frame,
|
||||
"local_world_aligned",
|
||||
),
|
||||
dtype=float,
|
||||
)[:, self._v_offsets]
|
||||
if jacobian.shape != (6, 7) or not np.isfinite(jacobian).all():
|
||||
raise ValueError("TCP Jacobian must be a finite 6x7 matrix")
|
||||
return jacobian
|
||||
|
||||
def _update_auxiliary_task_weights(self) -> None:
|
||||
if self._j4_task is not None:
|
||||
q4 = float(self._robot.state.q[self._q_offsets[3]])
|
||||
activation = _lower_margin_activation(
|
||||
q4,
|
||||
self._j4_min,
|
||||
self._j4_warn,
|
||||
)
|
||||
self._j4_task.configure(
|
||||
"j4_soft_buffer",
|
||||
"soft",
|
||||
self._j4_weight * activation,
|
||||
)
|
||||
if self._manipulability_task is not None:
|
||||
sigma_min = float(
|
||||
np.linalg.svd(
|
||||
self._active_tcp_jacobian(),
|
||||
compute_uv=False,
|
||||
)[-1]
|
||||
)
|
||||
activation = _lower_margin_activation(
|
||||
sigma_min,
|
||||
self._manipulability_sigma_stop,
|
||||
self._manipulability_sigma_warn,
|
||||
)
|
||||
self._manipulability_task.configure(
|
||||
"tcp_6d_manipulability",
|
||||
"soft",
|
||||
self._manipulability_weight * activation,
|
||||
)
|
||||
|
||||
def _restore_actual_joint_state(self) -> None:
|
||||
self._robot.state.q[self._q_offsets] = self._actual_joints
|
||||
self._robot.update_kinematics()
|
||||
|
||||
def solve(self, target_tool_pose: np.ndarray) -> list[float]:
|
||||
if self._actual_joints is None:
|
||||
raise RuntimeError("joint state must be initialized before QP solve")
|
||||
self._frame_task.T_a_b = _validated_transform(
|
||||
target_tool_pose
|
||||
)
|
||||
result = np.asarray(
|
||||
self._robot.state.q[self._q_offsets],
|
||||
dtype=float,
|
||||
).copy()
|
||||
position_error, orientation_error = self._target_errors()
|
||||
if (
|
||||
position_error <= QP_POSITION_TOLERANCE_M
|
||||
and orientation_error <= QP_ORIENTATION_TOLERANCE_RAD
|
||||
):
|
||||
return result.tolist()
|
||||
|
||||
for _ in range(QP_MAX_ITERATIONS):
|
||||
previous = result
|
||||
self._solver.solve(True)
|
||||
self._robot.update_kinematics()
|
||||
try:
|
||||
self._frame_task.T_a_b = _validated_transform(
|
||||
target_tool_pose
|
||||
)
|
||||
result = np.asarray(
|
||||
self._robot.state.q[self._q_offsets],
|
||||
dtype=float,
|
||||
).copy()
|
||||
self._validate_result(result, previous)
|
||||
position_error, orientation_error = self._target_errors()
|
||||
if (
|
||||
position_error <= QP_POSITION_TOLERANCE_M
|
||||
@@ -216,12 +337,32 @@ class PlacoIkSolver:
|
||||
):
|
||||
return result.tolist()
|
||||
|
||||
raise RuntimeError(
|
||||
"QP did not converge after "
|
||||
f"{QP_MAX_ITERATIONS} iterations: "
|
||||
f"position_error={position_error:.6f} m, "
|
||||
f"orientation_error={orientation_error:.6f} rad"
|
||||
)
|
||||
for _ in range(QP_MAX_ITERATIONS):
|
||||
previous = result
|
||||
self._update_auxiliary_task_weights()
|
||||
self._solver.solve(True)
|
||||
self._robot.update_kinematics()
|
||||
result = np.asarray(
|
||||
self._robot.state.q[self._q_offsets],
|
||||
dtype=float,
|
||||
).copy()
|
||||
self._validate_result(result, previous)
|
||||
position_error, orientation_error = self._target_errors()
|
||||
if (
|
||||
position_error <= QP_POSITION_TOLERANCE_M
|
||||
and orientation_error <= QP_ORIENTATION_TOLERANCE_RAD
|
||||
):
|
||||
return result.tolist()
|
||||
|
||||
raise RuntimeError(
|
||||
"QP did not converge after "
|
||||
f"{QP_MAX_ITERATIONS} iterations: "
|
||||
f"position_error={position_error:.6f} m, "
|
||||
f"orientation_error={orientation_error:.6f} rad"
|
||||
)
|
||||
except Exception:
|
||||
self._restore_actual_joint_state()
|
||||
raise
|
||||
|
||||
def _validate_result(
|
||||
self,
|
||||
|
||||
@@ -201,6 +201,14 @@ class SingleArmVelocityTeleop(Node):
|
||||
self.declare_parameter("xr_to_robot_matrix", [0.0, 0.0, -1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0])
|
||||
self.declare_parameter("use_mock", True)
|
||||
self.declare_parameter("robot_urdf_path", "")
|
||||
self.declare_parameter("qp_j3_reference_deg", 0.0)
|
||||
self.declare_parameter("qp_j3_weight", 1e-5)
|
||||
self.declare_parameter("qp_j4_min_deg", 10.0)
|
||||
self.declare_parameter("qp_j4_warn_deg", 25.0)
|
||||
self.declare_parameter("qp_j4_weight", 1e-4)
|
||||
self.declare_parameter("qp_manipulability_sigma_stop", 0.01)
|
||||
self.declare_parameter("qp_manipulability_sigma_warn", 0.04)
|
||||
self.declare_parameter("qp_manipulability_weight", 1e-4)
|
||||
self.declare_parameter("robot_ip", "192.168.1.18")
|
||||
self.declare_parameter("robot_port", 8080)
|
||||
self.declare_parameter("realtime_push_host_ip", "")
|
||||
@@ -260,6 +268,30 @@ class SingleArmVelocityTeleop(Node):
|
||||
self._low_z_min_radius = float(self.get_parameter("low_z_min_radius").value)
|
||||
self._xr_to_robot_matrix = self._float_list_parameter("xr_to_robot_matrix", 9)
|
||||
self._use_mock = self._bool_parameter("use_mock")
|
||||
self._qp_j3_reference_deg = float(
|
||||
self.get_parameter("qp_j3_reference_deg").value
|
||||
)
|
||||
self._qp_j3_weight = float(
|
||||
self.get_parameter("qp_j3_weight").value
|
||||
)
|
||||
self._qp_j4_min_deg = float(
|
||||
self.get_parameter("qp_j4_min_deg").value
|
||||
)
|
||||
self._qp_j4_warn_deg = float(
|
||||
self.get_parameter("qp_j4_warn_deg").value
|
||||
)
|
||||
self._qp_j4_weight = float(
|
||||
self.get_parameter("qp_j4_weight").value
|
||||
)
|
||||
self._qp_manipulability_sigma_stop = float(
|
||||
self.get_parameter("qp_manipulability_sigma_stop").value
|
||||
)
|
||||
self._qp_manipulability_sigma_warn = float(
|
||||
self.get_parameter("qp_manipulability_sigma_warn").value
|
||||
)
|
||||
self._qp_manipulability_weight = float(
|
||||
self.get_parameter("qp_manipulability_weight").value
|
||||
)
|
||||
self._follow = self._bool_parameter("follow")
|
||||
self._enable_tool_control = self._bool_parameter("enable_tool_control")
|
||||
self._enable_trigger_gripper_control = self._bool_parameter("enable_trigger_gripper_control")
|
||||
@@ -328,6 +360,18 @@ class SingleArmVelocityTeleop(Node):
|
||||
str(self.get_parameter("robot_urdf_path").value),
|
||||
self._dt,
|
||||
peripheral_arm,
|
||||
j3_reference_deg=self._qp_j3_reference_deg,
|
||||
j3_weight=self._qp_j3_weight,
|
||||
j4_min_deg=self._qp_j4_min_deg,
|
||||
j4_warn_deg=self._qp_j4_warn_deg,
|
||||
j4_weight=self._qp_j4_weight,
|
||||
manipulability_sigma_stop=(
|
||||
self._qp_manipulability_sigma_stop
|
||||
),
|
||||
manipulability_sigma_warn=(
|
||||
self._qp_manipulability_sigma_warn
|
||||
),
|
||||
manipulability_weight=self._qp_manipulability_weight,
|
||||
)
|
||||
debug_ns = f"{self._debug_topic_prefix}/{self._arm_name}"
|
||||
self._joint_state_pub = self.create_publisher(
|
||||
@@ -730,12 +774,16 @@ class SingleArmVelocityTeleop(Node):
|
||||
joint_target = self._solve_joint_target(target_pose)
|
||||
qp_ms = (time.perf_counter_ns() - qp_started_ns) * 1e-6
|
||||
send_started_ns = time.perf_counter_ns()
|
||||
sent = self._send_joint_target(joint_target)
|
||||
sent = self._send_and_commit_joint_target(
|
||||
joint_target,
|
||||
filtered_target,
|
||||
filtered_orientation,
|
||||
sent_target,
|
||||
sent_orientation,
|
||||
now,
|
||||
)
|
||||
send_ms = (time.perf_counter_ns() - send_started_ns) * 1e-6
|
||||
if sent:
|
||||
self._last_sent_target = sent_target
|
||||
self._last_sent_orientation = sent_orientation.copy()
|
||||
self._last_command_time = now
|
||||
self._stop_sent = False
|
||||
total_ms = (time.perf_counter_ns() - tick_started_ns) * 1e-6
|
||||
try:
|
||||
@@ -867,17 +915,15 @@ class SingleArmVelocityTeleop(Node):
|
||||
|
||||
def _filter_target(self, target: list[float]) -> list[float]:
|
||||
if self._filtered_target is None:
|
||||
self._filtered_target = list(target)
|
||||
return list(target)
|
||||
|
||||
delta = [target[i] - self._filtered_target[i] for i in range(3)]
|
||||
distance = _norm(delta)
|
||||
alpha = self._adaptive_filter_alpha(distance)
|
||||
self._filtered_target = [
|
||||
return [
|
||||
alpha * target[i] + (1.0 - alpha) * self._filtered_target[i]
|
||||
for i in range(3)
|
||||
]
|
||||
return list(self._filtered_target)
|
||||
|
||||
def _adaptive_filter_alpha(self, distance: float) -> float:
|
||||
if self._target_filter_fast_threshold_m <= 1e-9:
|
||||
@@ -916,17 +962,15 @@ class SingleArmVelocityTeleop(Node):
|
||||
|
||||
def _filter_orientation_target(self, target_rotation: np.ndarray) -> np.ndarray:
|
||||
if self._filtered_orientation_target is None:
|
||||
self._filtered_orientation_target = _project_rotation(target_rotation)
|
||||
return self._filtered_orientation_target.copy()
|
||||
return _project_rotation(target_rotation)
|
||||
|
||||
error = _so3_log(
|
||||
target_rotation @ self._filtered_orientation_target.T
|
||||
)
|
||||
self._filtered_orientation_target = _project_rotation(
|
||||
return _project_rotation(
|
||||
_so3_exp(self._orientation_filter_alpha * error)
|
||||
@ self._filtered_orientation_target
|
||||
)
|
||||
return self._filtered_orientation_target.copy()
|
||||
|
||||
def _limit_orientation_step(
|
||||
self,
|
||||
@@ -1196,7 +1240,10 @@ class SingleArmVelocityTeleop(Node):
|
||||
self._last_valid_joint_target = list(snapshot.positions)
|
||||
return current_pose
|
||||
|
||||
def _solve_joint_target(self, target_pose: np.ndarray) -> list[float]:
|
||||
def _solve_joint_target(
|
||||
self,
|
||||
target_pose: np.ndarray,
|
||||
) -> list[float] | None:
|
||||
if self._last_valid_joint_target is None:
|
||||
raise RuntimeError("valid joint feedback has not been initialized")
|
||||
try:
|
||||
@@ -1206,10 +1253,28 @@ class SingleArmVelocityTeleop(Node):
|
||||
f"{self._arm_name} QP 求解失败,保持上一组关节目标:{exc}",
|
||||
throttle_duration_sec=1.0,
|
||||
)
|
||||
return list(self._last_valid_joint_target)
|
||||
self._last_valid_joint_target = list(result)
|
||||
return None
|
||||
return list(result)
|
||||
|
||||
def _send_and_commit_joint_target(
|
||||
self,
|
||||
joint_target: list[float] | None,
|
||||
filtered_target: list[float],
|
||||
filtered_orientation: np.ndarray,
|
||||
sent_target: list[float],
|
||||
sent_orientation: np.ndarray,
|
||||
now: Time,
|
||||
) -> bool:
|
||||
if joint_target is None or not self._send_joint_target(joint_target):
|
||||
return False
|
||||
self._last_valid_joint_target = list(joint_target)
|
||||
self._filtered_target = list(filtered_target)
|
||||
self._filtered_orientation_target = filtered_orientation.copy()
|
||||
self._last_sent_target = list(sent_target)
|
||||
self._last_sent_orientation = sent_orientation.copy()
|
||||
self._last_command_time = now
|
||||
return True
|
||||
|
||||
def _safe_stop(self, reset_active: bool) -> None:
|
||||
if not self._stop_sent:
|
||||
self._send_stop_once()
|
||||
@@ -1461,6 +1526,35 @@ class SingleArmVelocityTeleop(Node):
|
||||
raise ValueError("joint_max_speed must be > 0")
|
||||
if self._joint_command_max_acceleration <= 0.0:
|
||||
raise ValueError("joint_max_acc must be > 0")
|
||||
qp_weights = (
|
||||
self._qp_j3_weight,
|
||||
self._qp_j4_weight,
|
||||
self._qp_manipulability_weight,
|
||||
)
|
||||
if not all(
|
||||
math.isfinite(value) and value >= 0.0
|
||||
for value in qp_weights
|
||||
):
|
||||
raise ValueError("QP auxiliary weights must be finite and non-negative")
|
||||
if not math.isfinite(self._qp_j3_reference_deg):
|
||||
raise ValueError("qp_j3_reference_deg must be finite")
|
||||
if not all(
|
||||
math.isfinite(value)
|
||||
for value in (self._qp_j4_min_deg, self._qp_j4_warn_deg)
|
||||
):
|
||||
raise ValueError("QP J4 angles must be finite")
|
||||
if self._qp_j4_warn_deg <= self._qp_j4_min_deg:
|
||||
raise ValueError("qp_j4_warn_deg must exceed qp_j4_min_deg")
|
||||
if not (
|
||||
math.isfinite(self._qp_manipulability_sigma_stop)
|
||||
and math.isfinite(self._qp_manipulability_sigma_warn)
|
||||
and 0.0 < self._qp_manipulability_sigma_stop
|
||||
< self._qp_manipulability_sigma_warn
|
||||
):
|
||||
raise ValueError(
|
||||
"QP manipulability sigma thresholds must satisfy "
|
||||
"0 < stop < warn"
|
||||
)
|
||||
|
||||
def _shutdown_tool_worker(self) -> None:
|
||||
if self._tool_worker_thread is None or self._tool_command_queue is None:
|
||||
|
||||
Reference in New Issue
Block a user