#![cfg_attr(not(feature = "std"), no_std)]
use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SwitchedError {
InvalidMode,
DwellViolation,
InvalidParameter,
}
pub struct SwitchedLti<S, const N: usize, const I: usize, const M: usize> {
a_modes: [[[S; N]; N]; M],
b_modes: [[[S; I]; N]; M],
c_modes: [[S; N]; M],
state: [S; N],
mode: usize,
min_dwell: usize,
time_in_mode: usize,
total_switches: usize,
p_lyapunov: [[S; N]; N],
p_set: bool,
}
impl<S: ControlScalar, const N: usize, const I: usize, const M: usize> SwitchedLti<S, N, I, M> {
pub fn new(
a_modes: [[[S; N]; N]; M],
b_modes: [[[S; I]; N]; M],
c_modes: [[S; N]; M],
min_dwell: usize,
) -> Result<Self, SwitchedError> {
if M == 0 || N == 0 {
return Err(SwitchedError::InvalidParameter);
}
let state = [S::ZERO; N];
let p_lyapunov = [[S::ZERO; N]; N];
Ok(Self {
a_modes,
b_modes,
c_modes,
state,
mode: 0,
min_dwell,
time_in_mode: 0,
total_switches: 0,
p_lyapunov,
p_set: false,
})
}
pub fn switch_to(&mut self, new_mode: usize) -> Result<(), SwitchedError> {
if new_mode >= M {
return Err(SwitchedError::InvalidMode);
}
if self.time_in_mode < self.min_dwell {
return Err(SwitchedError::DwellViolation);
}
if new_mode != self.mode {
self.mode = new_mode;
self.time_in_mode = 0;
self.total_switches += 1;
}
Ok(())
}
pub fn step(&mut self, u: &[S; I]) -> Result<S, SwitchedError> {
let m = self.mode;
let mut x_new = [S::ZERO; N];
for (i, x_new_i) in x_new.iter_mut().enumerate() {
let mut acc = S::ZERO;
for (j, x_j) in self.state.iter().enumerate() {
acc += self.a_modes[m][i][j] * *x_j;
}
for (k, u_k) in u.iter().enumerate() {
acc += self.b_modes[m][i][k] * *u_k;
}
*x_new_i = acc;
}
self.state = x_new;
self.time_in_mode += 1;
let mut y = S::ZERO;
for (j, x_j) in self.state.iter().enumerate() {
y += self.c_modes[m][j] * *x_j;
}
Ok(y)
}
pub fn set_lyapunov(&mut self, p: [[S; N]; N]) {
self.p_lyapunov = p;
self.p_set = true;
}
pub fn lyapunov_value(&self) -> Option<S> {
if !self.p_set {
return None;
}
let mut v = S::ZERO;
for i in 0..N {
for j in 0..N {
v += self.state[i] * self.p_lyapunov[i][j] * self.state[j];
}
}
Some(v)
}
pub fn lyapunov_decreasing(&self, x_prev: &[S; N]) -> Option<bool> {
if !self.p_set {
return None;
}
let mut v_prev = S::ZERO;
for i in 0..N {
for j in 0..N {
v_prev += x_prev[i] * self.p_lyapunov[i][j] * x_prev[j];
}
}
let v_curr = self.lyapunov_value()?;
Some(v_curr < v_prev)
}
#[inline]
pub fn mode(&self) -> usize {
self.mode
}
#[inline]
pub fn total_switches(&self) -> usize {
self.total_switches
}
#[inline]
pub fn state(&self) -> &[S; N] {
&self.state
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_2s1i2m() -> SwitchedLti<f64, 2, 1, 2> {
let a0 = [[0.5, 0.0], [0.0, 0.5]];
let a1 = [[0.9, 0.0], [0.0, 0.9]];
let b0 = [[1.0], [0.0]];
let b1 = [[0.0], [1.0]];
let c0 = [1.0, 0.0];
let c1 = [0.0, 1.0];
SwitchedLti::new([a0, a1], [b0, b1], [c0, c1], 1).unwrap()
}
#[test]
fn dynamics_correct_mode0() {
let mut sys = make_2s1i2m();
let y = sys.step(&[2.0]).unwrap();
assert!((y - 2.0).abs() < 1e-12, "Expected output 2.0, got {y}");
assert!((sys.state()[0] - 2.0).abs() < 1e-12);
assert!((sys.state()[1]).abs() < 1e-12);
}
#[test]
fn dynamics_correct_mode1() {
let mut sys = make_2s1i2m();
sys.step(&[0.0]).unwrap(); sys.switch_to(1).unwrap();
let y = sys.step(&[3.0]).unwrap();
assert!(
(y - 3.0).abs() < 1e-12,
"Expected output 3.0 in mode1, got {y}"
);
}
#[test]
fn dwell_violation_returns_error() {
let mut sys = make_2s1i2m();
let err = sys.switch_to(1);
assert_eq!(err.err(), Some(SwitchedError::DwellViolation));
}
#[test]
fn mode_switch_changes_output() {
let mut sys = make_2s1i2m();
sys.step(&[4.0]).unwrap(); sys.switch_to(1).unwrap(); let y = sys.step(&[0.0]).unwrap();
assert!(
(y).abs() < 1e-12,
"Output should be 0 in mode1 with C1=[0,1]"
);
}
#[test]
fn lyapunov_value_computed() {
let mut sys = make_2s1i2m();
sys.set_lyapunov([[1.0, 0.0], [0.0, 1.0]]);
sys.step(&[3.0]).unwrap();
let v = sys.lyapunov_value().unwrap();
assert!(
(v - 9.0).abs() < 1e-9,
"Lyapunov value should be 9.0, got {v}"
);
}
#[test]
fn lyapunov_none_when_not_set() {
let sys = make_2s1i2m();
assert!(sys.lyapunov_value().is_none());
assert!(sys.lyapunov_decreasing(&[1.0, 0.0]).is_none());
}
#[test]
fn total_switches_increments() {
let mut sys = make_2s1i2m();
assert_eq!(sys.total_switches(), 0);
sys.step(&[0.0]).unwrap(); sys.switch_to(1).unwrap();
assert_eq!(sys.total_switches(), 1);
sys.step(&[0.0]).unwrap(); sys.switch_to(0).unwrap();
assert_eq!(sys.total_switches(), 2);
}
#[test]
fn siso_output_matches_manual() {
let a = [[[0.8_f64]]];
let b = [[[1.0_f64]]];
let c = [[1.0_f64]];
let mut sys = SwitchedLti::<f64, 1, 1, 1>::new(a, b, c, 0).unwrap();
let y1 = sys.step(&[2.0]).unwrap();
assert!((y1 - 2.0).abs() < 1e-12);
let y2 = sys.step(&[0.0]).unwrap();
assert!((y2 - 1.6).abs() < 1e-12);
}
#[test]
fn lyapunov_decreasing_check() {
let mut sys = make_2s1i2m();
sys.set_lyapunov([[1.0, 0.0], [0.0, 1.0]]);
sys.step(&[4.0]).unwrap();
let x_after_first = *sys.state(); sys.step(&[0.0]).unwrap();
let decreasing = sys.lyapunov_decreasing(&x_after_first).unwrap();
assert!(decreasing, "Lyapunov should be decreasing (stable mode)");
}
#[test]
fn invalid_mode_error() {
let mut sys =
SwitchedLti::<f64, 1, 1, 1>::new([[[0.5_f64]]], [[[1.0_f64]]], [[1.0_f64]], 0).unwrap();
assert_eq!(sys.switch_to(5).err(), Some(SwitchedError::InvalidMode));
}
}