Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The impedance backend

Target control tracks the generator’s setpoints with torques of its own by default. This page is the derivation: where the law comes from, why it is a joint law with a Cartesian term rather than the other way round, what the joint gains do to the stiffness you feel, how the Cartesian interface gets a joint goal, how the goal and the arm are kept under the joint velocity limits and inside the joint position limits, how the generator is anchored without an echo, what happens at the handover, and how it differs from the operational-space law of the cartesian_impedance_active_control example. The options and defaults are on the how-to page; the code is robot/target_control/impedance.rs, torque/, ik/, position.rs and velocity.rs.

The law and its provenance

Kp  = Jᵀ Kx J + diag(Kq)          Kd = Jᵀ Kxd J + diag(Kqd)
tau = Kp (q_goal − q) + Kd (g dq_goal − dq) + coriolis(q, dq),   clamped to ±torque_limits

This is the structure of HybridJointImpedanceControl from polymetis (its feedback module HybridJointSpacePD forms Kp and Kd exactly so), the controller that DROID (Khazatsky et al., 2024, DROID: A Large-Scale In-The-Wild Robot Manipulation Dataset, arXiv:2403.12945) ran on its Panda arms for 76k teleoperated trajectories. ImpedanceGains::DROID is its gains as they were, and with velocity_feedforward off (g = 0, the damping on the absolute velocity, the default) the law is polymetis’s. ImpedanceGains::CARTESIAN differs from it: it raises the translational damping from 37 to 50, 50, 90 Ns/m: the arm’s apparent mass at the end effector near the ready pose is about 0.94 kg along x and y and 3.9 kg along z (from the model’s mass matrix), so 37 Ns/m against 750 N/m leaves z at a damping ratio of 0.34, which rings; the defaults bring all three to about 0.8. Damping the absolute velocity resists the motion the goal asks for, and a goal moving at v is tracked Kd v / Kp behind it. velocity_feedforward = true makes g the velocity_feedforward_gain (default 1), with dq_goal the goal’s velocity: the generator’s on the joint interface, the finite difference of the IK solution on the Cartesian one, zero while holding. It is off by default because real arms vibrated with it on: Kd carries the finite difference’s noise into the torque. velocity_feedforward_cutoff low-pass filters dq_goal and is the knob to try against it; no value has been validated on hardware. J is Model::zero_jacobian(Frame::EndEffector, state), the 6x7 base-frame Jacobian at the measured q, so Kx acts at the configured end-effector frame, the frame the O_T_EE targets are in. Gravity is compensated by the robot; coriolis is the model’s, as in every torque example of the crate. The clamp comes before the low-pass filter (cutoff_frequency, 100 Hz by default) and the torque-rate limiter that control_torques applies, in that order.

Why a joint law with a Cartesian term

Jᵀ Kx J is the Cartesian spring expressed in joint coordinates: for a small joint error Δq, J Δq is the end-effector error and Jᵀ Kx J Δq the joint torque of the spring Kx pulling on it. It dominates the stiffness: a joint whose lever arm to the end effector is 0.5 m sees 750 N/m as 750 × 0.5² ≈ 190 Nm/rad, against 25 to 50 Nm/rad in Kq. But on a seven-joint arm the 6x7 Jacobian has a one-dimensional nullspace, the motion that leaves the end effector where it is (the elbow’s swing), and Jᵀ Kx J is zero along it. diag(Kq) regularises that direction, and near a singularity, where the Cartesian term collapses in more directions, it keeps every joint held. So the Cartesian gains set the compliance you feel at the tool and the joint gains set how firmly the arm keeps its shape. The joint interface uses the same code with Kx = Kxd = 0, which leaves a joint PD with Coriolis feed-forward, the fer_joint_impedance example’s law and gains.

What the joint gains do to the stiffness you feel

