use crate::real::{Real, approx_eq, is_exact_zero, le_exact, lt};
use crate::{CscMatrix, SparseError};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Equilibration {
Geometric {
max_iters: usize,
power_of_two: bool,
},
CurtisReid {
max_iters: usize,
},
InfinityNorm,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Scaling<T: Real = f64> {
pub row: Vec<T>,
pub col: Vec<T>,
}
impl<T: Real> Scaling<T> {
#[must_use]
pub fn compute(a: &CscMatrix<T>, policy: Equilibration) -> Self {
let (row, col) = match policy {
Equilibration::Geometric {
max_iters,
power_of_two,
} => geometric(a, max_iters, power_of_two),
Equilibration::CurtisReid { max_iters } => curtis_reid(a, max_iters),
Equilibration::InfinityNorm => infinity_norm(a),
};
let s = Self {
row: row.into_iter().map(T::from_f64).collect(),
col: col.into_iter().map(T::from_f64).collect(),
};
debug_assert_eq!(s.check_invariants(), Ok(()));
debug_assert!(
!matches!(
policy,
Equilibration::CurtisReid { .. }
| Equilibration::Geometric {
power_of_two: true,
..
}
) || s.is_power_of_two(),
"(S8): the policy rounds to powers of two"
);
s
}
pub fn apply(&self, a: &mut CscMatrix<T>) {
self.assert_fits(a);
for j in 0..a.n_cols() {
let cj = self.col[j];
for p in a.col_ptr[j]..a.col_ptr[j + 1] {
let ri = self.row[a.row_idx[p] as usize];
a.val[p] = a.val[p] * ri * cj;
}
}
}
pub fn unapply(&self, a: &mut CscMatrix<T>) {
self.assert_fits(a);
for j in 0..a.n_cols() {
let cj = self.col[j];
for p in a.col_ptr[j]..a.col_ptr[j + 1] {
let ri = self.row[a.row_idx[p] as usize];
a.val[p] = a.val[p] / ri / cj;
}
}
}
pub fn spread(&self, a: &CscMatrix<T>) -> T {
self.assert_fits(a);
let mut big = T::ZERO;
let mut small = T::ZERO;
let mut any = false;
for j in 0..a.n_cols() {
for p in a.col_ptr[j]..a.col_ptr[j + 1] {
let v = (a.val[p] * self.row[a.row_idx[p] as usize] * self.col[j]).abs();
if any {
big = big.max(v);
small = small.min(v);
} else {
(big, small, any) = (v, v, true);
}
}
}
if any && !is_exact_zero(small) {
big / small
} else {
T::ONE
}
}
pub fn check_invariants(&self) -> Result<(), SparseError> {
for (what, f) in [
("row scale factor", &self.row),
("column scale factor", &self.col),
] {
for (k, &x) in f.iter().enumerate() {
if !x.is_finite() || !lt(T::ZERO, x) {
return Err(SparseError::InvalidValue {
what,
index: k,
value: x.to_f64(),
});
}
}
}
Ok(())
}
fn is_power_of_two(&self) -> bool {
self.row
.iter()
.chain(&self.col)
.all(|&x| approx_eq(x.to_f64(), round_pow2(x.to_f64()), 0.0))
}
fn assert_fits(&self, a: &CscMatrix<T>) {
assert_eq!(self.row.len(), a.n_rows, "scaling: row factors do not fit");
assert_eq!(
self.col.len(),
a.n_cols(),
"scaling: column factors do not fit"
);
}
}
fn round_pow2(x: f64) -> f64 {
x.log2().round().exp2()
}
fn geometric<T: Real>(
a: &CscMatrix<T>,
max_iters: usize,
power_of_two: bool,
) -> (Vec<f64>, Vec<f64>) {
let (m, n) = (a.n_rows, a.n_cols());
let mut row = vec![1.0_f64; m];
let mut col = vec![1.0_f64; n];
let mut rmax = vec![0.0_f64; m];
let mut rmin = vec![f64::INFINITY; m];
for _ in 0..max_iters {
rmax.fill(0.0);
rmin.fill(f64::INFINITY);
for (j, &cj) in col.iter().enumerate() {
for p in a.col_ptr[j]..a.col_ptr[j + 1] {
let i = a.row_idx[p] as usize;
let v = a.val[p].to_f64().abs() * cj;
rmax[i] = rmax[i].max(v);
rmin[i] = rmin[i].min(v);
}
}
for i in 0..m {
if lt(0.0, rmax[i]) {
row[i] = (rmax[i] * rmin[i]).sqrt().recip();
}
}
for (j, cj) in col.iter_mut().enumerate() {
let (mut big, mut small) = (0.0_f64, f64::INFINITY);
for p in a.col_ptr[j]..a.col_ptr[j + 1] {
let v = row[a.row_idx[p] as usize] * a.val[p].to_f64().abs();
big = big.max(v);
small = small.min(v);
}
if lt(0.0, big) {
*cj = (big * small).sqrt().recip();
}
}
}
if power_of_two {
for f in row.iter_mut().chain(col.iter_mut()) {
*f = round_pow2(*f);
}
}
(row, col)
}
#[allow(clippy::cast_precision_loss)] fn curtis_reid<T: Real>(a: &CscMatrix<T>, max_iters: usize) -> (Vec<f64>, Vec<f64>) {
let (m, n) = (a.n_rows, a.n_cols());
let mut cnt = vec![0.0_f64; m + n];
let mut g = vec![0.0_f64; m + n];
for j in 0..n {
for p in a.col_ptr[j]..a.col_ptr[j + 1] {
let i = a.row_idx[p] as usize;
let v = a.val[p].to_f64().abs().log2();
g[i] += v;
g[m + j] += v;
cnt[i] += 1.0;
cnt[m + j] += 1.0;
}
}
let apply_m = |z: &[f64], out: &mut [f64]| {
for (o, (&c, &zk)) in out.iter_mut().zip(cnt.iter().zip(z)) {
*o = c * zk;
}
for j in 0..n {
for p in a.col_ptr[j]..a.col_ptr[j + 1] {
let i = a.row_idx[p] as usize;
out[i] += z[m + j];
out[m + j] += z[i];
}
}
};
let pinv: Vec<f64> = cnt
.iter()
.map(|&c| if lt(0.0, c) { c.recip() } else { 0.0 })
.collect();
let dot = |x: &[f64], y: &[f64]| -> f64 { x.iter().zip(y).map(|(a, b)| a * b).sum() };
let mut z = vec![0.0_f64; m + n];
let mut res: Vec<f64> = g.iter().map(|&v| -v).collect();
let mut s: Vec<f64> = res.iter().zip(&pinv).map(|(r, p)| r * p).collect();
let mut dir = s.clone();
let mut q = vec![0.0_f64; m + n];
let mut rs = dot(&res, &s);
let stop = rs * f64::EPSILON;
for _ in 0..max_iters {
if le_exact(rs, stop) {
break;
}
apply_m(&dir, &mut q);
let dq = dot(&dir, &q);
if !lt(0.0, dq) {
break;
}
let alpha = rs / dq;
for k in 0..m + n {
z[k] += alpha * dir[k];
res[k] -= alpha * q[k];
s[k] = res[k] * pinv[k];
}
let rs_new = dot(&res, &s);
let beta = rs_new / rs;
for k in 0..m + n {
dir[k] = s[k] + beta * dir[k];
}
rs = rs_new;
}
let row = z[..m].iter().map(|&x| x.round().exp2()).collect();
let col = z[m..].iter().map(|&x| x.round().exp2()).collect();
(row, col)
}
fn infinity_norm<T: Real>(a: &CscMatrix<T>) -> (Vec<f64>, Vec<f64>) {
let (m, n) = (a.n_rows, a.n_cols());
let mut rmax = vec![0.0_f64; m];
for j in 0..n {
for p in a.col_ptr[j]..a.col_ptr[j + 1] {
let i = a.row_idx[p] as usize;
rmax[i] = rmax[i].max(a.val[p].to_f64().abs());
}
}
let row: Vec<f64> = rmax
.iter()
.map(|&v| if lt(0.0, v) { v.recip() } else { 1.0 })
.collect();
let col = (0..n)
.map(|j| {
let big = (a.col_ptr[j]..a.col_ptr[j + 1])
.map(|p| row[a.row_idx[p] as usize] * a.val[p].to_f64().abs())
.fold(0.0_f64, f64::max);
if lt(0.0, big) { big.recip() } else { 1.0 }
})
.collect();
(row, col)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Col, Row};
use proptest::prelude::*;
fn wide() -> CscMatrix {
CscMatrix::from_triplets(
3,
3,
&[
(Row(0), Col(0), 1e-4),
(Row(0), Col(2), 3e2),
(Row(1), Col(0), 2.0),
(Row(1), Col(1), -7e5),
(Row(2), Col(1), 5e-3),
(Row(2), Col(2), 1.5),
],
)
.unwrap()
}
const POLICIES: [Equilibration; 4] = [
Equilibration::Geometric {
max_iters: 6,
power_of_two: true,
},
Equilibration::Geometric {
max_iters: 6,
power_of_two: false,
},
Equilibration::CurtisReid { max_iters: 50 },
Equilibration::InfinityNorm,
];
#[test]
fn every_policy_reduces_the_spread() {
let a = wide();
let unit = Scaling {
row: vec![1.0; 3],
col: vec![1.0; 3],
};
let before = unit.spread(&a);
for policy in POLICIES {
let s = Scaling::compute(&a, policy);
assert_eq!(s.check_invariants(), Ok(()));
assert!(
lt(s.spread(&a), before),
"{policy:?}: {} vs {before}",
s.spread(&a)
);
}
}
#[test]
fn s8_round_trip() {
let a = wide();
for policy in POLICIES {
let s = Scaling::compute(&a, policy);
let mut b = a.clone();
s.apply(&mut b);
s.unapply(&mut b);
if s.is_power_of_two() {
assert_eq!(a, b, "{policy:?}");
} else {
for (x, y) in a.val.iter().zip(&b.val) {
assert!(approx_eq(*x, *y, 4.0 * f64::EPSILON), "{policy:?}");
}
}
}
}
#[test]
fn infinity_norm_makes_columns_unit() {
let a = wide();
let s = Scaling::compute(&a, Equilibration::InfinityNorm);
let mut b = a.clone();
s.apply(&mut b);
for j in 0..3 {
let (_, v) = b.col(Col::from_usize(j));
let big = v.iter().fold(0.0_f64, |m, x| m.max(x.abs()));
assert!(approx_eq(big, 1.0, 4.0 * f64::EPSILON));
}
}
#[test]
fn empty_rows_and_columns_keep_unit_factors() {
let a = CscMatrix::from_triplets(3, 3, &[(Row(0), Col(0), 8.0)]).unwrap();
for policy in POLICIES {
let s = Scaling::compute(&a, policy);
assert!(approx_eq(s.row[1], 1.0, 0.0) && approx_eq(s.col[2], 1.0, 0.0));
assert!(approx_eq(s.spread(&a), 1.0, 0.0));
}
let e = CscMatrix::<f64>::zeros(2, 2);
assert!(approx_eq(
Scaling::compute(&e, POLICIES[0]).spread(&e),
1.0,
0.0
));
}
#[test]
fn curtis_reid_is_exact_on_a_rank_one_magnitude_pattern() {
let p = [3, -5, 0];
let q = [-2, 7, 1];
let mut t = Vec::new();
for (i, &pi) in p.iter().enumerate() {
for (j, &qj) in q.iter().enumerate() {
let sign = if (i + j) % 2 == 0 { 1.0 } else { -1.0 };
t.push((
Row::from_usize(i),
Col::from_usize(j),
sign * f64::from(pi + qj).exp2(),
));
}
}
let a = CscMatrix::from_triplets(3, 3, &t).unwrap();
let s = Scaling::compute(&a, Equilibration::CurtisReid { max_iters: 20 });
assert!(approx_eq(s.spread(&a), 1.0, 0.0));
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn s8_round_trip_is_exact_for_powers_of_two(
m in 1_usize..12, n in 1_usize..12,
entries in proptest::collection::vec((0_usize..12, 0_usize..12, -40_i32..40, 0.5_f64..1.0), 1..60),
) {
let t: Vec<_> = entries
.iter()
.filter(|e| e.0 < m && e.1 < n)
.map(|&(i, j, e, f)| (Row::from_usize(i), Col::from_usize(j), f * f64::from(e).exp2()))
.collect();
let a = CscMatrix::from_triplets(m, n, &t).unwrap();
for policy in POLICIES {
let s = Scaling::compute(&a, policy);
let mut b = a.clone();
s.apply(&mut b);
if policy == Equilibration::InfinityNorm {
for j in 0..n {
let big = b.col(Col::from_usize(j)).1.iter().fold(0.0_f64, |m, x| m.max(x.abs()));
prop_assert!(is_exact_zero(big) || approx_eq(big, 1.0, 4.0 * f64::EPSILON));
}
}
s.unapply(&mut b);
if s.is_power_of_two() {
prop_assert_eq!(&a, &b);
}
}
}
}
}