use crate::core::scalar::ControlScalar;
use crate::flatness::FlatnessError;
#[derive(Debug, Clone, Copy, Default)]
pub struct UnicycleFlatMap<S: ControlScalar> {
pub speed_threshold: S,
_phantom: core::marker::PhantomData<S>,
}
impl<S: ControlScalar> UnicycleFlatMap<S> {
pub fn new(speed_threshold: S) -> Result<Self, FlatnessError> {
if speed_threshold < S::ZERO {
return Err(FlatnessError::InvalidParameter(
"speed_threshold must be non-negative",
));
}
Ok(Self {
speed_threshold,
_phantom: core::marker::PhantomData,
})
}
pub fn flat_to_control(
&self,
x_dot: S,
y_dot: S,
x_ddot: S,
y_ddot: S,
) -> Result<(S, S), FlatnessError> {
let v2 = x_dot * x_dot + y_dot * y_dot;
let v = v2.sqrt();
if v < self.speed_threshold {
return Err(FlatnessError::Singular);
}
let omega = (x_ddot * y_dot - x_dot * y_ddot) / v2;
Ok((v, omega))
}
pub fn flat_to_state(&self, x: S, y: S, x_dot: S, y_dot: S) -> Result<[S; 3], FlatnessError> {
let v2 = x_dot * x_dot + y_dot * y_dot;
let v = v2.sqrt();
if v < self.speed_threshold {
return Err(FlatnessError::Singular);
}
let theta = y_dot.atan2(x_dot);
Ok([x, y, theta])
}
}
#[derive(Debug, Clone, Copy)]
pub struct ParametricPath<S: ControlScalar, const N: usize> {
pub xs: [S; N],
pub ys: [S; N],
pub arc_length: S,
}
impl<S: ControlScalar, const N: usize> ParametricPath<S, N> {
pub fn new(xs: [S; N], ys: [S; N]) -> Option<Self> {
if N < 2 {
return None;
}
let mut arc = S::ZERO;
for i in 1..N {
let dx = xs[i] - xs[i - 1];
let dy = ys[i] - ys[i - 1];
arc += (dx * dx + dy * dy).sqrt();
}
Some(Self {
xs,
ys,
arc_length: arc,
})
}
pub fn eval(&self, s: S) -> (S, S) {
let s = s.clamp_val(S::ZERO, S::ONE);
let n = N;
let idx_f = s * S::from_f64((n - 1) as f64);
let idx = {
let i = idx_f.to_f64() as usize;
i.min(n - 2)
};
let frac = idx_f - S::from_f64(idx as f64);
let frac = frac.clamp_val(S::ZERO, S::ONE);
let x = self.xs[idx] + frac * (self.xs[idx + 1] - self.xs[idx]);
let y = self.ys[idx] + frac * (self.ys[idx + 1] - self.ys[idx]);
(x, y)
}
pub fn tangent(&self, s: S) -> (S, S) {
let s = s.clamp_val(S::ZERO, S::ONE);
let n = N;
let idx_f = s * S::from_f64((n - 1) as f64);
let idx = (idx_f.to_f64() as usize).min(n - 2);
let dx = self.xs[idx + 1] - self.xs[idx];
let dy = self.ys[idx + 1] - self.ys[idx];
let norm = (dx * dx + dy * dy).sqrt();
if norm < S::EPSILON {
(S::ONE, S::ZERO)
} else {
(dx / norm, dy / norm)
}
}
pub fn closest_param(&self, px: S, py: S) -> S {
let mut best_s = S::ZERO;
let mut best_dist2 = S::from_f64(f64::MAX);
for i in 0..(N - 1) {
let ax = self.xs[i];
let ay = self.ys[i];
let bx = self.xs[i + 1];
let by = self.ys[i + 1];
let abx = bx - ax;
let aby = by - ay;
let ab2 = abx * abx + aby * aby;
let t = if ab2 < S::EPSILON {
S::ZERO
} else {
let apx = px - ax;
let apy = py - ay;
let t = (apx * abx + apy * aby) / ab2;
t.clamp_val(S::ZERO, S::ONE)
};
let cx = ax + t * abx;
let cy = ay + t * aby;
let dx = px - cx;
let dy = py - cy;
let dist2 = dx * dx + dy * dy;
if dist2 < best_dist2 {
best_dist2 = dist2;
let seg_s0 = S::from_f64(i as f64 / (N - 1) as f64);
let seg_s1 = S::from_f64((i + 1) as f64 / (N - 1) as f64);
best_s = seg_s0 + t * (seg_s1 - seg_s0);
}
}
best_s
}
}
#[derive(Debug, Clone, Copy)]
pub struct FlatPathTracker<S: ControlScalar, const N: usize> {
path: ParametricPath<S, N>,
flat_map: UnicycleFlatMap<S>,
pub nominal_speed: S,
pub lookahead_dist: S,
pub heading_gain: S,
current_s: S,
}
impl<S: ControlScalar, const N: usize> FlatPathTracker<S, N> {
pub fn new(
path: ParametricPath<S, N>,
nominal_speed: S,
lookahead_dist: S,
heading_gain: S,
speed_threshold: S,
) -> Result<Self, FlatnessError> {
if nominal_speed <= S::ZERO {
return Err(FlatnessError::InvalidParameter(
"nominal_speed must be positive",
));
}
if lookahead_dist <= S::ZERO {
return Err(FlatnessError::InvalidParameter(
"lookahead_dist must be positive",
));
}
if heading_gain < S::ZERO {
return Err(FlatnessError::InvalidParameter(
"heading_gain must be non-negative",
));
}
let flat_map = UnicycleFlatMap::new(speed_threshold)?;
Ok(Self {
path,
flat_map,
nominal_speed,
lookahead_dist,
heading_gain,
current_s: S::ZERO,
})
}
pub fn update(
&mut self,
current_x: S,
current_y: S,
current_theta: S,
) -> Result<(S, S), FlatnessError> {
if self.path.arc_length < S::EPSILON {
return Err(FlatnessError::OutOfRange);
}
let s_closest = self.path.closest_param(current_x, current_y);
let ds_lookahead = self.lookahead_dist / self.path.arc_length;
let s_lookahead = (s_closest + ds_lookahead).clamp_val(S::ZERO, S::ONE);
self.current_s = s_lookahead;
let (tx, ty) = self.path.tangent(s_lookahead);
let theta_desired = ty.atan2(tx);
let mut dtheta = theta_desired - current_theta;
while dtheta > S::PI {
dtheta -= S::TWO * S::PI;
}
while dtheta < -S::PI {
dtheta += S::TWO * S::PI;
}
let remaining = S::ONE - s_closest;
let speed_factor = remaining.clamp_val(S::ZERO, S::ONE);
let v_cmd = self.nominal_speed * speed_factor;
let omega_cmd = self.heading_gain * dtheta;
if v_cmd >= self.flat_map.speed_threshold {
let x_dot = v_cmd * current_theta.cos();
let y_dot = v_cmd * current_theta.sin();
let _ = self
.flat_map
.flat_to_state(current_x, current_y, x_dot, y_dot)?;
}
Ok((v_cmd, omega_cmd))
}
pub fn progress(&self) -> S {
self.current_s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn circle_path_constant_v_and_omega() {
let flat_map = UnicycleFlatMap::<f64>::new(1e-6).expect("flat map");
let r = 2.0_f64;
let omega_param = 1.0_f64; let v_true = r * omega_param;
let omega_expected = -omega_param;
for i in 0..16 {
let angle = 2.0 * core::f64::consts::PI * (i as f64) / 16.0;
let xd = -r * omega_param * angle.sin();
let yd = r * omega_param * angle.cos();
let xdd = -r * omega_param * omega_param * angle.cos();
let ydd = -r * omega_param * omega_param * angle.sin();
let (v, omega) = flat_map
.flat_to_control(xd, yd, xdd, ydd)
.expect("flat_to_control");
assert!(
(v - v_true).abs() < 1e-10,
"angle={:.2}: v={:.6} expected {:.6}",
angle,
v,
v_true
);
assert!(
(omega - omega_expected).abs() < 1e-10,
"angle={:.2}: ω={:.6} expected {:.6}",
angle,
omega,
omega_expected
);
}
}
#[test]
fn straight_line_zero_omega() {
let flat_map = UnicycleFlatMap::<f64>::new(1e-6).expect("flat map");
let v = 1.5_f64;
let (speed, omega) = flat_map
.flat_to_control(v, 0.0, 0.0, 0.0)
.expect("flat_to_control");
assert!((speed - v).abs() < 1e-12, "speed={:.6}", speed);
assert!(omega.abs() < 1e-12, "omega={:.2e} should be 0", omega);
}
#[test]
fn flat_to_state_heading() {
let flat_map = UnicycleFlatMap::<f64>::new(1e-6).expect("flat map");
let v = 2.0_f64;
let angle = core::f64::consts::PI / 4.0;
let xd = v * angle.cos();
let yd = v * angle.sin();
let state = flat_map.flat_to_state(1.0, 2.0, xd, yd).expect("state");
assert!((state[0] - 1.0).abs() < 1e-12, "x");
assert!((state[1] - 2.0).abs() < 1e-12, "y");
assert!(
(state[2] - angle).abs() < 1e-10,
"theta={:.6} expected {:.6}",
state[2],
angle
);
}
#[test]
fn singular_below_threshold() {
let threshold = 0.1_f64;
let flat_map = UnicycleFlatMap::<f64>::new(threshold).expect("flat map");
let result = flat_map.flat_to_control(0.05, 0.0, 0.0, 0.0);
assert!(
matches!(result, Err(FlatnessError::Singular)),
"Expected Singular, got {:?}",
result
);
}
#[test]
fn path_tracker_straight_line() {
let xs = [0.0_f64, 1.0, 2.0, 3.0, 4.0];
let ys = [0.0_f64, 0.0, 0.0, 0.0, 0.0];
let path = ParametricPath::<f64, 5>::new(xs, ys).expect("path");
let mut tracker = FlatPathTracker::new(
path, 1.0, 0.5, 2.0, 1e-6,
)
.expect("tracker");
let (v_cmd, omega_cmd) = tracker.update(0.0, 0.0, 0.0).expect("update");
assert!(v_cmd >= 0.0, "v_cmd={:.4}", v_cmd);
assert!(
omega_cmd.abs() < 1e-6,
"omega_cmd={:.2e} should be ~0 on straight path",
omega_cmd
);
}
#[test]
fn closest_param_correct() {
let xs = [0.0_f64, 1.0, 2.0, 3.0, 4.0];
let ys = [0.0_f64, 0.0, 0.0, 0.0, 0.0];
let path = ParametricPath::<f64, 5>::new(xs, ys).expect("path");
let s = path.closest_param(2.0, 1.0);
assert!(
(s - 0.5).abs() < 0.15,
"closest_param: s={:.4} should be ~0.5",
s
);
let s_start = path.closest_param(-1.0, 0.0);
assert!(s_start < 0.01, "s_start={:.4}", s_start);
let s_end = path.closest_param(5.0, 0.0);
assert!(s_end > 0.99, "s_end={:.4}", s_end);
}
}