use crate::core::{matrix::Matrix, scalar::ControlScalar};
#[derive(Debug, Clone, Copy)]
pub struct DScale<S: ControlScalar, const N: usize> {
pub d: [S; N],
}
impl<S: ControlScalar, const N: usize> DScale<S, N> {
pub fn identity() -> Self {
Self { d: [S::ONE; N] }
}
pub fn apply(&self, m: &Matrix<S, N, N>) -> Matrix<S, N, N> {
Matrix {
data: core::array::from_fn(|i| {
core::array::from_fn(|j| m.data[i][j] * self.d[i] / self.d[j])
}),
}
}
pub fn scaled_spectral_bound(&self, m: &Matrix<S, N, N>) -> S {
self.apply(m).frob_norm()
}
}
pub fn mu_upper_bound<S: ControlScalar, const N: usize>(
m: &Matrix<S, N, N>,
d: &DScale<S, N>,
) -> S {
d.scaled_spectral_bound(m)
}
pub struct DkIteration<S: ControlScalar, const N: usize, const I: usize> {
pub d_scale: DScale<S, N>,
pub gamma: S,
pub tol: S,
pub max_iter: u32,
_phantom: core::marker::PhantomData<[S; I]>,
}
impl<S: ControlScalar, const N: usize, const I: usize> DkIteration<S, N, I> {
pub fn new(gamma: S, tol: S, max_iter: u32) -> Self {
Self {
d_scale: DScale::identity(),
gamma,
tol,
max_iter,
_phantom: core::marker::PhantomData,
}
}
pub fn d_step(&mut self, m: &Matrix<S, N, N>) -> S {
let mut best_bound = mu_upper_bound(m, &self.d_scale);
for _ in 0..self.max_iter {
let prev_bound = best_bound;
for i in 0..N {
let mut best_di = self.d_scale.d[i];
let mut best_local = best_bound;
for &scale in &[
S::from_f64(1.1),
S::from_f64(0.9091), S::from_f64(1.5),
S::from_f64(0.6667),
] {
let d_try = self.d_scale.d[i] * scale;
if d_try <= S::ZERO {
continue;
}
let mut d_test = self.d_scale;
d_test.d[i] = d_try;
let bound = mu_upper_bound(m, &d_test);
if bound < best_local {
best_local = bound;
best_di = d_try;
}
}
self.d_scale.d[i] = best_di;
best_bound = best_local;
}
if (prev_bound - best_bound).abs() < self.tol {
break;
}
}
best_bound
}
}
pub fn mu_lower_bound_unstructured<S: ControlScalar, const N: usize>(m: &Matrix<S, N, N>) -> S {
m.frob_norm()
}
pub fn is_robustly_stable<S: ControlScalar, const N: usize>(
m: &Matrix<S, N, N>,
d: &DScale<S, N>,
) -> bool {
mu_upper_bound(m, d) < S::ONE
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_d_scale_gives_frob_norm() {
let m = Matrix::<f64, 2, 2> {
data: [[0.5, 0.1], [-0.2, 0.3]],
};
let d = DScale::identity();
let bound = mu_upper_bound(&m, &d);
assert!((bound - m.frob_norm()).abs() < 1e-10);
}
#[test]
fn d_scaling_reduces_bound() {
let m = Matrix::<f64, 2, 2> {
data: [[0.1, 0.9], [0.01, 0.1]],
};
let d_id = DScale::identity();
let bound_id = mu_upper_bound(&m, &d_id);
let mut dk = DkIteration::<f64, 2, 2>::new(1.0, 1e-6, 50);
let bound_opt = dk.d_step(&m);
assert!(
bound_opt <= bound_id + 1e-10,
"optimized bound {:.6} > identity bound {:.6}",
bound_opt,
bound_id
);
}
#[test]
fn robustly_stable_small_perturbation() {
let m = Matrix::<f64, 2, 2> {
data: [[0.5, 0.0], [0.0, 0.5]],
};
let d = DScale::identity();
assert!(is_robustly_stable(&m, &d));
}
#[test]
fn not_robustly_stable_large_perturbation() {
let m = Matrix::<f64, 2, 2> {
data: [[2.0, 0.0], [0.0, 2.0]],
};
let d = DScale::identity();
assert!(!is_robustly_stable(&m, &d));
}
#[test]
fn d_scale_apply_scales_off_diagonal() {
let d = DScale::<f64, 2> { d: [2.0, 1.0] };
let m = Matrix::<f64, 2, 2> {
data: [[1.0, 0.5], [0.25, 1.0]],
};
let scaled = d.apply(&m);
assert!(
(scaled.data[0][1] - 1.0).abs() < 1e-10,
"scaled[0][1]={}",
scaled.data[0][1]
);
assert!((scaled.data[1][0] - 0.125).abs() < 1e-10);
}
}