Skip to main content

franka/
otg.rs

1//! Online trajectory generation: a smooth 1 kHz command from a stream of stepped targets.
2//!
3//! [`Otg`] turns a target position that may change at any time -- every cycle, in bursts, or
4//! not for seconds -- into a per-cycle position command whose velocity, acceleration and jerk
5//! never exceed the limits it was built with, whose acceleration is continuous (the trajectory
6//! is C2), and which reaches the target and then stays there exactly. It is the causal
7//! alternative to a spline, which needs future knots a low-rate commander cannot supply, and to
8//! a low-pass filter, whose peak speed grows with the size of the step. [`MultiOtg`] runs one
9//! per axis, optionally synchronised; [`CartesianOtg`] is the three-axis one. No allocation.
10//!
11//! # Algorithm
12//! Every call to [`Otg::step`] re-plans from the current state `(p, v, a)` to `(target, 0, 0)`
13//! and then follows that plan for one cycle. The plan is the classical seven-segment,
14//! jerk-limited profile: a time-optimal *velocity transfer* from `(v, a)` to a peak velocity
15//! `v_p` with zero acceleration (jerk `+j_max` up to a peak acceleration, hold it, jerk
16//! `-j_max` back to zero -- three segments with closed-form durations), a cruise at `v_p`, and
17//! the mirror transfer from `v_p` to rest. The only free parameter is `v_p`: it is `±v_max`
18//! with a cruise when the target is far enough, and otherwise a root of
19//! `f(v_p) = target - p`, where `f` is the displacement of the two transfers. `f` is
20//! increasing in `v_p` except for one hump next to `v_rd = v + a|a| / (2 j_max)`, the velocity
21//! reached by ramping the acceleration straight to zero (an unsaturated reversal there costs
22//! `2 sqrt(d / j_max)` of extra time at about `v_rd` for a change `d` of peak velocity), so
23//! the domain is split at `v_rd` and `v_rd ± a_max² / j_max`, every piece whose ends bracket
24//! the target is bisected, and the shortest plan found wins. A target closer than the braking
25//! distance gives a root of the opposite sign: the profile passes the target, stops and comes
26//! back, all within the limits and without a jerk spike. This is the profile structure of
27//! Haschke, Weitnauer and Ritter, *On-line planning of time-optimal, jerk-limited
28//! trajectories* (IROS 2008), evaluated one cycle at a time the way Ruckig (Berscheid and
29//! Kröger, RSS 2021) does, whose four jerk patterns are the pieces above. The acceleration
30//! along the plan is piecewise linear with slope `±j_max` or 0, so the trajectory is C2 and a
31//! finite difference of the acceleration over any cycle length, including a 2 ms cycle after a
32//! lost packet, stays within `j_max`; the velocity transfer keeps `v_rd` within `±v_max`,
33//! which is what makes the velocity bound hold through the transient of every re-plan.
34//!
35//! The target velocity is always zero: the generator is for positional targets from a planner,
36//! a vision loop or a hand, not for velocity tracking. The synchronisation in [`MultiOtg`]
37//! stretches the faster axes to the slowest one's duration by lowering their peak velocity (a
38//! second bisection, on the scale of `v_p`); an axis whose profile has no peak to lower (one
39//! that is exactly braking to its target) keeps its minimum duration.
40//!
41//! # In a control loop
42//! Three rules, learnt from a run on a real FER in which the first version of the bridge in
43//! `examples/nonrealtime_commander.rs` was clamped by the rate limiter behind it and then
44//! orbited at the velocity cap for twenty seconds. The limits are **per axis**: two axes at
45//! full acceleration have a vector norm `sqrt 2` above it, so a budget that is a norm (which is
46//! what `limit_rate_cartesian_pose` bounds) needs [`OtgLimits::per_axis_for_norm`]. Step **one
47//! nominal cycle per command** (`DELTA_T`), not the measured period: the robot and the rate
48//! limiter check every packet against a 1 ms budget, so a 2 ms step after a lost packet is a
49//! doubled velocity to them. And **re-anchor on the robot's echo of the position** every
50//! cycle with [`Otg::set_position`] (`O_T_EE_c`; not its twist, see [`Otg::set_state`]), so
51//! that if anything behind the generator does alter a command, the next plan starts from
52//! what was actually sent; a rate limiter that tracks a pose it has fallen behind has no
53//! braking logic and never catches up.
54//!
55//! ```
56//! use franka::otg::{Otg, OtgLimits};
57//! let limits = OtgLimits { max_velocity: 0.3, max_acceleration: 0.5, max_jerk: 20.0 };
58//! let mut otg = Otg::new(0.0, limits).unwrap();
59//! otg.set_target(0.05).unwrap();
60//! let mut t = 0.0f64;
61//! while otg.position() != 0.05 {
62//!     otg.step(0.001);
63//!     t += 0.001;
64//! }
65//! assert!((t - 0.658).abs() < 0.01, "a 5 cm S-curve under these limits takes 0.658 s");
66//! ```
67
68use crate::error::{FrankaError, FrankaResult};
69
70/// The per-axis limits of an [`Otg`]: all three must be finite and positive.
71#[derive(Debug, Clone, Copy, PartialEq)]
72pub struct OtgLimits {
73    /// Maximum absolute velocity, in the position unit per second.
74    pub max_velocity: f64,
75    /// Maximum absolute acceleration, per second squared.
76    pub max_acceleration: f64,
77    /// Maximum absolute jerk, per second cubed.
78    pub max_jerk: f64,
79}
80
81impl OtgLimits {
82    /// Per-axis limits whose vector norm over `axes` axes never exceeds `self`: every limit
83    /// divided by `sqrt(axes)`. What to build a [`MultiOtg`] with when the budget is a norm.
84    pub fn per_axis_for_norm(self, axes: usize) -> Self {
85        self.scaled(1.0 / (axes.max(1) as f64).sqrt())
86    }
87
88    /// Every limit multiplied by `factor`.
89    pub fn scaled(self, factor: f64) -> Self {
90        Self {
91            max_velocity: self.max_velocity * factor,
92            max_acceleration: self.max_acceleration * factor,
93            max_jerk: self.max_jerk * factor,
94        }
95    }
96}
97
98/// One constant-jerk piece of a plan.
99#[derive(Debug, Clone, Copy, Default)]
100struct Segment {
101    jerk: f64,
102    duration: f64,
103}
104
105/// A seven-segment plan from the current state to rest at the target: the transfer to the
106/// peak velocity (three segments), the cruise, and the transfer to rest (three segments).
107#[derive(Debug, Clone, Copy, Default)]
108struct Plan {
109    segments: [Segment; 7],
110    peak_velocity: f64,
111}
112
113impl Plan {
114    fn duration(&self) -> f64 {
115        self.segments.iter().map(|s| s.duration).sum()
116    }
117}
118
119/// Advances `(p, v, a)` by `t` seconds of constant `jerk`.
120fn integrate(state: &mut (f64, f64, f64), jerk: f64, t: f64) {
121    let (p, v, a) = *state;
122    state.0 = p + v * t + a * t * t / 2.0 + jerk * t * t * t / 6.0;
123    state.1 = v + a * t + jerk * t * t / 2.0;
124    state.2 = a + jerk * t;
125}
126
127/// The time-optimal transfer from velocity `v` with acceleration `a` to velocity `v_goal`
128/// with zero acceleration under `a_max` and `j_max`, as three constant-jerk segments.
129fn transfer(v: f64, a: f64, v_goal: f64, a_max: f64, j_max: f64) -> [Segment; 3] {
130    let dv = v_goal - v;
131    // Ramping the acceleration straight to zero changes the velocity by a|a| / (2 j_max); if
132    // that alone passes the goal, the transfer has to swing the acceleration the other way.
133    let s = 1.0f64.copysign(dv - a * a.abs() / (2.0 * j_max));
134    // In the frame where the transfer accelerates, dv >= a0|a0| / (2 j_max) holds.
135    let (dv, a0) = (s * dv, s * a);
136    let a_peak = (j_max * dv + 0.5 * a0 * a0).max(0.0).sqrt().min(a_max);
137    let t1 = ((a_peak - a0) / j_max).max(0.0);
138    let t3 = a_peak / j_max;
139    let t2 = if a_peak > 0.0 {
140        ((dv - (2.0 * a_peak * a_peak - a0 * a0) / (2.0 * j_max)) / a_peak).max(0.0)
141    } else {
142        0.0
143    };
144    [(s * j_max, t1), (0.0, t2), (-s * j_max, t3)]
145        .map(|(jerk, duration)| Segment { jerk, duration })
146}
147
148/// A single-axis online trajectory generator; see the [module documentation](self).
149#[derive(Debug, Clone, Copy)]
150pub struct Otg {
151    limits: OtgLimits,
152    position: f64,
153    velocity: f64,
154    acceleration: f64,
155    target: f64,
156}
157
158impl Otg {
159    /// A generator at rest at `position`, whose target is `position`.
160    ///
161    /// # Errors
162    /// [`FrankaError::InvalidArgument`] if `position` is not finite or a limit is not finite
163    /// and positive.
164    pub fn new(position: f64, limits: OtgLimits) -> FrankaResult<Self> {
165        let ok = |x: f64| x.is_finite() && x > 0.0;
166        let valid = ok(limits.max_velocity) && ok(limits.max_acceleration) && ok(limits.max_jerk);
167        if !(valid && position.is_finite()) {
168            return Err(FrankaError::InvalidArgument(format!(
169                "otg: limits must be finite and positive and the position finite, got \
170                 {limits:?} at {position}"
171            )));
172        }
173        Ok(Self {
174            limits,
175            position,
176            velocity: 0.0,
177            acceleration: 0.0,
178            target: position,
179        })
180    }
181
182    /// Sets the target position; the next [`step`](Self::step) re-plans towards it.
183    ///
184    /// # Errors
185    /// [`FrankaError::InvalidArgument`], with the target unchanged, if `target` is not finite.
186    pub fn set_target(&mut self, target: f64) -> FrankaResult<()> {
187        if !target.is_finite() {
188            return Err(FrankaError::InvalidArgument(format!(
189                "otg: target must be finite, got {target}"
190            )));
191        }
192        self.target = target;
193        Ok(())
194    }
195
196    /// Re-anchors the generator on the position actually commanded -- the robot's echo of the
197    /// last command -- keeping its own velocity, acceleration and target, so that whatever
198    /// runs behind it can shape one command but never build up a lag it plans against.
199    ///
200    /// # Errors
201    /// [`FrankaError::InvalidArgument`], with the state unchanged, if `position` is not finite.
202    pub fn set_position(&mut self, position: f64) -> FrankaResult<()> {
203        self.set_state(position, self.velocity, self.acceleration)
204    }
205
206    /// Sets the whole state; the target is kept. The velocity and acceleration are clamped
207    /// into the limits. They must be the state at the *end* of the last cycle, as this
208    /// generator's are: a finite difference such as the robot's `O_dP_EE_c` is the *mean* over
209    /// the cycle, half an acceleration step behind, and re-anchoring on that every cycle
210    /// throttles the plan to a crawl -- use [`set_position`](Self::set_position) with an echo.
211    ///
212    /// # Errors
213    /// [`FrankaError::InvalidArgument`], with the state unchanged, if a value is not finite.
214    pub fn set_state(
215        &mut self,
216        position: f64,
217        velocity: f64,
218        acceleration: f64,
219    ) -> FrankaResult<()> {
220        if !(position.is_finite() && velocity.is_finite() && acceleration.is_finite()) {
221            return Err(FrankaError::InvalidArgument(format!(
222                "otg: state must be finite, got {position}, {velocity}, {acceleration}"
223            )));
224        }
225        let (v_max, a_max) = (self.limits.max_velocity, self.limits.max_acceleration);
226        self.position = position;
227        self.velocity = velocity.clamp(-v_max, v_max);
228        self.acceleration = acceleration.clamp(-a_max, a_max);
229        Ok(())
230    }
231
232    /// Puts the generator at rest at `position`, with the target there too.
233    pub fn reset(&mut self, position: f64) {
234        (self.position, self.velocity, self.acceleration) = (position, 0.0, 0.0);
235        self.target = position;
236    }
237
238    /// Advances the state by `dt` seconds along the re-planned profile and returns the new
239    /// position. A `dt` that is not positive and finite leaves the state untouched.
240    pub fn step(&mut self, dt: f64) -> f64 {
241        self.follow(&self.fastest(), dt)
242    }
243
244    /// The current position.
245    pub fn position(&self) -> f64 {
246        self.position
247    }
248
249    /// The current velocity.
250    pub fn velocity(&self) -> f64 {
251        self.velocity
252    }
253
254    /// The current acceleration.
255    pub fn acceleration(&self) -> f64 {
256        self.acceleration
257    }
258
259    /// The current target.
260    pub fn target(&self) -> f64 {
261        self.target
262    }
263
264    /// The duration of the time-optimal profile from the current state to rest at the target;
265    /// zero when at rest there.
266    pub fn duration(&self) -> f64 {
267        self.fastest().duration()
268    }
269
270    /// The plan through `peak_velocity`, and the displacement its two transfers leave for the
271    /// cruise (negative if they alone pass the target, in which case the cruise is empty).
272    fn plan(&self, peak_velocity: f64) -> (Plan, f64) {
273        let (a_max, j_max) = (self.limits.max_acceleration, self.limits.max_jerk);
274        let (v, a) = (self.velocity, self.acceleration);
275        let mut plan = Plan {
276            peak_velocity,
277            ..Plan::default()
278        };
279        plan.segments[..3].copy_from_slice(&transfer(v, a, peak_velocity, a_max, j_max));
280        plan.segments[4..].copy_from_slice(&transfer(peak_velocity, 0.0, 0.0, a_max, j_max));
281        let mut state = (self.position, v, a);
282        for segment in &plan.segments {
283            integrate(&mut state, segment.jerk, segment.duration);
284        }
285        // Below a nanometre the residual is the integration's rounding, and divided by a peak
286        // velocity near zero it would become a cruise of milliseconds; the snap at the end of
287        // the plan absorbs it instead.
288        let residual = self.target - state.0;
289        if peak_velocity != 0.0 && residual.abs() >= 1e-9 {
290            plan.segments[3].duration = (residual / peak_velocity).max(0.0);
291        }
292        (plan, residual)
293    }
294
295    /// The time-optimal plan: the fastest peak velocity whose profile lands on the target
296    /// (see the module documentation for the pieces).
297    fn fastest(&self) -> Plan {
298        let OtgLimits {
299            max_velocity: v_max,
300            max_acceleration: a_max,
301            max_jerk: j_max,
302        } = self.limits;
303        if self.position == self.target && self.velocity == 0.0 && self.acceleration == 0.0 {
304            return Plan::default();
305        }
306        let (plan, residual) = self.plan(v_max);
307        if residual >= 0.0 {
308            return plan;
309        }
310        let (plan, residual) = self.plan(-v_max);
311        if residual <= 0.0 {
312            return plan;
313        }
314        let v_rd = self.velocity + self.acceleration * self.acceleration.abs() / (2.0 * j_max);
315        let width = a_max * a_max / j_max;
316        let bounds =
317            [-v_max, v_rd - width, v_rd, v_rd + width, v_max].map(|b| b.clamp(-v_max, v_max));
318        let mut best: Option<Plan> = None;
319        for pair in bounds.windows(2) {
320            let (mut lo, mut hi) = (pair[0], pair[1]);
321            let (r_lo, r_hi) = (self.plan(lo).1, self.plan(hi).1);
322            if lo >= hi || r_lo.signum() == r_hi.signum() {
323                continue;
324            }
325            while hi - lo > 1e-10 * v_max {
326                let mid = 0.5 * (lo + hi);
327                if self.plan(mid).1.signum() == r_lo.signum() {
328                    lo = mid;
329                } else {
330                    hi = mid;
331                }
332            }
333            let candidate = self.plan(0.5 * (lo + hi)).0;
334            if best.is_none_or(|b| candidate.duration() < b.duration()) {
335                best = Some(candidate);
336            }
337        }
338        best.unwrap_or(plan)
339    }
340
341    /// The plan through a peak velocity scaled down from `plan`'s so that it lasts `duration`
342    /// seconds (bisection on the scale; the duration grows without bound as the scale goes to
343    /// zero). Returns `plan` itself when it has no peak to lower.
344    fn stretched(&self, plan: Plan, duration: f64) -> Plan {
345        if plan.peak_velocity == 0.0 || plan.duration() >= duration {
346            return plan;
347        }
348        // A scaled peak is only usable if its transfers do not pass the target (a cruise of
349        // non-negative length brings the plan onto it); near the cusp of `f` they can, and
350        // such a plan would overshoot. Treat those like plans that are too short.
351        let long_enough = |scale: f64| {
352            let (candidate, residual) = self.plan(scale * plan.peak_velocity);
353            residual * plan.peak_velocity >= 0.0 && candidate.duration() >= duration
354        };
355        let (mut lo, mut hi) = (1e-9, 1.0);
356        if !long_enough(lo) {
357            return plan;
358        }
359        for _ in 0..40 {
360            let mid = 0.5 * (lo + hi);
361            if long_enough(mid) {
362                lo = mid;
363            } else {
364                hi = mid;
365            }
366        }
367        self.plan(lo * plan.peak_velocity).0
368    }
369
370    /// Follows `plan` for `dt` seconds, snapping to rest at the target when the plan ends
371    /// within the cycle.
372    fn follow(&mut self, plan: &Plan, dt: f64) -> f64 {
373        if !(dt > 0.0 && dt.is_finite()) {
374            return self.position;
375        }
376        if plan.duration() <= dt {
377            self.reset(self.target);
378            return self.position;
379        }
380        let mut state = (self.position, self.velocity, self.acceleration);
381        let mut left = dt;
382        for segment in &plan.segments {
383            let t = segment.duration.min(left);
384            integrate(&mut state, segment.jerk, t);
385            left -= t;
386            if left <= 0.0 {
387                break;
388            }
389        }
390        let (v_max, a_max) = (self.limits.max_velocity, self.limits.max_acceleration);
391        // The plan respects the limits; this only trims rounding in the last bit.
392        self.position = state.0;
393        self.velocity = state.1.clamp(-v_max, v_max);
394        self.acceleration = state.2.clamp(-a_max, a_max);
395        self.position
396    }
397}
398
399/// `N` [`Otg`]s with the same limits, stepped together, optionally synchronised so that all
400/// axes arrive at their targets at the same time (the faster axes are slowed down).
401#[derive(Debug, Clone, Copy)]
402pub struct MultiOtg<const N: usize> {
403    axes: [Otg; N],
404    synchronize: bool,
405}
406
407/// A three-axis [`MultiOtg`] for a Cartesian position.
408pub type CartesianOtg = MultiOtg<3>;
409
410impl<const N: usize> MultiOtg<N> {
411    /// A generator at rest at `position`, whose target is `position`.
412    ///
413    /// # Errors
414    /// [`FrankaError::InvalidArgument`] if a coordinate is not finite or a limit is not finite
415    /// and positive.
416    pub fn new(position: [f64; N], limits: OtgLimits, synchronize: bool) -> FrankaResult<Self> {
417        Self::with_limits(position, [limits; N], synchronize)
418    }
419
420    /// [`MultiOtg::new`] with limits of its own for every axis, which is what seven joints
421    /// with seven different envelopes need.
422    ///
423    /// # Errors
424    /// [`FrankaError::InvalidArgument`] if a coordinate is not finite or a limit is not finite
425    /// and positive.
426    pub fn with_limits(
427        position: [f64; N],
428        limits: [OtgLimits; N],
429        synchronize: bool,
430    ) -> FrankaResult<Self> {
431        let mut axes = [Otg::new(0.0, limits[0])?; N];
432        for (axis, (p, l)) in axes.iter_mut().zip(position.into_iter().zip(limits)) {
433            *axis = Otg::new(p, l)?;
434        }
435        Ok(Self { axes, synchronize })
436    }
437
438    /// Sets the target position; the next [`step`](Self::step) re-plans towards it.
439    ///
440    /// # Errors
441    /// [`FrankaError::InvalidArgument`], with the target unchanged, if a coordinate is not
442    /// finite.
443    pub fn set_target(&mut self, target: [f64; N]) -> FrankaResult<()> {
444        if target.iter().any(|t| !t.is_finite()) {
445            return Err(FrankaError::InvalidArgument(format!(
446                "otg: target must be finite, got {target:?}"
447            )));
448        }
449        for (axis, t) in self.axes.iter_mut().zip(target) {
450            axis.target = t;
451        }
452        Ok(())
453    }
454
455    /// Advances every axis by `dt` seconds and returns the new position. A `dt` that is not
456    /// positive and finite leaves the state untouched.
457    pub fn step(&mut self, dt: f64) -> [f64; N] {
458        let mut plans = self.axes.map(|axis| axis.fastest());
459        let slowest = plans.iter().map(Plan::duration).fold(0.0, f64::max);
460        if self.synchronize && slowest > dt {
461            for (axis, plan) in self.axes.iter().zip(plans.iter_mut()) {
462                *plan = axis.stretched(*plan, slowest);
463            }
464        }
465        let mut position = [0.0; N];
466        for ((axis, plan), p) in self.axes.iter_mut().zip(&plans).zip(position.iter_mut()) {
467            *p = axis.follow(plan, dt);
468        }
469        position
470    }
471
472    /// The current position.
473    pub fn position(&self) -> [f64; N] {
474        self.axes.map(|axis| axis.position)
475    }
476
477    /// The per-axis generators, for their velocities, accelerations and targets.
478    pub fn axes(&self) -> &[Otg; N] {
479        &self.axes
480    }
481
482    /// Re-anchors every axis on the position actually commanded; see [`Otg::set_position`].
483    ///
484    /// # Errors
485    /// [`FrankaError::InvalidArgument`], with the state unchanged, if a coordinate is not
486    /// finite.
487    pub fn set_position(&mut self, position: [f64; N]) -> FrankaResult<()> {
488        let (v, a) = (
489            self.axes.map(|x| x.velocity),
490            self.axes.map(|x| x.acceleration),
491        );
492        self.set_state(position, v, a)
493    }
494
495    /// Sets every axis's state; see [`Otg::set_state`] for what the velocity and
496    /// acceleration must be.
497    ///
498    /// # Errors
499    /// [`FrankaError::InvalidArgument`], with the state unchanged, if a value is not finite.
500    pub fn set_state(&mut self, p: [f64; N], v: [f64; N], a: [f64; N]) -> FrankaResult<()> {
501        if p.iter().chain(&v).chain(&a).any(|x| !x.is_finite()) {
502            return Err(FrankaError::InvalidArgument(format!(
503                "otg: state must be finite, got {p:?}, {v:?}, {a:?}"
504            )));
505        }
506        for (i, axis) in self.axes.iter_mut().enumerate() {
507            axis.set_state(p[i], v[i], a[i])?;
508        }
509        Ok(())
510    }
511
512    /// Puts every axis at rest at `position`, with the target there too.
513    pub fn reset(&mut self, position: [f64; N]) {
514        for (axis, p) in self.axes.iter_mut().zip(position) {
515            axis.reset(p);
516        }
517    }
518}
519
520#[cfg(test)]
521pub(crate) mod tests;