Skip to main content

franka/robot/target_control/
ik.rs

1//! Differential inverse kinematics of the Cartesian torque backend: from the previous joint
2//! goal, a few damped-least-squares steps toward the desired pose, a posture bias through the
3//! nullspace, a cap on the step and a clamp inside the joint position limits. The joint goal
4//! so follows the pose stream continuously and, where the pose is unreachable or singular,
5//! lags instead of jumping.
6
7use std::sync::Arc;
8
9use nalgebra::{Matrix3, SMatrix, SVector, Vector3};
10
11use super::rotation::{log, rotation_of, translation_of};
12use crate::error::{FrankaError, FrankaResult};
13use crate::model::{Frame, Model};
14
15/// Options of the Cartesian backend's differential inverse kinematics: every cycle, from the
16/// previous joint goal, up to `iterations` damped-least-squares steps toward the pose, a
17/// posture bias through the nullspace, the step capped at `max_step` and clamped inside the
18/// joint limits.
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct IkOptions {
21    /// `λ` of the damped least squares `Jᵀ (J Jᵀ + λ² I)⁻¹`. Default 0.05.
22    pub damping: f64,
23    /// Gain, 1/s, of the nullspace pull toward the posture; the pull moves no joint faster
24    /// than [`MAX_POSTURE_RATE`] whatever the gain and the distance. Default 1.0.
25    pub nullspace_gain: f64,
26    /// Steps per cycle. Default 3.
27    pub iterations: u32,
28    /// Residual (m plus rad, one norm) below which a cycle stops iterating. Default 1e-6.
29    pub tolerance: f64,
30    /// How far, rad, inside the joint position limits the solution is kept, its start (the
31    /// measured configuration) included. Default 0.02.
32    pub limit_margin: f64,
33    /// The most, rad, any joint of the solution moves in one cycle: a larger step is scaled
34    /// down as a whole, so an unreachable or singular pose is approached at a bounded rate
35    /// instead of jumped at. Default 0.01 (10 rad/s).
36    pub max_step: f64,
37}
38
39/// The most, rad/s, the posture bias moves any joint: the pull `nullspace_gain × distance`
40/// toward a far posture is scaled down as a whole to this rate, so a posture 2 rad away is
41/// approached at 0.5 rad/s, not 2.
42pub const MAX_POSTURE_RATE: f64 = 0.5;
43
44impl Default for IkOptions {
45    fn default() -> Self {
46        IkOptions {
47            damping: 0.05,
48            nullspace_gain: 1.0,
49            iterations: 3,
50            tolerance: 1e-6,
51            limit_margin: 0.02,
52            max_step: 0.01,
53        }
54    }
55}
56
57impl IkOptions {
58    /// # Errors
59    /// [`FrankaError::InvalidArgument`] unless `damping` and `max_step` are finite and
60    /// positive, `nullspace_gain`, `tolerance` and `limit_margin` finite and non-negative, and
61    /// `iterations` at least one.
62    pub fn validate(&self) -> FrankaResult<()> {
63        let positive = |x: f64| x.is_finite() && x > 0.0;
64        let non_negative = |x: f64| x.is_finite() && x >= 0.0;
65        let valid = positive(self.damping)
66            && positive(self.max_step)
67            && non_negative(self.nullspace_gain)
68            && non_negative(self.tolerance)
69            && non_negative(self.limit_margin)
70            && self.iterations > 0;
71        if !valid {
72            return Err(FrankaError::InvalidArgument(format!(
73                "target control: ik needs a finite, positive damping and max_step, finite and \
74                 non-negative nullspace_gain, tolerance and limit_margin, and at least one \
75                 iteration, got {self:?}"
76            )));
77        }
78        Ok(())
79    }
80}
81
82/// See the [module documentation](self).
83pub(super) struct Ik {
84    model: Arc<Model>,
85    options: IkOptions,
86    lower: [f64; 7],
87    upper: [f64; 7],
88    f_t_ee: [f64; 16],
89    ee_t_k: [f64; 16],
90    q: [f64; 7],
91}
92
93impl Ik {
94    pub(super) fn new(
95        model: Arc<Model>,
96        options: IkOptions,
97        limits: ([f64; 7], [f64; 7]),
98        q0: [f64; 7],
99        f_t_ee: [f64; 16],
100        ee_t_k: [f64; 16],
101    ) -> Self {
102        let mut lower = limits.0.map(|l| l + options.limit_margin);
103        let mut upper = limits.1.map(|u| u - options.limit_margin);
104        for i in 0..7 {
105            // A margin wider than half a joint's range pins that joint at the range's middle.
106            if lower[i] > upper[i] {
107                lower[i] = 0.5 * (limits.0[i] + limits.1[i]);
108                upper[i] = lower[i];
109            }
110        }
111        Ik {
112            model,
113            options,
114            lower,
115            upper,
116            f_t_ee,
117            ee_t_k,
118            q: std::array::from_fn(|i| q0[i].clamp(lower[i], upper[i])),
119        }
120    }
121
122    /// One cycle toward `pose` (column-major, as `O_T_EE`); returns the solution and the
123    /// residual pose error norm. The first step always runs and carries the posture bias (a
124    /// rate, integrated over `dt` once per call); the remaining steps refine the pose until
125    /// the residual is below `tolerance`; the whole move is then capped at `max_step` on its
126    /// largest joint.
127    pub(super) fn step(
128        &mut self,
129        pose: &[f64; 16],
130        posture: &[f64; 7],
131        dt: f64,
132    ) -> ([f64; 7], f64) {
133        let p_des = Vector3::from(translation_of(pose));
134        let r_des = rotation_of(pose);
135        let posture = SVector::<f64, 7>::from(*posture);
136        let damping =
137            SMatrix::<f64, 6, 6>::identity() * (self.options.damping * self.options.damping);
138        let from = self.q;
139        let mut residual = 0.0;
140        for iteration in 0..=self.options.iterations {
141            let error = self.error(&p_des, &r_des);
142            residual = error.norm();
143            if iteration == self.options.iterations
144                || (iteration > 0 && residual < self.options.tolerance)
145            {
146                break;
147            }
148            let j = SMatrix::<f64, 6, 7>::from_column_slice(&self.model.zero_jacobian_q(
149                Frame::EndEffector,
150                &self.q,
151                &self.f_t_ee,
152                &self.ee_t_k,
153            ));
154            let Some(inverse) = (j * j.transpose() + damping).try_inverse() else {
155                break;
156            };
157            let pseudo_inverse = j.transpose() * inverse;
158            let q = SVector::<f64, 7>::from(self.q);
159            let mut next = q + pseudo_inverse * error;
160            if iteration == 0 {
161                let nullspace = SMatrix::<f64, 7, 7>::identity() - pseudo_inverse * j;
162                let mut bias = nullspace * (posture - q) * (self.options.nullspace_gain * dt);
163                let (largest, cap) = (bias.amax(), MAX_POSTURE_RATE * dt);
164                if largest > cap {
165                    bias *= cap / largest;
166                }
167                next += bias;
168            }
169            for i in 0..7 {
170                self.q[i] = next[i].clamp(self.lower[i], self.upper[i]);
171            }
172        }
173        let largest = (0..7).fold(0.0f64, |m, i| m.max((self.q[i] - from[i]).abs()));
174        if largest > self.options.max_step {
175            // Scaled as a whole toward `from`, which is within the limits, so it stays there.
176            let scale = self.options.max_step / largest;
177            for (q, from) in self.q.iter_mut().zip(&from) {
178                *q = from + scale * (*q - from);
179            }
180            residual = self.error(&p_des, &r_des).norm();
181        }
182        (self.q, residual)
183    }
184
185    #[cfg(test)]
186    pub(super) fn q(&self) -> [f64; 7] {
187        self.q
188    }
189
190    /// `[p_des - p(q); log(R_des R(q)ᵀ)]`, base frame.
191    fn error(&self, p_des: &Vector3<f64>, r_des: &Matrix3<f64>) -> SVector<f64, 6> {
192        let fk = self
193            .model
194            .pose_q(Frame::EndEffector, &self.q, &self.f_t_ee, &self.ee_t_k);
195        let p = p_des - Vector3::from(translation_of(&fk));
196        let turn = log(&(r_des * rotation_of(&fk).transpose()));
197        SVector::<f64, 6>::new(p[0], p[1], p[2], turn[0], turn[1], turn[2])
198    }
199}