use crate::core::scalar::ControlScalar;
use crate::koopman::lifting_functions::KoopmanError;
#[derive(Clone, Debug)]
pub struct KoopmanGreedyMpc<S, const L: usize, const I: usize> {
k_mat: [[S; L]; L],
b_u: [[S; I]; L],
c_out: [S; L],
q: S,
r_w: S,
u_min: [S; I],
u_max: [S; I],
}
impl<S: ControlScalar, const L: usize, const I: usize> KoopmanGreedyMpc<S, L, I> {
pub fn new(
k_mat: [[S; L]; L],
b_u: [[S; I]; L],
c_out: [S; L],
q: S,
r_w: S,
u_min: [S; I],
u_max: [S; I],
) -> Result<Self, KoopmanError> {
if q <= S::ZERO || r_w <= S::ZERO {
return Err(KoopmanError::InvalidParameter);
}
for i in 0..I {
if u_min[i] > u_max[i] {
return Err(KoopmanError::InvalidParameter);
}
}
Ok(Self {
k_mat,
b_u,
c_out,
q,
r_w,
u_min,
u_max,
})
}
#[allow(clippy::needless_range_loop)]
pub fn control(&self, psi: &[S; L], y_ref: S) -> Result<[S; I], KoopmanError> {
let mut k_psi = [S::ZERO; L];
for row in 0..L {
for col in 0..L {
k_psi[row] += self.k_mat[row][col] * psi[col];
}
}
let mut y_pred = S::ZERO;
for l in 0..L {
y_pred += self.c_out[l] * k_psi[l];
}
let error = y_ref - y_pred;
let mut u_out = [S::ZERO; I];
for i in 0..I {
let mut cb_i = S::ZERO;
for l in 0..L {
cb_i += self.c_out[l] * self.b_u[l][i];
}
let numerator = self.q * cb_i * error;
let denominator = self.q * cb_i * cb_i + self.r_w;
let u_star = numerator / denominator;
u_out[i] = u_star.clamp_val(self.u_min[i], self.u_max[i]);
}
Ok(u_out)
}
pub fn k_matrix(&self) -> &[[S; L]; L] {
&self.k_mat
}
pub fn b_matrix(&self) -> &[[S; I]; L] {
&self.b_u
}
pub fn c_out(&self) -> &[S; L] {
&self.c_out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn simple_mpc() -> KoopmanGreedyMpc<f64, 1, 1> {
let k_mat = [[1.0_f64]];
let b_u = [[1.0_f64]];
let c_out = [1.0_f64];
KoopmanGreedyMpc::new(k_mat, b_u, c_out, 1.0, 1.0, [-10.0], [10.0]).expect("new")
}
#[test]
fn zero_error_near_zero_control() {
let mpc = simple_mpc();
let psi = [3.0_f64];
let u = mpc.control(&psi, 3.0).expect("control");
assert!(u[0].abs() < 1e-12, "u[0]={}", u[0]);
}
#[test]
fn control_sign_correct() {
let mpc = simple_mpc();
let psi = [3.0_f64];
let u = mpc.control(&psi, 5.0).expect("control");
assert!(u[0] > 0.0, "expected positive control, got {}", u[0]);
}
#[test]
fn saturation_clamps_to_u_max() {
let k_mat = [[1.0_f64]];
let b_u = [[1.0_f64]];
let c_out = [1.0_f64];
let mpc = KoopmanGreedyMpc::new(k_mat, b_u, c_out, 1e9, 1.0, [-1.0], [1.0]).expect("new");
let psi = [0.0_f64];
let u = mpc.control(&psi, 100.0).expect("control");
assert!(
(u[0] - 1.0).abs() < 1e-9,
"u should be clamped to 1.0, got {}",
u[0]
);
}
#[test]
fn saturation_clamps_to_u_min() {
let k_mat = [[1.0_f64]];
let b_u = [[1.0_f64]];
let c_out = [1.0_f64];
let mpc = KoopmanGreedyMpc::new(k_mat, b_u, c_out, 1e9, 1.0, [-1.0], [1.0]).expect("new");
let psi = [0.0_f64];
let u = mpc.control(&psi, -100.0).expect("control");
assert!(
(u[0] - (-1.0)).abs() < 1e-9,
"u should be clamped to -1.0, got {}",
u[0]
);
}
#[test]
fn invalid_q_returns_error() {
let result = KoopmanGreedyMpc::<f64, 1, 1>::new(
[[1.0]],
[[1.0]],
[1.0],
0.0, 1.0,
[-1.0],
[1.0],
);
assert!(matches!(result, Err(KoopmanError::InvalidParameter)));
}
#[test]
fn invalid_r_returns_error() {
let result = KoopmanGreedyMpc::<f64, 1, 1>::new(
[[1.0]],
[[1.0]],
[1.0],
1.0,
0.0, [-1.0],
[1.0],
);
assert!(matches!(result, Err(KoopmanError::InvalidParameter)));
}
#[test]
fn u_min_greater_than_u_max_returns_error() {
let result = KoopmanGreedyMpc::<f64, 1, 1>::new(
[[1.0]],
[[1.0]],
[1.0],
1.0,
1.0,
[5.0], [1.0],
);
assert!(matches!(result, Err(KoopmanError::InvalidParameter)));
}
#[test]
fn multi_input_independent_channels() {
let k_mat = [[1.0_f64, 0.0], [0.0, 1.0]];
let b_u = [[1.0_f64, 0.0], [0.0, 1.0]];
let c_out = [1.0_f64, 0.0]; let mpc = KoopmanGreedyMpc::new(k_mat, b_u, c_out, 1.0, 1.0, [-10.0, -10.0], [10.0, 10.0])
.expect("new");
let psi = [2.0_f64, 0.0];
let u = mpc.control(&psi, 4.0).expect("control");
assert!((u[0] - 1.0).abs() < 1e-12, "u[0]={}", u[0]);
assert!(u[1].abs() < 1e-12, "u[1]={}", u[1]);
}
}