Skip to content

Commit 41804fc

Browse files
authored
ArmConfig: add Cartesian control limits, eliminate magic numbers (#153) (#155)
The arm declares its own dynamic capabilities — TeleopController and servo primitives read from arm.config instead of hardcoded constants. - ArmConfig: add max_cartesian_speed, max_cartesian_angular, reactive_gain (used by _step_pose_as_twist) - UR5e: 0.3 m/s, 1.0 rad/s (industrial) - Unify reactive lookahead: use config.lookahead_time instead of 2*control_dt (matches trajectory execution feedforward) JACO2 values set in ada_mj separately (0.06 m/s, 0.3 rad/s — conservative for human-proximate operation). Closes #153
1 parent bb99f16 commit 41804fc

6 files changed

Lines changed: 30 additions & 9 deletions

File tree

src/mj_manipulator/arms/ur5e.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ def create_ur5e_arm(
134134
),
135135
ee_site=ee_site,
136136
tcp_offset=tcp_offset,
137+
max_cartesian_speed=0.3, # industrial arm, faster
138+
max_cartesian_angular=1.0,
137139
)
138140

139141
arm = Arm(env, config)

src/mj_manipulator/config.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,15 @@ class ArmConfig(EntityConfig):
6464
"""Configuration for a single arm.
6565
6666
Gripper-specific fields (body names, actuator, hand type) belong on the
67-
Gripper protocol, not here. ArmConfig defines the kinematic chain only.
67+
Gripper protocol, not here. ArmConfig defines the kinematic chain and
68+
the arm's dynamic capabilities.
6869
6970
kinematic_limits is required and robot-specific. Use your robot's
7071
datasheet values (e.g., KinematicLimits for UR5e, Franka, Xarm).
72+
73+
Cartesian control parameters (max_cartesian_speed, max_cartesian_angular,
74+
reactive_gain) are used by TeleopController and servo primitives. Set
75+
these based on the arm's workspace, PD dynamics, and safety requirements.
7176
"""
7277

7378
kinematic_limits: KinematicLimits # required: robot-specific velocity/acceleration limits
@@ -78,6 +83,20 @@ class ArmConfig(EntityConfig):
7883
extra_arm_body_names: list[str] | None = None # Additional bodies to treat as part of arm for collision
7984
planning_defaults: PlanningDefaults = field(default_factory=PlanningDefaults)
8085

86+
# Cartesian control limits — the arm declares what it can do.
87+
# TeleopController and servo primitives read these.
88+
max_cartesian_speed: float = 0.2
89+
"""Maximum EE linear speed (m/s) for reactive control (teleop, servo).
90+
Set conservatively for arms near humans (e.g., 0.06 for feeding).
91+
Enforced via Jacobian-based speed checking in _check_and_commit."""
92+
93+
max_cartesian_angular: float = 0.5
94+
"""Maximum EE angular speed (rad/s) for reactive control."""
95+
96+
reactive_gain: float = 1.0
97+
"""Proportional gain for pose error → twist conversion in teleop.
98+
Higher = faster response to gizmo movement, but may overshoot."""
99+
81100
def __post_init__(self):
82101
"""Set entity_type to arm."""
83102
object.__setattr__(self, "entity_type", "arm")

src/mj_manipulator/physics_controller.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def step_reactive(
180180
)
181181

182182
# Reactive arm: small lookahead
183-
reactive_lookahead = 2.0 * self.control_dt
183+
reactive_lookahead = self.config.lookahead_time
184184
q_cmd = state.target_position + reactive_lookahead * state.target_velocity
185185
self.data.ctrl[state.actuator_ids] = q_cmd
186186

src/mj_manipulator/sim_context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,7 @@ def _set_reactive_target(self, arm_name: str, position: np.ndarray, velocity: np
657657
state.target_velocity = (
658658
np.asarray(velocity).copy() if velocity is not None else np.zeros(len(state.actuator_ids))
659659
)
660-
state.lookahead = 2.0 * self._controller.control_dt
660+
state.lookahead = self._controller.config.lookahead_time
661661

662662
def _step_cartesian_impl(self, arm_name: str, position: np.ndarray, velocity: np.ndarray | None) -> None:
663663
if self._controller is not None:

src/mj_manipulator/teleop.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -610,9 +610,9 @@ def _step_pose_as_twist(self, target_pose: np.ndarray) -> TeleopState:
610610
) / (2 * np.sin(angle))
611611
rot_err = axis * angle
612612

613-
gain = 1.0
614-
max_linear = 0.2
615-
max_angular = 0.5
613+
gain = self._arm.config.reactive_gain
614+
max_linear = self._arm.config.max_cartesian_speed
615+
max_angular = self._arm.config.max_cartesian_angular
616616

617617
linear_vel = np.clip(gain * pos_err, -max_linear, max_linear)
618618
angular_vel = np.clip(gain * rot_err, -max_angular, max_angular)

tests/test_physics_controller.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@ def test_step_reactive(self, controller, model_and_data):
103103
np.array([0.5, -0.5]),
104104
np.array([0.1, 0.1]),
105105
)
106-
# Actuators should have been commanded with reactive lookahead
107-
# cmd = 0.5 + 2*0.002*0.1 = 0.5004
108-
expected = 0.5 + 2.0 * controller.control_dt * 0.1
106+
# Actuators should have been commanded with lookahead_time feedforward
107+
# cmd = 0.5 + lookahead_time * 0.1
108+
expected = 0.5 + controller.config.lookahead_time * 0.1
109109
assert abs(data.ctrl[controller._arms["test_arm"].actuator_ids[0]] - expected) < 1e-6
110110

111111
def test_step_reactive_unknown_raises(self, controller):

0 commit comments

Comments
 (0)