use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PlatoonError {
InvalidParameter,
EmptyPlatoon,
}
impl core::fmt::Display for PlatoonError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
PlatoonError::InvalidParameter => write!(f, "invalid platoon parameter"),
PlatoonError::EmptyPlatoon => write!(f, "platoon must have at least one vehicle"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct VehiclePlatoon<S: ControlScalar, const N: usize> {
speeds: [S; N],
positions: [S; N],
a: S,
kp: S,
kd: S,
desired_spacing: S,
headway: S,
dt: S,
}
impl<S: ControlScalar, const N: usize> VehiclePlatoon<S, N> {
pub fn new(
a: S,
kp: S,
kd: S,
d_des: S,
headway: S,
dt: S,
v_lead_0: S,
) -> Result<Self, PlatoonError> {
if N == 0 {
return Err(PlatoonError::EmptyPlatoon);
}
if a <= S::ZERO {
return Err(PlatoonError::InvalidParameter);
}
if dt <= S::ZERO {
return Err(PlatoonError::InvalidParameter);
}
if d_des < S::ZERO {
return Err(PlatoonError::InvalidParameter);
}
if headway < S::ZERO {
return Err(PlatoonError::InvalidParameter);
}
let mut speeds = [v_lead_0; N];
let mut positions = [S::ZERO; N];
for i in 0..N {
speeds[i] = v_lead_0;
positions[i] = -S::from_f64(i as f64) * d_des;
}
Ok(Self {
speeds,
positions,
a,
kp,
kd,
desired_spacing: d_des,
headway,
dt,
})
}
pub fn step(&mut self, v_lead: S) -> Result<[S; N], PlatoonError> {
let pos_prev = self.positions;
self.speeds[0] = v_lead;
self.positions[0] += self.dt * v_lead;
let mut errors = [S::ZERO; N];
let mut speed_commands = [S::ZERO; N];
speed_commands[0] = v_lead;
for i in 1..N {
let gap = pos_prev[i - 1] - pos_prev[i];
let d_desired = self.desired_spacing + self.headway * self.speeds[i];
let e_i = gap - d_desired;
let e_dot_i = self.speeds[i - 1] - self.speeds[i];
let u_i = self.speeds[i - 1] + self.kp * e_i + self.kd * e_dot_i;
speed_commands[i] = u_i;
errors[i] = e_i;
}
#[allow(clippy::needless_range_loop)]
for i in 1..N {
let dxi = -self.a * self.speeds[i] + self.a * speed_commands[i];
self.speeds[i] += self.dt * dxi;
self.positions[i] += self.dt * self.speeds[i];
}
Ok(errors)
}
pub fn speeds(&self) -> &[S; N] {
&self.speeds
}
pub fn positions(&self) -> &[S; N] {
&self.positions
}
pub fn is_string_stable(&self, perturbation_amp: S) -> bool {
if N < 2 {
return true;
}
let mut sim = *self;
let v_nominal = sim.speeds[0];
let v_perturb = v_nominal + perturbation_amp;
for _ in 0..10 {
let _ = sim.step(v_perturb);
}
for _ in 0..200 {
let _ = sim.step(v_nominal);
}
let mut max_err = S::ZERO;
for i in 1..N {
let gap = sim.positions[i - 1] - sim.positions[i];
let d_desired = sim.desired_spacing + sim.headway * sim.speeds[i];
let abs_err = (gap - d_desired).abs();
if abs_err > max_err {
max_err = abs_err;
}
}
max_err < perturbation_amp
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_platoon() -> VehiclePlatoon<f64, 4> {
VehiclePlatoon::<f64, 4>::new(
1.0, 0.5, 1.0, 10.0, 0.0, 0.01, 20.0, )
.expect("valid params")
}
#[test]
fn equilibrium_spacing_holds() {
let mut p = make_platoon();
let mut max_err = 0.0_f64;
for _ in 0..500 {
let errs = p.step(20.0).expect("step ok");
for &e in errs.iter().skip(1) {
if e.abs() > max_err {
max_err = e.abs();
}
}
}
assert!(
max_err < 1e-6,
"spacing errors should stay near zero at equilibrium: max={:.2e}",
max_err
);
}
#[test]
fn lead_speed_step_followers_track() {
let mut p = make_platoon();
for _ in 0..200 {
p.step(20.0).expect("ok");
}
for _ in 0..2000 {
p.step(25.0).expect("ok");
}
let speeds = p.speeds();
for (i, &s) in speeds.iter().enumerate() {
assert!(
(s - 25.0).abs() < 1.0,
"vehicle {} speed should track lead (25 m/s): got {:.3}",
i,
s
);
}
}
#[test]
fn spacing_errors_bounded() {
let mut p = make_platoon();
for _ in 0..50 {
p.step(25.0).expect("ok");
}
let mut max_err_after = 0.0_f64;
for _ in 0..3000 {
let errs = p.step(20.0).expect("ok");
for &e in errs.iter().skip(1) {
if e.abs() > max_err_after {
max_err_after = e.abs();
}
}
}
assert!(
max_err_after < 50.0,
"spacing errors should stay bounded: max={:.3}",
max_err_after
);
}
#[test]
fn invalid_params_err() {
assert!(VehiclePlatoon::<f64, 4>::new(0.0, 0.5, 1.0, 10.0, 0.0, 0.01, 20.0).is_err());
assert!(VehiclePlatoon::<f64, 4>::new(1.0, 0.5, 1.0, -1.0, 0.0, 0.01, 20.0).is_err());
assert!(VehiclePlatoon::<f64, 4>::new(1.0, 0.5, 1.0, 10.0, 0.0, 0.0, 20.0).is_err());
assert!(VehiclePlatoon::<f64, 4>::new(1.0, 0.5, 1.0, 10.0, -0.5, 0.01, 20.0).is_err());
}
#[test]
fn cth_policy_spacing_grows_with_speed() {
let mut p_fast =
VehiclePlatoon::<f64, 3>::new(1.0, 0.5, 1.0, 5.0, 0.5, 0.01, 30.0).expect("ok");
let mut p_slow =
VehiclePlatoon::<f64, 3>::new(1.0, 0.5, 1.0, 5.0, 0.5, 0.01, 10.0).expect("ok");
for _ in 0..3000 {
p_fast.step(30.0).expect("ok");
p_slow.step(10.0).expect("ok");
}
let gap_fast = p_fast.positions()[0] - p_fast.positions()[1];
let gap_slow = p_slow.positions()[0] - p_slow.positions()[1];
assert!(
gap_fast > gap_slow,
"CTH: fast platoon gap ({:.2}) should exceed slow platoon gap ({:.2})",
gap_fast,
gap_slow
);
}
}