#![allow(clippy::needless_range_loop)]
use crate::core::matrix::{matvec, vec_dot, Matrix};
use crate::core::scalar::ControlScalar;
use crate::passivity::PassivityError;
#[inline]
fn raw_matvec<S: ControlScalar, const N: usize>(m: &[[S; N]; N], v: &[S; N]) -> [S; N] {
core::array::from_fn(|r| {
let mut s = S::ZERO;
for c in 0..N {
s += m[r][c] * v[c];
}
s
})
}
#[inline]
fn raw_mat_sub<S: ControlScalar, const N: usize>(a: &[[S; N]; N], b: &[[S; N]; N]) -> [[S; N]; N] {
core::array::from_fn(|r| core::array::from_fn(|c| a[r][c] - b[r][c]))
}
#[inline]
fn raw_matvec_ni<S: ControlScalar, const N: usize, const I: usize>(
m: &[[S; I]; N],
v: &[S; I],
) -> [S; N] {
core::array::from_fn(|r| {
let mut s = S::ZERO;
for c in 0..I {
s += m[r][c] * v[c];
}
s
})
}
#[inline]
fn raw_matvec_gt<S: ControlScalar, const N: usize, const I: usize>(
m: &[[S; I]; N],
v: &[S; N],
) -> [S; I] {
core::array::from_fn(|c| {
let mut s = S::ZERO;
for r in 0..N {
s += m[r][c] * v[r];
}
s
})
}
pub(crate) fn is_skew_symmetric<S: ControlScalar, const N: usize>(m: &[[S; N]; N], tol: S) -> bool {
for i in 0..N {
if m[i][i].abs() > tol {
return false;
}
for j in (i + 1)..N {
if (m[i][j] + m[j][i]).abs() > tol {
return false;
}
}
}
true
}
pub(crate) fn is_psd<S: ControlScalar, const N: usize>(m: &[[S; N]; N], tol: S) -> bool {
let mut l = [[S::ZERO; N]; N];
for i in 0..N {
for j in 0..=i {
let mut sum = S::ZERO;
for k in 0..j {
sum += l[i][k] * l[j][k];
}
if i == j {
let d = m[i][i] - sum;
if d < -tol {
return false;
}
let d_clamped = if d < S::ZERO { S::ZERO } else { d };
l[i][j] = d_clamped.sqrt();
} else {
let diag = l[j][j];
if diag.abs() < S::EPSILON * S::from_f64(1e4) {
if (m[i][j] - sum).abs() > tol {
return false;
}
l[i][j] = S::ZERO;
} else {
l[i][j] = (m[i][j] - sum) / diag;
}
}
}
}
true
}
pub(crate) fn is_pd<S: ControlScalar, const N: usize>(m: &[[S; N]; N], tol: S) -> bool {
let mut l = [[S::ZERO; N]; N];
for i in 0..N {
for j in 0..=i {
let mut sum = S::ZERO;
for k in 0..j {
sum += l[i][k] * l[j][k];
}
if i == j {
let d = m[i][i] - sum;
if d <= tol {
return false;
}
l[i][j] = d.sqrt();
} else {
let diag = l[j][j];
if diag.abs() < S::EPSILON * S::from_f64(1e4) {
return false;
}
l[i][j] = (m[i][j] - sum) / diag;
}
}
}
true
}
pub struct PortHamiltonian<S: ControlScalar, const N: usize, const I: usize> {
pub j_matrix: [[S; N]; N],
pub r_matrix: [[S; N]; N],
pub g_matrix: [[S; I]; N],
pub hamiltonian: fn(&[S; N]) -> S,
pub grad_hamiltonian: fn(&[S; N]) -> [S; N],
}
impl<S: ControlScalar, const N: usize, const I: usize> PortHamiltonian<S, N, I> {
pub fn dynamics(&self, x: &[S; N], u: &[S; I]) -> [S; N] {
let grad = (self.grad_hamiltonian)(x);
let jr = raw_mat_sub(&self.j_matrix, &self.r_matrix);
let jrg = raw_matvec(&jr, &grad);
let gu = raw_matvec_ni(&self.g_matrix, u);
core::array::from_fn(|i| jrg[i] + gu[i])
}
pub fn output(&self, x: &[S; N]) -> [S; I] {
let grad = (self.grad_hamiltonian)(x);
raw_matvec_gt(&self.g_matrix, &grad)
}
pub fn is_passive(&self) -> bool {
let tol = S::from_f64(1e-9);
is_skew_symmetric(&self.j_matrix, tol) && is_psd(&self.r_matrix, tol)
}
pub fn storage_function_rate(&self, x: &[S; N], u: &[S; I]) -> S {
let grad = (self.grad_hamiltonian)(x);
let xdot = self.dynamics(x, u);
vec_dot(&grad, &xdot)
}
pub fn supply_rate(&self, x: &[S; N], u: &[S; I]) -> S {
let y = self.output(x);
vec_dot(u, &y)
}
pub fn dissipation_rate(&self, x: &[S; N]) -> S {
let grad = (self.grad_hamiltonian)(x);
let rg = raw_matvec(&self.r_matrix, &grad);
vec_dot(&grad, &rg)
}
}
pub struct LinearPh<S: ControlScalar, const N: usize, const I: usize> {
pub j_matrix: Matrix<S, N, N>,
pub r_matrix: Matrix<S, N, N>,
pub q_matrix: Matrix<S, N, N>,
pub g_matrix: Matrix<S, N, I>,
}
impl<S: ControlScalar, const N: usize, const I: usize> LinearPh<S, N, I> {
pub fn new(
j: [[S; N]; N],
r: [[S; N]; N],
q: [[S; N]; N],
g: [[S; I]; N],
) -> Result<Self, PassivityError> {
let tol = S::from_f64(1e-9);
if !is_skew_symmetric(&j, tol) {
return Err(PassivityError::NotPassive);
}
if !is_psd(&r, tol) {
return Err(PassivityError::NotPassive);
}
if !is_pd(&q, tol) {
return Err(PassivityError::InvalidHamiltonian);
}
let j_mat = Matrix { data: j };
let r_mat = Matrix { data: r };
let q_mat = Matrix { data: q };
let g_mat = Matrix { data: g };
Ok(Self {
j_matrix: j_mat,
r_matrix: r_mat,
q_matrix: q_mat,
g_matrix: g_mat,
})
}
pub fn hamiltonian(&self, x: &[S; N]) -> S {
let qx = matvec(&self.q_matrix, x);
S::HALF * vec_dot(x, &qx)
}
pub fn grad_hamiltonian(&self, x: &[S; N]) -> [S; N] {
matvec(&self.q_matrix, x)
}
pub fn dynamics(&self, x: &[S; N], u: &[S; I]) -> [S; N] {
use crate::core::matrix::vec_add;
let grad = self.grad_hamiltonian(x);
let jr = self.j_matrix.sub_mat(&self.r_matrix);
let jrg = matvec(&jr, &grad);
let gu = matvec(&self.g_matrix, u);
vec_add(&jrg, &gu)
}
pub fn output(&self, x: &[S; N]) -> [S; I] {
let grad = self.grad_hamiltonian(x);
let gt = self.g_matrix.transpose();
matvec(>, &grad)
}
pub fn j_raw(&self) -> [[S; N]; N] {
self.j_matrix.data
}
pub fn r_raw(&self) -> [[S; N]; N] {
self.r_matrix.data
}
pub fn g_raw(&self) -> [[S; I]; N] {
self.g_matrix.data
}
pub fn q_raw(&self) -> [[S; N]; N] {
self.q_matrix.data
}
pub fn is_passive(&self) -> bool {
let tol = S::from_f64(1e-9);
is_skew_symmetric(&self.j_matrix.data, tol) && is_psd(&self.r_matrix.data, tol)
}
pub fn storage_function_rate(&self, x: &[S; N], u: &[S; I]) -> S {
let grad = self.grad_hamiltonian(x);
let xdot = self.dynamics(x, u);
vec_dot(&grad, &xdot)
}
pub fn dissipation_rate(&self, x: &[S; N]) -> S {
let grad = self.grad_hamiltonian(x);
let rg = matvec(&self.r_matrix, &grad);
vec_dot(&grad, &rg)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_msd_linear_ph(b: f64) -> LinearPh<f64, 2, 1> {
let j = [[0.0f64, 1.0], [-1.0, 0.0]];
let r = [[0.0f64, 0.0], [0.0, b]];
let q = [[1.0f64, 0.0], [0.0, 1.0]]; let g = [[0.0f64], [1.0]];
LinearPh::new(j, r, q, g).expect("Valid mass-spring-damper pH system")
}
#[test]
fn linear_ph_construction_succeeds() {
let _sys = make_msd_linear_ph(0.5);
}
#[test]
fn linear_ph_invalid_j_not_skew() {
let j = [[1.0f64, 1.0], [1.0, 0.0]];
let r = [[0.0f64, 0.0], [0.0, 0.1]];
let q = [[1.0f64, 0.0], [0.0, 1.0]];
let g = [[0.0f64], [1.0]];
let result = LinearPh::<f64, 2, 1>::new(j, r, q, g);
assert!(matches!(result, Err(PassivityError::NotPassive)));
}
#[test]
fn linear_ph_invalid_r_not_psd() {
let j = [[0.0f64, 1.0], [-1.0, 0.0]];
let r = [[0.0f64, 0.0], [0.0, -0.5]]; let q = [[1.0f64, 0.0], [0.0, 1.0]];
let g = [[0.0f64], [1.0]];
let result = LinearPh::<f64, 2, 1>::new(j, r, q, g);
assert!(matches!(result, Err(PassivityError::NotPassive)));
}
#[test]
fn linear_ph_invalid_q_not_pd() {
let j = [[0.0f64, 1.0], [-1.0, 0.0]];
let r = [[0.0f64, 0.0], [0.0, 0.5]];
let q = [[0.0f64, 0.0], [0.0, 1.0]]; let g = [[0.0f64], [1.0]];
let result = LinearPh::<f64, 2, 1>::new(j, r, q, g);
assert!(matches!(result, Err(PassivityError::InvalidHamiltonian)));
}
#[test]
fn linear_ph_is_passive() {
let sys = make_msd_linear_ph(0.5);
assert!(sys.is_passive(), "MSD pH system should be passive");
}
#[test]
fn linear_ph_hamiltonian_positive() {
let sys = make_msd_linear_ph(0.5);
let x = [1.0f64, 2.0]; let h = sys.hamiltonian(&x);
assert!((h - 2.5).abs() < 1e-12, "H={}", h);
}
#[test]
fn linear_ph_dynamics_correct() {
let sys = make_msd_linear_ph(0.3);
let x = [1.0f64, 2.0]; let u = [0.5f64];
let xdot = sys.dynamics(&x, &u);
assert!((xdot[0] - 2.0).abs() < 1e-12, "q̇={}", xdot[0]);
assert!((xdot[1] - (-1.1)).abs() < 1e-12, "ṗ={}", xdot[1]);
}
#[test]
fn linear_ph_output_correct() {
let sys = make_msd_linear_ph(0.3);
let x = [1.0f64, 2.0];
let y = sys.output(&x);
assert!((y[0] - 2.0).abs() < 1e-12, "y={}", y[0]);
}
#[test]
fn passivity_inequality_holds() {
let b = 0.5;
let sys = make_msd_linear_ph(b);
for &(q, p, u_val) in &[
(1.0, 0.5, 0.0),
(0.5, 1.0, 1.0),
(0.0, 0.0, 0.5),
(-1.0, 2.0, -0.3),
] {
let x = [q, p];
let u = [u_val];
let hdot = sys.storage_function_rate(&x, &u);
let supply = sys.dissipation_rate(&x);
let y = sys.output(&x);
let uty = u[0] * y[0];
assert!(
hdot <= uty + 1e-10,
"Passivity violated: Ḣ={:.6} > uᵀy={:.6} (q={}, p={}, u={})",
hdot,
uty,
q,
p,
u_val
);
assert!(
(hdot - (uty - supply)).abs() < 1e-10,
"Energy balance off: Ḣ={:.6}, uᵀy-D={:.6}",
hdot,
uty - supply
);
}
}
#[test]
fn nonlinear_ph_dynamics() {
let j_raw = [[0.0f64, 1.0], [-1.0, 0.0]];
let r_raw = [[0.0f64, 0.0], [0.0, 0.3]];
let g_raw = [[0.0f64], [1.0]];
fn ham(x: &[f64; 2]) -> f64 {
0.5 * (x[0] * x[0] + x[1] * x[1])
}
fn grad_ham(x: &[f64; 2]) -> [f64; 2] {
[x[0], x[1]]
}
let sys = PortHamiltonian {
j_matrix: j_raw,
r_matrix: r_raw,
g_matrix: g_raw,
hamiltonian: ham,
grad_hamiltonian: grad_ham,
};
assert!(sys.is_passive());
let x = [1.0f64, 2.0];
let u = [0.5f64];
let xdot = sys.dynamics(&x, &u);
assert!((xdot[0] - 2.0).abs() < 1e-12);
assert!((xdot[1] - (-1.1)).abs() < 1e-12);
}
#[test]
fn storage_function_rate_zero_input_negative() {
let sys = make_msd_linear_ph(0.5);
let x = [1.0f64, 2.0];
let u = [0.0f64];
let hdot = sys.storage_function_rate(&x, &u);
assert!(hdot <= 1e-12, "Ḣ with u=0 should be ≤ 0, got {}", hdot);
}
}