use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KoopmanError {
NotFitted,
InsufficientData,
SingularMatrix,
InvalidDimension,
InvalidParameter,
}
impl core::fmt::Display for KoopmanError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
KoopmanError::NotFitted => write!(f, "model is not fitted"),
KoopmanError::InsufficientData => write!(f, "insufficient data"),
KoopmanError::SingularMatrix => write!(f, "matrix is singular"),
KoopmanError::InvalidDimension => write!(f, "invalid dimension"),
KoopmanError::InvalidParameter => write!(f, "invalid parameter"),
}
}
}
pub trait LiftingMap<S, const N: usize, const L: usize>: Clone {
fn lift(&self, x: &[S; N]) -> [S; L];
#[inline]
fn output_dim(&self) -> usize {
L
}
}
#[derive(Clone, Debug)]
pub struct PolynomialLifting<S, const N: usize, const L: usize> {
_marker: core::marker::PhantomData<S>,
}
impl<S: ControlScalar, const N: usize, const L: usize> PolynomialLifting<S, N, L> {
pub fn new() -> Self {
Self {
_marker: core::marker::PhantomData,
}
}
#[allow(clippy::needless_range_loop)]
pub fn lift(&self, x: &[S; N]) -> [S; L] {
let mut out = [S::ZERO; L];
let linear_end = N.min(L);
out[..linear_end].copy_from_slice(&x[..linear_end]);
let mut idx = N;
'outer: for i in 0..N {
for j in i..N {
if idx >= L {
break 'outer;
}
out[idx] = x[i] * x[j];
idx += 1;
}
}
out
}
}
impl<S: ControlScalar, const N: usize, const L: usize> Default for PolynomialLifting<S, N, L> {
fn default() -> Self {
Self::new()
}
}
impl<S: ControlScalar, const N: usize, const L: usize> LiftingMap<S, N, L>
for PolynomialLifting<S, N, L>
{
fn lift(&self, x: &[S; N]) -> [S; L] {
PolynomialLifting::lift(self, x)
}
}
#[derive(Clone, Debug)]
pub struct RbfLifting<S, const N: usize, const K: usize, const L: usize> {
centers: [[S; N]; K],
sigma: S,
}
impl<S: ControlScalar, const N: usize, const K: usize, const L: usize> RbfLifting<S, N, K, L> {
pub fn new(centers: [[S; N]; K], sigma: S) -> Result<Self, KoopmanError> {
if sigma <= S::ZERO {
return Err(KoopmanError::InvalidParameter);
}
Ok(Self { centers, sigma })
}
#[allow(clippy::needless_range_loop)]
pub fn lift(&self, x: &[S; N]) -> [S; L] {
let mut out = [S::ZERO; L];
let linear_end = N.min(L);
out[..linear_end].copy_from_slice(&x[..linear_end]);
let sigma_f64 = self.sigma.to_f64();
let denom = 2.0_f64 * sigma_f64 * sigma_f64;
for k in 0..K {
let slot = N + k;
if slot >= L {
break;
}
let mut dist_sq = 0.0_f64;
for i in 0..N {
let diff = (x[i] - self.centers[k][i]).to_f64();
dist_sq += diff * diff;
}
out[slot] = S::from_f64(libm::exp(-dist_sq / denom));
}
out
}
}
impl<S: ControlScalar, const N: usize, const K: usize, const L: usize> LiftingMap<S, N, L>
for RbfLifting<S, N, K, L>
{
fn lift(&self, x: &[S; N]) -> [S; L] {
RbfLifting::lift(self, x)
}
}
#[derive(Clone, Debug)]
pub struct DelayEmbedding<S, const N: usize, const D: usize, const L: usize> {
history: [[S; N]; D],
head: usize,
filled: usize,
}
impl<S: ControlScalar, const N: usize, const D: usize, const L: usize> DelayEmbedding<S, N, D, L> {
pub fn new() -> Self {
Self {
history: [[S::ZERO; N]; D],
head: 0,
filled: 0,
}
}
pub fn push(&mut self, x: &[S; N]) -> [S; L] {
self.history[self.head] = *x;
self.head = (self.head + 1) % D;
if self.filled < D {
self.filled += 1;
}
let oldest = (self.head + D - self.filled) % D;
let mut out = [S::ZERO; L];
for slot in 0..self.filled {
let ring_idx = (oldest + slot) % D;
let base = slot * N;
for i in 0..N {
if base + i < L {
out[base + i] = self.history[ring_idx][i];
}
}
}
out
}
pub fn reset(&mut self) {
self.history = [[S::ZERO; N]; D];
self.head = 0;
self.filled = 0;
}
}
impl<S: ControlScalar, const N: usize, const D: usize, const L: usize> Default
for DelayEmbedding<S, N, D, L>
{
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn polynomial_first_n_slots_equal_x() {
let lift: PolynomialLifting<f64, 3, 9> = PolynomialLifting::new();
let x = [1.0_f64, 2.0, 3.0];
let out = lift.lift(&x);
assert!((out[0] - 1.0).abs() < 1e-12, "slot 0");
assert!((out[1] - 2.0).abs() < 1e-12, "slot 1");
assert!((out[2] - 3.0).abs() < 1e-12, "slot 2");
}
#[test]
fn polynomial_quadratic_terms_correct() {
let lift: PolynomialLifting<f64, 2, 6> = PolynomialLifting::new();
let x = [2.0_f64, 3.0];
let out = lift.lift(&x);
assert!((out[2] - 4.0).abs() < 1e-12, "x0*x0: {}", out[2]);
assert!((out[3] - 6.0).abs() < 1e-12, "x0*x1: {}", out[3]);
assert!((out[4] - 9.0).abs() < 1e-12, "x1*x1: {}", out[4]);
}
#[test]
fn polynomial_truncated_when_l_smaller() {
let lift: PolynomialLifting<f64, 3, 4> = PolynomialLifting::new();
let x = [1.0_f64, 2.0, 3.0];
let out = lift.lift(&x);
assert!((out[0] - 1.0).abs() < 1e-12);
assert!((out[1] - 2.0).abs() < 1e-12);
assert!((out[2] - 3.0).abs() < 1e-12);
assert!((out[3] - 1.0).abs() < 1e-12, "x[0]*x[0]=1: {}", out[3]);
}
#[test]
fn polynomial_output_dim_matches_l() {
let lift: PolynomialLifting<f64, 2, 6> = PolynomialLifting::new();
assert_eq!(lift.output_dim(), 6);
}
#[test]
fn rbf_at_center_equals_one() {
let centers = [[1.0_f64, 2.0]];
let lift: RbfLifting<f64, 2, 1, 3> = RbfLifting::new(centers, 1.0).expect("new");
let x = [1.0_f64, 2.0];
let out = lift.lift(&x);
assert!((out[2] - 1.0).abs() < 1e-12, "rbf at center: {}", out[2]);
}
#[test]
fn rbf_far_from_center_near_zero() {
let centers = [[0.0_f64, 0.0]];
let lift: RbfLifting<f64, 2, 1, 3> = RbfLifting::new(centers, 1.0).expect("new");
let x = [100.0_f64, 100.0];
let out = lift.lift(&x);
assert!(out[2] < 1e-10, "rbf far from center: {}", out[2]);
}
#[test]
fn rbf_invalid_sigma_returns_error() {
let result = RbfLifting::<f64, 2, 1, 3>::new([[0.0, 0.0]], 0.0);
assert!(matches!(result, Err(KoopmanError::InvalidParameter)));
}
#[test]
fn rbf_linear_slots_preserved() {
let centers = [[0.0_f64]];
let lift: RbfLifting<f64, 1, 1, 2> = RbfLifting::new(centers, 1.0).expect("new");
let x = [5.0_f64];
let out = lift.lift(&x);
assert!((out[0] - 5.0).abs() < 1e-12);
}
#[test]
fn delay_push_fills_correctly() {
let mut emb: DelayEmbedding<f64, 2, 3, 6> = DelayEmbedding::new();
let _ = emb.push(&[1.0, 2.0]);
let _ = emb.push(&[3.0, 4.0]);
let out = emb.push(&[5.0, 6.0]);
assert!((out[0] - 1.0).abs() < 1e-12, "out[0]={}", out[0]);
assert!((out[1] - 2.0).abs() < 1e-12, "out[1]={}", out[1]);
assert!((out[2] - 3.0).abs() < 1e-12, "out[2]={}", out[2]);
assert!((out[3] - 4.0).abs() < 1e-12, "out[3]={}", out[3]);
assert!((out[4] - 5.0).abs() < 1e-12, "out[4]={}", out[4]);
assert!((out[5] - 6.0).abs() < 1e-12, "out[5]={}", out[5]);
}
#[test]
fn delay_reset_clears() {
let mut emb: DelayEmbedding<f64, 2, 3, 6> = DelayEmbedding::new();
let _ = emb.push(&[10.0, 20.0]);
let _ = emb.push(&[30.0, 40.0]);
emb.reset();
let out = emb.push(&[0.0, 0.0]);
for v in out.iter() {
assert!(v.abs() < 1e-12, "expected 0 after reset, got {v}");
}
}
#[test]
fn delay_initial_zeros_before_filled() {
let mut emb: DelayEmbedding<f64, 2, 3, 6> = DelayEmbedding::new();
let out = emb.push(&[1.0, 2.0]);
assert!((out[0] - 1.0).abs() < 1e-12);
assert!((out[1] - 2.0).abs() < 1e-12);
assert!((out[2]).abs() < 1e-12);
assert!((out[3]).abs() < 1e-12);
}
#[test]
fn delay_circular_wrapping() {
let mut emb: DelayEmbedding<f64, 1, 2, 2> = DelayEmbedding::new();
let _ = emb.push(&[10.0]);
let _ = emb.push(&[20.0]);
let _ = emb.push(&[30.0]);
let out = emb.push(&[40.0]);
assert!((out[0] - 30.0).abs() < 1e-12, "out[0]={}", out[0]);
assert!((out[1] - 40.0).abs() < 1e-12, "out[1]={}", out[1]);
}
}