scirs2_sparse/
sym_coo.rs

1//! Symmetric Coordinate (SymCOO) module
2//!
3//! This module provides a specialized implementation of the COO format
4//! optimized for symmetric matrices, storing only the lower or upper
5//! triangular part of the matrix.
6
7use crate::coo::CooMatrix;
8use crate::coo_array::CooArray;
9use crate::error::{SparseError, SparseResult};
10use crate::sparray::SparseArray;
11use num_traits::Float;
12use std::fmt::Debug;
13use std::ops::{Add, Div, Mul, Sub};
14
15/// Symmetric Coordinate (SymCOO) matrix
16///
17/// This format stores only the lower triangular part of a symmetric matrix
18/// to save memory. It's particularly useful for construction of symmetric
19/// matrices and for conversion to other symmetric formats.
20///
21/// # Note
22///
23/// All operations maintain symmetry implicitly.
24#[derive(Debug, Clone)]
25pub struct SymCooMatrix<T>
26where
27    T: Float + Debug + Copy,
28{
29    /// Non-zero values in the lower triangular part
30    pub data: Vec<T>,
31
32    /// Row indices for each non-zero element
33    pub rows: Vec<usize>,
34
35    /// Column indices for each non-zero element
36    pub cols: Vec<usize>,
37
38    /// Matrix shape (rows, cols), always square
39    pub shape: (usize, usize),
40}
41
42impl<T> SymCooMatrix<T>
43where
44    T: Float + Debug + Copy,
45{
46    /// Create a new symmetric COO matrix from raw data
47    ///
48    /// # Arguments
49    ///
50    /// * `data` - Non-zero values in the lower triangular part
51    /// * `rows` - Row indices
52    /// * `cols` - Column indices
53    /// * `shape` - Matrix shape (n, n)
54    ///
55    /// # Returns
56    ///
57    /// A symmetric COO matrix
58    ///
59    /// # Errors
60    ///
61    /// Returns an error if:
62    /// - The shape is not square
63    /// - The arrays have inconsistent lengths
64    /// - Any index is out of bounds
65    /// - Upper triangular elements are included
66    pub fn new(
67        data: Vec<T>,
68        rows: Vec<usize>,
69        cols: Vec<usize>,
70        shape: (usize, usize),
71    ) -> SparseResult<Self> {
72        let (nrows, ncols) = shape;
73
74        // Ensure matrix is square
75        if nrows != ncols {
76            return Err(SparseError::ValueError(
77                "Symmetric matrix must be square".to_string(),
78            ));
79        }
80
81        // Check array lengths
82        let nnz = data.len();
83        if rows.len() != nnz || cols.len() != nnz {
84            return Err(SparseError::ValueError(format!(
85                "Data ({}), row ({}) and column ({}) arrays must have same length",
86                nnz,
87                rows.len(),
88                cols.len()
89            )));
90        }
91
92        // Check bounds and ensure only lower triangular elements
93        for i in 0..nnz {
94            let row = rows[i];
95            let col = cols[i];
96
97            if row >= nrows {
98                return Err(SparseError::IndexOutOfBounds {
99                    index: (row, 0),
100                    shape: (nrows, ncols),
101                });
102            }
103
104            if col >= ncols {
105                return Err(SparseError::IndexOutOfBounds {
106                    index: (row, col),
107                    shape: (nrows, ncols),
108                });
109            }
110
111            // For symmetric storage, we only keep the lower triangular part
112            if col > row {
113                return Err(SparseError::ValueError(
114                    "Symmetric COO should only store the lower triangular part".to_string(),
115                ));
116            }
117        }
118
119        Ok(Self {
120            data,
121            rows,
122            cols,
123            shape,
124        })
125    }
126
127    /// Convert a regular COO matrix to symmetric COO format
128    ///
129    /// This will verify that the matrix is symmetric and extract
130    /// the lower triangular part.
131    ///
132    /// # Arguments
133    ///
134    /// * `matrix` - COO matrix to convert
135    ///
136    /// # Returns
137    ///
138    /// A symmetric COO matrix
139    pub fn from_coo(matrix: &CooMatrix<T>) -> SparseResult<Self> {
140        let (rows, cols) = matrix.shape();
141
142        // Ensure matrix is square
143        if rows != cols {
144            return Err(SparseError::ValueError(
145                "Symmetric matrix must be square".to_string(),
146            ));
147        }
148
149        // Check if the matrix is symmetric
150        if !Self::is_symmetric(matrix) {
151            return Err(SparseError::ValueError(
152                "Matrix must be symmetric to convert to SymCOO format".to_string(),
153            ));
154        }
155
156        // Extract the lower triangular part
157        let mut data = Vec::new();
158        let mut row_indices = Vec::new();
159        let mut col_indices = Vec::new();
160
161        let rows_vec = matrix.row_indices();
162        let cols_vec = matrix.col_indices();
163        let data_vec = matrix.data();
164
165        for i in 0..data_vec.len() {
166            let row = rows_vec[i];
167            let col = cols_vec[i];
168
169            // Only include elements in lower triangular part (including diagonal)
170            if col <= row {
171                data.push(data_vec[i]);
172                row_indices.push(row);
173                col_indices.push(col);
174            }
175        }
176
177        Ok(Self {
178            data,
179            rows: row_indices,
180            cols: col_indices,
181            shape: (rows, cols),
182        })
183    }
184
185    /// Check if a COO matrix is symmetric
186    ///
187    /// # Arguments
188    ///
189    /// * `matrix` - COO matrix to check
190    ///
191    /// # Returns
192    ///
193    /// `true` if the matrix is symmetric, `false` otherwise
194    pub fn is_symmetric(matrix: &CooMatrix<T>) -> bool {
195        let (rows, cols) = matrix.shape();
196
197        // Must be square
198        if rows != cols {
199            return false;
200        }
201
202        // Convert to dense to check symmetry (more efficient for COO format)
203        let dense = matrix.to_dense();
204
205        for i in 0..rows {
206            for j in 0..i {
207                // Only need to check upper triangular elements
208                // Compare with sufficient tolerance for floating point comparisons
209                let diff = (dense[i][j] - dense[j][i]).abs();
210                let epsilon = T::epsilon() * T::from(100.0).unwrap();
211                if diff > epsilon {
212                    return false;
213                }
214            }
215        }
216
217        true
218    }
219
220    /// Get the shape of the matrix
221    ///
222    /// # Returns
223    ///
224    /// A tuple (rows, cols)
225    pub fn shape(&self) -> (usize, usize) {
226        self.shape
227    }
228
229    /// Get the number of stored non-zero elements
230    ///
231    /// # Returns
232    ///
233    /// The number of non-zero elements in the lower triangular part
234    pub fn nnz_stored(&self) -> usize {
235        self.data.len()
236    }
237
238    /// Get the total number of non-zero elements in the full matrix
239    ///
240    /// # Returns
241    ///
242    /// The total number of non-zero elements in the full symmetric matrix
243    pub fn nnz(&self) -> usize {
244        let mut count = 0;
245
246        for i in 0..self.data.len() {
247            let row = self.rows[i];
248            let col = self.cols[i];
249
250            if row == col {
251                // Diagonal element, count once
252                count += 1;
253            } else {
254                // Off-diagonal element, count twice (for both triangular parts)
255                count += 2;
256            }
257        }
258
259        count
260    }
261
262    /// Get a single element from the matrix
263    ///
264    /// # Arguments
265    ///
266    /// * `row` - Row index
267    /// * `col` - Column index
268    ///
269    /// # Returns
270    ///
271    /// The value at position (row, col)
272    pub fn get(&self, row: usize, col: usize) -> T {
273        // Check bounds
274        if row >= self.shape.0 || col >= self.shape.1 {
275            return T::zero();
276        }
277
278        // For symmetric matrix, if (row,col) is in upper triangular part,
279        // we look for (col,row) in the lower triangular part
280        let (actual_row, actual_col) = if row < col { (col, row) } else { (row, col) };
281
282        // Search for the element in COO format
283        for i in 0..self.data.len() {
284            if self.rows[i] == actual_row && self.cols[i] == actual_col {
285                return self.data[i];
286            }
287        }
288
289        T::zero()
290    }
291
292    /// Convert to standard COO matrix (reconstructing full symmetric matrix)
293    ///
294    /// # Returns
295    ///
296    /// A standard COO matrix with both upper and lower triangular parts
297    pub fn to_coo(&self) -> SparseResult<CooMatrix<T>> {
298        let mut data = Vec::new();
299        let mut rows = Vec::new();
300        let mut cols = Vec::new();
301
302        // Add the stored lower triangular elements
303        data.extend_from_slice(&self.data);
304        rows.extend_from_slice(&self.rows);
305        cols.extend_from_slice(&self.cols);
306
307        // Add the upper triangular elements by symmetry
308        for i in 0..self.data.len() {
309            let row = self.rows[i];
310            let col = self.cols[i];
311
312            // Skip diagonal elements (already included)
313            if row != col {
314                // Add the symmetric element
315                data.push(self.data[i]);
316                rows.push(col);
317                cols.push(row);
318            }
319        }
320
321        CooMatrix::new(data, rows, cols, self.shape)
322    }
323
324    /// Convert to dense matrix
325    ///
326    /// # Returns
327    ///
328    /// A dense matrix representation as a vector of vectors
329    pub fn to_dense(&self) -> Vec<Vec<T>> {
330        let n = self.shape.0;
331        let mut dense = vec![vec![T::zero(); n]; n];
332
333        // Fill the lower triangular part (directly from stored data)
334        for i in 0..self.data.len() {
335            let row = self.rows[i];
336            let col = self.cols[i];
337            dense[row][col] = self.data[i];
338
339            // Fill the upper triangular part (from symmetry)
340            if row != col {
341                dense[col][row] = self.data[i];
342            }
343        }
344
345        dense
346    }
347}
348
349/// Array-based SymCOO implementation compatible with SparseArray trait
350#[derive(Debug, Clone)]
351pub struct SymCooArray<T>
352where
353    T: Float + Debug + Copy,
354{
355    /// Inner matrix
356    inner: SymCooMatrix<T>,
357}
358
359impl<T> SymCooArray<T>
360where
361    T: Float
362        + Debug
363        + Copy
364        + 'static
365        + Add<Output = T>
366        + Sub<Output = T>
367        + Mul<Output = T>
368        + Div<Output = T>,
369{
370    /// Create a new SymCOO array from a SymCOO matrix
371    ///
372    /// # Arguments
373    ///
374    /// * `matrix` - Symmetric COO matrix
375    ///
376    /// # Returns
377    ///
378    /// SymCOO array
379    pub fn new(matrix: SymCooMatrix<T>) -> Self {
380        Self { inner: matrix }
381    }
382
383    /// Create a SymCOO array from triplets (row, col, value)
384    ///
385    /// # Arguments
386    ///
387    /// * `rows` - Row indices
388    /// * `cols` - Column indices
389    /// * `data` - Non-zero values
390    /// * `shape` - Matrix shape (n, n)
391    /// * `enforce_symmetric` - If true, enforce that matrix is symmetric by averaging a_ij and a_ji
392    ///
393    /// # Returns
394    ///
395    /// A symmetric COO array
396    pub fn from_triplets(
397        rows: &[usize],
398        cols: &[usize],
399        data: &[T],
400        shape: (usize, usize),
401        enforce_symmetric: bool,
402    ) -> SparseResult<Self> {
403        if shape.0 != shape.1 {
404            return Err(SparseError::ValueError(
405                "Symmetric matrix must be square".to_string(),
406            ));
407        }
408
409        if !enforce_symmetric {
410            // Create a temporary dense matrix to check symmetry
411            let n = shape.0;
412            let mut dense = vec![vec![T::zero(); n]; n];
413            let nnz = data.len().min(rows.len().min(cols.len()));
414
415            // Fill the matrix with the provided elements
416            for i in 0..nnz {
417                let row = rows[i];
418                let col = cols[i];
419
420                if row >= n || col >= n {
421                    return Err(SparseError::IndexOutOfBounds {
422                        index: (row, col),
423                        shape,
424                    });
425                }
426
427                dense[row][col] = data[i];
428            }
429
430            // Check if the matrix is symmetric
431            for i in 0..n {
432                for j in 0..i {
433                    if (dense[i][j] - dense[j][i]).abs() > T::epsilon() {
434                        return Err(SparseError::ValueError(
435                            "Input is not symmetric. Use enforce_symmetric=true to force symmetry"
436                                .to_string(),
437                        ));
438                    }
439                }
440            }
441
442            // Extract lower triangular part
443            let mut sym_data = Vec::new();
444            let mut sym_rows = Vec::new();
445            let mut sym_cols = Vec::new();
446
447            for (i, row) in dense.iter().enumerate().take(n) {
448                for (j, &val) in row.iter().enumerate().take(i + 1) {
449                    if val != T::zero() {
450                        sym_data.push(val);
451                        sym_rows.push(i);
452                        sym_cols.push(j);
453                    }
454                }
455            }
456
457            // Create the symmetric matrix
458            let sym_coo = SymCooMatrix::new(sym_data, sym_rows, sym_cols, shape)?;
459            return Ok(Self { inner: sym_coo });
460        }
461
462        // Create a symmetric matrix by averaging corresponding elements
463        let n = shape.0;
464
465        // First, build a dense matrix with all input elements
466        let mut dense = vec![vec![T::zero(); n]; n];
467        let nnz = data.len();
468
469        // Add all elements to the matrix
470        for i in 0..nnz {
471            if i >= rows.len() || i >= cols.len() {
472                return Err(SparseError::ValueError(
473                    "Inconsistent input arrays".to_string(),
474                ));
475            }
476
477            let row = rows[i];
478            let col = cols[i];
479
480            if row >= n || col >= n {
481                return Err(SparseError::IndexOutOfBounds {
482                    index: (row, col),
483                    shape: (n, n),
484                });
485            }
486
487            dense[row][col] = data[i];
488        }
489
490        // Make symmetric by averaging a_ij and a_ji
491        for i in 0..n {
492            for j in 0..i {
493                let avg = (dense[i][j] + dense[j][i]) / (T::one() + T::one());
494                dense[i][j] = avg;
495                dense[j][i] = avg;
496            }
497        }
498
499        // Extract the lower triangular part to create SymCOO
500        let mut sym_data = Vec::new();
501        let mut sym_rows = Vec::new();
502        let mut sym_cols = Vec::new();
503
504        for (i, row) in dense.iter().enumerate().take(n) {
505            for (j, &val) in row.iter().enumerate().take(i + 1) {
506                if val != T::zero() {
507                    sym_data.push(val);
508                    sym_rows.push(i);
509                    sym_cols.push(j);
510                }
511            }
512        }
513
514        let sym_coo = SymCooMatrix::new(sym_data, sym_rows, sym_cols, shape)?;
515        Ok(Self { inner: sym_coo })
516    }
517
518    /// Create a SymCOO array from a regular COO array
519    ///
520    /// # Arguments
521    ///
522    /// * `array` - COO array to convert
523    ///
524    /// # Returns
525    ///
526    /// A symmetric COO array
527    pub fn from_coo_array(array: &CooArray<T>) -> SparseResult<Self> {
528        let shape = array.shape();
529        let (rows, cols) = shape;
530
531        // Ensure matrix is square
532        if rows != cols {
533            return Err(SparseError::ValueError(
534                "Symmetric matrix must be square".to_string(),
535            ));
536        }
537
538        // Create a temporary COO matrix to check symmetry
539        let coo_matrix = CooMatrix::new(
540            array.get_data().to_vec(),
541            array.get_rows().to_vec(),
542            array.get_cols().to_vec(),
543            shape,
544        )?;
545
546        // Convert to symmetric COO
547        let sym_coo = SymCooMatrix::from_coo(&coo_matrix)?;
548
549        Ok(Self { inner: sym_coo })
550    }
551
552    /// Get the underlying matrix
553    ///
554    /// # Returns
555    ///
556    /// Reference to the inner SymCOO matrix
557    pub fn inner(&self) -> &SymCooMatrix<T> {
558        &self.inner
559    }
560
561    /// Get access to the underlying data array
562    ///
563    /// # Returns
564    ///
565    /// Reference to the data array
566    pub fn data(&self) -> &[T] {
567        &self.inner.data
568    }
569
570    /// Get access to the underlying rows array
571    ///
572    /// # Returns
573    ///
574    /// Reference to the rows array
575    pub fn rows(&self) -> &[usize] {
576        &self.inner.rows
577    }
578
579    /// Get access to the underlying cols array
580    ///
581    /// # Returns
582    ///
583    /// Reference to the cols array
584    pub fn cols(&self) -> &[usize] {
585        &self.inner.cols
586    }
587
588    /// Get the shape of the array
589    ///
590    /// # Returns
591    ///
592    /// A tuple (rows, cols)
593    pub fn shape(&self) -> (usize, usize) {
594        self.inner.shape
595    }
596
597    /// Convert to a standard COO array
598    ///
599    /// # Returns
600    ///
601    /// COO array containing the full symmetric matrix
602    pub fn to_coo_array(&self) -> SparseResult<CooArray<T>> {
603        let coo = self.inner.to_coo()?;
604
605        // Get triplets from CooMatrix
606        let rows = coo.row_indices();
607        let cols = coo.col_indices();
608        let data = coo.data();
609
610        // Create CooArray from triplets
611        CooArray::from_triplets(rows, cols, data, coo.shape(), false)
612    }
613}
614
615#[cfg(test)]
616mod tests {
617    use super::*;
618    use crate::sparray::SparseArray;
619
620    #[test]
621    fn test_sym_coo_creation() {
622        // Create a simple symmetric matrix stored in lower triangular format
623        // [2 1 0]
624        // [1 2 3]
625        // [0 3 1]
626
627        let data = vec![2.0, 1.0, 2.0, 3.0, 1.0];
628        let rows = vec![0, 1, 1, 2, 2];
629        let cols = vec![0, 0, 1, 1, 2];
630
631        let sym = SymCooMatrix::new(data, rows, cols, (3, 3)).unwrap();
632
633        assert_eq!(sym.shape(), (3, 3));
634        assert_eq!(sym.nnz_stored(), 5);
635
636        // Total non-zeros should count off-diagonal elements twice
637        assert_eq!(sym.nnz(), 7);
638
639        // Test accessing elements
640        assert_eq!(sym.get(0, 0), 2.0);
641        assert_eq!(sym.get(0, 1), 1.0);
642        assert_eq!(sym.get(1, 0), 1.0); // From symmetry
643        assert_eq!(sym.get(1, 1), 2.0);
644        assert_eq!(sym.get(1, 2), 3.0);
645        assert_eq!(sym.get(2, 1), 3.0); // From symmetry
646        assert_eq!(sym.get(2, 2), 1.0);
647        assert_eq!(sym.get(0, 2), 0.0);
648        assert_eq!(sym.get(2, 0), 0.0);
649    }
650
651    #[test]
652    fn test_sym_coo_from_standard() {
653        // Create a standard COO matrix that's symmetric
654        // [2 1 0]
655        // [1 2 3]
656        // [0 3 1]
657
658        let data = vec![2.0, 1.0, 1.0, 2.0, 3.0, 3.0, 1.0];
659        let rows = vec![0, 0, 1, 1, 1, 2, 2];
660        let cols = vec![0, 1, 0, 1, 2, 1, 2];
661
662        let coo = CooMatrix::new(data, rows, cols, (3, 3)).unwrap();
663        let sym = SymCooMatrix::from_coo(&coo).unwrap();
664
665        assert_eq!(sym.shape(), (3, 3));
666
667        // Convert back to standard COO
668        let coo2 = sym.to_coo().unwrap();
669        let dense = coo2.to_dense();
670
671        // Check the full matrix
672        assert_eq!(dense[0][0], 2.0);
673        assert_eq!(dense[0][1], 1.0);
674        assert_eq!(dense[0][2], 0.0);
675        assert_eq!(dense[1][0], 1.0);
676        assert_eq!(dense[1][1], 2.0);
677        assert_eq!(dense[1][2], 3.0);
678        assert_eq!(dense[2][0], 0.0);
679        assert_eq!(dense[2][1], 3.0);
680        assert_eq!(dense[2][2], 1.0);
681    }
682
683    #[test]
684    fn test_sym_coo_array() {
685        // Create a symmetric SymCOO matrix
686        let data = vec![2.0, 1.0, 2.0, 3.0, 1.0];
687        let rows = vec![0, 1, 1, 2, 2];
688        let cols = vec![0, 0, 1, 1, 2];
689
690        let sym_matrix = SymCooMatrix::new(data, rows, cols, (3, 3)).unwrap();
691        let sym_array = SymCooArray::new(sym_matrix);
692
693        assert_eq!(sym_array.inner().shape(), (3, 3));
694
695        // Convert to standard COO array
696        let coo_array = sym_array.to_coo_array().unwrap();
697
698        // Verify shape and values
699        assert_eq!(coo_array.shape(), (3, 3));
700        assert_eq!(coo_array.get(0, 0), 2.0);
701        assert_eq!(coo_array.get(0, 1), 1.0);
702        assert_eq!(coo_array.get(1, 0), 1.0);
703        assert_eq!(coo_array.get(1, 1), 2.0);
704        assert_eq!(coo_array.get(1, 2), 3.0);
705        assert_eq!(coo_array.get(2, 1), 3.0);
706        assert_eq!(coo_array.get(2, 2), 1.0);
707        assert_eq!(coo_array.get(0, 2), 0.0);
708        assert_eq!(coo_array.get(2, 0), 0.0);
709    }
710
711    #[test]
712    fn test_sym_coo_array_from_triplets() {
713        // Test creating a symmetric matrix from triplets
714        // This needs to be a truly symmetric matrix to work without enforce_symmetric
715        let rows = vec![0, 1, 1, 2, 1, 0, 2];
716        let cols = vec![0, 1, 2, 2, 0, 1, 1];
717        let data = vec![2.0, 2.0, 3.0, 1.0, 1.0, 1.0, 3.0];
718
719        let sym_array = SymCooArray::from_triplets(&rows, &cols, &data, (3, 3), false).unwrap();
720
721        assert_eq!(sym_array.shape(), (3, 3));
722
723        // Test with enforcement of symmetry (add asymmetric values)
724        let rows2 = vec![0, 0, 1, 1, 2, 1];
725        let cols2 = vec![0, 1, 1, 2, 2, 0];
726        let data2 = vec![2.0, 1.0, 2.0, 3.0, 1.0, 2.0]; // 1,0 element is 2.0 instead of 1.0
727
728        let sym_array2 = SymCooArray::from_triplets(&rows2, &cols2, &data2, (3, 3), true).unwrap();
729
730        // Should average the (1,0) and (0,1) elements to 1.5
731        assert_eq!(sym_array2.inner().get(1, 0), 1.5);
732        assert_eq!(sym_array2.inner().get(0, 1), 1.5);
733    }
734}