use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
use crate::error::{CoreError, CoreResult, ErrorContext};
#[inline]
fn matrix_norm_1(a: &ArrayView2<f64>) -> f64 {
let ncols = a.ncols();
let mut max_col = 0.0_f64;
for j in 0..ncols {
let col_sum: f64 = a.column(j).iter().map(|&x| x.abs()).sum();
if col_sum > max_col {
max_col = col_sum;
}
}
max_col
}
#[inline]
fn matrix_norm_inf(a: &ArrayView2<f64>) -> f64 {
let nrows = a.nrows();
let mut max_row = 0.0_f64;
for i in 0..nrows {
let row_sum: f64 = a.row(i).iter().map(|&x| x.abs()).sum();
if row_sum > max_row {
max_row = row_sum;
}
}
max_row
}
#[inline]
fn matrix_norm_frob(a: &ArrayView2<f64>) -> f64 {
let sum_sq: f64 = a.iter().map(|&x| x * x).sum();
sum_sq.sqrt()
}
#[inline]
fn vector_norm_2(x: &ArrayView1<f64>) -> f64 {
let max_abs = x.iter().map(|&v| v.abs()).fold(0.0_f64, f64::max);
if max_abs == 0.0 {
return 0.0;
}
let sum_sq: f64 = x.iter().map(|&v| (v / max_abs).powi(2)).sum();
max_abs * sum_sq.sqrt()
}
#[inline]
fn vector_norm_inf(x: &ArrayView1<f64>) -> f64 {
x.iter().map(|&v| v.abs()).fold(0.0_f64, f64::max)
}
#[inline]
fn vector_norm_1(x: &ArrayView1<f64>) -> f64 {
x.iter().map(|&v| v.abs()).sum()
}
pub fn condition_number_1norm(a: &ArrayView2<f64>) -> CoreResult<f64> {
let n = a.nrows();
if n != a.ncols() {
return Err(CoreError::ShapeError(ErrorContext::new(
"condition_number_1norm requires a square matrix",
)));
}
let norm_a = matrix_norm_1(a);
if norm_a == 0.0 {
return Ok(f64::INFINITY);
}
match invert_matrix(a) {
Some(inv) => Ok(norm_a * matrix_norm_1(&inv.view())),
None => Ok(f64::INFINITY),
}
}
pub fn condition_number_inf(a: &ArrayView2<f64>) -> CoreResult<f64> {
let n = a.nrows();
if n != a.ncols() {
return Err(CoreError::ShapeError(ErrorContext::new(
"condition_number_inf requires a square matrix",
)));
}
let norm_a = matrix_norm_inf(a);
if norm_a == 0.0 {
return Ok(f64::INFINITY);
}
match invert_matrix(a) {
Some(inv) => Ok(norm_a * matrix_norm_inf(&inv.view())),
None => Ok(f64::INFINITY),
}
}
pub fn condition_number_frob(a: &ArrayView2<f64>) -> CoreResult<f64> {
let n = a.nrows();
if n != a.ncols() {
return Err(CoreError::ShapeError(ErrorContext::new(
"condition_number_frob requires a square matrix",
)));
}
let norm_a = matrix_norm_frob(a);
if norm_a == 0.0 {
return Ok(f64::INFINITY);
}
match invert_matrix(a) {
Some(inv) => Ok(norm_a * matrix_norm_frob(&inv.view())),
None => Ok(f64::INFINITY),
}
}
pub fn backward_error_linear(
a: &ArrayView2<f64>,
b: &ArrayView1<f64>,
x: &ArrayView1<f64>,
) -> CoreResult<f64> {
let (m, n) = (a.nrows(), a.ncols());
if b.len() != m {
return Err(CoreError::ShapeError(ErrorContext::new(format!(
"backward_error_linear: b length {blen} != a rows {m}",
blen = b.len()
))));
}
if x.len() != n {
return Err(CoreError::ShapeError(ErrorContext::new(format!(
"backward_error_linear: x length {xlen} != a cols {n}",
xlen = x.len()
))));
}
let ax = matvec(a, x);
let r: Array1<f64> = Array1::from_iter(b.iter().zip(ax.iter()).map(|(&bi, &axi)| bi - axi));
let norm_r = vector_norm_2(&r.view());
let norm_a = matrix_norm_frob(a);
let norm_x = vector_norm_2(x);
let norm_b = vector_norm_2(b);
let denom = norm_a * norm_x + norm_b;
if denom == 0.0 {
if norm_r == 0.0 {
return Ok(0.0);
}
return Ok(f64::INFINITY);
}
Ok(norm_r / denom)
}
pub fn normwise_backward_error(
a: &ArrayView2<f64>,
b: &ArrayView1<f64>,
x: &ArrayView1<f64>,
) -> CoreResult<f64> {
let (m, n) = (a.nrows(), a.ncols());
if b.len() != m {
return Err(CoreError::ShapeError(ErrorContext::new(format!(
"normwise_backward_error: b length {blen} != a rows {m}",
blen = b.len()
))));
}
if x.len() != n {
return Err(CoreError::ShapeError(ErrorContext::new(format!(
"normwise_backward_error: x length {xlen} != a cols {n}",
xlen = x.len()
))));
}
let ax = matvec(a, x);
let r: Array1<f64> = Array1::from_iter(b.iter().zip(ax.iter()).map(|(&bi, &axi)| bi - axi));
let norm_r = vector_norm_inf(&r.view());
let norm_a = matrix_norm_inf(a);
let norm_x = vector_norm_inf(x);
let norm_b = vector_norm_inf(b);
let denom = norm_a * norm_x + norm_b;
if denom == 0.0 {
if norm_r == 0.0 {
return Ok(0.0);
}
return Ok(f64::INFINITY);
}
Ok(norm_r / denom)
}
pub fn componentwise_backward_error(
a: &ArrayView2<f64>,
b: &ArrayView1<f64>,
x: &ArrayView1<f64>,
) -> CoreResult<f64> {
let (m, n) = (a.nrows(), a.ncols());
if b.len() != m {
return Err(CoreError::ShapeError(ErrorContext::new(format!(
"componentwise_backward_error: b length {blen} != a rows {m}",
blen = b.len()
))));
}
if x.len() != n {
return Err(CoreError::ShapeError(ErrorContext::new(format!(
"componentwise_backward_error: x length {xlen} != a cols {n}",
xlen = x.len()
))));
}
let ax = matvec(a, x);
let r: Array1<f64> = Array1::from_iter(b.iter().zip(ax.iter()).map(|(&bi, &axi)| bi - axi));
let abs_a: Array2<f64> = a.mapv(f64::abs);
let abs_x: Array1<f64> = x.mapv(f64::abs);
let denom_vec = matvec(&abs_a.view(), &abs_x.view());
let mut max_ratio = 0.0_f64;
for i in 0..m {
let denom_i = denom_vec[i] + b[i].abs();
if denom_i == 0.0 {
if r[i].abs() > 0.0 {
return Ok(f64::INFINITY);
}
} else {
let ratio = r[i].abs() / denom_i;
if ratio > max_ratio {
max_ratio = ratio;
}
}
}
Ok(max_ratio)
}
pub fn perturbation_bound(
a: &ArrayView2<f64>,
delta_a: &ArrayView2<f64>,
b: &ArrayView1<f64>,
delta_b: &ArrayView1<f64>,
) -> CoreResult<f64> {
let n = a.nrows();
if n != a.ncols() {
return Err(CoreError::ShapeError(ErrorContext::new(
"perturbation_bound: A must be square",
)));
}
if delta_a.nrows() != n || delta_a.ncols() != n {
return Err(CoreError::ShapeError(ErrorContext::new(
"perturbation_bound: δA must have same shape as A",
)));
}
if b.len() != n {
return Err(CoreError::ShapeError(ErrorContext::new(format!(
"perturbation_bound: b length {blen} != n={n}",
blen = b.len()
))));
}
if delta_b.len() != n {
return Err(CoreError::ShapeError(ErrorContext::new(format!(
"perturbation_bound: δb length {dblen} != n={n}",
dblen = delta_b.len()
))));
}
let kappa = condition_number_1norm(a)?;
let norm_a = matrix_norm_1(a);
let norm_da = matrix_norm_1(delta_a);
let norm_b = vector_norm_1(b);
let norm_db = vector_norm_1(delta_b);
if norm_a == 0.0 {
return Ok(f64::INFINITY);
}
let rel_da = norm_da / norm_a;
let rel_db = if norm_b > 0.0 {
norm_db / norm_b
} else {
0.0
};
Ok(kappa * (rel_da + rel_db))
}
#[derive(Debug, Clone, Default)]
pub struct BackwardErrorAnalysis {
pub compute_normwise: bool,
pub compute_componentwise: bool,
pub compute_condition_number: bool,
}
#[derive(Debug, Clone)]
pub struct BackwardErrorReport {
pub normwise_backward_error: f64,
pub lapack_normwise_error: f64,
pub componentwise_backward_error: f64,
pub condition_number_1norm: f64,
pub condition_number_inf: f64,
pub condition_number_frob: f64,
pub residual_norm: f64,
pub nrows: usize,
pub ncols: usize,
}
impl BackwardErrorAnalysis {
#[must_use]
pub fn new() -> Self {
Self {
compute_normwise: true,
compute_componentwise: true,
compute_condition_number: true,
}
}
#[must_use]
pub fn without_condition_numbers(mut self) -> Self {
self.compute_condition_number = false;
self
}
pub fn analyze(
&self,
a: &ArrayView2<f64>,
b: &ArrayView1<f64>,
x: &ArrayView1<f64>,
) -> CoreResult<BackwardErrorReport> {
let (m, n) = (a.nrows(), a.ncols());
if b.len() != m {
return Err(CoreError::ShapeError(ErrorContext::new(format!(
"BackwardErrorAnalysis::analyze: b length {blen} != a rows {m}",
blen = b.len()
))));
}
if x.len() != n {
return Err(CoreError::ShapeError(ErrorContext::new(format!(
"BackwardErrorAnalysis::analyze: x length {xlen} != a cols {n}",
xlen = x.len()
))));
}
let ax = matvec(a, x);
let r: Array1<f64> =
Array1::from_iter(b.iter().zip(ax.iter()).map(|(&bi, &axi)| bi - axi));
let residual_norm = vector_norm_2(&r.view());
let normwise_be = backward_error_linear(a, b, x)?;
let lapack_be = normwise_backward_error(a, b, x)?;
let comp_be = componentwise_backward_error(a, b, x)?;
let (kappa_1, kappa_inf, kappa_f) = if self.compute_condition_number && m == n {
let k1 = condition_number_1norm(a).unwrap_or(f64::INFINITY);
let kinf = condition_number_inf(a).unwrap_or(f64::INFINITY);
let kf = condition_number_frob(a).unwrap_or(f64::INFINITY);
(k1, kinf, kf)
} else {
(f64::NAN, f64::NAN, f64::NAN)
};
Ok(BackwardErrorReport {
normwise_backward_error: normwise_be,
lapack_normwise_error: lapack_be,
componentwise_backward_error: comp_be,
condition_number_1norm: kappa_1,
condition_number_inf: kappa_inf,
condition_number_frob: kappa_f,
residual_norm,
nrows: m,
ncols: n,
})
}
}
fn matvec(a: &ArrayView2<f64>, x: &ArrayView1<f64>) -> Array1<f64> {
let m = a.nrows();
let n = a.ncols();
let mut result = Array1::<f64>::zeros(m);
for i in 0..m {
let mut s = 0.0_f64;
for j in 0..n {
s += a[[i, j]] * x[j];
}
result[i] = s;
}
result
}
fn invert_matrix(a: &ArrayView2<f64>) -> Option<Array2<f64>> {
let n = a.nrows();
let mut aug = Array2::<f64>::zeros((n, 2 * n));
for i in 0..n {
for j in 0..n {
aug[[i, j]] = a[[i, j]];
}
aug[[i, n + i]] = 1.0;
}
for col in 0..n {
let mut pivot_row = col;
let mut pivot_val = aug[[col, col]].abs();
for row in (col + 1)..n {
let v = aug[[row, col]].abs();
if v > pivot_val {
pivot_val = v;
pivot_row = row;
}
}
if pivot_val < f64::EPSILON * 1e6 {
return None; }
if pivot_row != col {
for k in 0..(2 * n) {
let tmp = aug[[col, k]];
aug[[col, k]] = aug[[pivot_row, k]];
aug[[pivot_row, k]] = tmp;
}
}
let diag = aug[[col, col]];
for row in (col + 1)..n {
let factor = aug[[row, col]] / diag;
for k in col..(2 * n) {
let v = aug[[row, k]] - factor * aug[[col, k]];
aug[[row, k]] = v;
}
}
}
for col in (0..n).rev() {
let diag = aug[[col, col]];
if diag.abs() < f64::EPSILON * 1e6 {
return None;
}
for k in 0..(2 * n) {
aug[[col, k]] /= diag;
}
for row in 0..col {
let factor = aug[[row, col]];
for k in 0..(2 * n) {
let v = aug[[row, k]] - factor * aug[[col, k]];
aug[[row, k]] = v;
}
}
}
let mut inv = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
inv[[i, j]] = aug[[i, n + j]];
}
}
Some(inv)
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
#[test]
fn test_matrix_norms() {
let a = array![[1.0_f64, 2.0], [3.0, 4.0]];
assert!((matrix_norm_1(&a.view()) - 6.0).abs() < 1e-12);
assert!((matrix_norm_inf(&a.view()) - 7.0).abs() < 1e-12);
assert!((matrix_norm_frob(&a.view()) - 30.0_f64.sqrt()).abs() < 1e-12);
}
#[test]
fn test_vector_norms() {
let x = array![3.0_f64, 4.0];
assert!((vector_norm_2(&x.view()) - 5.0).abs() < 1e-12);
assert!((vector_norm_inf(&x.view()) - 4.0).abs() < 1e-12);
assert!((vector_norm_1(&x.view()) - 7.0).abs() < 1e-12);
}
#[test]
fn test_condition_number_diagonal() {
let a = array![[1.0_f64, 0.0], [0.0, 4.0]];
let k1 = condition_number_1norm(&a.view()).expect("should succeed");
assert!((k1 - 4.0).abs() < 1e-10, "k1={k1}");
let kinf = condition_number_inf(&a.view()).expect("should succeed");
assert!((kinf - 4.0).abs() < 1e-10, "kinf={kinf}");
}
#[test]
fn test_condition_number_identity() {
let a = array![[1.0_f64, 0.0], [0.0, 1.0]];
let k1 = condition_number_1norm(&a.view()).expect("should succeed");
assert!((k1 - 1.0).abs() < 1e-10);
let kf = condition_number_frob(&a.view()).expect("should succeed");
assert!((kf - 2.0).abs() < 1e-10);
}
#[test]
fn test_backward_error_exact_solution() {
let a = array![[2.0_f64, 1.0], [1.0, 3.0]];
let b = array![5.0_f64, 10.0]; let x = array![1.0_f64, 3.0];
let err = backward_error_linear(&a.view(), &b.view(), &x.view()).expect("should succeed");
assert!(err < 1e-14, "Expected near-zero backward error, got {err}");
}
#[test]
fn test_normwise_backward_error_exact() {
let a = array![[2.0_f64, 1.0], [1.0, 3.0]];
let b = array![5.0_f64, 10.0];
let x = array![1.0_f64, 3.0];
let err = normwise_backward_error(&a.view(), &b.view(), &x.view()).expect("should succeed");
assert!(err < 1e-14, "Expected near-zero LAPACK backward error, got {err}");
}
#[test]
fn test_componentwise_backward_error_exact() {
let a = array![[2.0_f64, 1.0], [1.0, 3.0]];
let b = array![5.0_f64, 10.0];
let x = array![1.0_f64, 3.0];
let err = componentwise_backward_error(&a.view(), &b.view(), &x.view()).expect("should succeed");
assert!(err < 1e-14, "Expected near-zero componentwise backward error, got {err}");
}
#[test]
fn test_backward_error_perturbed_solution() {
let a = array![[2.0_f64, 0.0], [0.0, 2.0]];
let b = array![2.0_f64, 2.0];
let x = array![1.0_f64 + 1e-5, 1.0_f64 - 1e-5];
let err = backward_error_linear(&a.view(), &b.view(), &x.view()).expect("should succeed");
assert!(err > 0.0);
assert!(err < 1e-3);
}
#[test]
fn test_perturbation_bound_zero_perturbation() {
let a = array![[4.0_f64, 0.0], [0.0, 1.0]];
let da = array![[0.0_f64, 0.0], [0.0, 0.0]];
let b = array![4.0_f64, 1.0];
let db = array![0.0_f64, 0.0];
let bound = perturbation_bound(&a.view(), &da.view(), &b.view(), &db.view()).expect("should succeed");
assert!(bound < 1e-14);
}
#[test]
fn test_full_analysis() {
let a = array![[3.0_f64, 1.0], [1.0, 2.0]];
let b = array![7.0_f64, 4.0]; let x = array![2.0_f64, 1.0];
let report = BackwardErrorAnalysis::new()
.analyze(&a.view(), &b.view(), &x.view())
.expect("should succeed");
assert!(report.normwise_backward_error < 1e-13);
assert!(report.componentwise_backward_error < 1e-13);
assert!(report.condition_number_1norm >= 1.0);
assert!(report.residual_norm < 1e-13);
}
#[test]
fn test_shape_errors() {
let a = array![[1.0_f64, 0.0], [0.0, 1.0]];
let b = array![1.0_f64, 2.0, 3.0]; let x = array![1.0_f64, 0.0];
assert!(backward_error_linear(&a.view(), &b.view(), &x.view()).is_err());
let a_rect = array![[1.0_f64, 0.0, 0.0], [0.0, 1.0, 0.0]];
assert!(condition_number_1norm(&a_rect.view()).is_err());
}
#[test]
fn test_singular_matrix_condition_number() {
let a = array![[1.0_f64, 2.0], [2.0, 4.0]]; let k = condition_number_1norm(&a.view()).expect("should succeed");
assert!(k.is_infinite(), "Expected infinity for singular matrix, got {k}");
}
#[test]
fn test_matvec_correctness() {
let a = array![[1.0_f64, 2.0], [3.0, 4.0]];
let x = array![1.0_f64, 1.0];
let ax = matvec(&a.view(), &x.view());
assert!((ax[0] - 3.0).abs() < 1e-12);
assert!((ax[1] - 7.0).abs() < 1e-12);
}
}