Skip to main content

franka/robot/target_control/ik/
mod.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, the orientation weighed by
3//! [`IkOptions::rotation_weight`], each solved inside the cycle's box ([`active_set`]): the
4//! joint velocity limit, and the braking envelope toward the position limits less the margin.
5//! A posture bias rides in the free joints' nullspace, clamped away from the limits and faded
6//! out near them. The joint goal so follows the pose stream continuously and, where the pose
7//! is unreachable, singular or behind a limit, lags instead of jumping. While a limit stalls the
8//! goal, position comes first, to within micrometres ([`priority`]).
9
10pub(super) mod active_set;
11mod bounds;
12mod priority;
13pub(super) mod stall;
14
15use std::sync::Arc;
16
17use nalgebra::{Matrix3, Vector3};
18
19use super::position::JointLimits;
20use super::rotation::{log, rotation_of, translation_of};
21use crate::error::{FrankaError, FrankaResult};
22use crate::model::{Frame, Model};
23use active_set::{solve, Bias, Jacobian, Joints, Task};
24use bounds::Bounds;
25#[cfg(test)]
26pub(super) use bounds::{AS_RUN_LIMIT_MARGIN, POSTURE_LIMIT_DISTANCE};
27use priority::Priority;
28#[cfg(test)]
29pub(super) use priority::PRIORITY_RAMP_CYCLES;
30use stall::Stall;
31
32/// Options of the Cartesian backend's differential inverse kinematics: every cycle, from the
33/// previous joint goal, up to `iterations` damped-least-squares steps toward the pose inside
34/// the cycle's joint box, with a posture bias through the nullspace.
35#[derive(Debug, Clone, Copy, PartialEq)]
36pub struct IkOptions {
37    /// `λ` of the damped least squares `J̃ᵀ (J̃ J̃ᵀ + λ² I)⁻¹` on the weighted Jacobian
38    /// `J̃ = W J`, its rotation rows scaled by `rotation_weight`. Default 0.05.
39    pub damping: f64,
40    /// Gain, 1/s, of the nullspace pull toward the posture; the pull moves no joint faster
41    /// than [`MAX_POSTURE_RATE`] whatever the gain and the distance. Default 1.0.
42    pub nullspace_gain: f64,
43    /// Steps per cycle. Default 3.
44    pub iterations: u32,
45    /// Weighted residual below which a cycle stops iterating. Default 1e-6.
46    pub tolerance: f64,
47    /// m per rad: the orientation error's weight against the position error's in the task, the
48    /// residual and `tolerance`. At 0.1, 1 mm costs as much as 10 mrad, so a pose the joints
49    /// cannot reach gives up orientation before position; while a joint limit stalls the goal,
50    /// position comes first, to within micrometres. Within (0, 1]. Default 0.1.
51    pub rotation_weight: f64,
52}
53
54/// The most, rad/s, the posture bias moves any joint: the pull `nullspace_gain × distance`
55/// toward a far posture is scaled down as a whole to this rate, so a posture 2 rad away is
56/// approached at 0.5 rad/s, not 2.
57pub const MAX_POSTURE_RATE: f64 = 0.5;
58
59impl Default for IkOptions {
60    fn default() -> Self {
61        IkOptions {
62            damping: 0.05,
63            nullspace_gain: 1.0,
64            iterations: 3,
65            tolerance: 1e-6,
66            rotation_weight: 0.1,
67        }
68    }
69}
70
71impl IkOptions {
72    /// # Errors
73    /// [`FrankaError::InvalidArgument`] unless it has a finite, positive damping, finite and
74    /// non-negative nullspace_gain and tolerance, a rotation_weight within (0, 1], and at least
75    /// one iteration.
76    pub fn validate(&self) -> FrankaResult<()> {
77        let non_negative = |x: f64| x.is_finite() && x >= 0.0;
78        let valid = self.damping.is_finite()
79            && self.damping > 0.0
80            && non_negative(self.nullspace_gain)
81            && non_negative(self.tolerance)
82            && self.rotation_weight > 0.0
83            && self.rotation_weight <= 1.0
84            && self.iterations > 0;
85        if !valid {
86            return Err(FrankaError::InvalidArgument(format!(
87                "target control: ik needs a finite, positive damping, finite and non-negative \
88                 nullspace_gain and tolerance, a rotation_weight within (0, 1], and at least \
89                 one iteration, got {self:?}"
90            )));
91        }
92        Ok(())
93    }
94}
95
96/// See the [module documentation](self).
97pub(super) struct Ik {
98    model: Arc<Model>,
99    options: IkOptions,
100    bounds: Bounds,
101    f_t_ee: [f64; 16],
102    ee_t_k: [f64; 16],
103    q: [f64; 7],
104    /// The weighted Jacobian at `q`, the next cycle's first.
105    jacobian: Jacobian,
106    act: [i8; 7],
107    /// The position stage's active set, warm across its solves.
108    position_act: [i8; 7],
109    priority: Priority,
110    pinned: [i8; 7],
111    stall: Stall,
112    passes: u32,
113    /// The largest raw step norm, rad, over the cycle's solves, before its box clip, and what
114    /// that same solve's clip took off. The two are one solve's pair, not two maxima.
115    step_norm: f64,
116    clipped_norm: f64,
117    #[cfg(test)]
118    fallbacks: u64,
119    cap_scale: f64,
120}
121
122impl Ik {
123    pub(super) fn new(
124        model: Arc<Model>,
125        options: IkOptions,
126        limits: JointLimits,
127        q0: [f64; 7],
128        f_t_ee: [f64; 16],
129        ee_t_k: [f64; 16],
130    ) -> Self {
131        let bounds = Bounds::Guarded(limits);
132        Ik::with_bounds(model, options, bounds, q0, f_t_ee, ee_t_k)
133    }
134
135    /// The solver as it ran before the guard: the start clamped into `limits` inset by
136    /// [`AS_RUN_LIMIT_MARGIN`], each step clipped there, the whole move scaled under
137    /// `max_velocity` (rad/s), the orientation unweighted, the raw posture, no pins, no stall.
138    #[cfg(test)]
139    pub(super) fn as_run(
140        model: Arc<Model>,
141        options: IkOptions,
142        limits: ([f64; 7], [f64; 7]),
143        max_velocity: [f64; 7],
144        q0: [f64; 7],
145        f_t_ee: [f64; 16],
146        ee_t_k: [f64; 16],
147    ) -> Self {
148        let (bounds, q0) = Bounds::as_run(limits, max_velocity, q0);
149        let options = IkOptions {
150            rotation_weight: 1.0,
151            ..options
152        };
153        Ik::with_bounds(model, options, bounds, q0, f_t_ee, ee_t_k)
154    }
155
156    fn with_bounds(
157        model: Arc<Model>,
158        options: IkOptions,
159        bounds: Bounds,
160        q: [f64; 7],
161        f_t_ee: [f64; 16],
162        ee_t_k: [f64; 16],
163    ) -> Self {
164        let mut ik = Ik {
165            model,
166            options,
167            bounds,
168            f_t_ee,
169            ee_t_k,
170            q,
171            jacobian: Jacobian::zeros(),
172            act: [0; 7],
173            position_act: [0; 7],
174            priority: Priority::default(),
175            pinned: [0; 7],
176            stall: Stall::default(),
177            passes: 0,
178            step_norm: 0.0,
179            clipped_norm: 0.0,
180            #[cfg(test)]
181            fallbacks: 0,
182            cap_scale: 1.0,
183        };
184        ik.jacobian = ik.jacobian_at(&q);
185        ik
186    }
187
188    /// The two options a running session may move; see
189    /// [`LiveTuning`](super::tuning::LiveTuning). Nothing else is touched: `q`, the active sets
190    /// and the Jacobian are the goal this solver is holding, not how it finds one, and the box
191    /// the next step is bounded in is derived from `q` on the cycle it is used.
192    pub(super) fn retune(&mut self, damping: f64, nullspace_gain: f64) {
193        self.options.damping = damping;
194        self.options.nullspace_gain = nullspace_gain;
195    }
196
197    /// The `λ` this solver is running, which the torque law's nullspace projector has to agree
198    /// with cycle by cycle.
199    pub(super) fn damping(&self) -> f64 {
200        self.options.damping
201    }
202
203    /// All of the options it is running, for a test that checks what a retune reached.
204    #[cfg(test)]
205    pub(super) fn options(&self) -> IkOptions {
206        self.options
207    }
208
209    /// One cycle toward `pose` (column-major, as `O_T_EE`); returns the solution and its
210    /// weighted residual. The first step always runs and carries the posture bias (a rate,
211    /// integrated over `dt` once per call); the remaining steps refine the pose until the
212    /// residual is below `tolerance`. A non-finite solve keeps the previous goal.
213    pub(super) fn step(
214        &mut self,
215        pose: &[f64; 16],
216        posture: &[f64; 7],
217        dt: f64,
218    ) -> ([f64; 7], f64) {
219        let target = (Vector3::from(translation_of(pose)), rotation_of(pose));
220        let from = Joints::from(self.q);
221        let gain = self.options.nullspace_gain * dt;
222        let cycle = self.bounds.cycle(&self.q, posture, gain, dt);
223        self.priority.update(&self.stall);
224        let saved_act = (self.act, self.position_act);
225        let (mut q, mut j) = (from, self.jacobian);
226        let mut error = None;
227        self.passes = 0;
228        (self.step_norm, self.clipped_norm) = (0.0, 0.0);
229        for iteration in 0..self.options.iterations {
230            let e = self.error(&q, &target);
231            if iteration > 0 && e.norm() < self.options.tolerance {
232                error = Some(e);
233                break;
234            }
235            if iteration > 0 {
236                j = self.jacobian_at(&q.into());
237            }
238            let lo = Joints::from(cycle.bounds.lower) - (q - from);
239            let hi = Joints::from(cycle.bounds.upper) - (q - from);
240            let bias = (iteration == 0).then_some(Bias {
241                rate: cycle.posture,
242                cap: MAX_POSTURE_RATE * dt,
243            });
244            match self.prioritised(&j, &e, (&lo, &hi), bias, cycle.passes) {
245                Some(d) => q += d,
246                None => {
247                    // The step is undone whole: goal, Jacobian, both active sets and the
248                    // passes and step norms they cost, none of which describe the goal that
249                    // is kept.
250                    (q, j) = (from, self.jacobian);
251                    (self.act, self.position_act) = saved_act;
252                    self.passes = 0;
253                    (self.step_norm, self.clipped_norm) = (0.0, 0.0);
254                    #[cfg(test)]
255                    {
256                        self.fallbacks += 1;
257                    }
258                    error = Some(self.error(&q, &target));
259                    break;
260                }
261            }
262        }
263        let mut goal: [f64; 7] = q.into();
264        self.cap_scale = self.bounds.backstop(&self.q, &mut goal, &cycle);
265        let e = match error {
266            Some(e) if self.cap_scale == 1.0 => e,
267            _ => self.error(&goal.into(), &target),
268        };
269        if goal != self.q {
270            j = self.jacobian_at(&goal);
271        }
272        (self.q, self.jacobian) = (goal, j);
273        self.pinned = std::array::from_fn(|i| cycle.bounds.pin(i, self.act[i]));
274        self.stall
275            .update(&j, &e, &self.pinned, self.options.damping);
276        (goal, e.norm())
277    }
278
279    /// One iteration's step on `j`, `e` in `lo..hi`: the weighted solve, blended toward position
280    /// first by [`Priority`]; `None` when a solve is not finite.
281    fn prioritised(
282        &mut self,
283        j: &Jacobian,
284        e: &Task,
285        (lo, hi): (&Joints, &Joints),
286        bias: Option<Bias>,
287        passes: u32,
288    ) -> Option<Joints> {
289        let damping = self.options.damping;
290        let mut run = |j: &Jacobian, e: &Task, lo: &Joints, hi: &Joints, bias, act: &mut _| {
291            let solution = solve(j, e, (lo, hi), bias, act, damping, passes)?;
292            self.passes += solution.passes;
293            if solution.step > self.step_norm {
294                (self.step_norm, self.clipped_norm) = (solution.step, solution.clipped);
295            }
296            #[cfg(test)]
297            {
298                self.fallbacks += u64::from(!solution.converged);
299            }
300            Some(solution.d)
301        };
302        if self.priority.blend == 0.0 {
303            let d = run(j, e, lo, hi, bias, &mut self.act);
304            // The position stage starts from the weighted solve's pins when the blend sets in.
305            self.position_act = self.act;
306            return d;
307        }
308        let (jp, ep) = Priority::position(j, e);
309        let shift = run(&jp, &ep, lo, hi, None, &mut self.position_act)? * self.priority.blend;
310        let (jr, er) = self.priority.held(j, e, &shift);
311        let d = run(&jr, &er, &(lo - shift), &(hi - shift), bias, &mut self.act)?;
312        Some(d + shift)
313    }
314
315    /// The pins of the last step per joint: 0 free, ∓1 on the lower / upper position bound,
316    /// ∓2 on the velocity bound.
317    pub(super) fn pinned(&self) -> [i8; 7] {
318        self.pinned
319    }
320
321    /// The stall flag and the pins' pressures after the last step.
322    pub(super) fn stall(&self) -> Stall {
323        self.stall
324    }
325
326    /// Active-set passes of the last step, over its iterations.
327    pub(super) fn passes(&self) -> u32 {
328        self.passes
329    }
330
331    /// The last step's largest raw solve, rad: its norm before the box clip, and what the clip
332    /// took off it. While the blend is on, each iteration solves twice and the position stage's
333    /// norm is its own, before the blend scales it down.
334    pub(super) fn step_norms(&self) -> (f64, f64) {
335        (self.step_norm, self.clipped_norm)
336    }
337
338    /// Solves that were not finite or did not converge, since the start.
339    #[cfg(test)]
340    pub(super) fn fallbacks(&self) -> u64 {
341        self.fallbacks
342    }
343
344    /// The blend toward position first of the last step: 0 the weighted solve, 1 position first.
345    pub(super) fn priority(&self) -> f64 {
346        self.priority.blend
347    }
348
349    /// Sets the cycles the blend takes, [`priority::PRIORITY_RAMP_CYCLES`] by default; 0 holds
350    /// it at the weighted solve.
351    #[cfg(test)]
352    pub(super) fn set_priority_ramp(&mut self, cycles: u32) {
353        self.priority.set_ramp(cycles);
354    }
355
356    /// The scale the backstop cut the last [`step`](Self::step) by; 1 when it did not.
357    pub(super) fn cap_scale(&self) -> f64 {
358        self.cap_scale
359    }
360
361    /// Takes the end effector and stiffness frames, as `Robot::set_ee` and `set_k` may change them
362    /// during a session; the `F_T_EE` it had when they changed.
363    pub(super) fn set_frames(
364        &mut self,
365        f_t_ee: &[f64; 16],
366        ee_t_k: &[f64; 16],
367    ) -> Option<[f64; 16]> {
368        let bits = |t: &[f64; 16]| t.map(f64::to_bits);
369        if bits(&self.f_t_ee) == bits(f_t_ee) && bits(&self.ee_t_k) == bits(ee_t_k) {
370            return None;
371        }
372        let old = std::mem::replace(&mut self.f_t_ee, *f_t_ee);
373        self.ee_t_k = *ee_t_k;
374        self.jacobian = self.jacobian_at(&self.q);
375        Some(old)
376    }
377
378    /// The end effector's pose at `q`, column-major.
379    pub(super) fn pose_of(&self, q: &[f64; 7]) -> [f64; 16] {
380        self.model
381            .pose_q(Frame::EndEffector, q, &self.f_t_ee, &self.ee_t_k)
382    }
383
384    /// The solution of the last step, where the next one starts.
385    pub(super) fn q(&self) -> [f64; 7] {
386        self.q
387    }
388
389    /// `W J`, base frame.
390    fn jacobian_at(&self, q: &[f64; 7]) -> Jacobian {
391        let mut j = Jacobian::from_column_slice(&self.model.zero_jacobian_q(
392            Frame::EndEffector,
393            q,
394            &self.f_t_ee,
395            &self.ee_t_k,
396        ));
397        j.fixed_rows_mut::<3>(3)
398            .scale_mut(self.options.rotation_weight);
399        j
400    }
401
402    /// `W [p_des - p(q); log(R_des R(q)ᵀ)]`, base frame.
403    fn error(&self, q: &Joints, (p_des, r_des): &(Vector3<f64>, Matrix3<f64>)) -> Task {
404        let fk = self.pose_of(&(*q).into());
405        let p = p_des - Vector3::from(translation_of(&fk));
406        let turn =
407            log(&(r_des * rotation_of(&fk).transpose())).map(|x| x * self.options.rotation_weight);
408        Task::new(p[0], p[1], p[2], turn[0], turn[1], turn[2])
409    }
410}