Skip to main content

scirs2_linalg/sparse_dense/
mod.rs

1//! Sparse-Dense matrix operations
2//!
3//! This module provides operations between sparse and dense matrices, facilitating
4//! interoperability between the two formats. The operations are optimized for the
5//! common case where one matrix is sparse and the other is dense.
6//!
7//! ## Overview
8//!
9//! * Matrix-matrix multiplication between sparse and dense matrices
10//! * Matrix-vector multiplication with sparse matrices
11//! * Element-wise operations (addition, subtraction, multiplication)
12//! * Conversions between sparse and dense formats
13//!
14//! ## Examples
15//!
16//! ```
17//! use scirs2_core::ndarray::{Array2, Array1, array};
18//! use scirs2_linalg::sparse_dense::{sparse_from_ndarray, sparse_dense_matvec};
19//!
20//! // Create a dense matrix and convert to sparse
21//! let dense_mat = array![[1.0, 0.0, 2.0], [0.0, 0.0, 3.0], [4.0, 5.0, 0.0]];
22//! let sparse_mat = sparse_from_ndarray(&dense_mat.view(), 1e-10).expect("Operation failed");
23//!
24//! // Create a dense vector
25//! let dense_vec = array![1.0, 2.0, 3.0];
26//!
27//! // Perform sparse-dense matrix-vector multiplication
28//! let result = sparse_dense_matvec(&sparse_mat, &dense_vec.view()).expect("Operation failed");
29//! assert!(f64::abs(result[0] - 7.0) < 1e-10);
30//! assert!(f64::abs(result[1] - 9.0) < 1e-10);
31//! assert!(f64::abs(result[2] - 14.0) < 1e-10);
32//! ```
33
34use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis};
35use std::collections::HashMap;
36use std::fmt::Debug;
37use std::ops::{Add, Mul, Sub};
38
39use scirs2_core::numeric::{Float, NumAssign, NumCast, Zero};
40
41use crate::error::{LinalgError, LinalgResult};
42
43/// A simple view struct for sparse matrices in CSR format
44///
45/// This struct provides a simple interface to view a sparse matrix in CSR format.
46/// It is designed to be compatible with the CSR format used in scirs2-sparse.
47#[derive(Debug, Clone)]
48pub struct SparseMatrixView<T> {
49    /// Number of rows
50    pub rows: usize,
51    /// Number of columns
52    pub cols: usize,
53    /// Row pointers (size rows+1)
54    pub indptr: Vec<usize>,
55    /// Column indices
56    pub indices: Vec<usize>,
57    /// Data values
58    pub data: Vec<T>,
59}
60
61impl<T> SparseMatrixView<T>
62where
63    T: Clone + Copy + Debug + Zero,
64{
65    /// Create a new CSR sparse matrix view
66    ///
67    /// # Arguments
68    ///
69    /// * `data` - Vector of non-zero values
70    /// * `indptr` - Vector of row pointers (size rows+1)
71    /// * `indices` - Vector of column indices
72    /// * `shape` - Tuple containing the matrix dimensions (rows, cols)
73    ///
74    /// # Returns
75    ///
76    /// A new sparse matrix view
77    pub fn new(
78        data: Vec<T>,
79        indptr: Vec<usize>,
80        indices: Vec<usize>,
81        shape: (usize, usize),
82    ) -> LinalgResult<Self> {
83        let (rows, cols) = shape;
84
85        // Validate input data
86        if indptr.len() != rows + 1 {
87            return Err(LinalgError::DimensionError(format!(
88                "Row pointer array length must be rows + 1, got {} for {} rows",
89                indptr.len(),
90                rows
91            )));
92        }
93
94        if data.len() != indices.len() {
95            return Err(LinalgError::DimensionError(format!(
96                "Data and indices must have the same length, got {} and {}",
97                data.len(),
98                indices.len()
99            )));
100        }
101
102        // Check if indptr is monotonically increasing
103        for i in 1..indptr.len() {
104            if indptr[i] < indptr[i - 1] {
105                return Err(LinalgError::ValueError(
106                    "Row pointer array must be monotonically increasing".to_string(),
107                ));
108            }
109        }
110
111        // Check if the last indptr entry matches the data length
112        if indptr[rows] != data.len() {
113            return Err(LinalgError::ValueError(format!(
114                "Last row pointer entry must match data length, got {} and {}",
115                indptr[rows],
116                data.len()
117            )));
118        }
119
120        // Check if indices are within bounds
121        if let Some(&max_index) = indices.iter().max() {
122            if max_index >= cols {
123                return Err(LinalgError::ValueError(format!(
124                    "Column index out of bounds: {max_index} for a matrix with {cols} columns"
125                )));
126            }
127        }
128
129        Ok(SparseMatrixView {
130            rows,
131            cols,
132            indptr,
133            indices,
134            data,
135        })
136    }
137
138    /// Get the shape of the matrix
139    pub fn shape(&self) -> (usize, usize) {
140        (self.rows, self.cols)
141    }
142
143    /// Get the number of non-zero elements in the matrix
144    pub fn nnz(&self) -> usize {
145        self.data.len()
146    }
147
148    /// Get the number of rows
149    pub fn nrows(&self) -> usize {
150        self.rows
151    }
152
153    /// Get the number of columns
154    pub fn ncols(&self) -> usize {
155        self.cols
156    }
157
158    /// Check if the matrix is empty
159    pub fn is_empty(&self) -> bool {
160        self.nnz() == 0
161    }
162
163    /// Convert the sparse matrix to a dense ndarray
164    pub fn to_dense(&self) -> Array2<T> {
165        let mut dense = Array2::zeros((self.rows, self.cols));
166
167        for row in 0..self.rows {
168            for j in self.indptr[row]..self.indptr[row + 1] {
169                let col = self.indices[j];
170                dense[[row, col]] = self.data[j];
171            }
172        }
173
174        dense
175    }
176}
177
178/// Convert a dense ndarray to a sparse matrix in CSR format
179///
180/// # Arguments
181///
182/// * `array` - Dense ndarray to convert
183/// * `threshold` - Threshold below which values are considered zero
184///
185/// # Returns
186///
187/// A sparse matrix view in CSR format
188#[allow(dead_code)]
189pub fn sparse_from_ndarray<T, F>(
190    array: &ArrayView2<T>,
191    threshold: F,
192) -> LinalgResult<SparseMatrixView<T>>
193where
194    T: Clone + Copy + Debug + PartialOrd + Zero + NumCast,
195    F: Float + NumCast,
196{
197    let shape = array.dim();
198    let (rows, cols) = (shape.0, shape.1);
199
200    let mut data = Vec::new();
201    let mut indices = Vec::new();
202    let mut indptr = vec![0; rows + 1];
203
204    let threshold_abs: F = threshold.abs();
205
206    // Count non-zeros and fill data
207    for (i, row) in array.axis_iter(Axis(0)).enumerate() {
208        for (j, &val) in row.iter().enumerate() {
209            let val_abs: F = if let Some(v) = NumCast::from(val) {
210                let v_typed: F = v;
211                v_typed.abs()
212            } else {
213                F::zero()
214            };
215            if val_abs > threshold_abs {
216                data.push(val);
217                indices.push(j);
218                indptr[i + 1] += 1;
219            }
220        }
221    }
222
223    // Convert counts to offsets
224    for i in 1..=rows {
225        indptr[i] += indptr[i - 1];
226    }
227
228    SparseMatrixView::new(data, indptr, indices, (rows, cols))
229}
230
231/// Matrix-matrix multiplication between a sparse matrix and a dense matrix
232///
233/// # Arguments
234///
235/// * `sparse` - Sparse matrix in CSR format
236/// * `dense` - Dense matrix
237///
238/// # Returns
239///
240/// A dense matrix containing the result
241#[allow(dead_code)]
242pub fn sparse_dense_matmul<T>(
243    sparse: &SparseMatrixView<T>,
244    dense: &ArrayView2<T>,
245) -> LinalgResult<Array2<T>>
246where
247    T: Clone + Copy + Debug + Zero + Add<Output = T> + Mul<Output = T>,
248{
249    // Check dimensions
250    if sparse.cols != dense.dim().0 {
251        return Err(LinalgError::DimensionError(format!(
252            "Matrix dimensions incompatible for multiplication: {}x{} and {}x{}",
253            sparse.rows,
254            sparse.cols,
255            dense.dim().0,
256            dense.dim().1
257        )));
258    }
259
260    // Initialize result matrix
261    let result_rows = sparse.rows;
262    let result_cols = dense.dim().1;
263    let mut result = Array2::zeros((result_rows, result_cols));
264
265    // Perform matrix multiplication
266    for i in 0..sparse.rows {
267        for j in 0..result_cols {
268            let mut sum = T::zero();
269
270            // Multiply sparse row by dense column
271            for k in sparse.indptr[i]..sparse.indptr[i + 1] {
272                let col = sparse.indices[k];
273                sum = sum + sparse.data[k] * dense[[col, j]];
274            }
275
276            result[[i, j]] = sum;
277        }
278    }
279
280    Ok(result)
281}
282
283/// Matrix-matrix multiplication between a dense matrix and a sparse matrix
284///
285/// # Arguments
286///
287/// * `dense` - Dense matrix
288/// * `sparse` - Sparse matrix in CSR format
289///
290/// # Returns
291///
292/// A dense matrix containing the result
293#[allow(dead_code)]
294pub fn dense_sparse_matmul<T>(
295    dense: &ArrayView2<T>,
296    sparse: &SparseMatrixView<T>,
297) -> LinalgResult<Array2<T>>
298where
299    T: Clone + Copy + Debug + Zero + Add<Output = T> + Mul<Output = T>,
300{
301    // Check dimensions
302    if dense.dim().1 != sparse.rows {
303        return Err(LinalgError::DimensionError(format!(
304            "Matrix dimensions incompatible for multiplication: {}x{} and {}x{}",
305            dense.dim().0,
306            dense.dim().1,
307            sparse.rows,
308            sparse.cols
309        )));
310    }
311
312    // Initialize result matrix
313    let result_rows = dense.dim().0;
314    let result_cols = sparse.cols;
315    let mut result = Array2::zeros((result_rows, result_cols));
316
317    // Initialize column sums for each sparse column
318    let mut col_sums: HashMap<usize, Vec<T>> = HashMap::new();
319
320    // Populate column sums
321    for i in 0..sparse.rows {
322        for j in sparse.indptr[i]..sparse.indptr[i + 1] {
323            let col = sparse.indices[j];
324            let val = sparse.data[j];
325
326            col_sums
327                .entry(col)
328                .or_insert_with(|| vec![T::zero(); result_rows]);
329
330            // Compute dense_row * sparse_val for each dense row
331            for (k, dense_row) in dense.axis_iter(Axis(0)).enumerate() {
332                col_sums.get_mut(&col).expect("Operation failed")[k] =
333                    col_sums[&col][k] + dense_row[i] * val;
334            }
335        }
336    }
337
338    // Fill the result matrix
339    for (col, sums) in col_sums.iter() {
340        for (row, &sum) in sums.iter().enumerate() {
341            result[[row, *col]] = sum;
342        }
343    }
344
345    Ok(result)
346}
347
348/// Matrix-vector multiplication between a sparse matrix and a dense vector
349///
350/// # Arguments
351///
352/// * `sparse` - Sparse matrix in CSR format
353/// * `vector` - Dense vector
354///
355/// # Returns
356///
357/// A dense vector containing the result
358#[allow(dead_code)]
359pub fn sparse_dense_matvec<T>(
360    sparse: &SparseMatrixView<T>,
361    vector: &ArrayView1<T>,
362) -> LinalgResult<Array1<T>>
363where
364    T: Clone + Copy + Debug + Zero + Add<Output = T> + Mul<Output = T>,
365{
366    // Check dimensions
367    if sparse.cols != vector.len() {
368        return Err(LinalgError::DimensionError(format!(
369            "Matrix-vector dimensions incompatible: {}x{} and {}",
370            sparse.rows,
371            sparse.cols,
372            vector.len()
373        )));
374    }
375
376    // Initialize result vector
377    let mut result = Array1::zeros(sparse.rows);
378
379    // Perform sparse matrix-vector multiplication
380    for i in 0..sparse.rows {
381        let mut sum = T::zero();
382
383        for j in sparse.indptr[i]..sparse.indptr[i + 1] {
384            let col = sparse.indices[j];
385            sum = sum + sparse.data[j] * vector[col];
386        }
387
388        result[i] = sum;
389    }
390
391    Ok(result)
392}
393
394/// Matrix-vector multiplication between a dense matrix and a sparse vector
395///
396/// # Arguments
397///
398/// * `dense` - Dense matrix
399/// * `sparse` - Sparse vector in CSR format (as a single-row sparse matrix)
400///
401/// # Returns
402///
403/// A dense vector containing the result
404#[allow(dead_code)]
405pub fn dense_sparse_matvec<T>(
406    dense: &ArrayView2<T>,
407    sparse: &SparseMatrixView<T>,
408) -> LinalgResult<Array1<T>>
409where
410    T: Clone + Copy + Debug + Zero + Add<Output = T> + Mul<Output = T>,
411{
412    // Check if the sparse matrix is actually a vector (single row)
413    if sparse.rows != 1 {
414        return Err(LinalgError::DimensionError(format!(
415            "Expected a sparse vector (single row), got {} rows",
416            sparse.rows
417        )));
418    }
419
420    // Check dimensions
421    if dense.dim().1 != sparse.cols {
422        return Err(LinalgError::DimensionError(format!(
423            "Matrix-vector dimensions incompatible: {}x{} and {}",
424            dense.dim().0,
425            dense.dim().1,
426            sparse.cols
427        )));
428    }
429
430    // Initialize result vector
431    let mut result = Array1::zeros(dense.dim().0);
432
433    // Perform dense matrix-sparse vector multiplication
434    for j in sparse.indptr[0]..sparse.indptr[1] {
435        let col = sparse.indices[j];
436        let val = sparse.data[j];
437
438        for (i, row_val) in result.iter_mut().enumerate() {
439            *row_val = *row_val + dense[[i, col]] * val;
440        }
441    }
442
443    Ok(result)
444}
445
446/// Element-wise addition between a sparse matrix and a dense matrix
447///
448/// # Arguments
449///
450/// * `sparse` - Sparse matrix in CSR format
451/// * `dense` - Dense matrix
452///
453/// # Returns
454///
455/// A dense matrix containing the result
456#[allow(dead_code)]
457pub fn sparse_dense_add<T>(
458    sparse: &SparseMatrixView<T>,
459    dense: &ArrayView2<T>,
460) -> LinalgResult<Array2<T>>
461where
462    T: Clone + Copy + Debug + Zero + Add<Output = T>,
463{
464    // Check dimensions
465    if sparse.shape() != dense.dim() {
466        return Err(LinalgError::DimensionError(format!(
467            "Matrix dimensions incompatible for addition: {}x{} and {}x{}",
468            sparse.rows,
469            sparse.cols,
470            dense.dim().0,
471            dense.dim().1
472        )));
473    }
474
475    // Start with a copy of the dense matrix
476    let mut result = dense.to_owned();
477
478    // Add the sparse elements
479    for i in 0..sparse.rows {
480        for j in sparse.indptr[i]..sparse.indptr[i + 1] {
481            let col = sparse.indices[j];
482            result[[i, col]] = result[[i, col]] + sparse.data[j];
483        }
484    }
485
486    Ok(result)
487}
488
489/// Element-wise subtraction between a sparse matrix and a dense matrix
490///
491/// # Arguments
492///
493/// * `sparse` - Sparse matrix in CSR format
494/// * `dense` - Dense matrix
495///
496/// # Returns
497///
498/// A dense matrix containing the result (sparse - dense)
499#[allow(dead_code)]
500pub fn sparse_dense_sub<T>(
501    sparse: &SparseMatrixView<T>,
502    dense: &ArrayView2<T>,
503) -> LinalgResult<Array2<T>>
504where
505    T: Clone + Copy + Debug + Zero + Sub<Output = T> + Neg<Output = T>,
506{
507    // Check dimensions
508    if sparse.shape() != dense.dim() {
509        return Err(LinalgError::DimensionError(format!(
510            "Matrix dimensions incompatible for subtraction: {}x{} and {}x{}",
511            sparse.rows,
512            sparse.cols,
513            dense.dim().0,
514            dense.dim().1
515        )));
516    }
517
518    // Start with negative of the dense matrix
519    let mut result = dense.mapv(|x| -x);
520
521    // Add the sparse elements
522    for i in 0..sparse.rows {
523        for j in sparse.indptr[i]..sparse.indptr[i + 1] {
524            let col = sparse.indices[j];
525            result[[i, col]] = result[[i, col]] + sparse.data[j];
526        }
527    }
528
529    Ok(result)
530}
531
532/// Element-wise multiplication between a sparse matrix and a dense matrix
533///
534/// # Arguments
535///
536/// * `sparse` - Sparse matrix in CSR format
537/// * `dense` - Dense matrix
538///
539/// # Returns
540///
541/// A sparse matrix containing the result
542#[allow(dead_code)]
543pub fn sparse_dense_elementwise_mul<T>(
544    sparse: &SparseMatrixView<T>,
545    dense: &ArrayView2<T>,
546) -> LinalgResult<SparseMatrixView<T>>
547where
548    T: Clone + Copy + Debug + Zero + Mul<Output = T> + PartialEq,
549{
550    // Check dimensions
551    if sparse.shape() != dense.dim() {
552        return Err(LinalgError::DimensionError(format!(
553            "Matrix dimensions incompatible for element-wise multiplication: {}x{} and {}x{}",
554            sparse.rows,
555            sparse.cols,
556            dense.dim().0,
557            dense.dim().1
558        )));
559    }
560
561    // Prepare data structures for result
562    let mut data = Vec::new();
563    let mut indices = Vec::new();
564    let mut indptr = vec![0; sparse.rows + 1];
565
566    // Multiply sparse and dense elements
567    for i in 0..sparse.rows {
568        for j in sparse.indptr[i]..sparse.indptr[i + 1] {
569            let col = sparse.indices[j];
570            let val = sparse.data[j] * dense[[i, col]];
571
572            // Only include non-zero values
573            if val != T::zero() {
574                data.push(val);
575                indices.push(col);
576                indptr[i + 1] += 1;
577            }
578        }
579    }
580
581    // Convert counts to offsets
582    for i in 1..=sparse.rows {
583        indptr[i] += indptr[i - 1];
584    }
585
586    SparseMatrixView::new(data, indptr, indices, sparse.shape())
587}
588
589/// Transpose a sparse matrix
590///
591/// # Arguments
592///
593/// * `sparse` - Sparse matrix in CSR format
594///
595/// # Returns
596///
597/// A transposed sparse matrix
598#[allow(dead_code)]
599pub fn sparse_transpose<T>(sparse: &SparseMatrixView<T>) -> LinalgResult<SparseMatrixView<T>>
600where
601    T: Clone + Copy + Debug + Zero,
602{
603    // Compute the number of non-zeros per column
604    let mut col_counts = vec![0; sparse.cols];
605    for &col in &sparse.indices {
606        col_counts[col] += 1;
607    }
608
609    // Compute column pointers (cumulative sum)
610    let mut col_ptrs = vec![0; sparse.cols + 1];
611    for i in 0..sparse.cols {
612        col_ptrs[i + 1] = col_ptrs[i] + col_counts[i];
613    }
614
615    // Fill the transposed matrix
616    let nnz = sparse.nnz();
617    let mut indices_t = vec![0; nnz];
618    let mut data_t = vec![T::zero(); nnz];
619    let mut col_offsets = vec![0; sparse.cols];
620
621    for row in 0..sparse.rows {
622        for j in sparse.indptr[row]..sparse.indptr[row + 1] {
623            let col = sparse.indices[j];
624            let dest = col_ptrs[col] + col_offsets[col];
625
626            indices_t[dest] = row;
627            data_t[dest] = sparse.data[j];
628            col_offsets[col] += 1;
629        }
630    }
631
632    SparseMatrixView::new(data_t, col_ptrs, indices_t, (sparse.cols, sparse.rows))
633}
634
635/// Advanced sparse matrix operations and integration points
636pub mod advanced {
637    use super::*;
638
639    /// Adaptive algorithm selection based on sparsity patterns
640    pub fn adaptive_sparse_dense_solve<T>(
641        sparse: &SparseMatrixView<T>,
642        rhs: &ArrayView1<T>,
643        tolerance: T,
644        max_iterations: usize,
645    ) -> LinalgResult<Array1<T>>
646    where
647        T: Float
648            + Clone
649            + Copy
650            + Debug
651            + Zero
652            + Add<Output = T>
653            + Sub<Output = T>
654            + Mul<Output = T>
655            + PartialOrd
656            + std::iter::Sum
657            + scirs2_core::ndarray::ScalarOperand
658            + NumAssign
659            + Send
660            + Sync,
661    {
662        // Analyze sparsity pattern to choose optimal algorithm
663        let sparsity_ratio = sparse.nnz() as f64 / (sparse.nrows() * sparse.ncols()) as f64;
664        let avg_nnz_per_row = sparse.nnz() as f64 / sparse.nrows() as f64;
665
666        if sparsity_ratio < 0.1 && avg_nnz_per_row < 50.0 {
667            // Very sparse: use iterative methods
668            sparse_conjugate_gradient(sparse, rhs, tolerance, max_iterations)
669        } else if sparsity_ratio < 0.3 {
670            // Moderately sparse: use preconditioned methods
671            sparse_preconditioned_cg(sparse, rhs, tolerance, max_iterations)
672        } else {
673            // Dense-like: convert to dense and use direct methods
674            let dense = sparse.to_dense();
675            crate::solve::solve(&dense.view(), rhs, None)
676        }
677    }
678
679    /// Sparse Conjugate Gradient solver
680    pub fn sparse_conjugate_gradient<T>(
681        a: &SparseMatrixView<T>,
682        b: &ArrayView1<T>,
683        tolerance: T,
684        max_iterations: usize,
685    ) -> LinalgResult<Array1<T>>
686    where
687        T: Float
688            + Clone
689            + Copy
690            + Debug
691            + Zero
692            + Add<Output = T>
693            + Sub<Output = T>
694            + Mul<Output = T>
695            + PartialOrd
696            + scirs2_core::ndarray::ScalarOperand,
697    {
698        let n = a.nrows();
699        if a.ncols() != n {
700            return Err(LinalgError::DimensionError(
701                "Matrix must be square for conjugate gradient".to_string(),
702            ));
703        }
704
705        // Initial guess (zero vector)
706        let mut x = Array1::zeros(n);
707
708        // Initial residual: r = b - A*x = b (since x = 0)
709        let mut r = b.to_owned();
710        let mut p = r.clone();
711
712        let mut rsold = r.dot(&r);
713        let b_norm = b.dot(b).sqrt();
714
715        if b_norm < T::epsilon() {
716            return Ok(x);
717        }
718
719        for _ in 0..max_iterations {
720            // Compute A*p using sparse matrix-vector multiplication
721            let ap = sparse_dense_matvec(a, &p.view())?;
722
723            // Compute step size
724            let pap = p.dot(&ap);
725            if pap <= T::zero() {
726                return Err(LinalgError::ComputationError(
727                    "Matrix is not positive definite".to_string(),
728                ));
729            }
730
731            let alpha = rsold / pap;
732
733            // Update solution and residual
734            x = x + &p * alpha;
735            r = r - &ap * alpha;
736
737            let rsnew = r.dot(&r);
738
739            // Check convergence
740            if rsnew.sqrt() < tolerance * b_norm {
741                return Ok(x);
742            }
743
744            // Update search direction
745            let beta = rsnew / rsold;
746            p = &r + &p * beta;
747            rsold = rsnew;
748        }
749
750        // Return best solution found
751        Ok(x)
752    }
753
754    /// Preconditioned Conjugate Gradient with diagonal preconditioning
755    pub fn sparse_preconditioned_cg<T>(
756        a: &SparseMatrixView<T>,
757        b: &ArrayView1<T>,
758        tolerance: T,
759        max_iterations: usize,
760    ) -> LinalgResult<Array1<T>>
761    where
762        T: Float
763            + Clone
764            + Copy
765            + Debug
766            + Zero
767            + Add<Output = T>
768            + Sub<Output = T>
769            + Mul<Output = T>
770            + PartialOrd
771            + scirs2_core::ndarray::ScalarOperand,
772    {
773        // Extract diagonal for preconditioning
774        let mut diag = Array1::zeros(a.nrows());
775        for i in 0..a.nrows() {
776            for j in a.indptr[i]..a.indptr[i + 1] {
777                if a.indices[j] == i {
778                    diag[i] = a.data[j];
779                    break;
780                }
781            }
782            // Avoid division by zero
783            if diag[i].abs() < T::epsilon() {
784                diag[i] = T::one();
785            }
786        }
787
788        // Create diagonal preconditioner M^-1
789        let m_inv = diag.mapv(|x| T::one() / x);
790
791        let n = a.nrows();
792        let mut x = Array1::zeros(n);
793        let mut r = b.to_owned();
794        let mut z = &r * &m_inv; // Apply preconditioner
795        let mut p = z.clone();
796
797        let mut rzold = r.dot(&z);
798        let b_norm = b.dot(b).sqrt();
799
800        for _ in 0..max_iterations {
801            let ap = sparse_dense_matvec(a, &p.view())?;
802            let pap = p.dot(&ap);
803
804            if pap <= T::zero() {
805                return Err(LinalgError::ComputationError(
806                    "Matrix is not positive definite".to_string(),
807                ));
808            }
809
810            let alpha = rzold / pap;
811
812            x = x + &p * alpha;
813            r = r - &ap * alpha;
814
815            // Check convergence
816            let r_norm = r.dot(&r).sqrt();
817            if r_norm < tolerance * b_norm {
818                return Ok(x);
819            }
820
821            // Apply preconditioner
822            z = &r * &m_inv;
823            let rznew = r.dot(&z);
824            let beta = rznew / rzold;
825
826            p = &z + &p * beta;
827            rzold = rznew;
828        }
829
830        Ok(x)
831    }
832
833    /// Sparse matrix statistics for optimization decisions
834    #[derive(Debug, Clone)]
835    pub struct SparseMatrixStats {
836        pub sparsity_ratio: f64,
837        pub avg_nnz_per_row: f64,
838        pub max_nnz_per_row: usize,
839        pub bandwidth: usize,
840        pub is_symmetric: bool,
841        pub is_diagonal_dominant: bool,
842    }
843
844    /// Analyze sparse matrix structure for algorithm selection
845    pub fn analyze_sparse_structure<T>(sparse: &SparseMatrixView<T>) -> SparseMatrixStats
846    where
847        T: Float + Clone + Copy + Debug + PartialOrd,
848    {
849        let total_elements = sparse.nrows() * sparse.ncols();
850        let sparsity_ratio = sparse.nnz() as f64 / total_elements as f64;
851        let avg_nnz_per_row = sparse.nnz() as f64 / sparse.nrows() as f64;
852
853        // Find maximum non-zeros per row
854        let mut max_nnz_per_row = 0;
855        for i in 0..sparse.nrows() {
856            let row_nnz = sparse.indptr[i + 1] - sparse.indptr[i];
857            max_nnz_per_row = max_nnz_per_row.max(row_nnz);
858        }
859
860        // Estimate bandwidth (maximum distance from diagonal)
861        let mut bandwidth = 0;
862        for i in 0..sparse.nrows() {
863            for j in sparse.indptr[i]..sparse.indptr[i + 1] {
864                let col = sparse.indices[j];
865                let distance = i.abs_diff(col);
866                bandwidth = bandwidth.max(distance);
867            }
868        }
869
870        // Check for symmetry (approximate check)
871        let is_symmetric = if sparse.nrows() == sparse.ncols() {
872            check_sparse_symmetry(sparse)
873        } else {
874            false
875        };
876
877        // Check diagonal dominance
878        let is_diagonal_dominant = check_diagonal_dominance(sparse);
879
880        SparseMatrixStats {
881            sparsity_ratio,
882            avg_nnz_per_row,
883            max_nnz_per_row,
884            bandwidth,
885            is_symmetric,
886            is_diagonal_dominant,
887        }
888    }
889
890    /// Check if sparse matrix is approximately symmetric
891    fn check_sparse_symmetry<T>(sparse: &SparseMatrixView<T>) -> bool
892    where
893        T: Float + Clone + Copy + Debug + PartialOrd,
894    {
895        // Create a map of (row, col) -> value for quick lookup
896        let mut elements = HashMap::new();
897
898        for i in 0..sparse.nrows() {
899            for j in sparse.indptr[i]..sparse.indptr[i + 1] {
900                let col = sparse.indices[j];
901                elements.insert((i, col), sparse.data[j]);
902            }
903        }
904
905        // Check if A[i,j] ≈ A[j,i] for all non-zero elements
906        for i in 0..sparse.nrows() {
907            for j in sparse.indptr[i]..sparse.indptr[i + 1] {
908                let col = sparse.indices[j];
909                let val_ij = sparse.data[j];
910
911                if let Some(&val_ji) = elements.get(&(col, i)) {
912                    let diff = (val_ij - val_ji).abs();
913                    let tolerance = T::epsilon() * T::from(100.0).expect("Operation failed");
914                    if diff > tolerance {
915                        return false;
916                    }
917                } else if val_ij.abs() > T::epsilon() * T::from(100.0).expect("Operation failed") {
918                    // Non-zero element has no symmetric counterpart
919                    return false;
920                }
921            }
922        }
923
924        true
925    }
926
927    /// Check if matrix is diagonally dominant
928    fn check_diagonal_dominance<T>(sparse: &SparseMatrixView<T>) -> bool
929    where
930        T: Float + Clone + Copy + Debug + PartialOrd,
931    {
932        for i in 0..sparse.nrows() {
933            let mut diag_val = T::zero();
934            let mut off_diag_sum = T::zero();
935
936            for j in sparse.indptr[i]..sparse.indptr[i + 1] {
937                let col = sparse.indices[j];
938                let val = sparse.data[j].abs();
939
940                if col == i {
941                    diag_val = val;
942                } else {
943                    off_diag_sum = off_diag_sum + val;
944                }
945            }
946
947            if diag_val <= off_diag_sum {
948                return false;
949            }
950        }
951
952        true
953    }
954}
955
956/// Integration point for future scirs2-sparse crate
957/// This trait defines the interface that scirs2-sparse matrices should implement
958/// for seamless integration with scirs2-linalg
959pub trait SparseLinalg<T> {
960    /// Get matrix dimensions
961    fn shape(&self) -> (usize, usize);
962
963    /// Get number of non-zero elements
964    fn nnz(&self) -> usize;
965
966    /// Convert to CSR format for interoperability
967    fn to_csr(&self) -> LinalgResult<SparseMatrixView<T>>;
968
969    /// Matrix-vector multiplication
970    fn matvec(&self, x: &ArrayView1<T>) -> LinalgResult<Array1<T>>;
971
972    /// Solve linear system Ax = b
973    fn solve(&self, b: &ArrayView1<T>) -> LinalgResult<Array1<T>>;
974
975    /// Get sparsity statistics
976    fn stats(&self) -> advanced::SparseMatrixStats;
977}
978
979/// Blanket implementation for SparseMatrixView
980impl<T> SparseLinalg<T> for SparseMatrixView<T>
981where
982    T: Float
983        + Clone
984        + Copy
985        + Debug
986        + Zero
987        + Add<Output = T>
988        + Sub<Output = T>
989        + Mul<Output = T>
990        + PartialOrd
991        + std::iter::Sum
992        + scirs2_core::ndarray::ScalarOperand
993        + NumAssign
994        + Send
995        + Sync,
996{
997    fn shape(&self) -> (usize, usize) {
998        (self.rows, self.cols)
999    }
1000
1001    fn nnz(&self) -> usize {
1002        self.data.len()
1003    }
1004
1005    fn to_csr(&self) -> LinalgResult<SparseMatrixView<T>> {
1006        Ok(self.clone())
1007    }
1008
1009    fn matvec(&self, x: &ArrayView1<T>) -> LinalgResult<Array1<T>> {
1010        sparse_dense_matvec(self, x)
1011    }
1012
1013    fn solve(&self, b: &ArrayView1<T>) -> LinalgResult<Array1<T>> {
1014        advanced::adaptive_sparse_dense_solve(
1015            self,
1016            b,
1017            T::epsilon() * T::from(1000.0).expect("Operation failed"),
1018            1000,
1019        )
1020    }
1021
1022    fn stats(&self) -> advanced::SparseMatrixStats {
1023        advanced::analyze_sparse_structure(self)
1024    }
1025}
1026
1027/// Utility functions for sparse-dense interoperability
1028pub mod utils {
1029    use super::*;
1030
1031    /// Automatically choose between sparse and dense algorithms based on matrix properties
1032    pub fn auto_solve<T>(
1033        matrix: &ArrayView2<T>,
1034        rhs: &ArrayView1<T>,
1035        sparsity_threshold: f64,
1036    ) -> LinalgResult<Array1<T>>
1037    where
1038        T: Float
1039            + Clone
1040            + Copy
1041            + Debug
1042            + Zero
1043            + Add<Output = T>
1044            + Sub<Output = T>
1045            + Mul<Output = T>
1046            + PartialOrd
1047            + NumCast
1048            + NumAssign
1049            + std::iter::Sum
1050            + scirs2_core::ndarray::ScalarOperand
1051            + Send
1052            + Sync,
1053    {
1054        // Analyze matrix sparsity
1055        let total_elements = matrix.len();
1056        let mut zero_count = 0;
1057        let threshold = T::epsilon() * T::from(1000.0).expect("Operation failed");
1058
1059        for &val in matrix.iter() {
1060            if val.abs() < threshold {
1061                zero_count += 1;
1062            }
1063        }
1064
1065        let sparsity_ratio = zero_count as f64 / total_elements as f64;
1066
1067        if sparsity_ratio > sparsity_threshold {
1068            // Convert to sparse and solve
1069            let sparse = sparse_from_ndarray(matrix, threshold)?;
1070            sparse.solve(rhs)
1071        } else {
1072            // Use dense solver
1073            crate::solve::solve(matrix, rhs, None)
1074        }
1075    }
1076
1077    /// Convert between different sparse formats (placeholder for future expansion)
1078    pub fn convert_sparse_format<T>(
1079        sparse: &SparseMatrixView<T>,
1080        format: &str,
1081    ) -> LinalgResult<SparseMatrixView<T>>
1082    where
1083        T: Clone + Copy + Debug + Zero,
1084    {
1085        match format {
1086            "csr" => Ok(sparse.clone()),
1087            "csc" => sparse_transpose(sparse), // CSC is essentially transposed CSR
1088            _ => Err(LinalgError::ValueError(format!(
1089                "Unsupported sparse format: {format}"
1090            ))),
1091        }
1092    }
1093
1094    /// Estimate memory usage for sparse vs dense operations
1095    pub fn estimate_memory_usage<T>(shape: (usize, usize), nnz: usize) -> (usize, usize) {
1096        let (rows, cols) = shape;
1097        let elementsize = std::mem::size_of::<T>();
1098
1099        // Dense matrix memory
1100        let dense_memory = rows * cols * elementsize;
1101
1102        // Sparse matrix memory (CSR format)
1103        let sparse_memory = nnz * elementsize + // data
1104                          nnz * std::mem::size_of::<usize>() + // indices
1105                          (rows + 1) * std::mem::size_of::<usize>(); // indptr
1106
1107        (dense_memory, sparse_memory)
1108    }
1109}
1110
1111// Implementation of Neg trait for T
1112use std::ops::Neg;
1113
1114// Specialized eigenvalue solvers for sparse matrices (Arnoldi/Lanczos)
1115pub mod sparse_eigen;
1116
1117#[cfg(test)]
1118mod tests {
1119    use super::*;
1120    use approx::assert_abs_diff_eq;
1121    use scirs2_core::ndarray::array;
1122
1123    #[test]
1124    fn test_sparse_from_ndarray() {
1125        let dense = array![[1.0, 0.0, 2.0], [0.0, 0.0, 3.0], [4.0, 5.0, 0.0]];
1126
1127        let sparse = sparse_from_ndarray(&dense.view(), 1e-10).expect("Operation failed");
1128
1129        // Check dimensions
1130        assert_eq!(sparse.shape(), (3, 3));
1131        assert_eq!(sparse.nnz(), 5);
1132
1133        // Convert back to dense and compare
1134        let dense_again = sparse.to_dense();
1135        for i in 0..3 {
1136            for j in 0..3 {
1137                assert_abs_diff_eq!(dense[[i, j]], dense_again[[i, j]], epsilon = 1e-10);
1138            }
1139        }
1140    }
1141
1142    #[test]
1143    fn test_sparse_dense_matmul() {
1144        let dense_a = array![[1.0, 0.0, 2.0], [0.0, 0.0, 3.0], [4.0, 5.0, 0.0]];
1145
1146        let dense_b = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
1147
1148        let sparse_a = sparse_from_ndarray(&dense_a.view(), 1e-10).expect("Operation failed");
1149
1150        // Sparse-dense multiplication
1151        let result = sparse_dense_matmul(&sparse_a, &dense_b.view()).expect("Operation failed");
1152
1153        // Expected result of A * B
1154        let expected = array![[11.0, 14.0], [15.0, 18.0], [19.0, 28.0]];
1155
1156        // Compare
1157        for i in 0..3 {
1158            for j in 0..2 {
1159                assert_abs_diff_eq!(result[[i, j]], expected[[i, j]], epsilon = 1e-10);
1160            }
1161        }
1162    }
1163
1164    #[test]
1165    fn test_dense_sparse_matmul() {
1166        let dense_a = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
1167
1168        let dense_b = array![[1.0, 0.0, 2.0], [0.0, 0.0, 3.0], [4.0, 5.0, 0.0]];
1169
1170        let sparse_b = sparse_from_ndarray(&dense_b.view(), 1e-10).expect("Operation failed");
1171
1172        // Dense-sparse multiplication
1173        let result = dense_sparse_matmul(&dense_a.view(), &sparse_b).expect("Operation failed");
1174
1175        // Manually compute the expected result by direct multiplication
1176        let expected = dense_a.dot(&dense_b);
1177
1178        // Compare
1179        for i in 0..2 {
1180            for j in 0..3 {
1181                assert_abs_diff_eq!(result[[i, j]], expected[[i, j]], epsilon = 1e-10);
1182            }
1183        }
1184    }
1185
1186    #[test]
1187    fn test_sparse_dense_matvec() {
1188        let dense_a = array![[1.0, 0.0, 2.0], [0.0, 0.0, 3.0], [4.0, 5.0, 0.0]];
1189
1190        let vec_b = array![1.0, 2.0, 3.0];
1191
1192        let sparse_a = sparse_from_ndarray(&dense_a.view(), 1e-10).expect("Operation failed");
1193
1194        // Sparse-dense matvec
1195        let result = sparse_dense_matvec(&sparse_a, &vec_b.view()).expect("Operation failed");
1196
1197        // Expected result of A * v
1198        let expected = array![7.0, 9.0, 14.0];
1199
1200        // Compare
1201        for i in 0..3 {
1202            assert_abs_diff_eq!(result[i], expected[i], epsilon = 1e-10);
1203        }
1204    }
1205
1206    #[test]
1207    fn test_sparse_dense_add() {
1208        let dense_a = array![[1.0, 0.0, 2.0], [0.0, 0.0, 3.0], [4.0, 5.0, 0.0]];
1209
1210        let dense_b = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
1211
1212        let sparse_a = sparse_from_ndarray(&dense_a.view(), 1e-10).expect("Operation failed");
1213
1214        // Sparse + dense
1215        let result = sparse_dense_add(&sparse_a, &dense_b.view()).expect("Operation failed");
1216
1217        // Expected result of A + B
1218        let expected = array![[2.0, 2.0, 5.0], [4.0, 5.0, 9.0], [11.0, 13.0, 9.0]];
1219
1220        // Compare
1221        for i in 0..3 {
1222            for j in 0..3 {
1223                assert_abs_diff_eq!(result[[i, j]], expected[[i, j]], epsilon = 1e-10);
1224            }
1225        }
1226    }
1227
1228    #[test]
1229    fn test_sparse_dense_elementwise_mul() {
1230        let dense_a = array![[1.0, 0.0, 2.0], [0.0, 0.0, 3.0], [4.0, 5.0, 0.0]];
1231
1232        let dense_b = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
1233
1234        let sparse_a = sparse_from_ndarray(&dense_a.view(), 1e-10).expect("Operation failed");
1235
1236        // Element-wise multiplication
1237        let result =
1238            sparse_dense_elementwise_mul(&sparse_a, &dense_b.view()).expect("Operation failed");
1239
1240        // Convert to dense for comparison
1241        let result_dense = result.to_dense();
1242
1243        // Expected result of A .* B
1244        let expected = array![[1.0, 0.0, 6.0], [0.0, 0.0, 18.0], [28.0, 40.0, 0.0]];
1245
1246        // Compare
1247        for i in 0..3 {
1248            for j in 0..3 {
1249                assert_abs_diff_eq!(result_dense[[i, j]], expected[[i, j]], epsilon = 1e-10);
1250            }
1251        }
1252    }
1253
1254    #[test]
1255    fn test_sparse_transpose() {
1256        let dense_a = array![[1.0, 0.0, 2.0], [0.0, 0.0, 3.0], [4.0, 5.0, 0.0]];
1257
1258        let sparse_a = sparse_from_ndarray(&dense_a.view(), 1e-10).expect("Operation failed");
1259
1260        // Transpose
1261        let transposed = sparse_transpose(&sparse_a).expect("Operation failed");
1262
1263        // Convert to dense for comparison
1264        let transposed_dense = transposed.to_dense();
1265
1266        // Expected result of A^T
1267        let expected = array![[1.0, 0.0, 4.0], [0.0, 0.0, 5.0], [2.0, 3.0, 0.0]];
1268
1269        // Compare
1270        for i in 0..3 {
1271            for j in 0..3 {
1272                assert_abs_diff_eq!(transposed_dense[[i, j]], expected[[i, j]], epsilon = 1e-10);
1273            }
1274        }
1275    }
1276}