use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminalSmcError {
InvalidExponent,
InvalidGain,
InvalidDt,
}
impl core::fmt::Display for TerminalSmcError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
TerminalSmcError::InvalidExponent => {
f.write_str("exponent must satisfy 0 < p < q for fractional terminal surface")
}
TerminalSmcError::InvalidGain => {
f.write_str("k must be positive and r must be in (0, 1]")
}
TerminalSmcError::InvalidDt => f.write_str("dt must be strictly positive"),
}
}
}
#[inline]
fn sign<S: ControlScalar>(x: S) -> S {
if x > S::ZERO {
S::ONE
} else if x < S::ZERO {
-S::ONE
} else {
S::ZERO
}
}
#[derive(Debug, Clone, Copy)]
pub struct TerminalSmc<S: ControlScalar> {
beta: S,
p: S,
q: S,
k: S,
r: S,
dt: S,
x: S,
xdot: S,
}
impl<S: ControlScalar> TerminalSmc<S> {
pub fn new(beta: S, p: S, q: S, k: S, r: S, dt: S) -> Result<Self, TerminalSmcError> {
if p <= S::ZERO || q <= S::ZERO || p >= q {
return Err(TerminalSmcError::InvalidExponent);
}
if k <= S::ZERO || r <= S::ZERO || r > S::ONE {
return Err(TerminalSmcError::InvalidGain);
}
if dt <= S::ZERO {
return Err(TerminalSmcError::InvalidDt);
}
Ok(Self {
beta,
p,
q,
k,
r,
dt,
x: S::ZERO,
xdot: S::ZERO,
})
}
pub fn sliding_variable(&self) -> S {
let exponent = self.p / self.q;
let abs_x = self.x.abs();
let powered = if abs_x < S::EPSILON {
S::ZERO
} else {
abs_x.powf(exponent)
};
self.xdot + self.beta * powered * sign(self.x)
}
pub fn update(&mut self, x: S, xdot: S) -> Result<S, TerminalSmcError> {
self.x = x;
self.xdot = xdot;
let sigma = self.sliding_variable();
let exponent = self.p / self.q;
let coupling_exp = exponent - S::ONE; let abs_x = x.abs();
let coupling = if abs_x < S::EPSILON {
S::ZERO
} else {
self.beta * exponent * abs_x.powf(coupling_exp) * xdot
};
let abs_sigma = sigma.abs();
let u = -self.k * abs_sigma.powf(self.r) * sign(sigma) - coupling;
Ok(u)
}
pub fn finite_time_estimate(&self) -> S {
if self.r >= S::ONE {
return S::ZERO;
}
let sigma = self.sliding_variable();
let abs_sigma = sigma.abs();
if abs_sigma < S::EPSILON {
return S::ZERO;
}
let one_minus_r = S::ONE - self.r;
abs_sigma.powf(one_minus_r) / (self.k * one_minus_r)
}
pub fn reset(&mut self, x: S, xdot: S) {
self.x = x;
self.xdot = xdot;
}
pub fn dt(&self) -> S {
self.dt
}
}
#[cfg(test)]
mod tests {
use super::*;
const DT: f64 = 0.001;
#[test]
fn invalid_exponent_p_geq_q() {
let res = TerminalSmc::<f64>::new(1.0, 2.0, 1.0, 5.0, 0.5, DT);
assert!(
matches!(res, Err(TerminalSmcError::InvalidExponent)),
"expected InvalidExponent, got {:?}",
res.err()
);
}
#[test]
fn invalid_exponent_p_eq_q() {
let res = TerminalSmc::<f64>::new(1.0, 3.0, 3.0, 5.0, 0.5, DT);
assert!(
matches!(res, Err(TerminalSmcError::InvalidExponent)),
"expected InvalidExponent, got {:?}",
res.err()
);
}
#[test]
fn invalid_k_zero_returns_error() {
let res = TerminalSmc::<f64>::new(1.0, 1.0, 2.0, 0.0, 0.5, DT);
assert!(
matches!(res, Err(TerminalSmcError::InvalidGain)),
"expected InvalidGain, got {:?}",
res.err()
);
}
#[test]
fn invalid_r_gt_one_returns_error() {
let res = TerminalSmc::<f64>::new(1.0, 1.0, 2.0, 5.0, 1.5, DT);
assert!(
matches!(res, Err(TerminalSmcError::InvalidGain)),
"expected InvalidGain, got {:?}",
res.err()
);
}
#[test]
fn invalid_dt_returns_error() {
let res = TerminalSmc::<f64>::new(1.0, 1.0, 2.0, 5.0, 0.5, 0.0);
assert!(
matches!(res, Err(TerminalSmcError::InvalidDt)),
"expected InvalidDt, got {:?}",
res.err()
);
}
#[test]
fn zero_state_zero_control() {
let mut ctrl = TerminalSmc::<f64>::new(1.0, 1.0, 2.0, 5.0, 0.5, DT).expect("valid params");
let u = ctrl.update(0.0, 0.0).expect("update ok");
assert!(u.abs() < 1e-14, "u should be zero at origin, got {}", u);
}
#[test]
fn positive_sigma_gives_negative_control() {
let mut ctrl = TerminalSmc::<f64>::new(1.0, 1.0, 2.0, 5.0, 0.5, DT).expect("valid params");
let u = ctrl.update(0.0, 1.0).expect("update ok");
assert!(
u < 0.0,
"u should be negative for positive sigma, got {}",
u
);
}
#[test]
fn formula_verification() {
let beta = 1.0_f64;
let p = 1.0_f64;
let q = 2.0_f64;
let k = 2.0_f64;
let r = 1.0_f64;
let mut ctrl = TerminalSmc::<f64>::new(beta, p, q, k, r, DT).expect("valid params");
let u = ctrl.update(4.0, 0.0).expect("update ok");
let expected = -4.0_f64;
assert!(
(u - expected).abs() < 1e-10,
"expected u={}, got u={}",
expected,
u
);
}
#[test]
fn sliding_variable_converges() {
let mut ctrl = TerminalSmc::<f64>::new(1.0, 1.0, 2.0, 5.0, 0.5, DT).expect("valid params");
let mut x = 1.0_f64;
let mut xdot = 0.0_f64;
ctrl.reset(x, xdot);
let sigma_init = ctrl.sliding_variable().abs();
for _ in 0..2000 {
let u = ctrl.update(x, xdot).expect("update ok");
x += DT * xdot;
xdot += DT * u;
}
let sigma_final = ctrl.sliding_variable().abs();
assert!(
sigma_final < sigma_init,
"sliding variable should decrease: sigma_init={:.4}, sigma_final={:.4}",
sigma_init,
sigma_final
);
}
#[test]
fn finite_time_estimate_nonnegative() {
let mut ctrl = TerminalSmc::<f64>::new(1.0, 1.0, 2.0, 5.0, 0.5, DT).expect("valid params");
ctrl.update(1.0, 0.5).expect("update ok");
let t_est = ctrl.finite_time_estimate();
assert!(
t_est >= 0.0,
"finite time estimate must be non-negative: {}",
t_est
);
}
}