use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct MpcRegion<S: ControlScalar, const N: usize, const M: usize> {
pub gain: [[S; N]; M],
pub offset: [S; M],
pub x_min: [S; N],
pub x_max: [S; N],
}
impl<S: ControlScalar, const N: usize, const M: usize> MpcRegion<S, N, M> {
pub fn new(gain: [[S; N]; M], offset: [S; M], x_min: [S; N], x_max: [S; N]) -> Self {
Self {
gain,
offset,
x_min,
x_max,
}
}
pub fn contains(&self, x: &[S; N]) -> bool {
x.iter()
.enumerate()
.all(|(i, &xi)| xi >= self.x_min[i] && xi <= self.x_max[i])
}
pub fn compute(&self, x: &[S; N]) -> [S; M] {
core::array::from_fn(|i| {
self.offset[i]
+ self.gain[i]
.iter()
.zip(x.iter())
.fold(S::ZERO, |acc, (&g, &xi)| acc + g * xi)
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct ExplicitMpc<S: ControlScalar, const N: usize, const M: usize, const R: usize> {
regions: [Option<MpcRegion<S, N, M>>; R],
pub fallback: [S; M],
pub u_min: [S; M],
pub u_max: [S; M],
}
impl<S: ControlScalar, const N: usize, const M: usize, const R: usize> ExplicitMpc<S, N, M, R> {
pub fn new(u_min: [S; M], u_max: [S; M]) -> Self {
Self {
regions: core::array::from_fn(|_| None),
fallback: [S::ZERO; M],
u_min,
u_max,
}
}
pub fn add_region(&mut self, region: MpcRegion<S, N, M>) -> bool {
for slot in self.regions.iter_mut() {
if slot.is_none() {
*slot = Some(region);
return true;
}
}
false
}
pub fn compute(&self, x: &[S; N]) -> [S; M] {
for region in self.regions.iter().flatten() {
if region.contains(x) {
let u = region.compute(x);
return core::array::from_fn(|i| u[i].clamp_val(self.u_min[i], self.u_max[i]));
}
}
self.fallback
}
pub fn region_count(&self) -> usize {
self.regions.iter().filter(|r| r.is_some()).count()
}
}
pub fn build_scalar_explicit_mpc<S: ControlScalar>(
gain_k: S,
x_min: S,
x_max: S,
u_min: S,
u_max: S,
) -> ExplicitMpc<S, 1, 1, 2> {
let mut empc = ExplicitMpc::new([u_min], [u_max]);
let r1 = MpcRegion::new([[-gain_k]], [S::ZERO], [S::ZERO], [x_max]);
let r2 = MpcRegion::new([[-gain_k]], [S::ZERO], [x_min], [S::ZERO]);
empc.add_region(r1);
empc.add_region(r2);
empc
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn region_contains_check() {
let region = MpcRegion::new([[-1.0_f64]], [0.0], [-5.0], [5.0]);
assert!(region.contains(&[0.0_f64]));
assert!(region.contains(&[-5.0_f64]));
assert!(!region.contains(&[6.0_f64]));
}
#[test]
fn explicit_mpc_computes_correctly() {
let empc = build_scalar_explicit_mpc(0.5_f64, -10.0, 10.0, -1.0, 1.0);
let u = empc.compute(&[2.0_f64]);
assert!((u[0] - (-1.0)).abs() < 1e-10, "u={:.4}", u[0]);
let u2 = empc.compute(&[-1.0_f64]);
assert!((u2[0] - 0.5).abs() < 1e-10, "u2={:.4}", u2[0]);
}
#[test]
fn explicit_mpc_fallback_on_no_match() {
let mut empc = ExplicitMpc::<f64, 1, 1, 1>::new([-1.0], [1.0]);
let r = MpcRegion::new([[-0.5]], [0.0], [0.0], [5.0]);
empc.add_region(r);
empc.fallback = [0.0];
let u = empc.compute(&[-2.0_f64]);
assert_eq!(u[0], 0.0);
}
#[test]
fn region_count_correct() {
let empc = build_scalar_explicit_mpc(0.5_f64, -10.0, 10.0, -1.0, 1.0);
assert_eq!(empc.region_count(), 2);
}
#[test]
fn explicit_mpc_stabilizes_first_order() {
let empc = build_scalar_explicit_mpc(0.5_f64, -100.0, 100.0, -5.0, 5.0);
let mut x = 3.0_f64;
for _ in 0..50 {
let u = empc.compute(&[x]);
x = 0.9 * x + u[0];
}
assert!(x.abs() < 0.1, "x={:.4} should converge to 0", x);
}
}