#![allow(clippy::needless_range_loop)]
use crate::core::scalar::ControlScalar;
use crate::passivity::port_hamiltonian::{is_psd, is_skew_symmetric};
use crate::passivity::PassivityError;
#[inline]
fn raw_mv<S: ControlScalar, const N: usize>(m: &[[S; N]; N], v: &[S; N]) -> [S; N] {
core::array::from_fn(|r| {
let mut acc = S::ZERO;
for c in 0..N {
acc += m[r][c] * v[c];
}
acc
})
}
#[inline]
fn vec_sub_n<S: ControlScalar, const N: usize>(a: &[S; N], b: &[S; N]) -> [S; N] {
core::array::from_fn(|i| a[i] - b[i])
}
#[inline]
fn jr_matrix<S: ControlScalar, const N: usize>(j: &[[S; N]; N], r: &[[S; N]; N]) -> [[S; N]; N] {
core::array::from_fn(|row| core::array::from_fn(|col| j[row][col] - r[row][col]))
}
#[inline]
fn gt_mv<S: ControlScalar, const N: usize, const I: usize>(g: &[[S; I]; N], v: &[S; N]) -> [S; I] {
core::array::from_fn(|col| {
let mut acc = S::ZERO;
for row in 0..N {
acc += g[row][col] * v[row];
}
acc
})
}
#[inline]
fn gtg<S: ControlScalar, const N: usize, const I: usize>(g: &[[S; I]; N]) -> [[S; I]; I] {
core::array::from_fn(|i| {
core::array::from_fn(|j| {
let mut acc = S::ZERO;
for n in 0..N {
acc += g[n][i] * g[n][j];
}
acc
})
})
}
fn invert_small<S: ControlScalar, const I: usize>(m: [[S; I]; I]) -> Option<[[S; I]; I]> {
let mut a = m;
let mut inv: [[S; I]; I] =
core::array::from_fn(|r| core::array::from_fn(|c| if r == c { S::ONE } else { S::ZERO }));
for col in 0..I {
let mut max_row = col;
let mut max_val = a[col][col].abs();
for row in (col + 1)..I {
if a[row][col].abs() > max_val {
max_val = a[row][col].abs();
max_row = row;
}
}
if max_val < S::EPSILON * S::from_f64(1e6) {
return None;
}
if max_row != col {
a.swap(max_row, col);
inv.swap(max_row, col);
}
let pivot_inv = S::ONE / a[col][col];
for c in 0..I {
a[col][c] *= pivot_inv;
inv[col][c] *= pivot_inv;
}
for row in 0..I {
if row == col {
continue;
}
let factor = a[row][col];
for c in 0..I {
a[row][c] -= factor * a[col][c];
inv[row][c] -= factor * inv[col][c];
}
}
}
Some(inv)
}
#[inline]
fn mv_ii<S: ControlScalar, const I: usize>(m: &[[S; I]; I], v: &[S; I]) -> [S; I] {
core::array::from_fn(|r| {
let mut acc = S::ZERO;
for c in 0..I {
acc += m[r][c] * v[c];
}
acc
})
}
pub struct IdaPbcConfig<S: ControlScalar, const N: usize, const I: usize> {
pub j_desired: [[S; N]; N],
pub r_desired: [[S; N]; N],
pub grad_h_desired: fn(&[S; N]) -> [S; N],
_phantom: core::marker::PhantomData<[S; I]>,
}
impl<S: ControlScalar, const N: usize, const I: usize> IdaPbcConfig<S, N, I> {
pub fn new(
j_desired: [[S; N]; N],
r_desired: [[S; N]; N],
grad_h_desired: fn(&[S; N]) -> [S; N],
) -> Result<Self, PassivityError> {
let tol = S::from_f64(1e-9);
if !is_skew_symmetric(&j_desired, tol) {
return Err(PassivityError::NotPassive);
}
if !is_psd(&r_desired, tol) {
return Err(PassivityError::NotPassive);
}
Ok(Self {
j_desired,
r_desired,
grad_h_desired,
_phantom: core::marker::PhantomData,
})
}
}
pub struct IdaPbcController<S: ControlScalar, const N: usize, const I: usize> {
config: IdaPbcConfig<S, N, I>,
}
impl<S: ControlScalar, const N: usize, const I: usize> IdaPbcController<S, N, I> {
pub fn new(config: IdaPbcConfig<S, N, I>) -> Self {
Self { config }
}
pub fn compute<F1, F2>(
&self,
j_plant: &[[S; N]; N],
r_plant: &[[S; N]; N],
g_plant: &[[S; I]; N],
grad_h_plant: F1,
x: &[S; N],
) -> Result<[S; I], PassivityError>
where
F1: Fn(&[S; N]) -> [S; N],
F2: Fn(&[S; N]) -> [S; N],
{
let grad_h = grad_h_plant(x);
let grad_hd = (self.config.grad_h_desired)(x);
for &v in grad_hd.iter() {
if !v.is_finite() {
return Err(PassivityError::MatchingFailed);
}
}
let jr = jr_matrix(j_plant, r_plant);
let jrd = jr_matrix(&self.config.j_desired, &self.config.r_desired);
let jrg = raw_mv(&jr, &grad_h);
let jrdgd = raw_mv(&jrd, &grad_hd);
let rhs = vec_sub_n(&jrdgd, &jrg);
let gtrhs = gt_mv(g_plant, &rhs);
let gtg_mat = gtg(g_plant);
let gtg_inv = invert_small(gtg_mat).ok_or(PassivityError::SingularMatrix)?;
let u = mv_ii(>g_inv, >rhs);
Ok(u)
}
pub fn compute_with_closures(
&self,
j_plant: &[[S; N]; N],
r_plant: &[[S; N]; N],
g_plant: &[[S; I]; N],
grad_h_plant: fn(&[S; N]) -> [S; N],
x: &[S; N],
) -> Result<[S; I], PassivityError> {
let grad_h = grad_h_plant(x);
let grad_hd = (self.config.grad_h_desired)(x);
for &v in grad_hd.iter() {
if !v.is_finite() {
return Err(PassivityError::MatchingFailed);
}
}
let jr = jr_matrix(j_plant, r_plant);
let jrd = jr_matrix(&self.config.j_desired, &self.config.r_desired);
let jrg = raw_mv(&jr, &grad_h);
let jrdgd = raw_mv(&jrd, &grad_hd);
let rhs = vec_sub_n(&jrdgd, &jrg);
let gtrhs = gt_mv(g_plant, &rhs);
let gtg_mat = gtg(g_plant);
let gtg_inv = invert_small(gtg_mat).ok_or(PassivityError::SingularMatrix)?;
let u = mv_ii(>g_inv, >rhs);
Ok(u)
}
pub fn j_desired(&self) -> &[[S; N]; N] {
&self.config.j_desired
}
pub fn r_desired(&self) -> &[[S; N]; N] {
&self.config.r_desired
}
}
pub struct MechanicalIdaPbc<S: ControlScalar, const N: usize, const K: usize, const I: usize> {
pub md_inv: [[S; K]; K],
pub grad_vd: fn(&[S; K]) -> [S; K],
_phantom: core::marker::PhantomData<([S; N], [S; I])>,
}
impl<S: ControlScalar, const N: usize, const K: usize, const I: usize>
MechanicalIdaPbc<S, N, K, I>
{
pub fn new(md_inv: [[S; K]; K], grad_vd: fn(&[S; K]) -> [S; K]) -> Self {
Self {
md_inv,
grad_vd,
_phantom: core::marker::PhantomData,
}
}
pub fn grad_hd(&self, x: &[S; N]) -> [S; N] {
let mut q = [S::ZERO; K];
let mut p = [S::ZERO; K];
for i in 0..K {
if i < N {
q[i] = x[i];
}
if K + i < N {
p[i] = x[K + i];
}
}
let dvdq = (self.grad_vd)(&q);
let mut md_inv_p = [S::ZERO; K];
for r in 0..K {
let mut acc = S::ZERO;
for c in 0..K {
acc += self.md_inv[r][c] * p[c];
}
md_inv_p[r] = acc;
}
core::array::from_fn(|i| {
if i < K {
dvdq[i]
} else if i - K < K {
md_inv_p[i - K]
} else {
S::ZERO
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn plant_j() -> [[f64; 2]; 2] {
[[0.0, 1.0], [-1.0, 0.0]]
}
fn plant_r() -> [[f64; 2]; 2] {
[[0.0, 0.0], [0.0, 0.0]]
}
fn plant_g() -> [[f64; 1]; 2] {
[[0.0], [1.0]]
}
fn grad_h_plant(x: &[f64; 2]) -> [f64; 2] {
[x[0], x[1]] }
fn grad_hd_shifted(x: &[f64; 2]) -> [f64; 2] {
[x[0] - 1.0, x[1]] }
fn make_config(r_a: f64) -> IdaPbcConfig<f64, 2, 1> {
let j_d = [[0.0f64, 1.0], [-1.0, 0.0]];
let r_d = [[0.0f64, 0.0], [0.0, r_a]];
IdaPbcConfig::new(j_d, r_d, grad_hd_shifted).expect("Config should be valid")
}
#[test]
fn ida_pbc_config_valid() {
let _cfg = make_config(1.0);
}
#[test]
fn ida_pbc_config_invalid_j_not_skew() {
let j_d = [[1.0f64, 1.0], [1.0, 0.0]]; let r_d = [[0.0f64, 0.0], [0.0, 1.0]];
let result = IdaPbcConfig::<f64, 2, 1>::new(j_d, r_d, grad_hd_shifted);
assert!(matches!(result, Err(PassivityError::NotPassive)));
}
#[test]
fn ida_pbc_config_invalid_r_not_psd() {
let j_d = [[0.0f64, 1.0], [-1.0, 0.0]];
let r_d = [[0.0f64, 0.0], [0.0, -1.0]]; let result = IdaPbcConfig::<f64, 2, 1>::new(j_d, r_d, grad_hd_shifted);
assert!(matches!(result, Err(PassivityError::NotPassive)));
}
#[test]
fn ida_pbc_control_at_desired_equilibrium_is_zero() {
let cfg = make_config(1.0);
let ctrl = IdaPbcController::new(cfg);
let j_plant = plant_j();
let r_plant = plant_r();
let g_plant = plant_g();
let x_star = [1.0f64, 0.0];
let u = ctrl
.compute_with_closures(&j_plant, &r_plant, &g_plant, grad_h_plant, &x_star)
.expect("compute should succeed");
let grad_hd = grad_hd_shifted(&x_star);
let jrd = jr_matrix(ctrl.j_desired(), ctrl.r_desired());
let xdot_cl = raw_mv(&jrd, &grad_hd);
assert!(
xdot_cl[0].abs() < 1e-12,
"Closed-loop q̇ should be 0 at x*: {}",
xdot_cl[0]
);
assert!(
xdot_cl[1].abs() < 1e-12,
"Closed-loop ṗ should be 0 at x*: {}",
xdot_cl[1]
);
let _ = u; }
#[test]
fn ida_pbc_simulation_converges_to_desired() {
let r_a = 2.0; let cfg = make_config(r_a);
let ctrl = IdaPbcController::new(cfg);
let j_plant = plant_j();
let r_plant = plant_r();
let g_plant = plant_g();
let mut x = [0.0f64, 0.0];
let dt = 0.005;
for _ in 0..10_000 {
let u = ctrl
.compute_with_closures(&j_plant, &r_plant, &g_plant, grad_h_plant, &x)
.expect("compute ok");
let grad_h = grad_h_plant(&x);
let jr = jr_matrix(&j_plant, &r_plant);
let jrg = raw_mv(&jr, &grad_h);
let xdot = [jrg[0] + g_plant[0][0] * u[0], jrg[1] + g_plant[1][0] * u[0]];
x = [x[0] + dt * xdot[0], x[1] + dt * xdot[1]];
}
assert!(
(x[0] - 1.0).abs() < 0.05,
"Position should converge to q*=1: q={}",
x[0]
);
assert!(
x[1].abs() < 0.05,
"Momentum should converge to 0: p={}",
x[1]
);
}
#[test]
fn ida_pbc_singular_g_returns_error() {
let cfg = make_config(1.0);
let ctrl = IdaPbcController::new(cfg);
let j_plant = plant_j();
let r_plant = plant_r();
let g_zero = [[0.0f64], [0.0]]; let x = [0.5f64, 0.1];
let result = ctrl.compute_with_closures(&j_plant, &r_plant, &g_zero, grad_h_plant, &x);
assert!(matches!(result, Err(PassivityError::SingularMatrix)));
}
#[test]
fn mechanical_ida_pbc_grad_hd() {
fn grad_vd(q: &[f64; 1]) -> [f64; 1] {
[q[0] - 1.0]
}
let md_inv = [[2.0f64]];
let mech = MechanicalIdaPbc::<f64, 2, 1, 1>::new(md_inv, grad_vd);
let x = [1.5f64, 3.0]; let grad = mech.grad_hd(&x);
assert!((grad[0] - 0.5).abs() < 1e-12, "∂H_d/∂q={}", grad[0]);
assert!((grad[1] - 6.0).abs() < 1e-12, "∂H_d/∂p={}", grad[1]);
}
#[test]
fn ida_pbc_control_finite_far_from_eq() {
let cfg = make_config(1.0);
let ctrl = IdaPbcController::new(cfg);
let j = plant_j();
let r = plant_r();
let g = plant_g();
for &(q, p) in &[(0.0f64, 0.0f64), (2.0, 1.0), (-1.0, 0.5), (0.5, -2.0)] {
let x = [q, p];
let u = ctrl
.compute_with_closures(&j, &r, &g, grad_h_plant, &x)
.expect("compute ok");
assert!(u[0].is_finite(), "u not finite at q={}, p={}", q, p);
}
}
}