use crate::dense::matrix::SymmetricMatrix;
use crate::error::FeralError;
use crate::sparse::csc::CscMatrix;
#[allow(dead_code)] mod hungarian;
mod infnorm;
mod mc64;
mod value_bound;
pub fn mc64_matching(matrix: &CscMatrix) -> Result<(Vec<usize>, usize), FeralError> {
mc64::matching_perm(matrix)
}
pub(crate) use mc64::Mc64Cache;
pub(crate) use value_bound::{
mc64_value_bound_passes, precompute_mc64_validity, Mc64CacheValidity,
};
pub(crate) fn compute_mc64_cache(matrix: &CscMatrix) -> Result<Mc64Cache, FeralError> {
mc64::compute_matching(matrix)
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum ScalingStrategy {
InfNorm,
Mc64Symmetric,
Identity,
External(Vec<f64>),
#[default]
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mc64FallbackReason {
InfNormSpreadAcceptable,
Mc64WorseThanInfnorm,
Mc64ScalingDegenerate,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ScalingInfo {
Applied,
PartialSingular { n_unmatched: usize },
Mc64FallbackToInfnorm { reason: Mc64FallbackReason },
NotApplied,
}
impl ScalingInfo {
pub fn is_mc64_fallback(&self) -> bool {
matches!(self, ScalingInfo::Mc64FallbackToInfnorm { .. })
}
}
pub fn compute_scaling(
matrix: &CscMatrix,
strategy: &ScalingStrategy,
) -> Result<(Vec<f64>, ScalingInfo), FeralError> {
compute_scaling_with_cache(matrix, strategy, None)
}
pub(crate) fn compute_scaling_dense_fast(
matrix: &CscMatrix,
sym: &SymmetricMatrix,
strategy: &ScalingStrategy,
) -> Result<(Vec<f64>, ScalingInfo), FeralError> {
match strategy {
ScalingStrategy::Auto | ScalingStrategy::InfNorm => Ok(infnorm::compute_infnorm_dense(sym)),
_ => compute_scaling(matrix, strategy),
}
}
pub(crate) fn compute_scaling_with_cache(
matrix: &CscMatrix,
strategy: &ScalingStrategy,
cache: Option<&Mc64Cache>,
) -> Result<(Vec<f64>, ScalingInfo), FeralError> {
match strategy {
ScalingStrategy::Identity => Ok((vec![1.0; matrix.n], ScalingInfo::NotApplied)),
ScalingStrategy::External(s) => {
if s.len() != matrix.n {
return Err(FeralError::InvalidInput(format!(
"external scaling has length {} but matrix has n={}",
s.len(),
matrix.n,
)));
}
Ok((s.clone(), ScalingInfo::Applied))
}
ScalingStrategy::InfNorm => Ok(infnorm::compute_infnorm(matrix)),
ScalingStrategy::Mc64Symmetric => match cache {
Some(c) => Ok(mc64::scaling_from_cache(c)),
None => mc64::compute_symmetric(matrix),
},
ScalingStrategy::Auto => compute_scaling_auto_with_cache(matrix, cache),
}
}
fn compute_scaling_auto_with_cache(
matrix: &CscMatrix,
cache: Option<&Mc64Cache>,
) -> Result<(Vec<f64>, ScalingInfo), FeralError> {
const RAW_GUARD: f64 = 1e6;
const MC_OFF_GUARD: f64 = 1e6;
const RATIO_GUARD: f64 = 1e5;
const IN_SPREAD_GUARD: f64 = 1e3;
const MC64_SPREAD_GUARD: f64 = 1.0 / f64::EPSILON;
let picked = pick_scaling_strategy(matrix);
if !matches!(picked, ScalingStrategy::Mc64Symmetric) {
return compute_scaling(matrix, &picked);
}
let mc64_from_cache = |matrix: &CscMatrix| -> Result<(Vec<f64>, ScalingInfo), FeralError> {
match cache {
Some(c) => Ok(mc64::scaling_from_cache(c)),
None => mc64::compute_symmetric(matrix),
}
};
let (in_vec, _in_info) = infnorm::compute_infnorm(matrix);
if scaling_spread(&in_vec) < IN_SPREAD_GUARD {
return Ok((
in_vec,
ScalingInfo::Mc64FallbackToInfnorm {
reason: Mc64FallbackReason::InfNormSpreadAcceptable,
},
));
}
let (mc_vec, mc_info) = mc64_from_cache(matrix)?;
if scaling_spread(&mc_vec) > MC64_SPREAD_GUARD {
return Ok((
in_vec,
ScalingInfo::Mc64FallbackToInfnorm {
reason: Mc64FallbackReason::Mc64ScalingDegenerate,
},
));
}
if raw_diag_range(matrix) >= RAW_GUARD {
return Ok((mc_vec, mc_info));
}
let mc_off = max_off_diag_ratio(matrix, &mc_vec);
if mc_off <= MC_OFF_GUARD {
return Ok((mc_vec, mc_info));
}
let in_off = max_off_diag_ratio(matrix, &in_vec);
let ratio = if in_off > 0.0 {
mc_off / in_off
} else {
f64::INFINITY
};
if ratio > RATIO_GUARD {
Ok((
in_vec,
ScalingInfo::Mc64FallbackToInfnorm {
reason: Mc64FallbackReason::Mc64WorseThanInfnorm,
},
))
} else {
Ok((mc_vec, mc_info))
}
}
fn scaling_spread(s: &[f64]) -> f64 {
let mut lo = f64::INFINITY;
let mut hi = 0.0_f64;
for v in s {
let a = v.abs();
if a > 0.0 {
if a < lo {
lo = a;
}
if a > hi {
hi = a;
}
}
}
if lo.is_finite() && lo > 0.0 {
hi / lo
} else {
f64::INFINITY
}
}
fn raw_diag_range(matrix: &CscMatrix) -> f64 {
let n = matrix.n;
if n == 0 {
return 0.0;
}
let mut lo = f64::INFINITY;
let mut hi = 0.0_f64;
for j in 0..n {
for k in matrix.col_ptr[j]..matrix.col_ptr[j + 1] {
if matrix.row_idx[k] == j {
let a = matrix.values[k].abs();
if a > 0.0 {
if a < lo {
lo = a;
}
if a > hi {
hi = a;
}
}
}
}
}
if lo.is_finite() && lo > 0.0 {
hi / lo
} else {
f64::INFINITY
}
}
fn max_off_diag_ratio(matrix: &CscMatrix, scaling: &[f64]) -> f64 {
let n = matrix.n;
if n == 0 {
return 0.0;
}
let mut diag_abs = vec![0.0_f64; n];
let mut max_off = vec![0.0_f64; n];
for j in 0..n {
for k in matrix.col_ptr[j]..matrix.col_ptr[j + 1] {
let i = matrix.row_idx[k];
let v = (matrix.values[k] * scaling[i] * scaling[j]).abs();
if i == j {
diag_abs[j] = v;
} else {
if v > max_off[i] {
max_off[i] = v;
}
if v > max_off[j] {
max_off[j] = v;
}
}
}
}
let mut worst = 0.0_f64;
for j in 0..n {
let r = if diag_abs[j] > 0.0 {
max_off[j] / diag_abs[j]
} else if max_off[j] > 0.0 {
f64::INFINITY
} else {
0.0
};
if r > worst {
worst = r;
}
}
worst
}
pub fn pick_scaling_strategy(matrix: &CscMatrix) -> ScalingStrategy {
const MAX_COL_NNZ_FOR_INFNORM: usize = 32;
let n = matrix.n;
if n == 0 {
return ScalingStrategy::InfNorm;
}
let mut diag_only = 0usize;
let mut max_col_nnz = 0usize;
for j in 0..n {
let start = matrix.col_ptr[j];
let end = matrix.col_ptr[j + 1];
let mut nnz_col = 0usize;
let mut diag_nonzero = false;
for k in start..end {
if matrix.values[k] == 0.0 {
continue;
}
nnz_col += 1;
if matrix.row_idx[k] == j {
diag_nonzero = true;
}
}
if nnz_col > max_col_nnz {
max_col_nnz = nnz_col;
}
if nnz_col == 1 && diag_nonzero {
diag_only += 1;
}
}
let has_arrow_head = max_col_nnz > MAX_COL_NNZ_FOR_INFNORM;
let has_slack_mass = diag_only as f64 / n as f64 >= 0.3;
if has_arrow_head && has_slack_mass {
ScalingStrategy::Mc64Symmetric
} else {
ScalingStrategy::InfNorm
}
}
#[allow(unused_imports)]
pub(crate) use hungarian::{hungarian_match, CostGraph, Matching};
#[cfg(test)]
mod tests {
use super::*;
use crate::sparse::csc::CscMatrix;
fn shape_csc(n: usize, diag_only: usize, dense_off: usize) -> CscMatrix {
assert!(diag_only <= n);
let n_dense = n - diag_only;
assert!(dense_off < n, "dense_off must be < n");
let mut col_ptr = Vec::with_capacity(n + 1);
let mut row_idx: Vec<usize> = Vec::new();
let mut values: Vec<f64> = Vec::new();
col_ptr.push(0);
for j in 0..n {
row_idx.push(j);
values.push(1.0);
if j >= diag_only {
let take = dense_off.min(j);
for k in 0..take {
row_idx.push(k);
values.push(0.1);
}
let _ = n_dense; }
col_ptr.push(row_idx.len());
}
CscMatrix {
n,
col_ptr,
row_idx,
values,
}
}
fn build_synth_kkt(
ntheta: usize,
nx: usize,
theta_top: f64,
base: f64,
pcoef: f64,
nslack: usize,
) -> CscMatrix {
let nc = nx;
let n = ntheta + nx + nc + nslack;
let con0 = ntheta + nx; let slack0 = ntheta + nx + nc; let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for p in 0..ntheta {
let hp = if ntheta > 1 {
theta_top.powf(p as f64 / (ntheta - 1) as f64)
} else {
1.0
};
rows.push(p);
cols.push(p);
vals.push(hp);
for c in 0..nc {
rows.push(con0 + c);
cols.push(p);
vals.push(pcoef);
}
}
for s in 0..nx {
let js = ntheta + s;
rows.push(js);
cols.push(js);
vals.push(0.0);
if s >= 1 {
rows.push(con0 + s - 1);
cols.push(js);
vals.push(base);
}
rows.push(con0 + s);
cols.push(js);
vals.push(1.0);
}
for c in 0..nc {
let jc = con0 + c;
rows.push(jc);
cols.push(jc);
vals.push(0.0);
}
for s in 0..nslack {
let js = slack0 + s;
rows.push(js);
cols.push(js);
vals.push(1.0);
}
CscMatrix::from_triplets(n, &rows, &cols, &vals)
.expect("synthetic KKT triplets are valid lower-triangle")
}
#[test]
fn pick_scaling_strategy_picks_mc64_for_arrow_kkt() {
let csc = shape_csc(100, 80, 50);
assert_eq!(pick_scaling_strategy(&csc), ScalingStrategy::Mc64Symmetric);
}
#[test]
fn pick_scaling_strategy_picks_infnorm_for_banded_high_diag_only() {
let csc = shape_csc(100, 60, 4);
assert_eq!(pick_scaling_strategy(&csc), ScalingStrategy::InfNorm);
}
#[test]
fn pick_scaling_strategy_picks_infnorm_for_dense_low_diag_only() {
let csc = shape_csc(100, 0, 50);
assert_eq!(pick_scaling_strategy(&csc), ScalingStrategy::InfNorm);
}
#[test]
fn pick_scaling_strategy_diag_only_threshold_boundary() {
let below = shape_csc(100, 29, 50);
assert_eq!(pick_scaling_strategy(&below), ScalingStrategy::InfNorm);
let at = shape_csc(100, 30, 50);
assert_eq!(pick_scaling_strategy(&at), ScalingStrategy::Mc64Symmetric);
}
#[test]
fn pick_scaling_strategy_max_col_nnz_threshold_boundary() {
let at32 = shape_csc(100, 50, 31);
assert_eq!(pick_scaling_strategy(&at32), ScalingStrategy::InfNorm);
let at33 = shape_csc(100, 50, 32);
assert_eq!(pick_scaling_strategy(&at33), ScalingStrategy::Mc64Symmetric);
}
#[test]
fn pick_scaling_strategy_empty_matrix_picks_infnorm() {
let csc = CscMatrix {
n: 0,
col_ptr: vec![0],
row_idx: vec![],
values: vec![],
};
assert_eq!(pick_scaling_strategy(&csc), ScalingStrategy::InfNorm);
}
#[test]
fn pick_scaling_strategy_explicit_zero_diag_not_slack_mass() {
#[derive(Clone, Copy)]
enum Cdiag {
Zero,
Absent,
Real,
}
fn build(cdiag: Cdiag) -> CscMatrix {
let n = 100;
let n_dense = 50;
let mut col_ptr = vec![0usize];
let mut row_idx: Vec<usize> = Vec::new();
let mut values: Vec<f64> = Vec::new();
for j in 0..n_dense {
row_idx.push(j);
values.push(2.0);
for r in (j + 1)..(j + 41) {
row_idx.push(r);
values.push(0.3);
}
col_ptr.push(row_idx.len());
}
for j in n_dense..n {
match cdiag {
Cdiag::Zero => {
row_idx.push(j);
values.push(0.0);
}
Cdiag::Absent => {}
Cdiag::Real => {
row_idx.push(j);
values.push(1.0);
}
}
col_ptr.push(row_idx.len());
}
CscMatrix {
n,
col_ptr,
row_idx,
values,
}
}
assert_eq!(
pick_scaling_strategy(&build(Cdiag::Zero)),
ScalingStrategy::InfNorm,
"explicit-zero constraint diagonals are not slack mass"
);
assert_eq!(
pick_scaling_strategy(&build(Cdiag::Absent)),
ScalingStrategy::InfNorm
);
assert_eq!(
pick_scaling_strategy(&build(Cdiag::Real)),
ScalingStrategy::Mc64Symmetric
);
}
#[test]
fn pick_scaling_strategy_explicit_zero_offdiag_ignored() {
let n = 100;
let mut col_ptr = vec![0usize];
let mut row_idx: Vec<usize> = Vec::new();
let mut values: Vec<f64> = Vec::new();
for j in 0..50 {
row_idx.push(j);
values.push(2.0);
for r in (j + 1)..(j + 41) {
row_idx.push(r);
values.push(0.3);
}
col_ptr.push(row_idx.len());
}
for j in 50..n {
row_idx.push(0);
values.push(0.0); row_idx.push(j);
values.push(1.0); col_ptr.push(row_idx.len());
}
let csc = CscMatrix {
n,
col_ptr,
row_idx,
values,
};
assert_eq!(pick_scaling_strategy(&csc), ScalingStrategy::Mc64Symmetric);
}
#[test]
fn pick_scaling_strategy_routes_clnlbeam_to_infnorm() {
let path = std::path::Path::new("data/matrices/kkt-mittelmann/clnlbeam/clnlbeam_0000.mtx");
let mtx = match crate::io::mtx::read_mtx(path) {
Ok(m) => m,
Err(_) => return, };
let csc = mtx.to_csc().expect("clnlbeam_0000 CSC build");
assert_eq!(pick_scaling_strategy(&csc), ScalingStrategy::InfNorm);
}
#[test]
fn compute_scaling_auto_routes_to_mc64_on_arrow_kkt() {
let n = 80;
let mut col_ptr = vec![0usize];
let mut row_idx = Vec::new();
let mut values = Vec::new();
for j in 0..40 {
row_idx.push(j);
values.push(2.0);
col_ptr.push(row_idx.len());
}
for j in 40..n {
row_idx.push(j);
values.push(2.0);
for i in (j + 1)..n {
row_idx.push(i);
values.push(0.1);
}
col_ptr.push(row_idx.len());
}
let csc = CscMatrix {
n,
col_ptr,
row_idx,
values,
};
assert_eq!(pick_scaling_strategy(&csc), ScalingStrategy::Mc64Symmetric);
let (auto_s, _) =
compute_scaling(&csc, &ScalingStrategy::Auto).expect("Auto routing should succeed");
let (mc64_s, _) =
compute_scaling(&csc, &ScalingStrategy::Mc64Symmetric).expect("MC64 should succeed");
assert_eq!(auto_s, mc64_s);
}
#[test]
fn max_off_diag_ratio_basic_well_conditioned() {
let csc = CscMatrix {
n: 3,
col_ptr: vec![0, 2, 4, 5],
row_idx: vec![0, 1, 1, 2, 2],
values: vec![4.0, 1.0, 3.0, 1.0, 2.0],
};
let s = vec![1.0; 3];
let r = max_off_diag_ratio(&csc, &s);
assert!((r - 0.5).abs() < 1e-12, "got {r}");
}
#[test]
fn max_off_diag_ratio_zero_diag_gives_infinity() {
let csc = CscMatrix {
n: 2,
col_ptr: vec![0, 2, 3],
row_idx: vec![0, 1, 1],
values: vec![0.0, 1.0, 1.0],
};
let s = vec![1.0; 2];
let r = max_off_diag_ratio(&csc, &s);
assert!(r.is_infinite(), "got {r}");
}
#[test]
fn auto_surfaces_infnorm_spread_fallback_on_uniform_diag() {
let n = 40;
let mut col_ptr = Vec::with_capacity(n + 1);
let mut row_idx = Vec::new();
let mut values = Vec::new();
col_ptr.push(0);
for i in 0..n {
row_idx.push(i);
values.push(2.0);
}
col_ptr.push(row_idx.len());
for j in 1..n {
row_idx.push(j);
values.push(2.0);
col_ptr.push(row_idx.len());
}
let csc = CscMatrix {
n,
col_ptr,
row_idx,
values,
};
assert_eq!(pick_scaling_strategy(&csc), ScalingStrategy::Mc64Symmetric);
let (auto_s, info) = compute_scaling(&csc, &ScalingStrategy::Auto)
.expect("Auto on uniform diag should succeed");
match info {
ScalingInfo::Mc64FallbackToInfnorm {
reason: Mc64FallbackReason::InfNormSpreadAcceptable,
} => {}
other => panic!(
"expected Mc64FallbackToInfnorm{{InfNormSpreadAcceptable}}, got {:?}",
other
),
}
let (in_s, _) = compute_scaling(&csc, &ScalingStrategy::InfNorm)
.expect("InfNorm on uniform diag should succeed");
assert_eq!(auto_s, in_s, "fallback vector must be the InfNorm vector");
assert!(info.is_mc64_fallback());
}
#[test]
fn auto_falls_back_to_infnorm_on_mss1_0009() {
let path = std::path::Path::new("data/matrices/kkt/MSS1/MSS1_0009.mtx");
let mtx = match crate::io::mtx::read_mtx(path) {
Ok(m) => m,
Err(_) => return, };
let csc = mtx.to_csc().expect("MSS1_0009 CSC build");
assert_eq!(pick_scaling_strategy(&csc), ScalingStrategy::Mc64Symmetric);
let (auto_s, auto_info) = compute_scaling(&csc, &ScalingStrategy::Auto)
.expect("Auto on MSS1_0009 should succeed");
let (in_s, _) = compute_scaling(&csc, &ScalingStrategy::InfNorm)
.expect("InfNorm on MSS1_0009 should succeed");
let (mc_s, _) = compute_scaling(&csc, &ScalingStrategy::Mc64Symmetric)
.expect("MC64 on MSS1_0009 should succeed");
assert_eq!(auto_s, in_s, "Auto must fall back to InfNorm on MSS1_0009");
assert_ne!(
auto_s, mc_s,
"Auto must NOT use MC64 on MSS1_0009 (would regress residual to 1e-6)"
);
match auto_info {
ScalingInfo::Mc64FallbackToInfnorm { .. } => {}
other => panic!("MSS1_0009: expected Mc64FallbackToInfnorm, got {:?}", other),
}
}
#[test]
fn auto_keeps_mc64_on_vesuvia_0000() {
let path = std::path::Path::new("data/matrices/kkt/VESUVIA/VESUVIA_0000.mtx");
let mtx = match crate::io::mtx::read_mtx(path) {
Ok(m) => m,
Err(_) => return, };
let csc = mtx.to_csc().expect("VESUVIA_0000 CSC build");
let (auto_s, _) =
compute_scaling(&csc, &ScalingStrategy::Auto).expect("Auto on VESUVIA_0000");
let (mc_s, _) =
compute_scaling(&csc, &ScalingStrategy::Mc64Symmetric).expect("MC64 on VESUVIA_0000");
assert_eq!(auto_s, mc_s, "Auto must keep MC64 on VESUVIA_0000");
}
#[test]
fn auto_keeps_mc64_on_vesuviou_0000() {
let path = std::path::Path::new("data/matrices/kkt/VESUVIOU/VESUVIOU_0000.mtx");
let mtx = match crate::io::mtx::read_mtx(path) {
Ok(m) => m,
Err(_) => return, };
let csc = mtx.to_csc().expect("VESUVIOU_0000 CSC build");
let (auto_s, _) =
compute_scaling(&csc, &ScalingStrategy::Auto).expect("Auto on VESUVIOU_0000");
let (mc_s, _) =
compute_scaling(&csc, &ScalingStrategy::Mc64Symmetric).expect("MC64 on VESUVIOU_0000");
assert_eq!(auto_s, mc_s, "Auto must keep MC64 on VESUVIOU_0000");
}
#[test]
fn auto_picks_infnorm_on_acopp30_0064() {
let path = std::path::Path::new("data/matrices/kkt/ACOPP30/ACOPP30_0064.mtx");
let mtx = match crate::io::mtx::read_mtx(path) {
Ok(m) => m,
Err(_) => return, };
let csc = mtx.to_csc().expect("ACOPP30_0064 CSC build");
assert_eq!(pick_scaling_strategy(&csc), ScalingStrategy::InfNorm);
let (auto_s, _auto_info) =
compute_scaling(&csc, &ScalingStrategy::Auto).expect("Auto on ACOPP30_0064");
let (in_s, _) =
compute_scaling(&csc, &ScalingStrategy::InfNorm).expect("InfNorm on ACOPP30_0064");
let (mc_s, _) =
compute_scaling(&csc, &ScalingStrategy::Mc64Symmetric).expect("MC64 on ACOPP30_0064");
assert_eq!(
auto_s, in_s,
"Auto must pick InfNorm on ACOPP30_0064 (MC64 produces rel_ref=1.7e-1)"
);
assert_ne!(
auto_s, mc_s,
"Auto must NOT use MC64 on ACOPP30_0064 (regresses rel_ref to 1.7e-1)"
);
}
#[test]
fn auto_picks_infnorm_on_hs75_0000() {
let path = std::path::Path::new("data/matrices/kkt/HS75/HS75_0000.mtx");
let mtx = match crate::io::mtx::read_mtx(path) {
Ok(m) => m,
Err(_) => return, };
let csc = mtx.to_csc().expect("HS75_0000 CSC build");
let (auto_s, _) = compute_scaling(&csc, &ScalingStrategy::Auto).expect("Auto on HS75_0000");
let (in_s, _) =
compute_scaling(&csc, &ScalingStrategy::InfNorm).expect("InfNorm on HS75_0000");
assert_eq!(
auto_s, in_s,
"Auto must pick InfNorm on HS75_0000 (in_spread<1e3, InfNorm strictly wins)"
);
}
#[test]
fn scaling_spread_hand_oracle() {
assert!((scaling_spread(&[1e-3, 1.0, 4.0]) - 4000.0).abs() < 1e-9);
assert!((scaling_spread(&[0.0, -2.0, 8.0, 0.0]) - 4.0).abs() < 1e-12);
assert!(scaling_spread(&[0.0, 0.0]).is_infinite());
}
#[test]
fn auto_falls_back_on_catastrophic_mc64_spread() {
let csc = build_synth_kkt(8, 80, 1e8, 4.0, 0.5, 120);
assert_eq!(pick_scaling_strategy(&csc), ScalingStrategy::Mc64Symmetric);
let (mc_vec, _) = compute_scaling(&csc, &ScalingStrategy::Mc64Symmetric)
.expect("MC64 scaling should succeed");
let mc_spread = scaling_spread(&mc_vec);
assert!(
mc_spread > 1.0 / f64::EPSILON,
"test oracle invalid: MC64 spread {mc_spread:.3e} must exceed the guard"
);
let (in_vec, _) = compute_scaling(&csc, &ScalingStrategy::InfNorm)
.expect("InfNorm scaling should succeed");
let in_spread = scaling_spread(&in_vec);
assert!(
in_spread > 1e3,
"test oracle invalid: InfNorm spread {in_spread:.3e} must exceed IN_SPREAD_GUARD"
);
let (auto_vec, info) =
compute_scaling(&csc, &ScalingStrategy::Auto).expect("Auto scaling should succeed");
match info {
ScalingInfo::Mc64FallbackToInfnorm {
reason: Mc64FallbackReason::Mc64ScalingDegenerate,
} => {}
other => {
panic!("expected Mc64FallbackToInfnorm{{Mc64ScalingDegenerate}}, got {other:?}")
}
}
assert_eq!(auto_vec, in_vec, "fallback must return the InfNorm vector");
assert_ne!(
auto_vec, mc_vec,
"fallback must NOT return the degenerate MC64 vector"
);
}
#[test]
fn auto_keeps_mc64_when_spread_below_guard() {
let csc = build_synth_kkt(8, 80, 1e8, 1.1, 0.5, 120);
assert_eq!(pick_scaling_strategy(&csc), ScalingStrategy::Mc64Symmetric);
let (mc_vec, _) = compute_scaling(&csc, &ScalingStrategy::Mc64Symmetric)
.expect("MC64 scaling should succeed");
let mc_spread = scaling_spread(&mc_vec);
assert!(
mc_spread < 1.0 / f64::EPSILON,
"test oracle invalid: MC64 spread {mc_spread:.3e} must be below the guard"
);
let in_spread = scaling_spread(
&compute_scaling(&csc, &ScalingStrategy::InfNorm)
.expect("InfNorm scaling should succeed")
.0,
);
assert!(
in_spread > 1e3,
"test oracle invalid: InfNorm spread {in_spread:.3e} must exceed IN_SPREAD_GUARD"
);
let (auto_vec, info) =
compute_scaling(&csc, &ScalingStrategy::Auto).expect("Auto scaling should succeed");
assert_eq!(
auto_vec, mc_vec,
"Auto must keep the MC64 vector when spread is below the guard"
);
assert!(
!matches!(
info,
ScalingInfo::Mc64FallbackToInfnorm {
reason: Mc64FallbackReason::Mc64ScalingDegenerate,
}
),
"the spread guard must not fire below the threshold, got {info:?}"
);
}
#[test]
fn auto_solves_below_guard_matrix_correctly() {
use crate::numeric::factorize::factorize_multifrontal;
use crate::numeric::solve::solve_sparse;
use crate::symbolic::{symbolic_factorize_with_method, OrderingMethod, SupernodeParams};
use crate::NumericParams;
let csc = build_synth_kkt(8, 80, 1e8, 1.1, 0.5, 120);
let snode = SupernodeParams::default();
let np = NumericParams {
scaling: ScalingStrategy::Auto,
..NumericParams::default()
};
let sym = symbolic_factorize_with_method(&csc, &snode, OrderingMethod::Auto)
.expect("symbolic factorization should succeed");
let (factors, _inertia) =
factorize_multifrontal(&csc, &sym, &np).expect("numeric factorization should succeed");
let rhs = vec![1.0_f64; csc.n];
let x = solve_sparse(&factors, &rhs).expect("solve should succeed");
let mut ax = vec![0.0; csc.n];
csc.symv(&x, &mut ax);
let num = ax
.iter()
.zip(&rhs)
.fold(0.0_f64, |m, (&a, &b)| m.max((a - b).abs()));
let den = rhs.iter().fold(0.0_f64, |m, &b| m.max(b.abs())).max(1.0);
let relres = num / den;
assert!(
relres <= 1e-6,
"Auto solve relres {relres:.3e} exceeds 1e-6"
);
}
}