sublinear_solver/matrix/
sparse.rs

1//! Sparse matrix storage implementations.
2//!
3//! This module provides efficient storage formats for sparse matrices,
4//! including CSR, CSC, COO, and graph adjacency representations.
5
6use crate::types::{Precision, DimensionType, IndexType, NodeId};
7use crate::error::{SolverError, Result};
8use alloc::{vec::Vec, collections::BTreeMap};
9use core::iter;
10
11/// Compressed Sparse Row (CSR) storage format.
12/// 
13/// Efficient for row-wise operations and matrix-vector multiplication.
14#[derive(Debug, Clone)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct CSRStorage {
17    /// Non-zero values in row-major order
18    pub values: Vec<Precision>,
19    /// Column indices corresponding to values
20    pub col_indices: Vec<IndexType>,
21    /// Row pointers: row_ptr[i] is the start of row i in values/col_indices
22    pub row_ptr: Vec<IndexType>,
23}
24
25/// Compressed Sparse Column (CSC) storage format.
26/// 
27/// Efficient for column-wise operations.
28#[derive(Debug, Clone)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct CSCStorage {
31    /// Non-zero values in column-major order
32    pub values: Vec<Precision>,
33    /// Row indices corresponding to values
34    pub row_indices: Vec<IndexType>,
35    /// Column pointers: col_ptr[j] is the start of column j in values/row_indices
36    pub col_ptr: Vec<IndexType>,
37}
38
39/// Coordinate (COO) storage format.
40/// 
41/// Efficient for construction and random access patterns.
42#[derive(Debug, Clone)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub struct COOStorage {
45    /// Row indices
46    pub row_indices: Vec<IndexType>,
47    /// Column indices
48    pub col_indices: Vec<IndexType>,
49    /// Values
50    pub values: Vec<Precision>,
51}
52
53/// Graph adjacency list storage format.
54/// 
55/// Optimized for graph algorithms like push methods.
56#[derive(Debug, Clone)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub struct GraphStorage {
59    /// Outgoing edges for each node
60    pub out_edges: Vec<Vec<GraphEdge>>,
61    /// Incoming edges for each node (for backward push)
62    pub in_edges: Vec<Vec<GraphEdge>>,
63    /// Node degrees for normalization
64    pub degrees: Vec<Precision>,
65}
66
67/// Graph edge representation.
68#[derive(Debug, Clone, Copy, PartialEq)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
70pub struct GraphEdge {
71    /// Target node
72    pub target: NodeId,
73    /// Edge weight
74    pub weight: Precision,
75}
76
77// CSR Implementation
78impl CSRStorage {
79    /// Create CSR storage from COO format.
80    pub fn from_coo(coo: &COOStorage, rows: DimensionType, cols: DimensionType) -> Result<Self> {
81        if coo.is_empty() {
82            return Ok(Self {
83                values: Vec::new(),
84                col_indices: Vec::new(),
85                row_ptr: vec![0; rows + 1],
86            });
87        }
88        
89        // Sort by row, then by column
90        let mut sorted_entries: Vec<_> = coo.row_indices.iter()
91            .zip(&coo.col_indices)
92            .zip(&coo.values)
93            .map(|((&r, &c), &v)| (r as usize, c, v))
94            .collect();
95        sorted_entries.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
96        
97        let mut values = Vec::new();
98        let mut col_indices = Vec::new();
99        let mut row_ptr = vec![0; rows + 1];
100        
101        let mut current_row = 0;
102        let mut nnz_count = 0;
103        
104        for (row, col, value) in sorted_entries {
105            // Skip zeros
106            if value == 0.0 {
107                continue;
108            }
109            
110            // Update row pointers
111            while current_row < row {
112                current_row += 1;
113                row_ptr[current_row] = nnz_count as IndexType;
114            }
115            
116            values.push(value);
117            col_indices.push(col);
118            nnz_count += 1;
119        }
120        
121        // Finalize remaining row pointers
122        while current_row < rows {
123            current_row += 1;
124            row_ptr[current_row] = nnz_count as IndexType;
125        }
126        
127        Ok(Self {
128            values,
129            col_indices,
130            row_ptr,
131        })
132    }
133    
134    /// Create CSR storage from CSC format.
135    pub fn from_csc(csc: &CSCStorage, rows: DimensionType, cols: DimensionType) -> Result<Self> {
136        let triplets = csc.to_triplets()?;
137        let coo = COOStorage::from_triplets(triplets)?;
138        Self::from_coo(&coo, rows, cols)
139    }
140    
141    /// Get matrix element at (row, col).
142    pub fn get(&self, row: usize, col: usize) -> Option<Precision> {
143        if row >= self.row_ptr.len() - 1 {
144            return None;
145        }
146        
147        let start = self.row_ptr[row] as usize;
148        let end = self.row_ptr[row + 1] as usize;
149        
150        // Binary search for the column
151        match self.col_indices[start..end].binary_search(&(col as IndexType)) {
152            Ok(pos) => Some(self.values[start + pos]),
153            Err(_) => None,
154        }
155    }
156    
157    /// Iterate over non-zero elements in a row.
158    pub fn row_iter(&self, row: usize) -> CSRRowIter {
159        if row >= self.row_ptr.len() - 1 {
160            return CSRRowIter {
161                col_indices: &[],
162                values: &[],
163                pos: 0,
164            };
165        }
166        
167        let start = self.row_ptr[row] as usize;
168        let end = self.row_ptr[row + 1] as usize;
169        
170        CSRRowIter {
171            col_indices: &self.col_indices[start..end],
172            values: &self.values[start..end],
173            pos: 0,
174        }
175    }
176    
177    /// Iterate over non-zero elements in a column (slow for CSR).
178    pub fn col_iter(&self, col: usize) -> CSRColIter {
179        CSRColIter {
180            storage: self,
181            col: col as IndexType,
182            row: 0,
183        }
184    }
185    
186    /// Matrix-vector multiplication: result = A * x
187    pub fn multiply_vector(&self, x: &[Precision], result: &mut [Precision]) {
188        result.fill(0.0);
189        self.multiply_vector_add(x, result);
190    }
191    
192    /// Matrix-vector multiplication with accumulation: result += A * x
193    pub fn multiply_vector_add(&self, x: &[Precision], result: &mut [Precision]) {
194        for (row, mut row_sum) in result.iter_mut().enumerate() {
195            let start = self.row_ptr[row] as usize;
196            let end = self.row_ptr[row + 1] as usize;
197            
198            for i in start..end {
199                let col = self.col_indices[i] as usize;
200                *row_sum += self.values[i] * x[col];
201            }
202        }
203    }
204    
205    /// Get number of non-zero elements.
206    pub fn nnz(&self) -> usize {
207        self.values.len()
208    }
209    
210    /// Extract as coordinate triplets.
211    pub fn to_triplets(&self) -> Result<Vec<(usize, usize, Precision)>> {
212        let mut triplets = Vec::new();
213        
214        for row in 0..self.row_ptr.len() - 1 {
215            let start = self.row_ptr[row] as usize;
216            let end = self.row_ptr[row + 1] as usize;
217            
218            for i in start..end {
219                let col = self.col_indices[i] as usize;
220                let value = self.values[i];
221                triplets.push((row, col, value));
222            }
223        }
224        
225        Ok(triplets)
226    }
227    
228    /// Scale all values by a factor.
229    pub fn scale(&mut self, factor: Precision) {
230        for value in &mut self.values {
231            *value *= factor;
232        }
233    }
234    
235    /// Add a value to the diagonal.
236    pub fn add_diagonal(&mut self, alpha: Precision) {
237        for row in 0..self.row_ptr.len() - 1 {
238            let start = self.row_ptr[row] as usize;
239            let end = self.row_ptr[row + 1] as usize;
240            
241            // Look for diagonal element
242            if let Ok(pos) = self.col_indices[start..end].binary_search(&(row as IndexType)) {
243                self.values[start + pos] += alpha;
244            }
245            // Note: If diagonal element doesn't exist, we'd need to restructure the matrix
246        }
247    }
248}
249
250/// Iterator over non-zero elements in a CSR row.
251pub struct CSRRowIter<'a> {
252    col_indices: &'a [IndexType],
253    values: &'a [Precision],
254    pos: usize,
255}
256
257impl<'a> Iterator for CSRRowIter<'a> {
258    type Item = (IndexType, Precision);
259    
260    fn next(&mut self) -> Option<Self::Item> {
261        if self.pos < self.col_indices.len() {
262            let col = self.col_indices[self.pos];
263            let val = self.values[self.pos];
264            self.pos += 1;
265            Some((col, val))
266        } else {
267            None
268        }
269    }
270}
271
272/// Iterator over non-zero elements in a CSR column (inefficient).
273pub struct CSRColIter<'a> {
274    storage: &'a CSRStorage,
275    col: IndexType,
276    row: usize,
277}
278
279impl<'a> Iterator for CSRColIter<'a> {
280    type Item = (IndexType, Precision);
281    
282    fn next(&mut self) -> Option<Self::Item> {
283        while self.row < self.storage.row_ptr.len() - 1 {
284            let start = self.storage.row_ptr[self.row] as usize;
285            let end = self.storage.row_ptr[self.row + 1] as usize;
286            
287            if let Ok(pos) = self.storage.col_indices[start..end].binary_search(&self.col) {
288                let value = self.storage.values[start + pos];
289                let row = self.row as IndexType;
290                self.row += 1;
291                return Some((row, value));
292            }
293            
294            self.row += 1;
295        }
296        None
297    }
298}
299
300// CSC Implementation
301impl CSCStorage {
302    /// Create CSC storage from COO format.
303    pub fn from_coo(coo: &COOStorage, rows: DimensionType, cols: DimensionType) -> Result<Self> {
304        if coo.is_empty() {
305            return Ok(Self {
306                values: Vec::new(),
307                row_indices: Vec::new(),
308                col_ptr: vec![0; cols + 1],
309            });
310        }
311        
312        // Sort by column, then by row
313        let mut sorted_entries: Vec<_> = coo.row_indices.iter()
314            .zip(&coo.col_indices)
315            .zip(&coo.values)
316            .map(|((&r, &c), &v)| (r, c as usize, v))
317            .collect();
318        sorted_entries.sort_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0)));
319        
320        let mut values = Vec::new();
321        let mut row_indices = Vec::new();
322        let mut col_ptr = vec![0; cols + 1];
323        
324        let mut current_col = 0;
325        let mut nnz_count = 0;
326        
327        for (row, col, value) in sorted_entries {
328            // Skip zeros
329            if value == 0.0 {
330                continue;
331            }
332            
333            // Update column pointers
334            while current_col < col {
335                current_col += 1;
336                col_ptr[current_col] = nnz_count as IndexType;
337            }
338            
339            values.push(value);
340            row_indices.push(row);
341            nnz_count += 1;
342        }
343        
344        // Finalize remaining column pointers
345        while current_col < cols {
346            current_col += 1;
347            col_ptr[current_col] = nnz_count as IndexType;
348        }
349        
350        Ok(Self {
351            values,
352            row_indices,
353            col_ptr,
354        })
355    }
356    
357    /// Create CSC storage from CSR format.
358    pub fn from_csr(csr: &CSRStorage, rows: DimensionType, cols: DimensionType) -> Result<Self> {
359        let triplets = csr.to_triplets()?;
360        let coo = COOStorage::from_triplets(triplets)?;
361        Self::from_coo(&coo, rows, cols)
362    }
363    
364    /// Get matrix element at (row, col).
365    pub fn get(&self, row: usize, col: usize) -> Option<Precision> {
366        if col >= self.col_ptr.len() - 1 {
367            return None;
368        }
369        
370        let start = self.col_ptr[col] as usize;
371        let end = self.col_ptr[col + 1] as usize;
372        
373        // Binary search for the row
374        match self.row_indices[start..end].binary_search(&(row as IndexType)) {
375            Ok(pos) => Some(self.values[start + pos]),
376            Err(_) => None,
377        }
378    }
379    
380    /// Iterate over non-zero elements in a row (slow for CSC).
381    pub fn row_iter(&self, row: usize) -> CSCRowIter {
382        CSCRowIter {
383            storage: self,
384            row: row as IndexType,
385            col: 0,
386        }
387    }
388    
389    /// Iterate over non-zero elements in a column.
390    pub fn col_iter(&self, col: usize) -> CSCColIter {
391        if col >= self.col_ptr.len() - 1 {
392            return CSCColIter {
393                row_indices: &[],
394                values: &[],
395                pos: 0,
396            };
397        }
398        
399        let start = self.col_ptr[col] as usize;
400        let end = self.col_ptr[col + 1] as usize;
401        
402        CSCColIter {
403            row_indices: &self.row_indices[start..end],
404            values: &self.values[start..end],
405            pos: 0,
406        }
407    }
408    
409    /// Matrix-vector multiplication: result = A * x
410    pub fn multiply_vector(&self, x: &[Precision], result: &mut [Precision]) {
411        result.fill(0.0);
412        self.multiply_vector_add(x, result);
413    }
414    
415    /// Matrix-vector multiplication with accumulation: result += A * x
416    pub fn multiply_vector_add(&self, x: &[Precision], result: &mut [Precision]) {
417        for col in 0..self.col_ptr.len() - 1 {
418            let x_col = x[col];
419            if x_col == 0.0 {
420                continue;
421            }
422            
423            let start = self.col_ptr[col] as usize;
424            let end = self.col_ptr[col + 1] as usize;
425            
426            for i in start..end {
427                let row = self.row_indices[i] as usize;
428                result[row] += self.values[i] * x_col;
429            }
430        }
431    }
432    
433    /// Get number of non-zero elements.
434    pub fn nnz(&self) -> usize {
435        self.values.len()
436    }
437    
438    /// Extract as coordinate triplets.
439    pub fn to_triplets(&self) -> Result<Vec<(usize, usize, Precision)>> {
440        let mut triplets = Vec::new();
441        
442        for col in 0..self.col_ptr.len() - 1 {
443            let start = self.col_ptr[col] as usize;
444            let end = self.col_ptr[col + 1] as usize;
445            
446            for i in start..end {
447                let row = self.row_indices[i] as usize;
448                let value = self.values[i];
449                triplets.push((row, col, value));
450            }
451        }
452        
453        Ok(triplets)
454    }
455    
456    /// Scale all values by a factor.
457    pub fn scale(&mut self, factor: Precision) {
458        for value in &mut self.values {
459            *value *= factor;
460        }
461    }
462    
463    /// Add a value to the diagonal.
464    pub fn add_diagonal(&mut self, alpha: Precision) {
465        for col in 0..self.col_ptr.len() - 1 {
466            let start = self.col_ptr[col] as usize;
467            let end = self.col_ptr[col + 1] as usize;
468            
469            // Look for diagonal element
470            if let Ok(pos) = self.row_indices[start..end].binary_search(&(col as IndexType)) {
471                self.values[start + pos] += alpha;
472            }
473        }
474    }
475}
476
477/// Iterator over non-zero elements in a CSC row (inefficient).
478pub struct CSCRowIter<'a> {
479    storage: &'a CSCStorage,
480    row: IndexType,
481    col: usize,
482}
483
484impl<'a> Iterator for CSCRowIter<'a> {
485    type Item = (IndexType, Precision);
486    
487    fn next(&mut self) -> Option<Self::Item> {
488        while self.col < self.storage.col_ptr.len() - 1 {
489            let start = self.storage.col_ptr[self.col] as usize;
490            let end = self.storage.col_ptr[self.col + 1] as usize;
491            
492            if let Ok(pos) = self.storage.row_indices[start..end].binary_search(&self.row) {
493                let value = self.storage.values[start + pos];
494                let col = self.col as IndexType;
495                self.col += 1;
496                return Some((col, value));
497            }
498            
499            self.col += 1;
500        }
501        None
502    }
503}
504
505/// Iterator over non-zero elements in a CSC column.
506pub struct CSCColIter<'a> {
507    row_indices: &'a [IndexType],
508    values: &'a [Precision],
509    pos: usize,
510}
511
512impl<'a> Iterator for CSCColIter<'a> {
513    type Item = (IndexType, Precision);
514    
515    fn next(&mut self) -> Option<Self::Item> {
516        if self.pos < self.row_indices.len() {
517            let row = self.row_indices[self.pos];
518            let val = self.values[self.pos];
519            self.pos += 1;
520            Some((row, val))
521        } else {
522            None
523        }
524    }
525}
526
527// COO Implementation
528impl COOStorage {
529    /// Create COO storage from triplets.
530    pub fn from_triplets(triplets: Vec<(usize, usize, Precision)>) -> Result<Self> {
531        let mut row_indices = Vec::new();
532        let mut col_indices = Vec::new();
533        let mut values = Vec::new();
534        
535        for (row, col, value) in triplets {
536            if value != 0.0 {
537                row_indices.push(row as IndexType);
538                col_indices.push(col as IndexType);
539                values.push(value);
540            }
541        }
542        
543        Ok(Self {
544            row_indices,
545            col_indices,
546            values,
547        })
548    }
549    
550    /// Check if the storage is empty.
551    pub fn is_empty(&self) -> bool {
552        self.values.is_empty()
553    }
554    
555    /// Get matrix element at (row, col) - O(n) search.
556    pub fn get(&self, row: usize, col: usize) -> Option<Precision> {
557        for i in 0..self.values.len() {
558            if self.row_indices[i] as usize == row && self.col_indices[i] as usize == col {
559                return Some(self.values[i]);
560            }
561        }
562        None
563    }
564    
565    /// Iterate over non-zero elements in a row.
566    pub fn row_iter(&self, row: usize) -> COORowIter {
567        COORowIter {
568            storage: self,
569            target_row: row as IndexType,
570            pos: 0,
571        }
572    }
573    
574    /// Iterate over non-zero elements in a column.
575    pub fn col_iter(&self, col: usize) -> COOColIter {
576        COOColIter {
577            storage: self,
578            target_col: col as IndexType,
579            pos: 0,
580        }
581    }
582    
583    /// Matrix-vector multiplication: result = A * x
584    pub fn multiply_vector(&self, x: &[Precision], result: &mut [Precision]) {
585        result.fill(0.0);
586        self.multiply_vector_add(x, result);
587    }
588    
589    /// Matrix-vector multiplication with accumulation: result += A * x
590    pub fn multiply_vector_add(&self, x: &[Precision], result: &mut [Precision]) {
591        for i in 0..self.values.len() {
592            let row = self.row_indices[i] as usize;
593            let col = self.col_indices[i] as usize;
594            result[row] += self.values[i] * x[col];
595        }
596    }
597    
598    /// Get number of non-zero elements.
599    pub fn nnz(&self) -> usize {
600        self.values.len()
601    }
602    
603    /// Extract as coordinate triplets.
604    pub fn to_triplets(&self) -> Vec<(usize, usize, Precision)> {
605        self.row_indices.iter()
606            .zip(&self.col_indices)
607            .zip(&self.values)
608            .map(|((&r, &c), &v)| (r as usize, c as usize, v))
609            .collect()
610    }
611    
612    /// Scale all values by a factor.
613    pub fn scale(&mut self, factor: Precision) {
614        for value in &mut self.values {
615            *value *= factor;
616        }
617    }
618    
619    /// Add a value to the diagonal.
620    pub fn add_diagonal(&mut self, alpha: Precision, rows: DimensionType) {
621        // For COO, we'd need to add new diagonal entries if they don't exist
622        // This is a simplified implementation that only modifies existing diagonal entries
623        for i in 0..self.values.len() {
624            if self.row_indices[i] == self.col_indices[i] {
625                self.values[i] += alpha;
626            }
627        }
628    }
629}
630
631/// Iterator over non-zero elements in a COO row.
632pub struct COORowIter<'a> {
633    storage: &'a COOStorage,
634    target_row: IndexType,
635    pos: usize,
636}
637
638impl<'a> Iterator for COORowIter<'a> {
639    type Item = (IndexType, Precision);
640    
641    fn next(&mut self) -> Option<Self::Item> {
642        while self.pos < self.storage.values.len() {
643            if self.storage.row_indices[self.pos] == self.target_row {
644                let col = self.storage.col_indices[self.pos];
645                let val = self.storage.values[self.pos];
646                self.pos += 1;
647                return Some((col, val));
648            }
649            self.pos += 1;
650        }
651        None
652    }
653}
654
655/// Iterator over non-zero elements in a COO column.
656pub struct COOColIter<'a> {
657    storage: &'a COOStorage,
658    target_col: IndexType,
659    pos: usize,
660}
661
662impl<'a> Iterator for COOColIter<'a> {
663    type Item = (IndexType, Precision);
664    
665    fn next(&mut self) -> Option<Self::Item> {
666        while self.pos < self.storage.values.len() {
667            if self.storage.col_indices[self.pos] == self.target_col {
668                let row = self.storage.row_indices[self.pos];
669                let val = self.storage.values[self.pos];
670                self.pos += 1;
671                return Some((row, val));
672            }
673            self.pos += 1;
674        }
675        None
676    }
677}
678
679// Graph Implementation
680impl GraphStorage {
681    /// Create graph storage from triplets.
682    pub fn from_triplets(triplets: Vec<(usize, usize, Precision)>, nodes: DimensionType) -> Result<Self> {
683        let mut out_edges = vec![Vec::new(); nodes];
684        let mut in_edges = vec![Vec::new(); nodes];
685        let mut degrees = vec![0.0; nodes];
686        
687        for (row, col, weight) in triplets {
688            if weight != 0.0 && row < nodes && col < nodes {
689                out_edges[row].push(GraphEdge {
690                    target: col as NodeId,
691                    weight,
692                });
693                
694                if row != col { // Don't double-count self-loops for in_edges
695                    in_edges[col].push(GraphEdge {
696                        target: row as NodeId,
697                        weight,
698                    });
699                }
700                
701                degrees[row] += weight.abs();
702            }
703        }
704        
705        Ok(Self {
706            out_edges,
707            in_edges,
708            degrees,
709        })
710    }
711    
712    /// Get matrix element at (row, col).
713    pub fn get(&self, row: usize, col: usize) -> Option<Precision> {
714        if row >= self.out_edges.len() {
715            return None;
716        }
717        
718        for edge in &self.out_edges[row] {
719            if edge.target as usize == col {
720                return Some(edge.weight);
721            }
722        }
723        None
724    }
725    
726    /// Iterate over non-zero elements in a row.
727    pub fn row_iter(&self, row: usize) -> GraphRowIter {
728        if row >= self.out_edges.len() {
729            GraphRowIter {
730                edges: &[],
731                pos: 0,
732            }
733        } else {
734            GraphRowIter {
735                edges: &self.out_edges[row],
736                pos: 0,
737            }
738        }
739    }
740    
741    /// Iterate over non-zero elements in a column.
742    pub fn col_iter(&self, col: usize) -> GraphColIter {
743        if col >= self.in_edges.len() {
744            GraphColIter {
745                edges: &[],
746                pos: 0,
747            }
748        } else {
749            GraphColIter {
750                edges: &self.in_edges[col],
751                pos: 0,
752            }
753        }
754    }
755    
756    /// Matrix-vector multiplication: result = A * x
757    pub fn multiply_vector(&self, x: &[Precision], result: &mut [Precision]) {
758        result.fill(0.0);
759        self.multiply_vector_add(x, result);
760    }
761    
762    /// Matrix-vector multiplication with accumulation: result += A * x
763    pub fn multiply_vector_add(&self, x: &[Precision], result: &mut [Precision]) {
764        for (row, edges) in self.out_edges.iter().enumerate() {
765            for edge in edges {
766                let col = edge.target as usize;
767                if col < x.len() {
768                    result[row] += edge.weight * x[col];
769                }
770            }
771        }
772    }
773    
774    /// Get number of non-zero elements.
775    pub fn nnz(&self) -> usize {
776        self.out_edges.iter().map(|edges| edges.len()).sum()
777    }
778    
779    /// Extract as coordinate triplets.
780    pub fn to_triplets(&self) -> Result<Vec<(usize, usize, Precision)>> {
781        let mut triplets = Vec::new();
782        
783        for (row, edges) in self.out_edges.iter().enumerate() {
784            for edge in edges {
785                triplets.push((row, edge.target as usize, edge.weight));
786            }
787        }
788        
789        Ok(triplets)
790    }
791    
792    /// Scale all edge weights by a factor.
793    pub fn scale(&mut self, factor: Precision) {
794        for edges in &mut self.out_edges {
795            for edge in edges {
796                edge.weight *= factor;
797            }
798        }
799        
800        for edges in &mut self.in_edges {
801            for edge in edges {
802                edge.weight *= factor;
803            }
804        }
805        
806        for degree in &mut self.degrees {
807            *degree *= factor.abs();
808        }
809    }
810    
811    /// Add a value to the diagonal.
812    pub fn add_diagonal(&mut self, alpha: Precision) {
813        for (node, edges) in self.out_edges.iter_mut().enumerate() {
814            // Look for self-loop
815            let mut found = false;
816            for edge in edges.iter_mut() {
817                if edge.target as usize == node {
818                    edge.weight += alpha;
819                    found = true;
820                    break;
821                }
822            }
823            
824            // Add self-loop if it doesn't exist
825            if !found && alpha != 0.0 {
826                edges.push(GraphEdge {
827                    target: node as NodeId,
828                    weight: alpha,
829                });
830            }
831            
832            // Update degree
833            self.degrees[node] += alpha.abs();
834        }
835    }
836    
837    /// Get outgoing edges for a node.
838    pub fn out_neighbors(&self, node: usize) -> &[GraphEdge] {
839        if node < self.out_edges.len() {
840            &self.out_edges[node]
841        } else {
842            &[]
843        }
844    }
845    
846    /// Get incoming edges for a node.
847    pub fn in_neighbors(&self, node: usize) -> &[GraphEdge] {
848        if node < self.in_edges.len() {
849            &self.in_edges[node]
850        } else {
851            &[]
852        }
853    }
854    
855    /// Get node degree.
856    pub fn degree(&self, node: usize) -> Precision {
857        if node < self.degrees.len() {
858            self.degrees[node]
859        } else {
860            0.0
861        }
862    }
863}
864
865/// Iterator over non-zero elements in a graph row.
866pub struct GraphRowIter<'a> {
867    edges: &'a [GraphEdge],
868    pos: usize,
869}
870
871impl<'a> Iterator for GraphRowIter<'a> {
872    type Item = (IndexType, Precision);
873    
874    fn next(&mut self) -> Option<Self::Item> {
875        if self.pos < self.edges.len() {
876            let edge = self.edges[self.pos];
877            self.pos += 1;
878            Some((edge.target, edge.weight))
879        } else {
880            None
881        }
882    }
883}
884
885/// Iterator over non-zero elements in a graph column.
886pub struct GraphColIter<'a> {
887    edges: &'a [GraphEdge],
888    pos: usize,
889}
890
891impl<'a> Iterator for GraphColIter<'a> {
892    type Item = (IndexType, Precision);
893    
894    fn next(&mut self) -> Option<Self::Item> {
895        if self.pos < self.edges.len() {
896            let edge = self.edges[self.pos];
897            self.pos += 1;
898            Some((edge.target, edge.weight))
899        } else {
900            None
901        }
902    }
903}
904
905#[cfg(all(test, feature = "std"))]
906mod tests {
907    use super::*;
908    
909    #[test]
910    fn test_csr_creation() {
911        let triplets = vec![(0, 0, 1.0), (0, 2, 2.0), (1, 1, 3.0), (2, 0, 4.0), (2, 2, 5.0)];
912        let coo = COOStorage::from_triplets(triplets).unwrap();
913        let csr = CSRStorage::from_coo(&coo, 3, 3).unwrap();
914        
915        assert_eq!(csr.nnz(), 5);
916        assert_eq!(csr.get(0, 0), Some(1.0));
917        assert_eq!(csr.get(0, 2), Some(2.0));
918        assert_eq!(csr.get(1, 1), Some(3.0));
919        assert_eq!(csr.get(0, 1), None);
920    }
921    
922    #[test]
923    fn test_csr_matrix_vector_multiply() {
924        let triplets = vec![(0, 0, 2.0), (0, 1, 1.0), (1, 0, 1.0), (1, 1, 3.0)];
925        let coo = COOStorage::from_triplets(triplets).unwrap();
926        let csr = CSRStorage::from_coo(&coo, 2, 2).unwrap();
927        
928        let x = vec![1.0, 2.0];
929        let mut result = vec![0.0; 2];
930        
931        csr.multiply_vector(&x, &mut result);
932        assert_eq!(result, vec![4.0, 7.0]); // [2*1+1*2, 1*1+3*2]
933    }
934    
935    #[test]
936    fn test_graph_storage() {
937        let triplets = vec![(0, 1, 0.5), (1, 0, 0.3), (1, 2, 0.7), (2, 1, 0.2)];
938        let graph = GraphStorage::from_triplets(triplets, 3).unwrap();
939        
940        assert_eq!(graph.nnz(), 4);
941        assert_eq!(graph.out_neighbors(1).len(), 2);
942        assert_eq!(graph.in_neighbors(1).len(), 2);
943        assert!(graph.degree(1) > 0.0);
944    }
945    
946    #[test]
947    fn test_format_conversions() {
948        let triplets = vec![(0, 0, 1.0), (0, 2, 2.0), (1, 1, 3.0)];
949        
950        // COO -> CSR -> CSC -> COO roundtrip
951        let coo1 = COOStorage::from_triplets(triplets.clone()).unwrap();
952        let csr = CSRStorage::from_coo(&coo1, 2, 3).unwrap();
953        let csc = CSCStorage::from_csr(&csr, 2, 3).unwrap();
954        let triplets2 = csc.to_triplets().unwrap();
955        
956        // Sort both for comparison
957        let mut t1 = triplets.clone();
958        let mut t2 = triplets2;
959        t1.sort();
960        t2.sort();
961        
962        assert_eq!(t1, t2);
963    }
964}