#![allow(clippy::excessive_precision)]
use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct FrictionModel<S: ControlScalar> {
pub coulomb_coeff: S,
pub viscous_coeff: S,
pub stiction_thresh: S,
}
impl<S: ControlScalar> FrictionModel<S> {
pub fn new(coulomb_coeff: S, viscous_coeff: S, stiction_thresh: S) -> Self {
Self {
coulomb_coeff,
viscous_coeff,
stiction_thresh,
}
}
pub fn torque(&self, omega: S) -> S {
let abs_omega = omega.abs();
if abs_omega >= self.stiction_thresh && self.stiction_thresh >= S::ZERO {
self.coulomb_coeff * omega.signum() + self.viscous_coeff * omega
} else if self.stiction_thresh > S::EPSILON {
let gain = self.coulomb_coeff / self.stiction_thresh + self.viscous_coeff;
gain * omega
} else {
self.viscous_coeff * omega
}
}
pub fn adapt_viscous(&mut self, omega: S, torque_error: S, learning_rate: S) {
self.viscous_coeff += learning_rate * torque_error * omega;
}
}
#[derive(Debug, Clone, Copy)]
pub struct LoadObserver<S: ControlScalar> {
pub inertia: S,
pub friction: FrictionModel<S>,
pub observer_gain: S,
omega_hat: S,
tau_load_hat: S,
}
impl<S: ControlScalar> LoadObserver<S> {
pub fn new(inertia: S, friction: FrictionModel<S>, observer_gain: S) -> Self {
Self {
inertia,
friction,
observer_gain,
omega_hat: S::ZERO,
tau_load_hat: S::ZERO,
}
}
pub fn estimate(&mut self, omega_meas: S, u: S, dt: S) -> S {
let e_omega = omega_meas - self.omega_hat;
let tau_friction = self.friction.torque(self.omega_hat);
let l = self.observer_gain;
let j = self.inertia;
let omega_hat_dot = (u - tau_friction - self.tau_load_hat) / j + l * e_omega;
let tau_load_dot = -(l * l * j * e_omega);
self.omega_hat += omega_hat_dot * dt;
self.tau_load_hat += tau_load_dot * dt;
self.tau_load_hat
}
pub fn omega_hat(&self) -> S {
self.omega_hat
}
pub fn tau_load_hat(&self) -> S {
self.tau_load_hat
}
pub fn reset(&mut self) {
self.omega_hat = S::ZERO;
self.tau_load_hat = S::ZERO;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_friction_model_positive_speed() {
let fm = FrictionModel::<f32>::new(0.5, 0.1, 0.01);
let tau = fm.torque(10.0);
assert!((tau - 1.5_f32).abs() < 1e-5, "tau={tau}");
}
#[test]
fn test_friction_model_negative_speed() {
let fm = FrictionModel::<f32>::new(0.5, 0.1, 0.01);
let tau = fm.torque(-10.0);
assert!((tau + 1.5_f32).abs() < 1e-5, "tau={tau}");
}
#[test]
fn test_friction_model_stiction_region() {
let fm = FrictionModel::<f32>::new(1.0, 0.0, 1.0);
let tau = fm.torque(0.5);
assert!((tau - 0.5_f32).abs() < 1e-5, "tau={tau}");
}
#[test]
fn test_load_observer_converges() {
let fm = FrictionModel::<f32>::new(0.0, 0.0, 0.0);
let mut obs = LoadObserver::<f32>::new(0.01, fm, 100.0);
for _ in 0..2000 {
obs.estimate(0.0, 1.0, 0.0001);
}
let tau_hat = obs.tau_load_hat();
assert!(
tau_hat > 0.5,
"Observer should converge toward 1.0, got {tau_hat}"
);
}
#[test]
fn test_load_observer_reset() {
let fm = FrictionModel::<f32>::new(0.0, 0.0, 0.0);
let mut obs = LoadObserver::<f32>::new(0.01, fm, 10.0);
for _ in 0..100 {
obs.estimate(1.0, 0.0, 0.001);
}
obs.reset();
assert_eq!(obs.omega_hat(), 0.0_f32);
assert_eq!(obs.tau_load_hat(), 0.0_f32);
}
}