#![cfg_attr(not(feature = "std"), no_std)]
use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PwaError {
NoActiveRegion,
InvalidRegion,
InvalidParameter,
}
#[inline]
fn dot<S: ControlScalar, const N: usize>(a: &[S; N], b: &[S; N]) -> S {
let mut acc = S::ZERO;
for (ai, bi) in a.iter().zip(b.iter()) {
acc += *ai * *bi;
}
acc
}
pub struct PwaSystem<S, const N: usize, const I: usize, const R: usize> {
a_regions: [[[S; N]; N]; R],
b_regions: [[[S; I]; N]; R],
f_regions: [[S; N]; R],
h_coeffs: [[S; N]; R],
h_bounds: [S; R],
state: [S; N],
active_region: usize,
}
impl<S: ControlScalar, const N: usize, const I: usize, const R: usize> PwaSystem<S, N, I, R> {
pub fn new(
a_regions: [[[S; N]; N]; R],
b_regions: [[[S; I]; N]; R],
f_regions: [[S; N]; R],
h_coeffs: [[S; N]; R],
h_bounds: [S; R],
x0: [S; N],
) -> Result<Self, PwaError> {
if R == 0 || N == 0 {
return Err(PwaError::InvalidParameter);
}
let initial_region = (0..R)
.find(|&r| dot(&h_coeffs[r], &x0) <= h_bounds[r])
.unwrap_or(0);
Ok(Self {
a_regions,
b_regions,
f_regions,
h_coeffs,
h_bounds,
state: x0,
active_region: initial_region,
})
}
pub fn find_region(&self, x: &[S; N]) -> Option<usize> {
(0..R).find(|&r| dot(&self.h_coeffs[r], x) <= self.h_bounds[r])
}
pub fn step(&mut self, u: &[S; I]) -> Result<(usize, [S; N]), PwaError> {
let r = self
.find_region(&self.state)
.ok_or(PwaError::NoActiveRegion)?;
self.active_region = r;
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_regions[r][i][j] * *x_j;
}
for (k, u_k) in u.iter().enumerate() {
acc += self.b_regions[r][i][k] * *u_k;
}
acc += self.f_regions[r][i];
*x_new_i = acc;
}
self.state = x_new;
if let Some(new_r) = self.find_region(&self.state) {
self.active_region = new_r;
}
Ok((r, self.state))
}
#[inline]
pub fn state(&self) -> &[S; N] {
&self.state
}
#[inline]
pub fn active_region(&self) -> usize {
self.active_region
}
}
pub struct PwaController<S, const N: usize, const I: usize, const R: usize> {
k_gains: [[[S; N]; I]; R],
h_coeffs: [[S; N]; R],
h_bounds: [S; R],
}
impl<S: ControlScalar, const N: usize, const I: usize, const R: usize> PwaController<S, N, I, R> {
pub fn new(
k_gains: [[[S; N]; I]; R],
h_coeffs: [[S; N]; R],
h_bounds: [S; R],
) -> Result<Self, PwaError> {
if R == 0 || N == 0 {
return Err(PwaError::InvalidParameter);
}
Ok(Self {
k_gains,
h_coeffs,
h_bounds,
})
}
pub fn find_region(&self, x: &[S; N]) -> Option<usize> {
(0..R).find(|&r| dot(&self.h_coeffs[r], x) <= self.h_bounds[r])
}
pub fn control(&self, x: &[S; N]) -> Result<[S; I], PwaError> {
let r = self.find_region(x).ok_or(PwaError::NoActiveRegion)?;
let mut u = [S::ZERO; I];
for (i, u_i) in u.iter_mut().enumerate() {
*u_i = -dot(&self.k_gains[r][i], x);
}
Ok(u)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_1s1i2r() -> PwaSystem<f64, 1, 1, 2> {
let a0: [[f64; 1]; 1] = [[0.5_f64]];
let a1: [[f64; 1]; 1] = [[0.8_f64]];
let b0: [[f64; 1]; 1] = [[1.0_f64]];
let b1: [[f64; 1]; 1] = [[1.0_f64]];
let f0: [f64; 1] = [0.0_f64];
let f1: [f64; 1] = [0.0_f64];
let h_coeffs: [[f64; 1]; 2] = [[1.0_f64], [-1.0_f64]];
let h_bounds: [f64; 2] = [0.0_f64, 1e12_f64];
PwaSystem::new([a0, a1], [b0, b1], [f0, f1], h_coeffs, h_bounds, [-1.0_f64]).unwrap()
}
#[test]
fn region_selection_correct() {
let sys = make_1s1i2r();
assert_eq!(sys.find_region(&[-1.0_f64]), Some(0));
assert_eq!(sys.find_region(&[1.0_f64]), Some(1));
}
#[test]
fn two_region_step_different_dynamics() {
let mut sys = make_1s1i2r();
let (r, state) = sys.step(&[0.0_f64]).unwrap();
assert_eq!(r, 0, "Should be in region 0");
assert!((state[0] - (-0.5)).abs() < 1e-12);
let (r2, state2) = sys.step(&[0.0_f64]).unwrap();
assert_eq!(r2, 0);
assert!((state2[0] - (-0.25)).abs() < 1e-12);
}
#[test]
fn region1_dynamics_applied() {
let a0: [[f64; 1]; 1] = [[0.5_f64]];
let a1: [[f64; 1]; 1] = [[0.8_f64]];
let b0: [[f64; 1]; 1] = [[1.0_f64]];
let b1: [[f64; 1]; 1] = [[1.0_f64]];
let f0: [f64; 1] = [0.0_f64];
let f1: [f64; 1] = [0.0_f64];
let h_coeffs: [[f64; 1]; 2] = [[1.0_f64], [-1.0_f64]];
let h_bounds: [f64; 2] = [0.0_f64, 1e12_f64];
let mut sys =
PwaSystem::new([a0, a1], [b0, b1], [f0, f1], h_coeffs, h_bounds, [1.0_f64]).unwrap();
let (r, state) = sys.step(&[0.0_f64]).unwrap();
assert_eq!(r, 1, "Should be in region 1 for x=1");
assert!((state[0] - 0.8).abs() < 1e-12, "x_new should be 0.8*1.0");
}
#[test]
fn affine_offset_applied() {
let a: [[[f64; 1]; 1]; 2] = [[[0.0_f64]], [[0.0_f64]]];
let b: [[[f64; 1]; 1]; 2] = [[[0.0_f64]], [[0.0_f64]]];
let f: [[f64; 1]; 2] = [[2.0_f64], [5.0_f64]];
let h: [[f64; 1]; 2] = [[1.0_f64], [-1.0_f64]];
let hb: [f64; 2] = [0.0_f64, 1e12];
let mut sys = PwaSystem::new(a, b, f, h, hb, [-1.0_f64]).unwrap();
let (r, state) = sys.step(&[0.0_f64]).unwrap();
assert_eq!(r, 0);
assert!(
(state[0] - 2.0).abs() < 1e-12,
"Affine offset f[0]=2 should be added"
);
}
#[test]
fn no_active_region_returns_error() {
let a: [[[f64; 1]; 1]; 2] = [[[0.5_f64]], [[0.5_f64]]];
let b: [[[f64; 1]; 1]; 2] = [[[1.0_f64]], [[1.0_f64]]];
let f: [[f64; 1]; 2] = [[0.0_f64], [0.0_f64]];
let h: [[f64; 1]; 2] = [[1.0_f64], [1.0_f64]];
let hb: [f64; 2] = [-1e12_f64, -1e12_f64];
let mut sys = PwaSystem::new(a, b, f, h, hb, [0.0_f64]).unwrap();
let err = sys.step(&[0.0_f64]);
assert_eq!(err.err(), Some(PwaError::NoActiveRegion));
}
fn make_ctrl_2s1i2r() -> PwaController<f64, 2, 1, 2> {
let k0: [[f64; 2]; 1] = [[1.0_f64, 0.0_f64]];
let k1: [[f64; 2]; 1] = [[0.0_f64, 2.0_f64]];
let h: [[f64; 2]; 2] = [[1.0_f64, 0.0_f64], [-1.0_f64, 0.0_f64]];
let hb: [f64; 2] = [0.0_f64, 1e12_f64];
PwaController::new([k0, k1], h, hb).unwrap()
}
#[test]
fn pwa_controller_picks_right_gain() {
let ctrl = make_ctrl_2s1i2r();
let u = ctrl.control(&[-2.0, 5.0]).unwrap();
assert!((u[0] - 2.0).abs() < 1e-12, "Expected u=2.0, got {}", u[0]);
}
#[test]
fn pwa_controller_region1_gain() {
let ctrl = make_ctrl_2s1i2r();
let u = ctrl.control(&[3.0, 4.0]).unwrap();
assert!(
(u[0] - (-8.0)).abs() < 1e-12,
"Expected u=-8.0, got {}",
u[0]
);
}
#[test]
fn pwa_controller_no_region_error() {
let k: [[[f64; 1]; 1]; 2] = [[[1.0_f64]], [[1.0_f64]]];
let h: [[f64; 1]; 2] = [[1.0_f64], [1.0_f64]];
let hb: [f64; 2] = [-1e12_f64, -1e12_f64];
let ctrl = PwaController::<f64, 1, 1, 2>::new(k, h, hb).unwrap();
assert_eq!(
ctrl.control(&[0.0_f64]).err(),
Some(PwaError::NoActiveRegion)
);
}
#[test]
fn pwa_system_active_region_tracks() {
let mut sys = make_1s1i2r();
sys.step(&[0.0_f64]).unwrap();
assert_eq!(sys.active_region(), 0);
}
#[test]
fn pwa_controller_multi_input() {
let k0: [[f64; 2]; 2] = [[1.0_f64, 0.0_f64], [0.0_f64, 1.0_f64]];
let h: [[f64; 2]; 1] = [[-1.0_f64, 0.0_f64]]; let hb: [f64; 1] = [1e12_f64];
let ctrl = PwaController::<f64, 2, 2, 1>::new([k0], h, hb).unwrap();
let u = ctrl.control(&[3.0_f64, 5.0_f64]).unwrap();
assert!((u[0] - (-3.0)).abs() < 1e-12);
assert!((u[1] - (-5.0)).abs() < 1e-12);
}
#[test]
fn pwa_system_input_drives_state() {
let mut sys = make_1s1i2r();
let (r, state) = sys.step(&[2.0_f64]).unwrap();
assert_eq!(r, 0, "Started in region 0");
assert!((state[0] - 1.5).abs() < 1e-12, "x_new should be 1.5");
}
}