use crate::error::{Result, SolverError};
use crate::matrix::Matrix;
use crate::types::Precision;
pub const FULLY_COHERENT_MARGIN: Precision = 1.0;
pub fn coherence_score(matrix: &dyn Matrix) -> Precision {
let n = matrix.rows();
if n == 0 {
return FULLY_COHERENT_MARGIN;
}
let mut worst: Precision = Precision::INFINITY;
for i in 0..n {
let diag = matrix.get(i, i).unwrap_or(0.0).abs();
if diag <= 1e-300 {
return Precision::NEG_INFINITY;
}
let mut off_diag_sum: Precision = 0.0;
for j in 0..matrix.cols() {
if i != j {
off_diag_sum += matrix.get(i, j).unwrap_or(0.0).abs();
}
}
let row_score = (diag - off_diag_sum) / diag;
if row_score < worst {
worst = row_score;
}
}
worst
}
pub fn check_coherence_or_reject(
matrix: &dyn Matrix,
threshold: Precision,
) -> Result<Precision> {
if threshold <= 0.0 {
return Ok(coherence_score(matrix));
}
let coherence = coherence_score(matrix);
if !coherence.is_finite() || coherence < threshold {
return Err(SolverError::Incoherent {
coherence,
threshold,
});
}
Ok(coherence)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::matrix::SparseMatrix;
fn build(triplets: Vec<(usize, usize, Precision)>, n: usize) -> SparseMatrix {
SparseMatrix::from_triplets(triplets, n, n).unwrap()
}
#[test]
fn perfectly_diagonal_is_score_one() {
let m = build(vec![(0, 0, 5.0), (1, 1, 5.0), (2, 2, 5.0)], 3);
let s = coherence_score(&m);
assert!((s - 1.0).abs() < 1e-12, "expected 1.0, got {s}");
}
#[test]
fn moderately_dominant_scores_between_zero_and_one() {
let m = build(
vec![
(0, 0, 5.0), (0, 1, 1.0), (0, 2, 1.0),
(1, 0, 1.0), (1, 1, 5.0), (1, 2, 1.0),
(2, 0, 1.0), (2, 1, 1.0), (2, 2, 5.0),
],
3,
);
let s = coherence_score(&m);
assert!((s - 0.6).abs() < 1e-12, "expected 0.6, got {s}");
}
#[test]
fn boundary_case_scores_zero() {
let m = build(
vec![
(0, 0, 2.0), (0, 1, 1.0), (0, 2, 1.0),
(1, 0, 1.0), (1, 1, 2.0), (1, 2, 1.0),
(2, 0, 1.0), (2, 1, 1.0), (2, 2, 2.0),
],
3,
);
let s = coherence_score(&m);
assert!(s.abs() < 1e-12, "expected ~0, got {s}");
}
#[test]
fn non_dominant_scores_negative() {
let m = build(
vec![
(0, 0, 1.0), (0, 1, 2.0),
(1, 0, 2.0), (1, 1, 1.0),
],
2,
);
let s = coherence_score(&m);
assert!(s < 0.0, "expected negative, got {s}");
}
#[test]
fn zero_diagonal_scores_neg_infinity() {
let m = build(vec![(0, 0, 1.0), (1, 0, 1.0)], 2); let s = coherence_score(&m);
assert!(s.is_infinite() && s.is_sign_negative(), "got {s}");
}
#[test]
fn check_with_disabled_threshold_returns_ok() {
let m = build(vec![(0, 0, 1.0), (0, 1, 2.0), (1, 0, 2.0), (1, 1, 1.0)], 2);
let r = check_coherence_or_reject(&m, 0.0);
assert!(r.is_ok(), "disabled gate should never reject");
}
#[test]
fn check_with_enabled_threshold_rejects_incoherent_matrix() {
let m = build(vec![(0, 0, 1.0), (0, 1, 2.0), (1, 0, 2.0), (1, 1, 1.0)], 2);
let r = check_coherence_or_reject(&m, 0.05);
match r {
Err(SolverError::Incoherent { coherence, threshold }) => {
assert_eq!(threshold, 0.05);
assert!(coherence < threshold);
}
other => panic!("expected Err(Incoherent), got {other:?}"),
}
}
#[test]
fn check_with_enabled_threshold_passes_dominant_matrix() {
let m = build(
vec![
(0, 0, 5.0), (0, 1, 1.0),
(1, 0, 1.0), (1, 1, 5.0),
],
2,
);
let r = check_coherence_or_reject(&m, 0.05);
assert!(r.is_ok(), "5/1 dominant matrix should pass 0.05 threshold");
let score = r.unwrap();
assert!((score - 0.8).abs() < 1e-12, "expected 0.8, got {score}");
}
}