diag(Kq) is not confined to the nullspace: it acts on every joint, and the part of it that J maps to the end effector adds to Kx. The stiffness felt at the tool is (J Kp⁻¹ Jᵀ)⁻¹, and with the default gains at the ready pose that is about 990 to 1180 N/m in translation against the 750 N/m set, and two to three times Kx in rotation (computed from the model). DROID ran the law this way. ImpedanceOptions::project_joint_gains replaces diag(Kq) and diag(Kqd) by N Kq N and N Kqd N, with N = I − J⁺ J the nullspace projector of the damped pseudoinverse J⁺ = Jᵀ (J Jᵀ + λ² I)⁻¹ (λ the IK’s damping, 0.05): the joint springs then hold the elbow’s swing and nothing else, and the end effector feels Kx to within the damping of the projector, away from singularities, where N widens and the projected springs take over the collapsing directions. The price is that with the projection on the Cartesian gains are all that holds the tool, so zero Kx would leave it free. It is off by default to keep the default law DROID’s in structure.

Payload

Gravity is not in the law: the robot compensates it from the load configured in Desk or by Robot::set_load. The robot’s own controller corrects a wrong load as a position error; a spring does not. An unconfigured tool becomes a constant sag of its weight over the felt stiffness, 0.5 kg → 5 N → 6.7 mm at 750 N/m (less at the felt ~1000 N/m), in every pose, and a load heavier than configured pulls the same way. set_load matters more here than in position mode.

The differential inverse kinematics

The joint interface feeds the generator’s q in as q_goal. The Cartesian interface needs a joint goal for a pose, and gets one incrementally: from the previous q_goal, up to IkOptions::iterations (3) damped-least-squares steps toward the desired pose per cycle,

ẽ   = W [ p_des − p(q_goal) ;  log(R_des R(q_goal)ᵀ) ],   W = diag(I₃, w_r I₃),   J̃ = W J_g
δ   = argmin ½ ‖J̃ δ − ẽ‖² + ½ λ² ‖δ‖²   subject to   lb ≤ δ ≤ ub
      + (I − J̃_F⁺ J̃_F) k_null (posture − q_goal) dt       on the free joints F

with J_g the zero Jacobian at q_goal (not at the measured q), λ = 0.05 the damping that keeps the step finite at a singularity, w_r = IkOptions::rotation_weight (0.1 m/rad), [lb, ub] the cycle’s box of the joint position limit guard and the last term a drift toward posture (the start configuration unless set) at k_null = 1 /s, capped at 0.5 rad/s so that a far posture is approached rather than jumped at, projected into the free joints’ nullspace so it never moves the end effector. Iteration stops once ‖ẽ‖ is below tolerance (1e-6, so 1 µm or 10 µrad). The limits are rate_limiting::JOINT_POSITION_LIMITS for the FR3 and rate_limiting::fer::JOINT_POSITION_LIMITS for the FER, from the URDFs in the repository. The generator moves the pose by at most 0.3 mm a cycle under the default budget, so a step or three from the previous solution keeps the residual near zero; the observer sees ‖ẽ‖ as CartesianSent::ik_error. A pose out of reach, through a singularity or behind a joint limit leaves a residual and q_goal lags instead of jumping, so the impedance never gets a step to track.

The joint velocity envelope

Nothing in the law bounds a joint’s velocity: near a wrist singularity a modest turn of the hand asks joints 5 and 7 to spin in opposite directions, about 2 rad/s of joint per rad/s of hand once their axes are 30° apart. Teleoperated near such a pose, an FER can end in joint_velocity_violation while the generator is well inside its own budget and the arm follows the goal it is given.

