use alloc::vec::Vec;
#[cfg(not(feature = "std"))]
use num_traits::Float;
use num_traits::{AsPrimitive, ToPrimitive};
use crate::traits::{DenseValuedMatrix2D, Finite, Number};
#[derive(Debug, Clone, PartialEq)]
pub struct JacobiConfig {
pub tolerance: f64,
pub max_sweeps: usize,
}
impl Default for JacobiConfig {
#[inline]
fn default() -> Self {
Self { tolerance: 1e-12, max_sweeps: 1000 }
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct JacobiResult {
eigenvalues: Vec<f64>,
eigenvectors: Vec<f64>,
order: usize,
}
impl JacobiResult {
#[must_use]
#[inline]
pub fn eigenvalues(&self) -> &[f64] {
&self.eigenvalues
}
#[must_use]
#[inline]
pub fn eigenvector(&self, k: usize) -> &[f64] {
let start = k * self.order;
&self.eigenvectors[start..start + self.order]
}
#[must_use]
#[inline]
pub fn eigenvectors_flat(&self) -> &[f64] {
&self.eigenvectors
}
#[must_use]
#[inline]
pub fn order(&self) -> usize {
self.order
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum JacobiError {
#[error("The matrix must be square, but has {rows} rows and {columns} columns.")]
NonSquareMatrix {
rows: usize,
columns: usize,
},
#[error("Found a non-finite value at ({row}, {column}).")]
NonFiniteValue {
row: usize,
column: usize,
},
#[error(
"The matrix is not symmetric: value at ({row}, {column}) differs from ({column}, {row})."
)]
NonSymmetricMatrix {
row: usize,
column: usize,
},
#[error("The tolerance must be finite and strictly positive.")]
InvalidTolerance,
#[error("The maximum number of sweeps must be strictly positive.")]
InvalidMaxSweeps,
#[error("The Jacobi solver did not converge within {max_sweeps} sweeps.")]
DidNotConverge {
max_sweeps: usize,
},
#[error("The matrix is empty.")]
EmptyMatrix,
}
pub(crate) fn validate_config(config: &JacobiConfig) -> Result<(), JacobiError> {
if !config.tolerance.is_finite() || config.tolerance <= 0.0 {
return Err(JacobiError::InvalidTolerance);
}
if config.max_sweeps == 0 {
return Err(JacobiError::InvalidMaxSweeps);
}
Ok(())
}
#[allow(clippy::many_single_char_names)]
fn read_symmetric_matrix<M>(matrix: &M) -> Result<(Vec<f64>, usize), JacobiError>
where
M: DenseValuedMatrix2D,
M::Value: Number + ToPrimitive + Finite,
M::RowIndex: AsPrimitive<usize>,
M::ColumnIndex: AsPrimitive<usize>,
{
let num_rows: usize = matrix.number_of_rows().as_();
let num_cols: usize = matrix.number_of_columns().as_();
if num_rows != num_cols {
return Err(JacobiError::NonSquareMatrix { rows: num_rows, columns: num_cols });
}
let order = num_rows;
if order == 0 {
return Err(JacobiError::EmptyMatrix);
}
let mut flat = Vec::with_capacity(order * order);
for row_id in matrix.row_indices() {
let row_idx: usize = row_id.as_();
for (col_id, val) in matrix.column_indices().zip(matrix.row_values(row_id)) {
let col_idx: usize = col_id.as_();
if !val.is_finite() {
return Err(JacobiError::NonFiniteValue { row: row_idx, column: col_idx });
}
let value = val
.to_f64()
.ok_or(JacobiError::NonFiniteValue { row: row_idx, column: col_idx })?;
if !value.is_finite() {
return Err(JacobiError::NonFiniteValue { row: row_idx, column: col_idx });
}
flat.push(value);
}
}
for row in 0..order {
for col in (row + 1)..order {
let upper = flat[row * order + col];
let lower = flat[col * order + row];
let scale = upper.abs().max(lower.abs()).max(1.0);
if (upper - lower).abs() > 16.0 * f64::EPSILON * scale {
return Err(JacobiError::NonSymmetricMatrix { row, column: col });
}
}
}
for row in 0..order {
for col in (row + 1)..order {
let avg = flat[row * order + col].midpoint(flat[col * order + row]);
flat[row * order + col] = avg;
flat[col * order + row] = avg;
}
}
Ok((flat, order))
}
fn off_diag_norm_sq(matrix: &[f64], order: usize) -> f64 {
let mut sum = 0.0;
for row in 0..order {
for col in (row + 1)..order {
let val = matrix[row * order + col];
sum += 2.0 * val * val;
}
}
sum
}
#[allow(clippy::many_single_char_names)]
pub(crate) fn jacobi_decompose(
matrix: &mut [f64],
order: usize,
tol: f64,
max_sweeps: usize,
) -> Result<Vec<f64>, JacobiError> {
let mut evecs = vec![0.0; order * order];
for idx in 0..order {
evecs[idx * order + idx] = 1.0;
}
for _ in 0..max_sweeps {
if off_diag_norm_sq(matrix, order) < tol {
return Ok(evecs);
}
for pivot_row in 0..order {
for pivot_col in (pivot_row + 1)..order {
let off_diag = matrix[pivot_row * order + pivot_col];
if off_diag.abs() < 1e-15 {
continue;
}
let tau = (matrix[pivot_col * order + pivot_col]
- matrix[pivot_row * order + pivot_row])
/ (2.0 * off_diag);
let tan_theta = if tau >= 0.0 {
1.0 / (tau + (1.0 + tau * tau).sqrt())
} else {
-1.0 / (-tau + (1.0 + tau * tau).sqrt())
};
let cos_theta = 1.0 / (1.0 + tan_theta * tan_theta).sqrt();
let sin_theta = tan_theta * cos_theta;
let diag_row = matrix[pivot_row * order + pivot_row];
let diag_col = matrix[pivot_col * order + pivot_col];
matrix[pivot_row * order + pivot_row] = diag_row - tan_theta * off_diag;
matrix[pivot_col * order + pivot_col] = diag_col + tan_theta * off_diag;
matrix[pivot_row * order + pivot_col] = 0.0;
matrix[pivot_col * order + pivot_row] = 0.0;
for other in 0..order {
if other == pivot_row || other == pivot_col {
continue;
}
let elem_row = matrix[other * order + pivot_row];
let elem_col = matrix[other * order + pivot_col];
matrix[other * order + pivot_row] = cos_theta * elem_row - sin_theta * elem_col;
matrix[other * order + pivot_col] = sin_theta * elem_row + cos_theta * elem_col;
matrix[pivot_row * order + other] = matrix[other * order + pivot_row];
matrix[pivot_col * order + other] = matrix[other * order + pivot_col];
}
for idx in 0..order {
let vec_row = evecs[pivot_row * order + idx];
let vec_col = evecs[pivot_col * order + idx];
evecs[pivot_row * order + idx] = cos_theta * vec_row - sin_theta * vec_col;
evecs[pivot_col * order + idx] = sin_theta * vec_row + cos_theta * vec_col;
}
}
}
}
if off_diag_norm_sq(matrix, order) < tol {
Ok(evecs)
} else {
Err(JacobiError::DidNotConverge { max_sweeps })
}
}
pub(crate) fn sort_eigen(eigenvalues: &mut [f64], eigenvectors: &mut [f64], order: usize) {
let mut indices: Vec<usize> = (0..order).collect();
indices.sort_by(|&idx_a, &idx_b| {
eigenvalues[idx_b].partial_cmp(&eigenvalues[idx_a]).unwrap_or(core::cmp::Ordering::Equal)
});
let old_eigenvalues: Vec<f64> = eigenvalues.to_vec();
for (new_pos, &old_pos) in indices.iter().enumerate() {
eigenvalues[new_pos] = old_eigenvalues[old_pos];
}
let old_eigenvectors: Vec<f64> = eigenvectors.to_vec();
for (new_col, &old_col) in indices.iter().enumerate() {
for idx in 0..order {
eigenvectors[new_col * order + idx] = old_eigenvectors[old_col * order + idx];
}
}
}
pub trait Jacobi: DenseValuedMatrix2D + Sized
where
Self::Value: Number + ToPrimitive + Finite,
Self::RowIndex: AsPrimitive<usize>,
Self::ColumnIndex: AsPrimitive<usize>,
{
#[inline]
fn jacobi(&self, config: &JacobiConfig) -> Result<JacobiResult, JacobiError> {
validate_config(config)?;
let (mut flat, order) = read_symmetric_matrix(self)?;
let mut evecs = jacobi_decompose(&mut flat, order, config.tolerance, config.max_sweeps)?;
let mut eigenvalues: Vec<f64> = (0..order).map(|i| flat[i * order + i]).collect();
sort_eigen(&mut eigenvalues, &mut evecs, order);
Ok(JacobiResult { eigenvalues, eigenvectors: evecs, order })
}
}
impl<M: DenseValuedMatrix2D> Jacobi for M
where
M::Value: Number + ToPrimitive + Finite,
M::RowIndex: AsPrimitive<usize>,
M::ColumnIndex: AsPrimitive<usize>,
{
}