use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SwitchingLaw {
HardSign,
SaturationLayer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IsmcError {
NonPositiveSwitchingGain,
NonPositiveBoundaryLayer,
NonPositiveDt,
}
impl core::fmt::Display for IsmcError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
IsmcError::NonPositiveSwitchingGain => {
f.write_str("switching gain eta must be positive")
}
IsmcError::NonPositiveBoundaryLayer => {
f.write_str("boundary layer Phi must be positive")
}
IsmcError::NonPositiveDt => f.write_str("dt must be positive"),
}
}
}
#[inline]
fn sat<S: ControlScalar>(x: S, phi: S) -> S {
let ratio = x / phi;
if ratio > S::ONE {
S::ONE
} else if ratio < -S::ONE {
-S::ONE
} else {
ratio
}
}
#[derive(Debug, Clone, Copy)]
pub struct FirstOrderIsmc<S: ControlScalar> {
k_nom: S,
a: S,
b: S,
eta: S,
phi: S,
law: SwitchingLaw,
sigma: S,
dt: S,
}
impl<S: ControlScalar> FirstOrderIsmc<S> {
pub fn new(
a: S,
b: S,
k_nom: S,
eta: S,
phi: S,
law: SwitchingLaw,
dt: S,
) -> Result<Self, IsmcError> {
if eta <= S::ZERO {
return Err(IsmcError::NonPositiveSwitchingGain);
}
if law == SwitchingLaw::SaturationLayer && phi <= S::ZERO {
return Err(IsmcError::NonPositiveBoundaryLayer);
}
if dt <= S::ZERO {
return Err(IsmcError::NonPositiveDt);
}
Ok(Self {
k_nom,
a,
b,
eta,
phi,
law,
sigma: S::ZERO,
dt,
})
}
pub fn initialise(&mut self, e0: S) {
self.sigma = -e0;
}
pub fn reset(&mut self) {
self.sigma = S::ZERO;
}
pub fn update(&mut self, x: S, x_ref: S) -> S {
let e = x - x_ref;
let s = e + self.sigma;
let u_nom = (-self.a * x_ref - self.k_nom * e) / self.b;
let u_sw = match self.law {
SwitchingLaw::HardSign => {
if s > S::ZERO {
-self.eta
} else if s < S::ZERO {
self.eta
} else {
S::ZERO
}
}
SwitchingLaw::SaturationLayer => -self.eta * sat(s, self.phi),
};
let sigma_dot = -(self.a - self.k_nom) * e;
self.sigma += sigma_dot * self.dt;
u_nom + u_sw
}
pub fn surface(&self, x: S, x_ref: S) -> S {
(x - x_ref) + self.sigma
}
}
#[derive(Debug, Clone, Copy)]
pub struct SecondOrderIsmc<S: ControlScalar> {
c: S,
f_known: S,
b: S,
eta: S,
phi: S,
law: SwitchingLaw,
sigma: S,
dt: S,
}
impl<S: ControlScalar> SecondOrderIsmc<S> {
#[allow(clippy::too_many_arguments)]
pub fn new(
c: S,
f_known: S,
b: S,
eta: S,
phi: S,
law: SwitchingLaw,
dt: S,
) -> Result<Self, IsmcError> {
if eta <= S::ZERO {
return Err(IsmcError::NonPositiveSwitchingGain);
}
if law == SwitchingLaw::SaturationLayer && phi <= S::ZERO {
return Err(IsmcError::NonPositiveBoundaryLayer);
}
if dt <= S::ZERO {
return Err(IsmcError::NonPositiveDt);
}
Ok(Self {
c,
f_known,
b,
eta,
phi,
law,
sigma: S::ZERO,
dt,
})
}
pub fn initialise(&mut self, e0: S, de0: S) {
self.sigma = -(de0 + self.c * e0);
}
pub fn reset(&mut self) {
self.sigma = S::ZERO;
}
pub fn sample_period(&self) -> S {
self.dt
}
pub fn update(&mut self, x1: S, x2: S, x1_ref: S, x2_ref: S, x2_dot_ref: S) -> S {
let e = x1 - x1_ref;
let de = x2 - x2_ref;
let s = de + self.c * e + self.sigma;
let u_nom = (x2_dot_ref - self.f_known * x2 - self.c * de - self.c * self.c * e) / self.b;
let u_sw = match self.law {
SwitchingLaw::HardSign => {
if s > S::ZERO {
-self.eta / self.b
} else if s < S::ZERO {
self.eta / self.b
} else {
S::ZERO
}
}
SwitchingLaw::SaturationLayer => -self.eta * sat(s, self.phi) / self.b,
};
u_nom + u_sw
}
pub fn surface(&self, x1: S, x2: S, x1_ref: S, x2_ref: S) -> S {
(x2 - x2_ref) + self.c * (x1 - x1_ref) + self.sigma
}
}
#[cfg(test)]
mod tests {
use super::*;
const DT: f64 = 0.001;
fn step_1st(x: f64, u: f64, d: f64) -> f64 {
x + DT * (-x + u + d)
}
fn step_2nd(state: [f64; 2], u: f64, d: f64) -> [f64; 2] {
[
state[0] + DT * state[1],
state[1] + DT * (-0.5 * state[1] + u + d),
]
}
#[test]
fn ismc_error_on_invalid_eta() {
let res = FirstOrderIsmc::<f64>::new(
-1.0,
1.0,
2.0,
0.0, 0.1,
SwitchingLaw::HardSign,
DT,
);
assert!(res.is_err());
}
#[test]
fn ismc_error_on_invalid_phi() {
let res = FirstOrderIsmc::<f64>::new(
-1.0,
1.0,
2.0,
1.0,
0.0, SwitchingLaw::SaturationLayer,
DT,
);
assert!(res.is_err());
}
#[test]
fn first_order_hard_sign_tracks_with_disturbance() {
let a = -1.0_f64;
let b = 1.0_f64;
let k_nom = 3.0_f64;
let eta = 1.0_f64; let r = 1.0_f64;
let d = 0.3_f64;
let mut ctrl = FirstOrderIsmc::new(a, b, k_nom, eta, 0.1, SwitchingLaw::HardSign, DT)
.expect("valid params");
let mut x = 0.0_f64;
ctrl.initialise(x - r);
for _ in 0..5000 {
let u = ctrl.update(x, r);
x = step_1st(x, u, d);
}
assert!((x - r).abs() < 0.05, "x={:.4} should track r={}", x, r);
}
#[test]
fn first_order_saturation_layer_tracks() {
let a = -1.0_f64;
let b = 1.0_f64;
let k_nom = 3.0_f64;
let eta = 1.0_f64;
let phi = 0.1_f64;
let r = 1.0_f64;
let d = 0.3_f64;
let mut ctrl =
FirstOrderIsmc::new(a, b, k_nom, eta, phi, SwitchingLaw::SaturationLayer, DT)
.expect("valid params");
let mut x = 0.0_f64;
ctrl.initialise(x - r);
for _ in 0..5000 {
let u = ctrl.update(x, r);
x = step_1st(x, u, d);
}
assert!((x - r).abs() < 0.2, "x={:.4} should be near r={}", x, r);
}
#[test]
fn second_order_ismc_saturation_tracks() {
let c = 5.0_f64;
let f_known = -0.5_f64;
let b = 1.0_f64;
let eta = 1.0_f64; let phi = 0.1_f64;
let d = 0.4_f64;
let r = 1.0_f64;
let mut ctrl =
SecondOrderIsmc::new(c, f_known, b, eta, phi, SwitchingLaw::SaturationLayer, DT)
.expect("valid params");
let mut state = [0.0_f64; 2];
ctrl.initialise(state[0] - r, state[1] - 0.0);
for _ in 0..10000 {
let u = ctrl.update(state[0], state[1], r, 0.0, 0.0);
state = step_2nd(state, u, d);
}
assert!(
(state[0] - r).abs() < 0.2,
"position={:.4} should converge to r={}",
state[0],
r
);
}
#[test]
fn surface_is_zero_at_init() {
let a = -1.0_f64;
let b = 1.0_f64;
let r = 2.0_f64;
let x0 = 0.5_f64;
let mut ctrl = FirstOrderIsmc::new(a, b, 0.5, 1.0, 0.1, SwitchingLaw::HardSign, DT)
.expect("valid params");
ctrl.initialise(x0 - r);
let s = ctrl.surface(x0, r);
assert!(s.abs() < 1e-14, "surface at init should be zero: s={}", s);
}
#[test]
fn sat_function_clamps() {
assert_eq!(sat(2.0_f64, 1.0_f64), 1.0_f64);
assert_eq!(sat(-3.0_f64, 1.0_f64), -1.0_f64);
assert!((sat(0.5_f64, 1.0_f64) - 0.5_f64).abs() < 1e-15);
}
}