use crate::scaling::TSymScalingMethod;
use pounce_common::types::{Index, Number};
#[derive(Debug, Clone, Copy)]
pub struct RuizTSymScalingMethod {
pub max_iter: usize,
pub tol: Number,
pub min_row_value: Number,
}
impl Default for RuizTSymScalingMethod {
fn default() -> Self {
Self {
max_iter: 10,
tol: 1e-2,
min_row_value: Number::MIN_POSITIVE,
}
}
}
impl RuizTSymScalingMethod {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn with_tol(mut self, tol: Number) -> Self {
self.tol = tol;
self
}
}
impl TSymScalingMethod for RuizTSymScalingMethod {
fn compute_sym_t_scaling_factors(
&mut self,
n: Index,
nnz: Index,
airn: &[Index],
ajcn: &[Index],
a: &[Number],
scaling_factors: &mut [Number],
) -> bool {
let n_us = n as usize;
let nnz_us = nnz as usize;
debug_assert_eq!(scaling_factors.len(), n_us);
debug_assert!(airn.len() >= nnz_us);
debug_assert!(ajcn.len() >= nnz_us);
debug_assert!(a.len() >= nnz_us);
if n_us == 0 {
return true;
}
let mut min_idx = airn[0];
for k in 0..nnz_us {
if airn[k] < min_idx {
min_idx = airn[k];
}
if ajcn[k] < min_idx {
min_idx = ajcn[k];
}
}
let offset: Index = if min_idx >= 1 { 1 } else { 0 };
for s in scaling_factors.iter_mut() {
*s = 1.0;
}
let mut row_max = vec![0.0 as Number; n_us];
for _iter in 0..self.max_iter {
for v in row_max.iter_mut() {
*v = 0.0;
}
for k in 0..nnz_us {
let i = (airn[k] - offset) as usize;
let j = (ajcn[k] - offset) as usize;
if i >= n_us || j >= n_us {
return false;
}
let v = a[k].abs() * scaling_factors[i] * scaling_factors[j];
if v > row_max[i] {
row_max[i] = v;
}
if i != j && v > row_max[j] {
row_max[j] = v;
}
}
let mut max_imbalance: Number = 0.0;
for &r in row_max.iter() {
if r < self.min_row_value {
continue;
}
let imb = (r - 1.0).abs();
if imb > max_imbalance {
max_imbalance = imb;
}
}
if max_imbalance < self.tol {
break;
}
for i in 0..n_us {
let r = row_max[i];
if r >= self.min_row_value {
scaling_factors[i] /= r.sqrt();
}
}
}
for s in scaling_factors.iter() {
if !s.is_finite() {
return false;
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn equilibrates_diagonal_extremes() {
let mut method = RuizTSymScalingMethod::new();
let irn = [0, 1];
let jcn = [0, 1];
let vals = [1.0e6, 1.0e-6];
let mut s = vec![0.0; 2];
assert!(method.compute_sym_t_scaling_factors(2, 2, &irn, &jcn, &vals, &mut s));
let scaled_00 = vals[0] * s[0] * s[0];
let scaled_11 = vals[1] * s[1] * s[1];
assert!(
(scaled_00 - 1.0).abs() < 1e-3,
"diag(0)→{}, want ≈1; s={:?}",
scaled_00,
s
);
assert!(
(scaled_11 - 1.0).abs() < 1e-3,
"diag(1)→{}, want ≈1; s={:?}",
scaled_11,
s
);
}
#[test]
fn equilibrates_off_diagonal_block() {
let mut method = RuizTSymScalingMethod::new();
let irn = [0, 1, 1];
let jcn = [0, 0, 1];
let vals = [100.0, 10.0, 1.0];
let mut s = vec![0.0; 2];
assert!(method.compute_sym_t_scaling_factors(2, 3, &irn, &jcn, &vals, &mut s));
let row0 = (vals[0] * s[0] * s[0])
.abs()
.max((vals[1] * s[1] * s[0]).abs());
let row1 = (vals[1] * s[1] * s[0])
.abs()
.max((vals[2] * s[1] * s[1]).abs());
assert!(
(row0 - 1.0).abs() < method.tol + 1e-9,
"row0={}, want ≈1",
row0
);
assert!(
(row1 - 1.0).abs() < method.tol + 1e-9,
"row1={}, want ≈1",
row1
);
}
#[test]
fn zero_row_keeps_unit_scale() {
let mut method = RuizTSymScalingMethod::new();
let irn = [0];
let jcn = [0];
let vals = [4.0];
let mut s = vec![0.0; 2];
assert!(method.compute_sym_t_scaling_factors(2, 1, &irn, &jcn, &vals, &mut s));
assert!((s[0] - 0.5).abs() < 1e-6, "K_00=4 → s_0≈0.5, got {}", s[0]);
assert!(
(s[1] - 1.0).abs() < 1e-12,
"zero row keeps s=1, got {}",
s[1]
);
}
#[test]
fn fortran_index_style() {
let mut method = RuizTSymScalingMethod::new();
let irn = [1, 2];
let jcn = [1, 2];
let vals = [1.0e6, 1.0e-6];
let mut s = vec![0.0; 2];
assert!(method.compute_sym_t_scaling_factors(2, 2, &irn, &jcn, &vals, &mut s));
let scaled_00 = vals[0] * s[0] * s[0];
let scaled_11 = vals[1] * s[1] * s[1];
assert!((scaled_00 - 1.0).abs() < 1e-3);
assert!((scaled_11 - 1.0).abs() < 1e-3);
}
#[test]
fn fuzz_reduces_imbalance() {
let n = 5usize;
let mut irn: Vec<Index> = Vec::new();
let mut jcn: Vec<Index> = Vec::new();
let mut vals: Vec<Number> = Vec::new();
let raw = [
(0, 0, 1.0e4),
(1, 0, 1.0e2),
(1, 1, 1.0),
(2, 0, 1.0e-2),
(2, 2, 1.0e-4),
(3, 1, 5.0),
(3, 3, 50.0),
(4, 2, 0.1),
(4, 4, 25.0),
];
for (i, j, v) in raw.iter() {
irn.push(*i as Index);
jcn.push(*j as Index);
vals.push(*v);
}
let nnz = irn.len() as Index;
let mut pre = vec![0.0 as Number; n];
for k in 0..vals.len() {
let i = irn[k] as usize;
let j = jcn[k] as usize;
let v = vals[k].abs();
if v > pre[i] {
pre[i] = v;
}
if i != j && v > pre[j] {
pre[j] = v;
}
}
let pre_max = pre.iter().cloned().fold(0.0_f64, f64::max);
let pre_min = pre.iter().cloned().fold(f64::INFINITY, f64::min);
let mut method = RuizTSymScalingMethod::new();
let mut s = vec![0.0; n];
assert!(method.compute_sym_t_scaling_factors(n as Index, nnz, &irn, &jcn, &vals, &mut s));
let mut post = vec![0.0 as Number; n];
for k in 0..vals.len() {
let i = irn[k] as usize;
let j = jcn[k] as usize;
let v = (vals[k] * s[i] * s[j]).abs();
if v > post[i] {
post[i] = v;
}
if i != j && v > post[j] {
post[j] = v;
}
}
let post_max = post.iter().cloned().fold(0.0_f64, f64::max);
let post_min = post.iter().cloned().fold(f64::INFINITY, f64::min);
let pre_ratio = pre_max / pre_min;
let post_ratio = post_max / post_min;
assert!(
post_ratio < pre_ratio,
"Ruiz must reduce row-∞-norm ratio: pre={pre_ratio}, post={post_ratio}"
);
assert!(
(post_ratio - 1.0).abs() < method.tol + 5e-2,
"post ratio {} should be ≈1",
post_ratio
);
}
}