use crate::core::scalar::ControlScalar;
use crate::koopman::lifting_functions::KoopmanError;
#[allow(clippy::needless_range_loop)]
fn mat_inv_inplace<S: ControlScalar, const L: usize>(
mat: &mut [[S; L]; L],
inv: &mut [[S; L]; L],
) -> Result<(), KoopmanError> {
for i in 0..L {
for j in 0..L {
inv[i][j] = if i == j { S::ONE } else { S::ZERO };
}
}
for col in 0..L {
let mut max_val = mat[col][col].abs();
let mut max_row = col;
for row in (col + 1)..L {
let v = mat[row][col].abs();
if v > max_val {
max_val = v;
max_row = row;
}
}
let tol = S::from_f64(1e-14);
if max_val < tol {
return Err(KoopmanError::SingularMatrix);
}
if max_row != col {
mat.swap(col, max_row);
inv.swap(col, max_row);
}
let pivot = mat[col][col];
for j in 0..L {
mat[col][j] = mat[col][j] / pivot;
inv[col][j] = inv[col][j] / pivot;
}
for row in 0..L {
if row == col {
continue;
}
let factor = mat[row][col];
if factor == S::ZERO {
continue;
}
for j in 0..L {
let m_col_j = mat[col][j];
let i_col_j = inv[col][j];
mat[row][j] -= factor * m_col_j;
inv[row][j] -= factor * i_col_j;
}
}
}
Ok(())
}
fn mat_mul<S: ControlScalar, const L: usize>(a: &[[S; L]; L], b: &[[S; L]; L]) -> [[S; L]; L] {
let mut c = [[S::ZERO; L]; L];
for i in 0..L {
for k in 0..L {
if a[i][k] == S::ZERO {
continue;
}
for j in 0..L {
c[i][j] += a[i][k] * b[k][j];
}
}
}
c
}
#[derive(Clone, Debug)]
pub struct Edmd<S, const L: usize, const DATA: usize> {
k_matrix: [[S; L]; L],
fitted: bool,
}
impl<S: ControlScalar, const L: usize, const DATA: usize> Edmd<S, L, DATA> {
pub fn new() -> Self {
Self {
k_matrix: [[S::ZERO; L]; L],
fitted: false,
}
}
pub fn fit(
&mut self,
psi_x: &[[S; L]; DATA],
psi_x_next: &[[S; L]; DATA],
) -> Result<(), KoopmanError> {
if DATA < L {
return Err(KoopmanError::InsufficientData);
}
let mut a_mat = [[S::ZERO; L]; L];
let mut b_mat = [[S::ZERO; L]; L];
for k in 0..DATA {
for i in 0..L {
for j in 0..L {
a_mat[i][j] += psi_x_next[k][i] * psi_x[k][j];
b_mat[i][j] += psi_x[k][i] * psi_x[k][j];
}
}
}
let mut b_copy = b_mat;
let mut b_inv = [[S::ZERO; L]; L];
mat_inv_inplace::<S, L>(&mut b_copy, &mut b_inv)?;
self.k_matrix = mat_mul::<S, L>(&a_mat, &b_inv);
self.fitted = true;
Ok(())
}
#[allow(clippy::needless_range_loop)]
pub fn predict(&self, psi_x: &[S; L]) -> Result<[S; L], KoopmanError> {
if !self.fitted {
return Err(KoopmanError::NotFitted);
}
let mut out = [S::ZERO; L];
for i in 0..L {
for j in 0..L {
out[i] += self.k_matrix[i][j] * psi_x[j];
}
}
Ok(out)
}
pub fn k_matrix(&self) -> &[[S; L]; L] {
&self.k_matrix
}
pub fn reconstruction_error(
&self,
psi_x: &[[S; L]; DATA],
psi_x_next: &[[S; L]; DATA],
) -> Result<S, KoopmanError> {
if !self.fitted {
return Err(KoopmanError::NotFitted);
}
let mut total = S::ZERO;
for k in 0..DATA {
let pred = self.predict(&psi_x[k])?;
for i in 0..L {
let diff = pred[i] - psi_x_next[k][i];
total += diff * diff;
}
}
let n = S::from_f64((DATA * L) as f64);
Ok(total / n)
}
#[allow(clippy::needless_range_loop)]
pub fn eigenvalues_real_part(&self) -> [S; L] {
let mut ev = [S::ZERO; L];
for i in 0..L {
ev[i] = self.k_matrix[i][i];
}
ev
}
pub fn is_fitted(&self) -> bool {
self.fitted
}
}
impl<S: ControlScalar, const L: usize, const DATA: usize> Default for Edmd<S, L, DATA> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn linear_scalar_data<const DATA: usize>(a: f64) -> ([[f64; 1]; DATA], [[f64; 1]; DATA]) {
let mut psi = [[0.0_f64; 1]; DATA];
let mut psi_next = [[0.0_f64; 1]; DATA];
for k in 0..DATA {
let x = (k as f64) * 0.1 + 0.1; psi[k][0] = x;
psi_next[k][0] = a * x;
}
(psi, psi_next)
}
#[test]
fn edmd_recovers_linear_scalar_map() {
let (psi, psi_next) = linear_scalar_data::<10>(2.0);
let mut edmd: Edmd<f64, 1, 10> = Edmd::new();
edmd.fit(&psi, &psi_next).expect("fit failed");
let k = edmd.k_matrix();
assert!((k[0][0] - 2.0).abs() < 1e-8, "K[0][0]={}", k[0][0]);
}
#[test]
fn edmd_prediction_error_small_on_training_data() {
let a = 0.8_f64;
let (psi, psi_next) = linear_scalar_data::<20>(a);
let mut edmd: Edmd<f64, 1, 20> = Edmd::new();
edmd.fit(&psi, &psi_next).expect("fit failed");
let mut total_err = 0.0_f64;
for k in 0..20 {
let pred = edmd.predict(&psi[k]).expect("predict failed");
let err = (pred[0] - psi_next[k][0]).abs();
total_err += err;
}
assert!(total_err < 1e-8, "total_err={}", total_err);
}
#[test]
fn edmd_not_fitted_returns_error() {
let edmd: Edmd<f64, 2, 5> = Edmd::new();
let psi = [1.0_f64, 0.0];
let result = edmd.predict(&psi);
assert!(matches!(result, Err(KoopmanError::NotFitted)));
}
#[test]
fn edmd_k_matrix_recovers_half_identity() {
let psi: [[f64; 2]; 5] = [[1.0, 0.0], [0.0, 1.0], [1.0, 1.0], [2.0, 0.0], [0.0, 2.0]];
let psi_next: [[f64; 2]; 5] = [[0.5, 0.0], [0.0, 0.5], [0.5, 0.5], [1.0, 0.0], [0.0, 1.0]];
let mut edmd: Edmd<f64, 2, 5> = Edmd::new();
edmd.fit(&psi, &psi_next).expect("fit failed");
let k = edmd.k_matrix();
assert!((k[0][0] - 0.5).abs() < 1e-8, "K[0][0]={}", k[0][0]);
assert!((k[1][1] - 0.5).abs() < 1e-8, "K[1][1]={}", k[1][1]);
assert!(k[0][1].abs() < 1e-8);
assert!(k[1][0].abs() < 1e-8);
}
#[test]
fn edmd_insufficient_data_error() {
let psi: [[f64; 3]; 2] = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let psi_next: [[f64; 3]; 2] = [[0.5, 0.0, 0.0], [0.0, 0.5, 0.0]];
let mut edmd: Edmd<f64, 3, 2> = Edmd::new();
let result = edmd.fit(&psi, &psi_next);
assert!(matches!(result, Err(KoopmanError::InsufficientData)));
}
#[test]
fn edmd_singular_gram_matrix_error() {
let psi: [[f64; 2]; 4] = [[1.0, 1.0]; 4];
let psi_next: [[f64; 2]; 4] = [[2.0, 2.0]; 4];
let mut edmd: Edmd<f64, 2, 4> = Edmd::new();
let result = edmd.fit(&psi, &psi_next);
assert!(matches!(result, Err(KoopmanError::SingularMatrix)));
}
#[test]
fn edmd_reconstruction_error_approx_zero_on_training_data() {
let a = 1.5_f64;
let (psi, psi_next) = linear_scalar_data::<15>(a);
let mut edmd: Edmd<f64, 1, 15> = Edmd::new();
edmd.fit(&psi, &psi_next).expect("fit failed");
let err = edmd
.reconstruction_error(&psi, &psi_next)
.expect("recon error failed");
assert!(err < 1e-12, "reconstruction error={}", err);
}
#[test]
fn edmd_reconstruction_error_not_fitted_error() {
let psi: [[f64; 1]; 5] = [[1.0]; 5];
let psi_next: [[f64; 1]; 5] = [[2.0]; 5];
let edmd: Edmd<f64, 1, 5> = Edmd::new();
let result = edmd.reconstruction_error(&psi, &psi_next);
assert!(matches!(result, Err(KoopmanError::NotFitted)));
}
#[test]
fn edmd_eigenvalues_real_part_returns_diagonal() {
let a = 0.5_f64;
let (psi, psi_next) = linear_scalar_data::<10>(a);
let mut edmd: Edmd<f64, 1, 10> = Edmd::new();
edmd.fit(&psi, &psi_next).expect("fit failed");
let ev = edmd.eigenvalues_real_part();
assert!((ev[0] - a).abs() < 1e-8, "ev[0]={}", ev[0]);
}
}