#![allow(clippy::too_many_arguments)]
use crate::core::matrix::vec_dot;
use crate::core::scalar::ControlScalar;
pub struct StorageFunction<S: ControlScalar, const N: usize> {
pub evaluate: fn(&[S; N]) -> S,
pub gradient: fn(&[S; N]) -> [S; N],
}
impl<S: ControlScalar, const N: usize> StorageFunction<S, N> {
pub fn new(evaluate: fn(&[S; N]) -> S, gradient: fn(&[S; N]) -> [S; N]) -> Self {
Self { evaluate, gradient }
}
pub fn time_derivative<F>(&self, x: &[S; N], f: F) -> S
where
F: Fn(&[S; N]) -> [S; N],
{
let grad = (self.gradient)(x);
let xdot = f(x);
vec_dot(&grad, &xdot)
}
}
#[inline]
fn grid_point<S: ControlScalar, const N: usize>(
idx: usize,
n_pts: usize,
lo: &[S; N],
hi: &[S; N],
) -> [S; N] {
let mut rem = idx;
let mut out = [S::ZERO; N];
for i in (0..N).rev() {
let digit = rem % n_pts;
rem /= n_pts;
let t = S::from_f64(digit as f64 / (n_pts - 1).max(1) as f64);
out[i] = lo[i] + t * (hi[i] - lo[i]);
}
out
}
#[inline]
fn grid_total(n_pts: usize, dim: usize) -> Option<usize> {
let mut total: usize = 1;
for _ in 0..dim {
total = total.checked_mul(n_pts)?;
}
Some(total)
}
#[inline]
fn is_near_zero<S: ControlScalar, const N: usize>(x: &[S; N], tol: S) -> bool {
for &xi in x.iter() {
if xi.abs() > tol {
return false;
}
}
true
}
pub struct PassivityVerifier<S: ControlScalar, const N: usize> {
pub zero_tol: S,
}
impl<S: ControlScalar, const N: usize> PassivityVerifier<S, N> {
pub fn new(zero_tol: S) -> Self {
Self { zero_tol }
}
pub fn verify_positive_definite(
&self,
v: &StorageFunction<S, N>,
x_min: &[S; N],
x_max: &[S; N],
n_pts: usize,
) -> bool {
let total = match grid_total(n_pts.max(2), N) {
Some(t) => t,
None => return false, };
for idx in 0..total {
let x = grid_point(idx, n_pts.max(2), x_min, x_max);
if is_near_zero(&x, self.zero_tol) {
continue;
}
let val = (v.evaluate)(&x);
if !val.is_finite() || val <= S::ZERO {
return false;
}
}
true
}
pub fn verify_dissipation<Fdyn, Fout, Fu>(
&self,
v: &StorageFunction<S, N>,
dynamics: Fdyn,
output: Fout,
u_law: Fu,
x_min: &[S; N],
x_max: &[S; N],
n_pts: usize,
) -> bool
where
Fdyn: Fn(&[S; N], &[S; N]) -> [S; N], Fout: Fn(&[S; N]) -> [S; N], Fu: Fn(&[S; N]) -> [S; N], {
let total = match grid_total(n_pts.max(2), N) {
Some(t) => t,
None => return false,
};
for idx in 0..total {
let x = grid_point(idx, n_pts.max(2), x_min, x_max);
let u = u_law(&x);
let y = output(&x);
let xdot = dynamics(&x, &u);
let grad = (v.gradient)(&x);
let vdot = vec_dot(&grad, &xdot);
let supply = vec_dot(&u, &y);
let slack = S::from_f64(1e-8);
if vdot > supply + slack {
return false;
}
}
true
}
}
pub struct LyapunovStabilityCheck<S: ControlScalar, const N: usize> {
pub epsilon: S,
}
impl<S: ControlScalar, const N: usize> LyapunovStabilityCheck<S, N> {
pub fn new(epsilon: S) -> Self {
Self { epsilon }
}
pub fn check_asymptotic_stability<F>(
&self,
closed_loop_dynamics: F,
v: &StorageFunction<S, N>,
n_pts: usize,
) -> bool
where
F: Fn(&[S; N]) -> [S; N],
{
let eps = self.epsilon;
let lo: [S; N] = core::array::from_fn(|_| -eps);
let hi: [S; N] = core::array::from_fn(|_| eps);
let total = match grid_total(n_pts.max(2), N) {
Some(t) => t,
None => return false,
};
let zero_tol = eps * S::from_f64(1e-6);
for idx in 0..total {
let x = grid_point(idx, n_pts.max(2), &lo, &hi);
if is_near_zero(&x, zero_tol) {
continue;
}
let v_val = (v.evaluate)(&x);
if !v_val.is_finite() || v_val <= S::ZERO {
return false;
}
let grad = (v.gradient)(&x);
let xdot = closed_loop_dynamics(&x);
let vdot = vec_dot(&grad, &xdot);
if !vdot.is_finite() || vdot >= S::ZERO {
return false;
}
}
true
}
pub fn check_strict_decrease<F>(
&self,
closed_loop_dynamics: F,
v: &StorageFunction<S, N>,
n_pts: usize,
) -> bool
where
F: Fn(&[S; N]) -> [S; N],
{
let eps = self.epsilon;
let lo: [S; N] = core::array::from_fn(|_| -eps);
let hi: [S; N] = core::array::from_fn(|_| eps);
let total = match grid_total(n_pts.max(2), N) {
Some(t) => t,
None => return false,
};
let zero_tol = eps * S::from_f64(1e-6);
for idx in 0..total {
let x = grid_point(idx, n_pts.max(2), &lo, &hi);
if is_near_zero(&x, zero_tol) {
continue;
}
let grad = (v.gradient)(&x);
let xdot = closed_loop_dynamics(&x);
let vdot = vec_dot(&grad, &xdot);
if !vdot.is_finite() || vdot >= S::ZERO {
return false;
}
}
true
}
pub fn estimate_convergence_rate<F>(
&self,
closed_loop_dynamics: F,
v: &StorageFunction<S, N>,
n_pts: usize,
) -> Option<S>
where
F: Fn(&[S; N]) -> [S; N],
{
let eps = self.epsilon;
let lo: [S; N] = core::array::from_fn(|_| -eps);
let hi: [S; N] = core::array::from_fn(|_| eps);
let total = grid_total(n_pts.max(2), N)?;
let zero_tol = eps * S::from_f64(1e-6);
let mut min_rate = S::from_f64(f64::MAX);
let mut found_any = false;
for idx in 0..total {
let x = grid_point(idx, n_pts.max(2), &lo, &hi);
if is_near_zero(&x, zero_tol) {
continue;
}
let v_val = (v.evaluate)(&x);
if !v_val.is_finite() || v_val <= S::ZERO {
return None;
}
let grad = (v.gradient)(&x);
let xdot = closed_loop_dynamics(&x);
let vdot = vec_dot(&grad, &xdot);
if !vdot.is_finite() || vdot >= S::ZERO {
return None;
}
let rate = -vdot / v_val;
if rate < min_rate {
min_rate = rate;
}
found_any = true;
}
if found_any {
Some(min_rate)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn quadratic_v(x: &[f64; 2]) -> f64 {
0.5 * (x[0] * x[0] + x[1] * x[1])
}
fn quadratic_grad(x: &[f64; 2]) -> [f64; 2] {
[x[0], x[1]]
}
#[test]
fn quadratic_is_positive_definite() {
let v = StorageFunction::new(quadratic_v, quadratic_grad);
let verifier = PassivityVerifier::<f64, 2>::new(1e-8);
let lo = [-2.0f64, -2.0];
let hi = [2.0f64, 2.0];
assert!(
verifier.verify_positive_definite(&v, &lo, &hi, 7),
"Quadratic V should be positive definite"
);
}
#[test]
fn negative_v_fails_positive_definite() {
fn neg_v(x: &[f64; 2]) -> f64 {
-0.5 * (x[0] * x[0] + x[1] * x[1])
}
fn neg_grad(x: &[f64; 2]) -> [f64; 2] {
[-x[0], -x[1]]
}
let v = StorageFunction::new(neg_v, neg_grad);
let verifier = PassivityVerifier::<f64, 2>::new(1e-8);
let lo = [-1.0f64, -1.0];
let hi = [1.0f64, 1.0];
assert!(
!verifier.verify_positive_definite(&v, &lo, &hi, 5),
"Negative V should fail PD check"
);
}
#[test]
fn mass_damper_asymptotically_stable() {
let b = 2.0_f64;
let v = StorageFunction::new(|x: &[f64; 1]| 0.5 * x[0] * x[0], |x: &[f64; 1]| [x[0]]);
let dynamics = move |x: &[f64; 1]| [-b * x[0]];
let checker = LyapunovStabilityCheck::<f64, 1>::new(1.0);
assert!(
checker.check_asymptotic_stability(dynamics, &v, 9),
"Mass-damper should be asymptotically stable"
);
}
#[test]
fn undamped_not_asymptotically_stable() {
let v = StorageFunction::new(|x: &[f64; 1]| 0.5 * x[0] * x[0], |x: &[f64; 1]| [x[0]]);
let dynamics = |_x: &[f64; 1]| [0.0f64];
let checker = LyapunovStabilityCheck::<f64, 1>::new(1.0);
assert!(
!checker.check_asymptotic_stability(dynamics, &v, 9),
"Zero dynamics should fail asymptotic stability (V̇ = 0)"
);
}
#[test]
fn strict_decrease_2d() {
let v = StorageFunction::new(
|x: &[f64; 2]| 0.5 * (x[0] * x[0] + x[1] * x[1]),
|x: &[f64; 2]| [x[0], x[1]],
);
let dynamics = |x: &[f64; 2]| [-1.5 * x[0], -0.8 * x[1]];
let checker = LyapunovStabilityCheck::<f64, 2>::new(1.0);
assert!(
checker.check_strict_decrease(dynamics, &v, 7),
"Diagonal stable system should have V̇ < 0"
);
}
#[test]
fn dissipation_check_msd() {
let v = StorageFunction::new(
|x: &[f64; 2]| 0.5 * (x[0] * x[0] + x[1] * x[1]),
|x: &[f64; 2]| [x[0], x[1]],
);
let verifier = PassivityVerifier::<f64, 2>::new(1e-8);
let lo = [-2.0f64, -2.0];
let hi = [2.0f64, 2.0];
let dynamics = |x: &[f64; 2], _u: &[f64; 2]| [-x[0], -x[1]];
let output = |x: &[f64; 2]| [x[0], x[1]];
let u_law = |_x: &[f64; 2]| [0.0f64, 0.0f64];
assert!(
verifier.verify_dissipation(&v, dynamics, output, u_law, &lo, &hi, 7),
"Pure damper with u=0 should satisfy V̇ ≤ uᵀy=0"
);
}
#[test]
fn convergence_rate_estimate() {
let alpha = 3.0_f64;
let v = StorageFunction::new(|x: &[f64; 1]| 0.5 * x[0] * x[0], |x: &[f64; 1]| [x[0]]);
let dynamics = move |x: &[f64; 1]| [-alpha * x[0]];
let checker = LyapunovStabilityCheck::<f64, 1>::new(1.0);
let rate = checker
.estimate_convergence_rate(dynamics, &v, 11)
.expect("Should get a rate estimate");
assert!(
rate > alpha, "Convergence rate should be positive: {}",
rate
);
}
#[test]
fn storage_function_time_derivative() {
let v = StorageFunction::new(quadratic_v, quadratic_grad);
let x = [1.0f64, 1.0];
let vdot = v.time_derivative(&x, |x| [-x[0], -2.0 * x[1]]);
assert!((vdot - (-3.0)).abs() < 1e-12, "V̇={}", vdot);
}
#[test]
fn grid_point_bounds() {
let lo = [-1.0f64, -2.0, -3.0];
let hi = [1.0f64, 2.0, 3.0];
let p0 = grid_point::<f64, 3>(0, 3, &lo, &hi);
for i in 0..3 {
assert!(
(p0[i] - lo[i]).abs() < 1e-12,
"Corner point mismatch at dim {}: {}",
i,
p0[i]
);
}
}
}