use crate::anisotropic_friction::AnisotropicFriction;
use crate::math::Fix128;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TireInput {
pub slip_ratio: Fix128,
pub slip_tan_alpha: Fix128,
pub normal_load: Fix128,
pub grip: AnisotropicFriction,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TireForce {
pub longitudinal: Fix128,
pub lateral: Fix128,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BrushTire {
pub longitudinal_stiffness: Fix128,
pub cornering_stiffness: Fix128,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MagicFormulaTire {
pub b_x: Fix128,
pub c_x: Fix128,
pub e_x: Fix128,
pub b_y: Fix128,
pub c_y: Fix128,
pub e_y: Fix128,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TireModel {
Brush(BrushTire),
MagicFormula(MagicFormulaTire),
}
impl BrushTire {
#[must_use]
pub fn passenger_car() -> Self {
Self {
longitudinal_stiffness: Fix128::from_int(80_000),
cornering_stiffness: Fix128::from_int(60_000),
}
}
fn force(&self, input: &TireInput) -> TireForce {
let fz = input.normal_load;
if fz <= Fix128::ZERO {
return TireForce::default();
}
let (mu_xs, mu_xk) = axis_grip(
input.grip.longitudinal_static,
input.grip.longitudinal_kinetic,
);
let (mu_ys, mu_yk) = axis_grip(input.grip.transverse_static, input.grip.transverse_kinetic);
let x_on = mu_xs > Fix128::ZERO;
let y_on = mu_ys > Fix128::ZERO;
if !x_on && !y_on {
return TireForce::default();
}
let kappa = input.slip_ratio;
let tan_a = input.slip_tan_alpha;
let one_k = Fix128::ONE + kappa;
let wx = if x_on { mu_xs } else { Fix128::ONE };
let wy = if y_on { mu_ys } else { Fix128::ONE };
let bx = if x_on {
self.longitudinal_stiffness * kappa * wy
} else {
Fix128::ZERO
};
let by = if y_on {
self.cornering_stiffness * tan_a * wx
} else {
Fix128::ZERO
};
let d = (Fix128::from_int(3) * fz * one_k * wx * wy).max(Fix128::ZERO);
let (m, ux, uy) = magnitude_and_direction(bx, by);
if m < d {
let lam = m / d;
let shape = Fix128::ONE - lam + lam * lam / Fix128::from_int(3);
let longitudinal = if x_on {
self.longitudinal_stiffness * kappa / one_k * shape
} else {
Fix128::ZERO
};
let lateral = if y_on {
-(self.cornering_stiffness * tan_a / one_k * shape)
} else {
Fix128::ZERO
};
TireForce {
longitudinal,
lateral,
}
} else {
let w = d / m;
let vx = if x_on { kappa } else { Fix128::ZERO };
let vy = if y_on { tan_a } else { Fix128::ZERO };
let (_, sx, sy) = magnitude_and_direction(vx, vy);
let rest = Fix128::ONE - w;
let (_, dx, dy) = magnitude_and_direction(w * ux + rest * sx, w * uy + rest * sy);
let mu_x = mu_xk + (mu_xs - mu_xk) * w;
let mu_y = mu_yk + (mu_ys - mu_yk) * w;
TireForce {
longitudinal: fz * mu_x * dx,
lateral: -(fz * mu_y * dy),
}
}
}
}
impl MagicFormulaTire {
#[must_use]
pub fn passenger_car() -> Self {
Self {
b_x: Fix128::from_int(10),
c_x: Fix128::from_ratio(19, 10),
e_x: Fix128::from_ratio(97, 100),
b_y: Fix128::from_int(8),
c_y: Fix128::from_ratio(13, 10),
e_y: Fix128::from_int(-1),
}
}
fn force(&self, input: &TireInput) -> TireForce {
let fz = input.normal_load;
let d_x = input.grip.longitudinal_static * fz;
let d_y = input.grip.transverse_static * fz;
let mut nx = if d_x > Fix128::ZERO {
mf_curve(self.b_x, self.c_x, self.e_x, input.slip_ratio)
} else {
Fix128::ZERO
};
let tan_a = input.slip_tan_alpha;
let mut ny = if d_y > Fix128::ZERO && !tan_a.is_zero() {
-mf_curve(self.b_y, self.c_y, self.e_y, tan_a.atan())
} else {
Fix128::ZERO
};
let n2 = nx * nx + ny * ny;
if n2 > Fix128::ONE {
let n = n2.sqrt();
nx = nx / n;
ny = ny / n;
}
TireForce {
longitudinal: d_x * nx,
lateral: d_y * ny,
}
}
}
fn axis_grip(stat: Fix128, kin: Fix128) -> (Fix128, Fix128) {
let s = stat.max(Fix128::ZERO);
(s, kin.max(Fix128::ZERO).min(s))
}
fn magnitude_and_direction(a: Fix128, b: Fix128) -> (Fix128, Fix128, Fix128) {
let large = a.abs().max(b.abs());
if large.is_zero() {
return (Fix128::ZERO, Fix128::ZERO, Fix128::ZERO);
}
let ra = a / large;
let rb = b / large;
let n = (ra * ra + rb * rb).sqrt();
(large * n, ra / n, rb / n)
}
fn mf_curve(b: Fix128, c: Fix128, e: Fix128, s: Fix128) -> Fix128 {
if s.is_zero() {
return Fix128::ZERO;
}
let bs = b * s;
let inner = bs - e * (bs - bs.atan());
(c * inner.atan()).sin()
}
impl TireModel {
#[must_use]
pub fn force(&self, input: &TireInput) -> TireForce {
match self {
Self::Brush(b) => b.force(input),
Self::MagicFormula(m) => m.force(input),
}
}
#[must_use]
pub fn longitudinal_slope(&self, normal_load: Fix128, mu_peak: Fix128) -> Fix128 {
if normal_load <= Fix128::ZERO || mu_peak <= Fix128::ZERO {
return Fix128::ZERO;
}
match self {
Self::Brush(b) => b.longitudinal_stiffness,
Self::MagicFormula(m) => m.b_x * m.c_x * mu_peak * normal_load,
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::disallowed_methods)]
use super::*;
fn fx(v: f64) -> Fix128 {
Fix128::from_f64(v)
}
fn brush() -> TireModel {
TireModel::Brush(BrushTire::passenger_car())
}
fn mf() -> TireModel {
TireModel::MagicFormula(MagicFormulaTire::passenger_car())
}
fn input(kappa: f64, tan_alpha: f64, fz: f64, grip: AnisotropicFriction) -> TireInput {
TireInput {
slip_ratio: fx(kappa),
slip_tan_alpha: fx(tan_alpha),
normal_load: fx(fz),
grip,
}
}
fn exact_grip() -> AnisotropicFriction {
AnisotropicFriction {
longitudinal_static: Fix128::from_ratio(5, 4),
longitudinal_kinetic: Fix128::from_ratio(3, 4),
transverse_static: Fix128::ONE,
transverse_kinetic: Fix128::from_ratio(1, 2),
slip_threshold_m_s: Fix128::from_ratio(1, 20),
}
}
fn zero_grip() -> AnisotropicFriction {
AnisotropicFriction {
longitudinal_static: Fix128::ZERO,
longitudinal_kinetic: Fix128::ZERO,
transverse_static: Fix128::ZERO,
transverse_kinetic: Fix128::ZERO,
slip_threshold_m_s: Fix128::ZERO,
}
}
fn inverted_grip() -> AnisotropicFriction {
AnisotropicFriction {
longitudinal_static: Fix128::from_ratio(1, 2),
longitudinal_kinetic: Fix128::from_ratio(9, 10),
transverse_static: Fix128::from_ratio(4, 10),
transverse_kinetic: Fix128::from_ratio(8, 10),
slip_threshold_m_s: Fix128::from_ratio(1, 20),
}
}
fn grips() -> [AnisotropicFriction; 5] {
[
AnisotropicFriction::tyre_asphalt(),
AnisotropicFriction::ski_snow(),
AnisotropicFriction::skate_ice(),
exact_grip(),
inverted_grip(),
]
}
const KAPPAS: [f64; 15] = [
-1.0e6, -2.0, -1.0, -0.99, -0.5, -0.1, -0.01, 0.0, 0.003, 0.05, 0.2, 1.0, 5.0, 40.0, 1.0e6,
];
const TANS: [f64; 13] = [
-1.0e6, -3.0, -0.5, -0.1, -0.01, 0.0, 0.004, 0.08, 0.3, 1.0, 10.0, 200.0, 1.0e6,
];
const LOADS: [f64; 3] = [100.0, 4000.0, 20_000.0];
fn axis(s: Fix128, k: Fix128) -> (f64, f64) {
let s = s.to_f64().max(0.0);
(s, k.to_f64().clamp(0.0, s))
}
fn brush_ref(
ck: f64,
ca: f64,
kappa: f64,
t: f64,
fz: f64,
g: &AnisotropicFriction,
) -> (f64, f64) {
let (mxs, mxk) = axis(g.longitudinal_static, g.longitudinal_kinetic);
let (mys, myk) = axis(g.transverse_static, g.transverse_kinetic);
let one_k = 1.0 + kappa;
let w = if one_k > 0.0 {
let sx = kappa / one_k;
let sy = t / one_k;
let lam = (ck * sx / mxs).hypot(ca * sy / mys) / (3.0 * fz);
if lam < 1.0 {
let shape = 1.0 - lam + lam * lam / 3.0;
return (ck * sx * shape, -ca * sy * shape);
}
1.0 / lam
} else {
0.0
};
let (ax, ay) = (ck * kappa / mxs, ca * t / mys);
let ma = ax.hypot(ay);
let mv = kappa.hypot(t);
let vx = w * ax / ma + (1.0 - w) * kappa / mv;
let vy = w * ay / ma + (1.0 - w) * t / mv;
let n = vx.hypot(vy);
let mx = mxk + (mxs - mxk) * w;
let my = myk + (mys - myk) * w;
(fz * mx * vx / n, -fz * my * vy / n)
}
fn mf_curve_ref(b: f64, c: f64, e: f64, s: f64) -> f64 {
let bs = b * s;
(c * (bs - e * (bs - bs.atan())).atan()).sin()
}
fn mf_ref(
t: &MagicFormulaTire,
kappa: f64,
tan_a: f64,
fz: f64,
g: &AnisotropicFriction,
) -> (f64, f64) {
let dx = g.longitudinal_static.to_f64().max(0.0) * fz;
let dy = g.transverse_static.to_f64().max(0.0) * fz;
let mut nx = if dx > 0.0 {
mf_curve_ref(t.b_x.to_f64(), t.c_x.to_f64(), t.e_x.to_f64(), kappa)
} else {
0.0
};
let mut ny = if dy > 0.0 {
-mf_curve_ref(t.b_y.to_f64(), t.c_y.to_f64(), t.e_y.to_f64(), tan_a.atan())
} else {
0.0
};
let n = nx.hypot(ny);
if n > 1.0 {
nx /= n;
ny /= n;
}
(dx * nx, dy * ny)
}
fn mf_curve_tol(b: f64, c: f64, e: f64, lateral: bool) -> f64 {
const EPS_ATAN: f64 = 1e-12;
const EPS_SIN: f64 = 1e-11;
let alpha_term = if lateral {
b * (1.0 + e.abs()) * EPS_ATAN
} else {
0.0
};
2.0 * (EPS_SIN + c * (EPS_ATAN * (1.0 + e.abs()) + alpha_term))
}
fn ellipse_norm(f: TireForce, fz: f64, g: &AnisotropicFriction) -> f64 {
let mx = g.longitudinal_static.to_f64();
let my = g.transverse_static.to_f64();
let x = f.longitudinal.to_f64() / (mx * fz);
let y = f.lateral.to_f64() / (my * fz);
x * x + y * y
}
#[test]
fn brush_passenger_car_matches_doc() {
let b = BrushTire::passenger_car();
assert_eq!(b.longitudinal_stiffness, Fix128::from_int(80_000));
assert_eq!(b.cornering_stiffness, Fix128::from_int(60_000));
}
#[test]
fn brush_small_slip_slopes_are_c_kappa_and_minus_c_alpha_for_any_load() {
let (ck, ca) = (80_000.0, 60_000.0);
let g = AnisotropicFriction::tyre_asphalt();
for fz in [500.0, 2000.0, 8000.0] {
for s in [1e-7, -1e-7, 1e-6] {
let f = brush().force(&input(s, 0.0, fz, g));
let lam = ck * s.abs() / (3.0 * 1.1 * fz);
let tol = ck * (s.abs() / (1.0 - s.abs()) + lam + 1e-9);
let slope = f.longitudinal.to_f64() / s;
assert!(
(slope - ck).abs() <= tol,
"dFx/dκ {slope} at Fz {fz}, κ {s}"
);
assert!(f.lateral.is_zero());
let f = brush().force(&input(0.0, s, fz, g));
let lam = ca * s.abs() / (3.0 * 0.9 * fz);
let slope = f.lateral.to_f64() / s;
assert!(
(slope + ca).abs() <= ca * (lam + 1e-9),
"dFy/dtanα {slope} at Fz {fz}"
);
assert!(f.longitudinal.is_zero());
}
}
}
#[test]
fn brush_locked_wheel_is_exactly_minus_mu_kinetic_fz() {
let g = exact_grip();
let f = brush().force(&TireInput {
slip_ratio: Fix128::NEG_ONE,
slip_tan_alpha: Fix128::ZERO,
normal_load: Fix128::from_int(4000),
grip: g,
});
assert_eq!(f.longitudinal, Fix128::from_int(-3000));
assert_eq!(f.lateral, Fix128::ZERO);
let g = AnisotropicFriction::tyre_asphalt();
let fz = Fix128::from_ratio(73_291, 17);
let f = brush().force(&TireInput {
slip_ratio: Fix128::NEG_ONE,
slip_tan_alpha: Fix128::ZERO,
normal_load: fz,
grip: g,
});
assert_eq!(f.longitudinal, -(fz * g.longitudinal_kinetic));
assert_eq!(f.lateral, Fix128::ZERO);
}
#[test]
fn brush_locked_steered_wheel_force_opposes_the_slip_velocity() {
let iso = AnisotropicFriction {
longitudinal_static: Fix128::ONE,
longitudinal_kinetic: Fix128::from_ratio(3, 4),
transverse_static: Fix128::ONE,
transverse_kinetic: Fix128::from_ratio(3, 4),
slip_threshold_m_s: Fix128::from_ratio(1, 20),
};
let fz = 4000.0;
for k in [-1.0, -1.5, -40.0] {
for t in [-2.0, -0.3, -0.02, 0.05, 0.4, 3.0] {
let f = brush().force(&input(k, t, fz, iso));
let (x, y) = (f.longitudinal.to_f64(), f.lateral.to_f64());
let n = k.hypot(t);
let (ex, ey) = (0.75 * fz * k / n, -0.75 * fz * t / n);
let cross = (x * ey - y * ex) / (0.75 * fz * 0.75 * fz);
assert!(
cross.abs() < 1e-12,
"angle {cross} at κ {k}, tanα {t}: ({x}, {y})"
);
assert!(x * ex + y * ey > 0.0);
assert!(
(x.hypot(y) - 0.75 * fz).abs() < 1e-9 * fz,
"|F| {}",
x.hypot(y)
);
}
}
let g = AnisotropicFriction::tyre_asphalt();
for t in [-0.5, 0.2, 1.0] {
let f = brush().force(&input(-1.0, t, fz, g));
let n = 1.0f64.hypot(t);
assert!((f.longitudinal.to_f64() - -0.9 * fz / n).abs() < 1e-9 * fz);
assert!((f.lateral.to_f64() + 0.7 * fz * t / n).abs() < 1e-9 * fz);
}
}
#[test]
fn brush_sliding_direction_turns_from_stiffness_weighted_to_slip_velocity() {
let iso = AnisotropicFriction {
longitudinal_static: Fix128::ONE,
longitudinal_kinetic: Fix128::ONE,
transverse_static: Fix128::ONE,
transverse_kinetic: Fix128::ONE,
slip_threshold_m_s: Fix128::from_ratio(1, 20),
};
let fz = 4000.0;
let t = 0.3;
let mut last = f64::INFINITY;
for k in [-0.3, -0.7, -0.95, -0.999, -0.99999] {
let f = brush().force(&input(k, t, fz, iso));
let ang = (-f.lateral.to_f64()).atan2(f.longitudinal.to_f64()) - t.atan2(k);
assert!(ang.abs() < last, "κ {k}: angle {ang} not shrinking");
last = ang.abs();
}
assert!(last < 1e-3, "{last}");
}
#[test]
fn brush_stays_inside_static_friction_ellipse() {
for g in grips() {
for fz in LOADS {
for k in KAPPAS {
for t in TANS {
let f = brush().force(&input(k, t, fz, g));
let n = ellipse_norm(f, fz, &g);
assert!(
n <= 1.0 + 1e-12,
"outside ellipse: {n} (κ {k}, tanα {t}, Fz {fz})"
);
}
}
}
}
}
#[test]
fn brush_pure_lateral_matches_fiala_closed_form() {
let ca = 60_000.0;
let g = AnisotropicFriction::tyre_asphalt();
let (ms, mk) = (0.9, 0.7);
for fz in LOADS {
for t in [
-2.0f64, -0.3, -0.05, -0.004, 0.001, 0.02, 0.1, 0.25, 1.0, 50.0,
] {
let limit = 3.0 * ms * fz / ca;
let expect = if t.abs() < limit {
-ca * t + ca * ca * t.abs() * t / (3.0 * ms * fz)
- ca * ca * ca * t * t * t / (27.0 * ms * ms * fz * fz)
} else {
-t.signum() * fz * (mk + (ms - mk) * 3.0 * ms * fz / (ca * t.abs()))
};
let f = brush().force(&input(0.0, t, fz, g));
assert!(
(f.lateral.to_f64() - expect).abs() <= 1e-9 * ms * fz,
"Fy {} vs Fiala {expect} (tanα {t}, Fz {fz})",
f.lateral.to_f64()
);
assert!(f.longitudinal.is_zero());
}
}
}
#[test]
fn brush_combined_slip_matches_closed_form() {
let (ck, ca) = (80_000.0, 60_000.0);
for g in grips() {
for fz in LOADS {
for k in KAPPAS {
for t in TANS {
if k == 0.0 && t == 0.0 {
continue;
}
let (ex, ey) = brush_ref(ck, ca, k, t, fz, &g);
let f = brush().force(&input(k, t, fz, g));
let tol = 1e-9
* fz
* g.longitudinal_static
.to_f64()
.max(g.transverse_static.to_f64());
assert!(
(f.longitudinal.to_f64() - ex).abs() <= tol
&& (f.lateral.to_f64() - ey).abs() <= tol,
"({}, {}) vs ({ex}, {ey}) at κ {k}, tanα {t}, Fz {fz}",
f.longitudinal.to_f64(),
f.lateral.to_f64()
);
}
}
}
}
}
#[test]
fn brush_is_continuous_at_the_adhesion_limit() {
let g = exact_grip();
let fz = 4000.0;
let sigma = 3.0 * 1.25 * fz / 80_000.0;
let k_lim = sigma / (1.0 - sigma);
for k in [k_lim * (1.0 - 1e-9), k_lim * (1.0 + 1e-9)] {
let f = brush().force(&input(k, 0.0, fz, g));
assert!(
(f.longitudinal.to_f64() - 1.25 * fz).abs() < 1e-4,
"{}",
f.longitudinal.to_f64()
);
}
}
#[test]
fn mf_passenger_car_matches_doc() {
let m = MagicFormulaTire::passenger_car();
assert_eq!(m.b_x, Fix128::from_int(10));
assert_eq!(m.c_x, Fix128::from_ratio(19, 10));
assert_eq!(m.e_x, Fix128::from_ratio(97, 100));
assert_eq!(m.b_y, Fix128::from_int(8));
assert_eq!(m.c_y, Fix128::from_ratio(13, 10));
assert_eq!(m.e_y, Fix128::from_int(-1));
}
#[test]
fn mf_matches_pacejka_formula_on_slip_grid() {
let p = MagicFormulaTire::passenger_car();
let tx = mf_curve_tol(10.0, 1.9, 0.97, false);
let ty = mf_curve_tol(8.0, 1.3, -1.0, true);
for g in grips() {
for fz in LOADS {
for k in KAPPAS {
for t in TANS {
let (ex, ey) = mf_ref(&p, k, t, fz, &g);
let f = mf().force(&input(k, t, fz, g));
let dx = g.longitudinal_static.to_f64() * fz;
let dy = g.transverse_static.to_f64() * fz;
let (ax, ay) = (2.0 * (tx + ty) * dx, 2.0 * (tx + ty) * dy);
assert!(
(f.longitudinal.to_f64() - ex).abs() <= ax
&& (f.lateral.to_f64() - ey).abs() <= ay,
"({}, {}) vs ({ex}, {ey}) at κ {k}, tanα {t}, Fz {fz}",
f.longitudinal.to_f64(),
f.lateral.to_f64()
);
}
}
}
}
}
#[test]
fn mf_pure_slip_equals_d_sin_formula() {
let g = AnisotropicFriction::tyre_asphalt();
let fz = 3500.0;
for k in [-0.7, -0.08, 0.02, 0.12, 0.6] {
let f = mf().force(&input(k, 0.0, fz, g));
let d = 1.1 * fz;
let expect = d * mf_curve_ref(10.0, 1.9, 0.97, k);
assert!(
(f.longitudinal.to_f64() - expect).abs()
<= d * mf_curve_tol(10.0, 1.9, 0.97, false)
);
assert!(f.lateral.is_zero());
}
for t in [-0.7, -0.05, 0.01, 0.15, 2.0] {
let f = mf().force(&input(0.0, t, fz, g));
let d = 0.9 * fz;
let expect = -d * mf_curve_ref(8.0, 1.3, -1.0, t.atan());
assert!((f.lateral.to_f64() - expect).abs() <= d * mf_curve_tol(8.0, 1.3, -1.0, true));
assert!(f.longitudinal.is_zero());
}
}
#[test]
fn mf_stays_inside_static_friction_ellipse() {
for g in grips() {
for fz in LOADS {
for k in KAPPAS {
for t in TANS {
let f = mf().force(&input(k, t, fz, g));
let n = ellipse_norm(f, fz, &g);
assert!(
n <= 1.0 + 1e-12,
"outside ellipse: {n} (κ {k}, tanα {t}, Fz {fz})"
);
}
}
}
}
}
#[test]
fn mf_combined_slip_is_scaled_onto_the_ellipse() {
let g = AnisotropicFriction::tyre_asphalt();
let nx = mf_curve_ref(10.0, 1.9, 0.97, 0.2);
let ny = mf_curve_ref(8.0, 1.3, -1.0, 0.3f64.atan());
assert!(nx.hypot(ny) > 1.0);
let f = mf().force(&input(0.2, 0.3, 4000.0, g));
let n = ellipse_norm(f, 4000.0, &g);
assert!((n - 1.0).abs() < 1e-9, "{n}");
}
#[test]
fn signs_follow_the_slip_conventions_for_both_models() {
for model in [brush(), mf()] {
for g in grips() {
for fz in LOADS {
for k in [0.001, 0.1, 1.0, 1.0e6] {
for t in [-0.3, 0.0, 0.3] {
let f = model.force(&input(k, t, fz, g));
assert!(f.longitudinal > Fix128::ZERO, "κ {k} > 0 ⇒ Fx > 0");
let f = model.force(&input(-k.min(0.5), t, fz, g));
assert!(f.longitudinal < Fix128::ZERO, "κ < 0 ⇒ Fx < 0");
}
}
for t in [0.001, 0.1, 1.0, 1.0e6] {
for k in [-0.5, 0.0, 0.5] {
let f = model.force(&input(k, t, fz, g));
assert!(f.lateral < Fix128::ZERO, "tanα {t} > 0 ⇒ Fy < 0");
let f = model.force(&input(k, -t, fz, g));
assert!(f.lateral > Fix128::ZERO, "tanα < 0 ⇒ Fy > 0");
}
}
}
}
}
}
#[test]
fn non_positive_load_gives_exactly_zero_force() {
for model in [brush(), mf()] {
for fz in [0.0, -1.0, -5000.0] {
for (k, t) in [(0.1, 0.2), (-1.0, 0.0), (1.0e6, -1.0e6)] {
let f = model.force(&input(k, t, fz, AnisotropicFriction::tyre_asphalt()));
assert_eq!(f, TireForce::default(), "Fz {fz}");
}
assert_eq!(model.longitudinal_slope(fx(fz), fx(1.1)), Fix128::ZERO);
}
}
}
#[test]
fn zero_grip_gives_exactly_zero_force_and_slope() {
for model in [brush(), mf()] {
for (k, t) in [(0.1, 0.2), (-1.0, 0.0), (1.0e6, -1.0e6), (0.0, 0.3)] {
let f = model.force(&input(k, t, 4000.0, zero_grip()));
assert_eq!(f, TireForce::default());
}
assert_eq!(
model.longitudinal_slope(fx(4000.0), Fix128::ZERO),
Fix128::ZERO
);
}
}
#[test]
fn zero_grip_on_one_axis_keeps_the_other_axis_as_pure_slip() {
let g = AnisotropicFriction {
longitudinal_static: Fix128::ZERO,
longitudinal_kinetic: Fix128::ZERO,
..AnisotropicFriction::tyre_asphalt()
};
let (ca, ms, fz) = (60_000.0, 0.9, 4000.0);
for (k, t) in [(0.0, 0.01), (0.3, 0.01), (0.0, 0.5), (0.3, 0.5)] {
let f = brush().force(&input(k, t, fz, g));
assert!(f.longitudinal.is_zero());
let s = t / (1.0 + k);
let lam = ca * s / (3.0 * ms * fz);
let expect = if lam < 1.0 {
-ca * s * (1.0 - lam + lam * lam / 3.0)
} else {
-fz * (0.7 + 0.2 / lam)
};
assert!(
(f.lateral.to_f64() - expect).abs() < 1e-9 * fz,
"{} vs {expect}",
f.lateral.to_f64()
);
let f = mf().force(&input(k, t, fz, g));
assert!(f.longitudinal.is_zero());
let expect = -ms * fz * mf_curve_ref(8.0, 1.3, -1.0, t.atan());
assert!(
(f.lateral.to_f64() - expect).abs() <= ms * fz * mf_curve_tol(8.0, 1.3, -1.0, true)
);
}
}
#[test]
fn huge_slip_saturates_to_the_documented_values() {
let g = AnisotropicFriction::tyre_asphalt();
let fz = 4000.0;
let (ck, ca) = (80_000.0, 60_000.0);
let sx = 1.0e6 / (1.0 + 1.0e6);
let lam = ck * sx / (3.0 * 1.1 * fz);
let f = brush().force(&input(1.0e6, 0.0, fz, g));
assert!((f.longitudinal.to_f64() - fz * (0.9 + 0.2 / lam)).abs() < 1e-9 * fz);
assert!(f.lateral.is_zero());
let f = brush().force(&TireInput {
slip_ratio: Fix128::from_int(-1_000_000),
slip_tan_alpha: Fix128::ZERO,
normal_load: Fix128::from_int(4000),
grip: g,
});
assert_eq!(
f.longitudinal,
-(Fix128::from_int(4000) * g.longitudinal_kinetic)
);
let lam = ca * 1.0e6 / (3.0 * 0.9 * fz);
for s in [1.0, -1.0] {
let f = brush().force(&input(0.0, s * 1.0e6, fz, g));
assert!((f.lateral.to_f64() + s * fz * (0.7 + 0.2 / lam)).abs() < 1e-9 * fz);
}
for s in [1.0, -1.0] {
let f = mf().force(&input(s * 1.0e6, 0.0, fz, g));
let expect = 1.1 * fz * mf_curve_ref(10.0, 1.9, 0.97, s * 1.0e6);
assert!(
(f.longitudinal.to_f64() - expect).abs()
<= 1.1 * fz * mf_curve_tol(10.0, 1.9, 0.97, false)
);
let f = mf().force(&input(0.0, s * 1.0e6, fz, g));
let expect = -0.9 * fz * mf_curve_ref(8.0, 1.3, -1.0, (s * 1.0e6f64).atan());
assert!(
(f.lateral.to_f64() - expect).abs()
<= 0.9 * fz * mf_curve_tol(8.0, 1.3, -1.0, true)
);
}
}
#[test]
fn zero_slip_gives_exactly_zero_force() {
for model in [brush(), mf()] {
for g in grips() {
for fz in LOADS {
let f = model.force(&input(0.0, 0.0, fz, g));
assert_eq!(f, TireForce::default());
}
}
}
}
#[test]
fn longitudinal_slope_is_the_zero_slip_derivative() {
assert_eq!(
brush().longitudinal_slope(Fix128::from_int(4000), fx(1.1)),
Fix128::from_int(80_000)
);
assert_eq!(
brush().longitudinal_slope(Fix128::from_int(200), fx(0.1)),
Fix128::from_int(80_000)
);
let s = mf().longitudinal_slope(Fix128::from_int(4000), Fix128::from_ratio(11, 10));
assert!((s.to_f64() - 10.0 * 1.9 * 1.1 * 4000.0).abs() < 1e-9);
let g = AnisotropicFriction {
longitudinal_static: Fix128::from_ratio(11, 10),
..AnisotropicFriction::tyre_asphalt()
};
let h = 1e-6;
let fp = mf().force(&input(h, 0.0, 4000.0, g)).longitudinal.to_f64();
let fm = mf().force(&input(-h, 0.0, 4000.0, g)).longitudinal.to_f64();
assert!(((fp - fm) / (2.0 * h) - s.to_f64()).abs() / s.to_f64() < 1e-6);
}
}