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;