The cap. No joint of the goal’s step Δq = q_goal − q_goal_prev exceeds cap_i · 1 ms, cap_i = joint_velocity_fraction × limit_i (0.7 by default), before the finite difference that is dq_goal. On the joint interface the step is scaled as a whole by s = min(1, min_i cap_i · 1 ms / |Δq_i|), which keeps the joint-space direction. On the Cartesian interface the cap is a bound of the IK’s box, so the free joints make up what a capped one cannot; a whole-step scale behind the IK remains as a backstop that does not bind but for rounding. When the goal falls short the generator’s next cycle starts from what went out, not from its own plan: its velocity is its end-of-cycle velocity scaled by s, not the step’s mean (which sits above the end velocity whenever the plan brakes inside the step, and would replan past the target), and its acceleration is kept only where it brakes – zeroed where it would speed the axis back up (against a position limit, see the restart). The leash’s next reference is the capped goal too: the model’s forward kinematics of q_goal on the Cartesian interface, q_goal itself on the joint one. Without the re-anchor the generator would keep planning from the velocity it wanted and arrive late, then decelerate from a speed the arm never had. A stop’s hold on the joint interface is not capped: that goal only moves with an arm moved by hand, a leash ahead of it.

The fade. A joint lagging its capped goal catches up faster than the goal moves, and unchecked that catch-up pull cancels the barrier below. So above min(cap_i, onset_i − FADE_BAND × limit_i) of measured speed (never below rest), the law’s torque along the joint’s motion – the Coriolis term included – fades out linearly to none at the barrier’s onset; torque against the motion is never scaled. Away from a position limit the min floors the start FADE_BAND (0.15) under the onset regardless of the cap, because a narrower band adds more gain than the loop tolerates: at the default cap 0.7 the band is exactly [0.70, 0.85] of the limit, the same band a cap of 0.8 gets once floored, and both settle; fading instead from a cap of 0.8 straight to the onset – a third of that band – rings on a 0.03 kg m² wrist (tests/velocity.rs). Inside the position margin the band is [0, onset] instead, which on an arm whose limit is flat – the FER’s – is the narrower of the two; what bounds the gain there is the position fade below.

The barrier. Above velocity_barrier_fraction × limit_i (0.85 by default) of measured velocity the loop adds −20 (|dq_i| − onset_i) sign(dq_i) Nm, at most the joint’s torque limit, after the fade and before the clamp, the low-pass and the rate limiter. The gain is set by stability, not by strength: the torque is computed from the velocity measured one cycle before it acts and low-passed at 100 Hz, so a joint of effective inertia I damped with a total K – the law’s own joint damping plus the barrier’s, where it is active – is stable while K × 1 ms / I stays under 1; the rate limiter only slows the barrier’s onset. The link alone is light (0.003 kg m² on the FER’s joint 7) but the drive’s reflected inertia is not (MuJoCo’s FR3 carries 0.074 kg m² of armature on the wrist), and the JOINT preset’s 15 Nm s/rad on joint 7 is stable on an FER; the Cartesian preset puts at most about 6 Nm s/rad on a wrist joint at 1200 N/m, so the barrier’s 20 keeps K under 26. The offline velocity tests exercise the barrier behind the filter and rate limiter (tests/velocity.rs). The stability bound assumes that one cycle of delay; it says nothing about the push itself and does not claim to bound it – the velocity still overshoots the limit while the push is applied, before the barrier settles it back under.

Tests. tests/replay.rs provides an optional replay check for the velocity cap, tracking lag and error. tests/sim_target_control/velocity_cap.rs exercises the cap in the simulator. These tests do not establish a bound on externally imposed motion.

The barrier’s gain is lowered in proportion where torque_limits clamps a joint below its preset (86 Nm on joints 1 to 4, 11.5 Nm on 5 to 7), so a lighter clamp does not leave a stiffer barrier; at the presets it is 20 on every joint.

The joint position limit guard

