use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BicycleError {
InvalidParameter,
ZeroLongitudinalSpeed,
}
impl core::fmt::Display for BicycleError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
BicycleError::InvalidParameter => write!(f, "invalid bicycle parameter"),
BicycleError::ZeroLongitudinalSpeed => {
write!(f, "longitudinal speed must be nonzero for dynamic model")
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct KinematicBicycle<S: ControlScalar> {
pub state: [S; 4],
pub wheelbase: S,
pub dt: S,
}
impl<S: ControlScalar> KinematicBicycle<S> {
pub fn new(wheelbase: S, dt: S, x0: S, y0: S) -> Result<Self, BicycleError> {
if wheelbase <= S::ZERO {
return Err(BicycleError::InvalidParameter);
}
if dt <= S::ZERO {
return Err(BicycleError::InvalidParameter);
}
Ok(Self {
state: [x0, y0, S::ZERO, S::ZERO],
wheelbase,
dt,
})
}
fn derivatives(s: &[S; 4], v: S, delta_dot: S, wheelbase: S) -> [S; 4] {
let psi = s[2];
let delta = s[3];
let dx = v * psi.cos();
let dy = v * psi.sin();
let dpsi = v * delta.tan() / wheelbase;
let ddelta = delta_dot;
[dx, dy, dpsi, ddelta]
}
pub fn step(&mut self, v: S, delta_dot: S) -> Result<[S; 4], BicycleError> {
let d = Self::derivatives(&self.state, v, delta_dot, self.wheelbase);
let dt = self.dt;
for (s, di) in self.state.iter_mut().zip(d.iter()) {
*s += dt * *di;
}
Ok(self.state)
}
pub fn state(&self) -> [S; 4] {
self.state
}
}
#[derive(Debug, Clone, Copy)]
pub struct DynamicBicycle<S: ControlScalar> {
pub state: [S; 4],
pub m: S,
pub iz: S,
pub lf: S,
pub lr: S,
pub cf: S,
pub cr: S,
pub vx: S,
pub dt: S,
}
impl<S: ControlScalar> DynamicBicycle<S> {
#[allow(clippy::too_many_arguments)]
pub fn new(
m: S,
iz: S,
lf: S,
lr: S,
cf: S,
cr: S,
vx: S,
dt: S,
) -> Result<Self, BicycleError> {
if m <= S::ZERO || iz <= S::ZERO {
return Err(BicycleError::InvalidParameter);
}
if lf <= S::ZERO || lr <= S::ZERO {
return Err(BicycleError::InvalidParameter);
}
if cf <= S::ZERO || cr <= S::ZERO {
return Err(BicycleError::InvalidParameter);
}
if vx.abs() < S::EPSILON {
return Err(BicycleError::ZeroLongitudinalSpeed);
}
if dt <= S::ZERO {
return Err(BicycleError::InvalidParameter);
}
Ok(Self {
state: [S::ZERO; 4],
m,
iz,
lf,
lr,
cf,
cr,
vx,
dt,
})
}
fn derivatives(&self, s: &[S; 4], delta: S) -> [S; 4] {
let psi = s[1];
let vy = s[2];
let r = s[3];
let two = S::TWO;
let vx = self.vx;
let cf_plus_cr = self.cf + self.cr;
let cf_lf_minus_cr_lr = self.cf * self.lf - self.cr * self.lr;
let cf_lf2_plus_cr_lr2 = self.cf * self.lf * self.lf + self.cr * self.lr * self.lr;
let dy = vy + vx * psi;
let dpsi = r;
let dvy = (-(two * cf_plus_cr / vx) * vy
- (two * cf_lf_minus_cr_lr / vx + self.m * vx) * r
+ two * self.cf * delta)
/ self.m;
let dr = (-(two * cf_lf_minus_cr_lr / vx) * vy - (two * cf_lf2_plus_cr_lr2 / vx) * r
+ two * self.cf * self.lf * delta)
/ self.iz;
[dy, dpsi, dvy, dr]
}
pub fn step(&mut self, delta: S) -> Result<[S; 4], BicycleError> {
let s = self.state;
let dt = self.dt;
let k1 = self.derivatives(&s, delta);
let s2: [S; 4] = core::array::from_fn(|i| s[i] + S::HALF * dt * k1[i]);
let k2 = self.derivatives(&s2, delta);
let s3: [S; 4] = core::array::from_fn(|i| s[i] + S::HALF * dt * k2[i]);
let k3 = self.derivatives(&s3, delta);
let s4: [S; 4] = core::array::from_fn(|i| s[i] + dt * k3[i]);
let k4 = self.derivatives(&s4, delta);
let sixth = S::ONE / S::from_f64(6.0);
for i in 0..4 {
self.state[i] += sixth * dt * (k1[i] + S::TWO * k2[i] + S::TWO * k3[i] + k4[i]);
}
Ok(self.state)
}
pub fn state(&self) -> [S; 4] {
self.state
}
pub fn lateral_error(&self) -> S {
self.state[0]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kinematic_zero_input_straight_line() {
let mut bike = KinematicBicycle::<f64>::new(2.5, 0.01, 0.0, 0.0).expect("valid params");
for _ in 0..100 {
bike.step(1.0, 0.0).expect("step ok");
}
let s = bike.state();
assert!(s[0] > 0.9, "x should increase: {}", s[0]);
assert!(s[1].abs() < 1e-10, "y should stay 0: {}", s[1]);
assert!(s[2].abs() < 1e-10, "psi should stay 0: {}", s[2]);
}
#[test]
fn kinematic_turning_radius() {
let l = 2.5_f64;
let delta = 0.1_f64; let v = 5.0_f64;
let dt = 0.001_f64;
let mut bike = KinematicBicycle::<f64>::new(l, dt, 0.0, 0.0).expect("valid params");
bike.state[3] = delta;
let r_expected = l / delta.tan();
let period = 2.0 * core::f64::consts::PI * r_expected / v;
let steps = (period / dt) as usize;
for _ in 0..steps {
bike.step(v, 0.0).expect("step ok");
}
let s = bike.state();
let dist = (s[0] * s[0] + s[1] * s[1]).sqrt();
assert!(
dist < r_expected * 0.05,
"after full circle dist={:.3}, expected < {:.3}",
dist,
r_expected * 0.05
);
}
#[test]
fn kinematic_steering_rate_changes_heading() {
let mut bike = KinematicBicycle::<f64>::new(2.5, 0.01, 0.0, 0.0).expect("valid params");
for _ in 0..50 {
bike.step(1.0, 0.02).expect("step ok");
}
let s = bike.state();
assert!(s[3] > 0.0, "steering angle should be positive: {}", s[3]);
assert!(s[2] > 0.0, "heading should be positive: {}", s[2]);
assert!(s[1] > 0.0, "y should drift left: {}", s[1]);
}
#[test]
fn kinematic_invalid_params() {
assert!(KinematicBicycle::<f64>::new(0.0, 0.01, 0.0, 0.0).is_err());
assert!(KinematicBicycle::<f64>::new(-1.0, 0.01, 0.0, 0.0).is_err());
assert!(KinematicBicycle::<f64>::new(2.5, 0.0, 0.0, 0.0).is_err());
assert!(KinematicBicycle::<f64>::new(2.5, -0.01, 0.0, 0.0).is_err());
}
fn make_dynamic() -> DynamicBicycle<f64> {
DynamicBicycle::<f64>::new(
1500.0, 2500.0, 1.2, 1.4, 80_000.0, 80_000.0, 20.0, 0.001, )
.expect("valid params")
}
#[test]
fn dynamic_zero_input_decays() {
let mut bike = make_dynamic();
bike.state[2] = 1.0; bike.state[3] = 0.1; let initial_energy = bike.state[2].powi(2) + bike.state[3].powi(2);
for _ in 0..2000 {
bike.step(0.0).expect("step ok");
}
let final_energy = bike.state[2].powi(2) + bike.state[3].powi(2);
assert!(
final_energy < initial_energy,
"lateral energy should decay: initial={:.4}, final={:.4}",
initial_energy,
final_energy
);
}
#[test]
fn dynamic_step_steer_response() {
let mut bike = make_dynamic();
let delta = 0.02_f64; for _ in 0..500 {
bike.step(delta).expect("step ok");
}
let s = bike.state();
assert!(
s[2].abs() > 1e-6,
"vy should be nonzero after step steer: {}",
s[2]
);
assert!(
s[3].abs() > 1e-6,
"r should be nonzero after step steer: {}",
s[3]
);
}
#[test]
fn dynamic_lateral_error_accessor() {
let mut bike = make_dynamic();
bike.step(0.01).expect("step ok");
assert!(
(bike.lateral_error() - bike.state()[0]).abs() < f64::EPSILON,
"lateral_error() must equal state[0]: {} vs {}",
bike.lateral_error(),
bike.state()[0]
);
}
#[test]
fn dynamic_invalid_params() {
assert!(DynamicBicycle::<f64>::new(
1500.0, 2500.0, 1.2, 1.4, 80_000.0, 80_000.0, 0.0, 0.001
)
.is_err());
assert!(
DynamicBicycle::<f64>::new(0.0, 2500.0, 1.2, 1.4, 80_000.0, 80_000.0, 20.0, 0.001)
.is_err()
);
assert!(DynamicBicycle::<f64>::new(
1500.0, 2500.0, -1.0, 1.4, 80_000.0, 80_000.0, 20.0, 0.001
)
.is_err());
assert!(DynamicBicycle::<f64>::new(
1500.0, 2500.0, 1.2, 1.4, 80_000.0, 80_000.0, 20.0, 0.0
)
.is_err());
assert!(
DynamicBicycle::<f64>::new(1500.0, 2500.0, 1.2, 1.4, 0.0, 80_000.0, 20.0, 0.001)
.is_err()
);
}
}