use crate::error::{LinalgError, LinalgResult};
use scirs2_core::ndarray::ArrayView2;
use scirs2_core::numeric::{Float, NumAssign, ToPrimitive};
use std::fmt;
#[allow(dead_code)]
pub struct MatrixDiagnostics<F: Float> {
pub shape: (usize, usize),
pub condition_number: Option<F>,
pub rank: Option<usize>,
pub is_symmetric: bool,
pub is_positive_definite: Option<bool>,
pub max_abs_value: F,
pub min_abs_value: F,
pub frobenius_norm: F,
pub precision_loss_estimate: Option<f64>,
pub sparsity_ratio: F,
pub max_diagonal: Option<F>,
pub min_diagonal: Option<F>,
pub near_zero_eigenvalues: Option<usize>,
pub gershgorin_radius: Option<F>,
pub suggestions: Vec<String>,
}
impl<F: Float + fmt::Display> fmt::Display for MatrixDiagnostics<F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Matrix Diagnostics:")?;
writeln!(f, " Shape: {:?}", self.shape)?;
writeln!(f, " Frobenius norm: {}", self.frobenius_norm)?;
writeln!(f, " Max absolute value: {}", self.max_abs_value)?;
writeln!(f, " Min absolute value: {}", self.min_abs_value)?;
writeln!(f, " Symmetric: {}", self.is_symmetric)?;
if let Some(cond) = self.condition_number {
writeln!(f, " Condition number: {cond}")?;
}
if let Some(rank) = self.rank {
writeln!(f, " Rank: {rank}")?;
}
if let Some(pd) = self.is_positive_definite {
writeln!(f, " Positive definite: {pd}")?;
}
writeln!(f, " Sparsity ratio: {:.3}", self.sparsity_ratio)?;
if let Some(precision_loss) = self.precision_loss_estimate {
writeln!(
f,
" Estimated precision loss: {precision_loss:.1} decimal digits"
)?;
}
if let Some(max_diag) = self.max_diagonal {
writeln!(f, " Max diagonal element: {max_diag}")?;
}
if let Some(min_diag) = self.min_diagonal {
writeln!(f, " Min diagonal element: {min_diag}")?;
}
if let Some(gershgorin) = self.gershgorin_radius {
writeln!(f, " Gershgorin circle radius: {gershgorin}")?;
}
if let Some(near_zero) = self.near_zero_eigenvalues {
writeln!(f, " Estimated near-zero eigenvalues: {near_zero}")?;
}
if !self.suggestions.is_empty() {
writeln!(f, "\nSuggestions:")?;
for suggestion in &self.suggestions {
writeln!(f, " - {suggestion}")?;
}
}
Ok(())
}
}
#[allow(dead_code)]
pub fn analyzematrix<F>(a: &ArrayView2<F>) -> MatrixDiagnostics<F>
where
F: Float
+ NumAssign
+ std::iter::Sum
+ fmt::Display
+ scirs2_core::ndarray::ScalarOperand
+ Send
+ Sync,
{
let shape = (a.nrows(), a.ncols());
let mut diagnostics = MatrixDiagnostics {
shape,
condition_number: None,
rank: None,
is_symmetric: false,
is_positive_definite: None,
max_abs_value: F::zero(),
min_abs_value: F::infinity(),
frobenius_norm: F::zero(),
precision_loss_estimate: None,
sparsity_ratio: F::zero(),
max_diagonal: None,
min_diagonal: None,
near_zero_eigenvalues: None,
gershgorin_radius: None,
suggestions: Vec::new(),
};
let mut frobenius_sum = F::zero();
let mut near_zero_count = 0;
let total_elements = a.len();
let zero_threshold = F::epsilon() * F::from(1000.0).expect("Operation failed");
for &elem in a.iter() {
let abs_elem = elem.abs();
if abs_elem > diagnostics.max_abs_value {
diagnostics.max_abs_value = abs_elem;
}
if abs_elem < diagnostics.min_abs_value && abs_elem > F::zero() {
diagnostics.min_abs_value = abs_elem;
}
frobenius_sum += elem * elem;
if abs_elem < zero_threshold {
near_zero_count += 1;
}
}
diagnostics.frobenius_norm = frobenius_sum.sqrt();
diagnostics.sparsity_ratio = F::from(near_zero_count).expect("Operation failed")
/ F::from(total_elements).expect("Operation failed");
if a.nrows() == a.ncols() && a.nrows() > 0 {
let mut max_diag = a[[0, 0]].abs();
let mut min_diag = a[[0, 0]].abs();
for i in 0..a.nrows() {
let diag_elem = a[[i, i]].abs();
max_diag = max_diag.max(diag_elem);
min_diag = min_diag.min(diag_elem);
}
diagnostics.max_diagonal = Some(max_diag);
diagnostics.min_diagonal = Some(min_diag);
diagnostics.gershgorin_radius = compute_gershgorin_radius(a);
diagnostics.near_zero_eigenvalues = estimate_near_zero_eigenvalues(a);
}
if a.nrows() == a.ncols() {
diagnostics.is_symmetric = is_symmetric(a);
if let Ok(cond) = estimate_condition_number(a) {
diagnostics.condition_number = Some(cond);
if let Some(cond_f64) = cond.to_f64() {
if cond_f64 > 1.0 {
let precision_loss = cond_f64.log10();
diagnostics.precision_loss_estimate = Some(precision_loss);
}
}
if cond > F::from(1e12).expect("Operation failed") {
diagnostics.suggestions.push(
"Matrix is extremely ill-conditioned. Consider regularization or using higher precision arithmetic.".to_string()
);
} else if cond > F::from(1e6).expect("Operation failed") {
diagnostics
.suggestions
.push("Matrix is ill-conditioned. Results may be inaccurate.".to_string());
}
}
if a.nrows() == 2 && a.ncols() == 2 {
use crate::basic::det;
if let Ok(det_val) = det(a, None) {
if det_val.abs() < F::from(1e-8).expect("Operation failed") {
diagnostics
.suggestions
.push("Matrix is nearly singular (very small determinant).".to_string());
}
}
}
}
if diagnostics.min_abs_value < F::epsilon() {
diagnostics.suggestions.push(
"Matrix contains near-zero elements which may cause numerical instability.".to_string(),
);
}
if diagnostics.max_abs_value / diagnostics.min_abs_value
> F::from(1e15).expect("Operation failed")
{
diagnostics.suggestions.push(
"Matrix has extreme scale differences. Consider normalizing or preconditioning."
.to_string(),
);
}
if diagnostics.sparsity_ratio > F::from(0.5).expect("Operation failed") {
diagnostics.suggestions.push(
"Matrix is sparse. Consider using sparse matrix algorithms for better performance."
.to_string(),
);
}
if let (Some(max_diag), Some(min_diag)) = (diagnostics.max_diagonal, diagnostics.min_diagonal) {
if min_diag < F::epsilon() {
diagnostics.suggestions.push(
"Matrix has zero or near-zero diagonal elements. Consider pivoting or regularization.".to_string()
);
} else if max_diag / min_diag > F::from(1e12).expect("Operation failed") {
diagnostics.suggestions.push(
"Matrix has poorly scaled diagonal elements. Consider diagonal scaling."
.to_string(),
);
}
}
if let Some(gershgorin) = diagnostics.gershgorin_radius {
if gershgorin > diagnostics.frobenius_norm {
diagnostics.suggestions.push(
"Large off-diagonal elements detected. Matrix may be poorly conditioned for certain operations.".to_string()
);
}
}
if a.nrows() <= 3 && a.nrows() == a.ncols() {
use crate::basic::det;
if let Ok(det_val) = det(a, None) {
if det_val.abs() < F::epsilon() {
diagnostics
.suggestions
.push("Matrix is singular (determinant is zero).".to_string());
} else if det_val.abs() < F::from(1e-10).expect("Operation failed") {
diagnostics
.suggestions
.push("Matrix is nearly singular (determinant is very small).".to_string());
}
}
}
diagnostics
}
#[allow(dead_code)]
fn is_symmetric<F: Float>(a: &ArrayView2<F>) -> bool {
if a.nrows() != a.ncols() {
return false;
}
for i in 0..a.nrows() {
for j in i + 1..a.ncols() {
if (a[[i, j]] - a[[j, i]]).abs()
> F::epsilon() * F::from(100.0).expect("Operation failed")
{
return false;
}
}
}
true
}
#[allow(dead_code)]
fn estimate_condition_number<F>(a: &ArrayView2<F>) -> LinalgResult<F>
where
F: Float
+ NumAssign
+ std::iter::Sum
+ fmt::Display
+ scirs2_core::ndarray::ScalarOperand
+ Send
+ Sync,
{
use crate::basic::det;
use crate::norm::matrix_norm;
let norm_a = matrix_norm(a, "1", None)?;
if norm_a < F::epsilon() {
return Ok(F::infinity());
}
if a.nrows() <= 3 {
if let Ok(det_a) = det(a, None) {
if det_a.abs() < F::epsilon() {
return Ok(F::infinity());
}
let condition_est = norm_a * norm_a / det_a.abs();
if condition_est > F::from(1e12).expect("Operation failed") {
return Ok(condition_est);
}
}
}
let mut min_diag = F::infinity();
let mut max_diag = F::zero();
for i in 0..a.nrows().min(a.ncols()) {
let diag_elem = a[[i, i]].abs();
if diag_elem < min_diag {
min_diag = diag_elem;
}
if diag_elem > max_diag {
max_diag = diag_elem;
}
}
if min_diag < F::epsilon() {
return Ok(F::infinity());
}
Ok((max_diag / min_diag) * norm_a)
}
#[allow(dead_code)]
pub fn enhanced_error<F>(
base_error: LinalgError,
matrix: Option<&ArrayView2<F>>,
operation: &str,
) -> LinalgError
where
F: Float
+ NumAssign
+ std::iter::Sum
+ fmt::Display
+ scirs2_core::ndarray::ScalarOperand
+ Send
+ Sync,
{
if let Some(a) = matrix {
let diagnostics = analyzematrix(a);
let message = format!("{base_error}\n\nOperation: {operation}\n{diagnostics}");
LinalgError::ComputationError(message)
} else {
base_error
}
}
#[allow(dead_code)]
pub fn regularization_suggestions<F>(matrix: &ArrayView2<F>, operation: &str) -> LinalgError
where
F: Float
+ NumAssign
+ std::iter::Sum
+ fmt::Display
+ scirs2_core::ndarray::ScalarOperand
+ Send
+ Sync,
{
let mut diagnostics = analyzematrix(matrix);
diagnostics.suggestions.push(
"Consider adding a small multiple of the identity matrix (Tikhonov regularization)."
.to_string(),
);
diagnostics
.suggestions
.push("Try using an iterative solver with a stopping criterion.".to_string());
diagnostics
.suggestions
.push("Consider using the pseudoinverse for least-squares solutions.".to_string());
let message =
format!("Matrix is singular or nearly singular\n\nOperation: {operation}\n{diagnostics}");
LinalgError::SingularMatrixError(message)
}
#[allow(dead_code)]
fn compute_gershgorin_radius<F: Float + NumAssign>(a: &ArrayView2<F>) -> Option<F> {
if a.nrows() != a.ncols() {
return None;
}
let n = a.nrows();
let mut max_radius = F::zero();
for i in 0..n {
let mut row_sum = F::zero();
for j in 0..n {
if i != j {
row_sum += a[[i, j]].abs();
}
}
max_radius = max_radius.max(row_sum);
}
Some(max_radius)
}
#[allow(dead_code)]
fn estimate_near_zero_eigenvalues<
F: Float + NumAssign + std::iter::Sum + Send + Sync + scirs2_core::ndarray::ScalarOperand,
>(
a: &ArrayView2<F>,
) -> Option<usize> {
if a.nrows() != a.ncols() || a.nrows() == 0 {
return None;
}
let n = a.nrows();
let mut zero_count = 0;
let threshold = F::epsilon() * F::from(100.0).expect("Operation failed");
for i in 0..n {
if a[[i, i]].abs() < threshold {
zero_count += 1;
}
}
if n == 2 {
use crate::basic::det;
if let Ok(det_val) = det(a, None) {
if det_val.abs() < threshold {
zero_count = zero_count.max(1);
}
}
}
Some(zero_count)
}
#[allow(dead_code)]
pub fn advanced_stability_check<F>(a: &ArrayView2<F>) -> StabilityReport<F>
where
F: Float
+ NumAssign
+ std::iter::Sum
+ fmt::Display
+ ToPrimitive
+ scirs2_core::ndarray::ScalarOperand
+ Send
+ Sync,
{
let mut report = StabilityReport {
is_stable: true,
warnings: Vec::new(),
recommendations: Vec::new(),
numerical_rank_estimate: None,
effective_condition_number: None,
_phantom: std::marker::PhantomData,
};
let diagnostics = analyzematrix(a);
if let Some(cond) = diagnostics.condition_number {
report.effective_condition_number = cond.to_f64();
if cond > F::from(1e14).expect("Operation failed") {
report.is_stable = false;
report
.warnings
.push("Extremely poor conditioning detected".to_string());
report
.recommendations
.push("Use higher precision arithmetic or regularization".to_string());
} else if cond > F::from(1e10).expect("Operation failed") {
report
.warnings
.push("Poor conditioning detected".to_string());
report
.recommendations
.push("Consider iterative refinement or preconditioning".to_string());
}
}
if diagnostics.max_abs_value > F::zero() && diagnostics.min_abs_value > F::zero() {
let scale_ratio = diagnostics.max_abs_value / diagnostics.min_abs_value;
if let Some(ratio_f64) = scale_ratio.to_f64() {
if ratio_f64 > 1e12 {
report
.warnings
.push("Severe scaling issues detected".to_string());
report
.recommendations
.push("Apply row/column scaling before factorization".to_string());
}
}
}
if diagnostics.sparsity_ratio > F::from(0.7).expect("Operation failed") {
report
.recommendations
.push("Matrix is very sparse - consider sparse algorithms".to_string());
}
if a.nrows() == a.ncols() {
let rank_estimate = estimate_numerical_rank(a);
report.numerical_rank_estimate = rank_estimate;
if let Some(rank) = rank_estimate {
if rank < a.nrows() {
report
.warnings
.push(format!("Matrix appears rank deficient (rank ≈ {rank})"));
report
.recommendations
.push("Consider using rank-revealing decompositions".to_string());
}
}
}
report
}
#[allow(dead_code)]
fn estimate_numerical_rank<
F: Float + NumAssign + std::iter::Sum + Send + Sync + scirs2_core::ndarray::ScalarOperand,
>(
a: &ArrayView2<F>,
) -> Option<usize> {
if a.nrows() != a.ncols() {
return None;
}
let n = a.nrows();
let mut apparent_rank = n;
for i in 0..n {
if a[[i, i]].abs() < F::epsilon() * F::from(1000.0).expect("Operation failed") {
apparent_rank -= 1;
}
}
if n <= 3 {
use crate::basic::det;
if let Ok(det_val) = det(a, None) {
if det_val.abs() < F::epsilon() * F::from(1000.0).expect("Operation failed") {
apparent_rank = apparent_rank.saturating_sub(1);
}
}
}
Some(apparent_rank)
}
pub struct StabilityReport<F: Float> {
pub is_stable: bool,
pub warnings: Vec<String>,
pub recommendations: Vec<String>,
pub numerical_rank_estimate: Option<usize>,
pub effective_condition_number: Option<f64>,
pub _phantom: std::marker::PhantomData<F>,
}
impl<F: Float + fmt::Display> fmt::Display for StabilityReport<F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Numerical Stability Report:")?;
writeln!(
f,
" Overall stability: {}",
if self.is_stable { "Good" } else { "Poor" }
)?;
if let Some(cond) = self.effective_condition_number {
writeln!(f, " Effective condition number: {cond:.2e}")?;
}
if let Some(rank) = self.numerical_rank_estimate {
writeln!(f, " Estimated numerical rank: {rank}")?;
}
if !self.warnings.is_empty() {
writeln!(f, "\nWarnings:")?;
for warning in &self.warnings {
writeln!(f, " ⚠ {warning}")?;
}
}
if !self.recommendations.is_empty() {
writeln!(f, "\nRecommendations:")?;
for rec in &self.recommendations {
writeln!(f, " → {rec}")?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn test_analyzematrix() {
let a = array![[1.0, 2.0], [2.0, 4.0]];
let diagnostics = analyzematrix(&a.view());
assert_eq!(diagnostics.shape, (2, 2));
assert!(diagnostics.is_symmetric);
assert!(!diagnostics.suggestions.is_empty());
}
#[test]
fn test_ill_conditionedmatrix() {
let a = array![[1.0, 1.0], [1.0, 1.0 + 1e-10]];
let diagnostics = analyzematrix(&a.view());
println!("Condition number: {:?}", diagnostics.condition_number);
println!("Suggestions: {:?}", diagnostics.suggestions);
assert!(!diagnostics.suggestions.is_empty());
}
}