Clamping a goal inside a joint limit does not ensure that the moving arm can stop there. The guard uses braking bounds in the IK, a stall flag that re-anchors the generator, and a torque fade and spring near the measured joint limit. Three rules keep it independent of the tool:

  • Joint space only. Margin, box, pins, fade and spring are per joint; there is no workspace or link geometry.
  • The tool is the robot’s. F_T_EE and EE_T_K come from the state, the frame the targets are in. The rotation weight acts at that point: a 0.25 m tool with the end effector left at the flange turns 0.1 rad of rotation shortfall into 25 mm at the tip, so configure the tip.
  • No tool-specific constant. Flange, Franka Hand, a third-party gripper and a long tool run the same numbers; the IK tests run all four.

The box. Per joint and side, with s the previous goal’s distance to that limit, m = joint_position_margin (0.05 rad), f = joint_velocity_fraction and x = s − m:

E(x)  = min(v_flat, sqrt(k x + c²) − c)  for x > 0, else 0     braking envelope
u     = min(f · v_σ · Δt,  max(0, min(x, f · E(x) · Δt)))       step allowed toward that side

v_σ is the arm’s velocity limit toward that side. On the FR3 it is position-dependent: min(dq_max, max(0, sqrt(2 ddq_dec d) − dq_offset)) at distance d from the limit, less libfranka’s tolerance, using Franka’s robot-specification parameters. On the FER it uses the flat MAX_JOINT_VELOCITY; no published position-dependent coefficients are available for that arm.

E is the guard’s own braking profile, sqrt(k x + c²) − c with k = 2a, held under v_flat, the joint’s velocity limit away from the position limits (the FER’s flat MAX_JOINT_VELOCITY, the FR3’s dq_max): it stays under sqrt(2 a x), the speed a joint decelerating at a still stops from in x, and c (0.2 to 0.35 rad/s, libfranka’s FR3 offsets) keeps its slope at the stop finite. a is the FR3’s published ddq_dec (2.585 to 17 rad/s²) on that arm and half the FER’s published joint acceleration limit (3.75 to 10 rad/s²) on the FER, which leaves the other half to the arm following the goal and, unlike the FR3 constants, stays inside the FER’s own rating on every joint — 10 mrad before the stop it allows 0.19 rad/s on joint 1 and 0.17 on joint 5, against 0.16 and 0.33 for the FR3’s. v_σ and E are both taken where the step ends (u ≤ f · v_σ(from ± u) · Δt, u ≤ f · E(x − u) · Δt), and the loop’s onset and fade start below at the arm and a cycle on. The box always contains 0: a goal started inside the margin stays where it is rather than jumping out.

The solve. A primal active set, warm-started from the last cycle: each pass pins the free joint that violates its bound most, or releases the pinned joint whose multiplier points inward, and re-solves the free joints with the pinned ones on their bounds, at most 12 passes per solve (CartesianSent::ik_passes, over the cycle’s solves). The result is the exact box-constrained least-squares step, so the free joints take over what a pinned one cannot give, and it is continuous however the set changes, with no dwell. A solve that does not converge is clipped into the box; a non-finite one keeps the previous goal. Fixed-size, nothing allocates. The posture is clamped m + 0.3 rad inside the limits and its pull fades out as the nearest joint closes from m + 0.3 to m, so it cannot drive a joint along the margin.

