use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct BangBangPhases<S: ControlScalar> {
pub t1: S,
pub t2: S,
pub t3: S,
pub total: S,
}
#[derive(Debug, Clone, Copy)]
pub struct TimeOptimalProfile<S: ControlScalar> {
pub x0: S,
pub v0: S,
pub x1: S,
pub v1: S,
pub v_max: S,
pub a_max: S,
pub phases: BangBangPhases<S>,
sign: S,
v_peak: S,
}
impl<S: ControlScalar> TimeOptimalProfile<S> {
pub fn plan(x0: S, v0: S, x1: S, v1: S, v_max: S, a_max: S) -> Self {
let v1 = v1.clamp_val(-v_max, v_max);
let v0 = v0.clamp_val(-v_max, v_max);
let profile_pos = Self::try_plan(x0, v0, x1, v1, v_max, a_max, S::ONE);
let profile_neg = Self::try_plan(x0, v0, x1, v1, v_max, a_max, -S::ONE);
match (profile_pos, profile_neg) {
(Some(p), Some(n)) => {
if p.phases.total <= n.phases.total {
p
} else {
n
}
}
(Some(p), None) => p,
(None, Some(n)) => n,
(None, None) => Self::zero_profile(x0, v0, x1, v1, v_max, a_max),
}
}
fn try_plan(x0: S, v0: S, x1: S, v1: S, v_max: S, a_max: S, sign: S) -> Option<Self> {
let v_peak_candidate = sign * v_max;
let dv1 = v_peak_candidate - v0;
let t1 = dv1 / (sign * a_max);
if t1 < -S::EPSILON {
return None;
}
let t1 = t1.max(S::ZERO);
let dv3 = v_peak_candidate - v1;
let t3 = dv3 / (sign * a_max);
if t3 < -S::EPSILON {
return None;
}
let t3 = t3.max(S::ZERO);
let x_total = x1 - x0;
let x_accel = v0 * t1 + sign * a_max * t1 * t1 * S::HALF;
let x_decel = v_peak_candidate * t3 - sign * a_max * t3 * t3 * S::HALF;
let x_remaining = x_total - x_accel - x_decel;
let t2_raw = x_remaining / v_peak_candidate;
if t2_raw < -S::EPSILON {
return Self::try_plan_no_coast(x0, v0, x1, v1, a_max, sign);
}
let t2 = t2_raw.max(S::ZERO);
let total = t1 + t2 + t3;
if total < -S::EPSILON {
return None;
}
Some(Self {
x0,
v0,
x1,
v1,
v_max,
a_max,
phases: BangBangPhases {
t1,
t2,
t3,
total: total.max(S::ZERO),
},
sign,
v_peak: v_peak_candidate,
})
}
fn try_plan_no_coast(x0: S, v0: S, x1: S, v1: S, a_max: S, sign: S) -> Option<Self> {
let dx = x1 - x0;
let a = sign * a_max;
let discriminant = a * dx + (v0 * v0 + v1 * v1) * S::HALF;
if discriminant < S::ZERO {
return None;
}
let v_peak = discriminant.sqrt();
if v_peak.abs() < S::EPSILON {
return None;
}
let t1_raw = (v_peak - v0) / a;
let t3_raw = (v_peak - v1) / a;
if t1_raw < -S::EPSILON || t3_raw < -S::EPSILON {
return None;
}
let t1 = t1_raw.max(S::ZERO);
let t3 = t3_raw.max(S::ZERO);
let total = t1 + t3;
Some(Self {
x0,
v0,
x1,
v1,
v_max: v_peak,
a_max,
phases: BangBangPhases {
t1,
t2: S::ZERO,
t3,
total,
},
sign,
v_peak,
})
}
fn zero_profile(x0: S, v0: S, x1: S, v1: S, v_max: S, a_max: S) -> Self {
Self {
x0,
v0,
x1,
v1,
v_max,
a_max,
phases: BangBangPhases {
t1: S::ZERO,
t2: S::ZERO,
t3: S::ZERO,
total: S::ZERO,
},
sign: S::ONE,
v_peak: S::ZERO,
}
}
pub fn query(&self, t: S) -> (S, S, S) {
let t = t.clamp_val(S::ZERO, self.phases.total);
let a = self.sign * self.a_max;
if t <= self.phases.t1 {
let pos = self.x0 + self.v0 * t + S::HALF * a * t * t;
let vel = self.v0 + a * t;
(pos, vel, a)
} else if t <= self.phases.t1 + self.phases.t2 {
let tc = t - self.phases.t1;
let x1_end =
self.x0 + self.v0 * self.phases.t1 + S::HALF * a * self.phases.t1 * self.phases.t1;
let pos = x1_end + self.v_peak * tc;
(pos, self.v_peak, S::ZERO)
} else {
let t3 = t - self.phases.t1 - self.phases.t2;
let x_accel_end =
self.x0 + self.v0 * self.phases.t1 + S::HALF * a * self.phases.t1 * self.phases.t1;
let x_coast_end = x_accel_end + self.v_peak * self.phases.t2;
let pos = x_coast_end + self.v_peak * t3 - S::HALF * a * t3 * t3;
let vel = self.v_peak - a * t3;
(pos, vel, -a)
}
}
pub fn duration(&self) -> S {
self.phases.total
}
pub fn is_valid(&self) -> bool {
self.phases.total > S::ZERO
}
}
use crate::trajectory::TrajectoryError;
use heapless::Vec as HVec;
#[derive(Debug, Clone, Copy)]
pub struct TimeOptimalSegment<S: ControlScalar> {
pub duration: S,
pub v_start: S,
pub v_end: S,
pub accel: S,
}
pub fn plan_1dof<S: ControlScalar>(
distance: S,
v_max: S,
a_max: S,
v_start: S,
v_end: S,
) -> Result<HVec<TimeOptimalSegment<S>, 3>, TrajectoryError> {
if v_max <= S::ZERO || a_max <= S::ZERO {
return Err(TrajectoryError::InvalidParameter);
}
let v0 = v_start.clamp_val(S::ZERO, v_max);
let v1 = v_end.clamp_val(S::ZERO, v_max);
let mut segments: HVec<TimeOptimalSegment<S>, 3> = HVec::new();
let d_accel_full = (v_max * v_max - v0 * v0) / (S::TWO * a_max);
let d_decel_full = (v_max * v_max - v1 * v1) / (S::TWO * a_max);
let d_min = d_accel_full + d_decel_full;
if d_min.to_f64() <= distance.to_f64() {
let t1 = (v_max - v0) / a_max;
if t1 > S::ZERO {
segments
.push(TimeOptimalSegment {
duration: t1,
v_start: v0,
v_end: v_max,
accel: a_max,
})
.map_err(|_| TrajectoryError::BufferFull)?;
}
let d_cruise = distance - d_min;
let t2 = d_cruise / v_max;
if t2 > S::ZERO {
segments
.push(TimeOptimalSegment {
duration: t2,
v_start: v_max,
v_end: v_max,
accel: S::ZERO,
})
.map_err(|_| TrajectoryError::BufferFull)?;
}
let t3 = (v_max - v1) / a_max;
if t3 > S::ZERO {
segments
.push(TimeOptimalSegment {
duration: t3,
v_start: v_max,
v_end: v1,
accel: -a_max,
})
.map_err(|_| TrajectoryError::BufferFull)?;
}
} else {
let disc = a_max * distance + (v0 * v0 + v1 * v1) * S::HALF;
let v_peak = if disc > S::ZERO {
disc.sqrt().min(v_max)
} else {
v0.max(v1)
};
let t1 = if v_peak > v0 {
(v_peak - v0) / a_max
} else {
S::ZERO
};
if t1 > S::ZERO {
segments
.push(TimeOptimalSegment {
duration: t1,
v_start: v0,
v_end: v_peak,
accel: a_max,
})
.map_err(|_| TrajectoryError::BufferFull)?;
}
let t3 = if v_peak > v1 {
(v_peak - v1) / a_max
} else {
S::ZERO
};
if t3 > S::ZERO {
segments
.push(TimeOptimalSegment {
duration: t3,
v_start: v_peak,
v_end: v1,
accel: -a_max,
})
.map_err(|_| TrajectoryError::BufferFull)?;
}
}
Ok(segments)
}
pub fn total_time<S: ControlScalar>(segments: &HVec<TimeOptimalSegment<S>, 3>) -> S {
segments.iter().fold(S::ZERO, |acc, seg| acc + seg.duration)
}
pub fn sample_at<S: ControlScalar>(
segments: &HVec<TimeOptimalSegment<S>, 3>,
t: S,
) -> Result<(S, S), TrajectoryError> {
if segments.is_empty() {
return Err(TrajectoryError::InvalidParameter);
}
let t_total = total_time(segments);
let t = t.clamp_val(S::ZERO, t_total);
let mut pos = S::ZERO;
let mut elapsed = S::ZERO;
for seg in segments.iter() {
if t <= elapsed + seg.duration {
let dt = t - elapsed;
let p = seg.v_start * dt + S::HALF * seg.accel * dt * dt;
let v = seg.v_start + seg.accel * dt;
return Ok((pos + p, v));
}
pos += seg.v_start * seg.duration + S::HALF * seg.accel * seg.duration * seg.duration;
elapsed += seg.duration;
}
let last = segments[segments.len() - 1];
Ok((pos, last.v_end))
}
#[cfg(test)]
mod segment_tests {
use super::*;
#[test]
fn trapezoidal_profile_reaches_distance() {
let segs = plan_1dof(10.0_f64, 5.0, 2.0, 0.0, 0.0).expect("should plan");
assert!(segs.len() >= 2, "expected ≥2 segments, got {}", segs.len());
let t_tot = total_time(&segs);
let (pos, vel) = sample_at(&segs, t_tot).expect("sample ok");
assert!((pos - 10.0).abs() < 1e-9, "pos={}", pos);
assert!(vel.abs() < 1e-9, "vel={}", vel);
}
#[test]
fn triangular_profile_short_distance() {
let segs = plan_1dof(4.0_f64, 5.0, 2.0, 0.0, 0.0).expect("should plan");
let t_tot = total_time(&segs);
let (pos, _vel) = sample_at(&segs, t_tot).expect("sample ok");
assert!((pos - 4.0).abs() < 1e-9, "pos={}", pos);
}
#[test]
fn non_zero_boundary_velocities() {
let segs = plan_1dof(8.0_f64, 4.0, 2.0, 1.0, 1.0).expect("should plan");
let t_tot = total_time(&segs);
let (pos, vel) = sample_at(&segs, t_tot).expect("sample ok");
assert!((pos - 8.0).abs() < 1e-9, "pos={}", pos);
assert!((vel - 1.0).abs() < 1e-6, "vel={}", vel);
}
#[test]
fn sample_at_zero_returns_start() {
let segs = plan_1dof(5.0_f64, 3.0, 1.5, 0.0, 0.0).expect("plan ok");
let (pos, vel) = sample_at(&segs, 0.0).expect("sample ok");
assert!(pos.abs() < 1e-12, "pos={}", pos);
assert!(vel.abs() < 1e-12, "vel={}", vel);
}
#[test]
fn total_time_empty_is_zero() {
let segs: HVec<TimeOptimalSegment<f64>, 3> = HVec::new();
assert_eq!(total_time(&segs), 0.0);
}
#[test]
fn invalid_v_max_returns_error() {
let r = plan_1dof(5.0_f64, 0.0, 2.0, 0.0, 0.0);
assert!(r.is_err());
}
#[test]
fn invalid_a_max_returns_error() {
let r = plan_1dof(5.0_f64, 3.0, 0.0, 0.0, 0.0);
assert!(r.is_err());
}
#[test]
fn position_monotonically_increases() {
let segs = plan_1dof(10.0_f64, 5.0, 2.0, 0.0, 0.0).expect("plan ok");
let t_tot = total_time(&segs);
let n = 50usize;
let mut prev_pos = -1.0_f64;
for i in 0..=n {
let t = t_tot * (i as f64) / (n as f64);
let (pos, _) = sample_at(&segs, t).expect("sample ok");
assert!(
pos >= prev_pos - 1e-10,
"pos not monotone at t={}: {}",
t,
pos
);
prev_pos = pos;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn forward_move_reaches_target() {
let p = TimeOptimalProfile::plan(0.0_f64, 0.0, 10.0, 0.0, 5.0, 2.0);
assert!(p.is_valid());
let (x_end, v_end, _) = p.query(p.duration());
assert!((x_end - 10.0).abs() < 1e-6, "x_end={}", x_end);
assert!(v_end.abs() < 1e-6, "v_end={}", v_end);
}
#[test]
fn backward_move_reaches_target() {
let p = TimeOptimalProfile::plan(0.0_f64, 0.0, -8.0, 0.0, 4.0, 2.0);
assert!(p.is_valid());
let (x_end, v_end, _) = p.query(p.duration());
assert!((x_end + 8.0).abs() < 1e-4, "x_end={}", x_end);
assert!(v_end.abs() < 1e-4, "v_end={}", v_end);
}
#[test]
fn zero_displacement_profile() {
let p = TimeOptimalProfile::plan(5.0_f64, 0.0, 5.0, 0.0, 3.0, 2.0);
let (x_end, _, _) = p.query(p.duration());
assert!((x_end - 5.0).abs() < 1e-6, "x_end={}", x_end);
}
#[test]
fn initial_velocity_handled() {
let p = TimeOptimalProfile::plan(0.0_f64, 2.0, 10.0, 0.0, 5.0, 3.0);
assert!(p.is_valid());
let (x_end, v_end, _) = p.query(p.duration());
assert!((x_end - 10.0).abs() < 1e-4, "x_end={}", x_end);
assert!(v_end.abs() < 1e-4, "v_end={}", v_end);
}
}