use crate::dense::matrix::SymmetricMatrix;
use crate::error::FeralError;
use crate::sparse::csc::CscMatrix;
#[allow(dead_code)] mod hungarian;
mod infnorm;
mod mc64;
pub fn mc64_matching(matrix: &CscMatrix) -> Result<(Vec<usize>, usize), FeralError> {
mc64::matching_perm(matrix)
}
pub(crate) use mc64::Mc64Cache;
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,
}
#[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::NotApplied))
}
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;
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,
},
));
}
if raw_diag_range(matrix) >= RAW_GUARD {
return mc64_from_cache(matrix);
}
let (mc_vec, mc_info) = mc64_from_cache(matrix)?;
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 nnz_col = end - start;
if nnz_col > max_col_nnz {
max_col_nnz = nnz_col;
}
if nnz_col == 1 && matrix.row_idx[start] == j {
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,
}
}
#[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_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)"
);
}
}