Pins and the stall. pinned reports per joint 0 free, ∓1 on the lower / upper position bound, ∓2 on a velocity bound. A pin’s pressure is side · ĵ_iᵀ ẽ, the residual along its unit column and only into its bound, over the whole weighted task or its translation rows alone, whichever pushes harder; above 2e-5 (weighted m per cycle) on a position pin the goal is stalled, cleared on 20 of the last 40 cycles in which no position pin pushes at all. Position first takes the goal as far as the pins allow, which lowers the very pressure that set the flag, so a flag cleared on a magnitude can cycle between the two modes. The generator is re-anchored on its own anchor moved by the goal’s own step, and never by more than it planned, which keeps the IK’s lag behind it without a goal closing a lag dragging the desired along. What went out is always the goal’s own step. While held, every cycle, its velocity and acceleration into the wall are removed per block (translation, rotation), the lead into the wall with them, and what is left is cut to the speed the goal’s step carried, but for a cycle of its acceleration: the generator neither winds up behind a wall nor runs ahead of a goal that follows only in part. The wall is the direction of the motion the pins withhold and the free joints cannot make up, refreshed by every push and dropped 20 cycles after the last. On a velocity pin alone each block restarts at the fraction of its step that went out, which cap_scale reports (exactly 1 on every other cycle). The removal is one-sided, so a reversed target leaves the generator as fast as a start from rest. A stop() held at a wall the target lies beyond counts as landed once the goal is within Settle::tolerance of the target on every other axis and has come no tolerance closer to it for the time a generator needs from rest to cover two of them (0.1 s at the default budget). On the joint interface the step is scaled as a whole into the box, and a joint target, like a posture, is refused inside max(JOINT_LIMIT_INSET, m), so a joint goal always reaches its target. The one goal not taken through the box is the joint hold: it is the leashed measured anchor, which lies between the frozen goal and the measured q and so is never further out than the arm.

At the measured arm. With b = m − POSITION_FADE_BAND (0.03 rad), the law’s torque toward a limit fades out linearly as the measured joint closes from m to b; torque away from it is never scaled. Inside b a spring of POSITION_BARRIER_STIFFNESS × torque_limits_i per rad (1075 Nm/rad at 86 Nm, 144 at 11.5) pushes the joint out, at most its clamp, ramped in over the loop’s first 500 cycles so an arm started inside eases out. The velocity barrier’s onset toward the limit drops to velocity_barrier_fraction × E(s − b) and the velocity fade’s cap to f × E(s − m), both at most the arm’s limit there. A 0.02 rad overshoot of the margin reaches b; 10 mrad from the limit the spring gives 21.5 Nm on joints 1 to 4 and 2.9 Nm on the wrist. tau_position is the spring less the faded torque, so tau = clamp(law + tau_envelope + tau_position).

The two fades multiply, and inside the margin the velocity fade has no band of its own: its cap is f × E(s − m), which is 0 at and past the margin, so it starts at rest and runs to the onset rather than over FADE_BAND of the limit. On the FER, whose limit is flat, that is the narrower band, so what bounds the gain there is this position fade taking the same push to nothing over the same rad. Both vanish linearly at b, so the product stays finite; it rises across the margin and peaks at m, at |law| / (velocity_barrier_fraction × E(POSITION_FADE_BAND)). At the 86 Nm clamp and the default fraction that is 429 Nm s/rad on joint 2 and 357 on joint 4, 1.61 and 1.33 times the |law| / (FADE_BAND × limit) the band alone would give (266 and 268) (tests/torque_position.rs); across the arm the factor runs 1.16 to 1.61, and the cycle’s lookahead adds about another 1 %. On the FR3 the limit has itself come down by the margin, so the velocity band is the wider of the two there and FADE_BAND does bound the gain.

The rotation weight. When several joints are pinned at their bounds, the pose may be unreachable and the IK must trade position against orientation. The default rotation_weight is 0.1 m/rad: 1 mm of position error has the same weight as 10 mrad of orientation error. The weight is fixed rather than derived from cartesian_stiffness, which may be zero in rotation.

Position first at a wall. While the goal is stalled, each iteration first solves translation within the joint bounds, then solves the full task with translation weighted 30 times more strongly than orientation. Orientation follows as far as the remaining freedom allows. The solve can settle at a local optimum. The switch eases in and out over 0.5 s so the goal velocity has no step; away from a wall the weighted solve is unchanged. Under a speed limit, translation takes priority within the velocity bounds too.

constantvaluewhy
ImpedanceOptions::joint_position_margin0.05 rad, in [0.035, 0.5]an overshoot of 0.02, the FR3 envelope’s zero up to 12 mrad in, and the fade band
IkOptions::rotation_weight0.1 m/rad, in (0, 1]the trade above; 1 weighs 1 m as 1 rad
POSITION_FADE_BAND0.02 radb ≥ 0.015 keeps E(s − b) under the arm’s velocity limit near the limit
POSITION_BARRIER_STIFFNESS12.5 /rad × torque clampstable on the wrist behind the 1 ms delay and the low-pass
active-set passes12bounds the work per solve; position first solves twice per iteration
stall on / quiet cycles2e-5 weighted m per cycle / 20 of the last 40above the damped solve’s lag; the flag holds while a pin pushes at all, and a pin brushed now and then still clears it
posture distance0.3 rad beyond the marginwhere the posture is clamped and its pull starts to fade
spring ramp500 cycles from the loop’s startan arm started inside b eases out

Recorded. CartesianSent and JointSent carry pinned and tau_position, CartesianSent also stall_pressure, stalled and ik_passes; ik_error is the weighted norm. CartesianSent reports the solve itself too: ik_step and ik_step_clipped are the largest raw step norm of the cycle’s solves and how much of it the box took off, ik_blend is the position-first blend, and held with wall_age say whether a wall was standing and how long since it was last pushed on. The flight recorder’s entities are on Record and replay a run.

Limits. Self-collision is not modelled; a joint position margin does not prevent it. The spring is sized for an overshoot of the margin by 0.02 rad; a push that drives a joint further meets at most its torque clamp, and the robot’s own limit reflex remains the last stop. The guard is exercised offline, on the model with a simulated plant (tests/position.rs, tests/active_set.rs, tests/ik/, tests/torque_position/), not yet on a robot.

Near full stretch: a self-motion the law cannot see

Near a singular configuration, the joints can move substantially while the tool’s position barely changes. Cartesian position gains cannot damp motion in that direction; joint gains and, where orientation changes, rotational gains remain relevant.

Oscillation near full stretch is an unresolved limitation. The initiating mechanism is not yet identified, and no specific remedy is implemented. A small end-effector error does not establish that joint motion is well controlled. Use the recorded joint states, IK diagnostics and commanded orientation when investigating this behavior.

Anchoring without an echo: the leash

In the robot-controller backend the third rule of the generator re-anchors the plan every cycle on the robot’s echo of the last command (O_T_EE_c, q_d), so that the backstop behind the generator can shape a command but never accumulate a lag. There is no echo of a torque command: the robot reports tau_J_d, not a pose or joint goal. Neither extreme works as a substitute. Anchoring on the measured pose would fold the spring’s deflection into the plan every cycle, so that a load would stall the plan short of the target and a push on the arm would move it, since a compliant arm is meant to sit off its setpoint under a force. Anchoring on the generator’s own last output runs it open loop: held back by a hand, an obstacle or an unreachable pose, the arm falls arbitrarily far behind a plan that goes on without it, the spring force grows with the distance until a collision reflex ends the session, and on release the arm springs to wherever the plan has got to.

The Leash is the middle: each cycle the anchor is the measured pose (the model’s, for the measured q, the frame the desired pose and the IK live in) pulled toward the previous desired pose by at most leash.translation (0.025 m) and leash.rotation (0.15 rad),

e_t = p_des − p_meas        e_r = log(R_des R_measᵀ)
s   = min(1, leash.translation / |e_t|, leash.rotation / |e_r|)
anchor = ( p_meas + s e_t,  exp(s e_r) R_meas )

and on the joint interface each joint’s goal clamped to within leash.joint (0.1 rad) of the measured joint, the feedforward velocity being the finite difference of that leashed goal. While the arm follows, s = 1 and the anchor is the previous desired, so the generator runs from its own output and its budget is the whole budget, as before. Held back, the desired stays within the leash of the arm, so the spring force on whoever holds it is limited by the leashed position error. This is not a limit on total reaction force, which also includes damping. On the joint interface it is the torque clamp, not the leash, that bounds the torque: the JOINT preset’s 600 Nm/rad × 0.1 rad is 60 Nm on joints 1 to 4, under their 86 Nm clamp but far over the 20 Nm joint threshold the examples set, which such a joint reaches at 0.033 rad of error; on the wrist (250 / 150 / 50 Nm/rad) joints 5 and 6 meet the 11.5 Nm clamp before the leash does. Target control sets no collision thresholds; see Collision thresholds. When the arm is let go the generator resumes from where the arm is, under its budget. The leash keeps acting during the stop’s hold, so an arm held during stop() is not pulled harder either. A deflection under load smaller than the leash costs nothing, and the from-start deviation guard is unchanged. What the leash took off is reported to the observer as leash_alteration (m, or rad on the joint interface) and leash_angular_alteration (rad), zero in normal tracking. The backstop role otherwise passes to the torque clamp, the filter and the torque-rate limiter, which shape the torque and leave the setpoint alone.

The handover at start and stop

At the start, the desired pose is the model’s pose of the measured configuration and q_goal = q. The IK therefore starts with zero residual. The first command is −Kd dq + coriolis(q, dq).

At stop(), the generator lands on the last target and holds the setpoint for Settle::cycles (250). It sets motion_finished once every joint moves slower than REST_JOINT_VELOCITY (0.01 rad/s), or after STOP_TIMEOUT_CYCLES (5 s) more cycles. The impedance law continues to act on the held goal until then. The velocity check allows the arm to settle before control returns to the robot’s controller.

The law itself, impedance_torques, is public at the crate root for a loop of your own.

What a spring does not do

A spring has no integrator. The tracking error at a hold is the arm’s residual force (load or friction) over the stiffness, as it is under the robot’s own impedance controller, so it halves when the stiffness doubles; millimetre placement needs a higher stiffness. And the leash bounds the position error, not the force: a fast push adds the damping term (50 to 90 N s/m times the speed), so there is no dedicated cap on the reaction force, spring and damper together, beyond the torque clamp. The backend is not yet validated on an FR3.

Tuning the law while it runs

A Cartesian impedance session accepts changes to its gains, its inverse kinematics and its budgets while the arm moves; the API, the clamping and how a change crosses into the loop are in Command from a low-rate program. Two of the limits in LiveTuning::BOUNDS are this page’s. The joint damping ceiling is 60 Nm s/rad on joints 1 to 4 and 40 on the wrist, below the reference stack’s 80, because near a joint’s velocity limit the barrier’s 20 Nm per rad/s is added to the law’s own damping and it is the sum the stability bound K × 1 ms / I < 1 applies to (The joint velocity envelope). And each joint damping has a floor of MIN_JOINT_DAMPING_RATIO √K, the ratio 0.25, under every shipped preset’s own D / √K: without it one change reaches a spring of up to 1200 Nm/rad with no damping at all, and the barrier is no help there, since it engages only past velocity_barrier_fraction of the joint’s velocity limit and contributes nothing at working speed.

Compared with the operational-space law

examples/cartesian_impedance_active_control.rs is libfranka’s Cartesian impedance example:

tau = Jᵀ (−K e − D J dq) + coriolis,   e = [ p − p_d ; −R vec(q_ee⁻¹ q_d) ]

with K = diag(150, 150, 150, 10, 10, 10) and D = 2√K. The two are first-order the same in the six task directions, Jᵀ Kx J (q_goal − q) ≈ Jᵀ Kx (x_goal − x), and differ in three ways. The operational-space law measures the error in the task space, orientation through a quaternion, and needs no inverse kinematics; the hybrid law measures it in joint space and needs the joint goal, which is why the Cartesian interface runs the IK above. The operational-space law leaves the nullspace free unless a separate term fills it, as cartesian_impedance_figure_eight does with a nullspace joint spring through a projector; the hybrid law fills it with Kq in the same expression. And a joint goal makes the two interfaces one code path, with a joint target the degenerate case Kx = 0. Both add the model’s Coriolis term and leave gravity to the robot.