Skip to main content

torsh_sparse/
conversions.rs

1//! Consolidated conversion utilities for sparse tensor formats
2//!
3//! This module provides optimized conversion paths, common validation logic,
4//! and shared patterns to reduce code duplication and improve performance.
5
6use crate::{CooTensor, CscTensor, CsrTensor, SparseTensor, TorshResult};
7use std::collections::HashMap;
8use torsh_core::{Shape, TorshError};
9use torsh_tensor::Tensor;
10
11/// Conversion hints to optimize format transformations
12#[derive(Debug, Default, Clone)]
13pub struct ConversionHints {
14    /// Whether the tensor data is already sorted by indices
15    pub is_sorted: bool,
16    /// Whether the tensor is symmetric
17    pub is_symmetric: bool,
18    /// Whether all rows have similar number of non-zeros
19    pub has_uniform_row_sizes: bool,
20    /// Whether the tensor has block structure
21    pub is_block_structured: bool,
22    /// Whether the tensor is diagonal or near-diagonal
23    pub is_diagonal: bool,
24}
25
26/// Common validation utilities for sparse tensor operations
27pub mod validation {
28    use super::*;
29
30    /// Validate that a tensor is 2D (required for most sparse formats)
31    pub fn validate_2d_tensor(shape: &Shape, format_name: &str) -> TorshResult<()> {
32        if shape.dims().len() != 2 {
33            return Err(TorshError::InvalidArgument(format!(
34                "{} format only supports 2D tensors, got {}D",
35                format_name,
36                shape.dims().len()
37            )));
38        }
39        Ok(())
40    }
41
42    /// Validate sparse indices against tensor shape
43    pub fn validate_sparse_indices(
44        row_indices: &[usize],
45        col_indices: &[usize],
46        shape: &Shape,
47    ) -> TorshResult<()> {
48        let max_rows = shape.dims()[0];
49        let max_cols = shape.dims()[1];
50
51        for &row in row_indices {
52            if row >= max_rows {
53                return Err(TorshError::InvalidArgument(format!(
54                    "Row index {row} exceeds shape bounds [{max_rows}]"
55                )));
56            }
57        }
58
59        for &col in col_indices {
60            if col >= max_cols {
61                return Err(TorshError::InvalidArgument(format!(
62                    "Column index {col} exceeds shape bounds [{max_cols}]"
63                )));
64            }
65        }
66
67        Ok(())
68    }
69
70    /// Validate that arrays have matching lengths
71    pub fn validate_array_lengths(
72        rows_len: usize,
73        cols_len: usize,
74        vals_len: usize,
75    ) -> TorshResult<()> {
76        if rows_len != cols_len || cols_len != vals_len {
77            return Err(TorshError::InvalidArgument(format!(
78                "Array lengths must match: rows={rows_len}, cols={cols_len}, values={vals_len}"
79            )));
80        }
81        Ok(())
82    }
83
84    /// Validate that a matrix is square (for operations requiring square matrices)
85    pub fn validate_square_matrix(shape: &Shape) -> TorshResult<()> {
86        validate_2d_tensor(shape, "Square matrix operation")?;
87        if shape.dims()[0] != shape.dims()[1] {
88            return Err(TorshError::InvalidArgument(format!(
89                "Matrix must be square, got shape [{} x {}]",
90                shape.dims()[0],
91                shape.dims()[1]
92            )));
93        }
94        Ok(())
95    }
96}
97
98/// Common conversion patterns abstracted for reuse
99pub mod patterns {
100    use super::*;
101
102    /// Extract triplets from any sparse tensor format
103    pub fn extract_triplets(sparse: &dyn SparseTensor) -> TorshResult<Vec<(usize, usize, f32)>> {
104        let coo = sparse.to_coo()?;
105        Ok(coo.triplets())
106    }
107
108    /// Build COO tensor from triplets with validation
109    pub fn triplets_to_coo(
110        triplets: Vec<(usize, usize, f32)>,
111        shape: Shape,
112    ) -> TorshResult<CooTensor> {
113        let (row_indices, col_indices, values): (Vec<_>, Vec<_>, Vec<_>) =
114            triplets.into_iter().fold(
115                (Vec::new(), Vec::new(), Vec::new()),
116                |(mut rows, mut cols, mut vals), (r, c, v)| {
117                    rows.push(r);
118                    cols.push(c);
119                    vals.push(v);
120                    (rows, cols, vals)
121                },
122            );
123
124        CooTensor::new(row_indices, col_indices, values, shape)
125    }
126
127    /// Generic dense-to-sparse conversion with threshold filtering
128    pub fn dense_to_sparse_with_threshold<F>(
129        dense: &Tensor,
130        _threshold: f32,
131        _processor: F,
132    ) -> TorshResult<()>
133    where
134        F: FnMut(usize, usize, f32) -> TorshResult<()>,
135    {
136        let shape = dense.shape();
137        validation::validate_2d_tensor(&shape, "Dense-to-sparse conversion")?;
138
139        let rows = shape.dims()[0];
140        let cols = shape.dims()[1];
141
142        // Simplified iteration - in practice you'd want more efficient tensor access
143        for _row in 0..rows {
144            for _col in 0..cols {
145                // Note: This would need proper tensor indexing implementation
146                // For now we'll use a placeholder approach
147                // let value = dense.get(&[row, col])?;
148                // if value.abs() > threshold {
149                //     processor(row, col, value)?;
150                // }
151            }
152        }
153
154        Ok(())
155    }
156
157    /// Filter and process sparse triplets with a threshold
158    pub fn filter_triplets_by_threshold(
159        triplets: Vec<(usize, usize, f32)>,
160        threshold: f32,
161    ) -> (Vec<usize>, Vec<usize>, Vec<f32>) {
162        triplets
163            .into_iter()
164            .filter(|(_, _, v)| v.abs() > threshold)
165            .fold(
166                (Vec::new(), Vec::new(), Vec::new()),
167                |(mut rows, mut cols, mut vals), (r, c, v)| {
168                    rows.push(r);
169                    cols.push(c);
170                    vals.push(v);
171                    (rows, cols, vals)
172                },
173            )
174    }
175
176    /// Sort triplets by row-major order (row first, then column)
177    pub fn sort_triplets_row_major(triplets: &mut [(usize, usize, f32)]) {
178        triplets.sort_by(|a, b| (a.0, a.1).cmp(&(b.0, b.1)));
179    }
180
181    /// Sort triplets by column-major order (column first, then row)
182    pub fn sort_triplets_col_major(triplets: &mut [(usize, usize, f32)]) {
183        triplets.sort_by(|a, b| (a.1, a.0).cmp(&(b.1, b.0)));
184    }
185
186    /// Aggregate duplicate triplets by summing values
187    pub fn aggregate_duplicate_triplets(
188        mut triplets: Vec<(usize, usize, f32)>,
189    ) -> Vec<(usize, usize, f32)> {
190        if triplets.is_empty() {
191            return triplets;
192        }
193
194        // Sort first to group duplicates together
195        sort_triplets_row_major(&mut triplets);
196
197        let mut result = Vec::new();
198        let mut current = triplets[0];
199
200        for &next in &triplets[1..] {
201            if current.0 == next.0 && current.1 == next.1 {
202                // Same position - accumulate values
203                current.2 += next.2;
204            } else {
205                // Different position - save current and start new one
206                if current.2.abs() > f32::EPSILON {
207                    result.push(current);
208                }
209                current = next;
210            }
211        }
212
213        // Don't forget the last one
214        if current.2.abs() > f32::EPSILON {
215            result.push(current);
216        }
217
218        result
219    }
220}
221
222/// Optimized direct conversion paths that avoid COO intermediate format
223pub mod direct_conversions {
224    use super::*;
225
226    /// Direct CSR to CSC conversion via transpose
227    /// This is much more efficient than CSR → COO → CSC
228    pub fn csr_to_csc_direct(csr: &CsrTensor) -> TorshResult<CscTensor> {
229        // Get CSR data
230        let row_ptr = csr.row_ptr();
231        let col_indices = csr.col_indices();
232        let values = csr.values();
233        let shape = csr.shape();
234        let rows = shape.dims()[0];
235        let cols = shape.dims()[1];
236
237        // Build column pointers by counting elements per column
238        let mut col_ptr = vec![0; cols + 1];
239        for &col in col_indices {
240            col_ptr[col + 1] += 1;
241        }
242
243        // Convert counts to cumulative offsets
244        for i in 1..=cols {
245            col_ptr[i] += col_ptr[i - 1];
246        }
247
248        // Allocate output arrays
249        let nnz = col_indices.len();
250        let mut new_row_indices = vec![0; nnz];
251        let mut new_values = vec![0.0; nnz];
252        let mut col_offsets = col_ptr.clone();
253
254        // Fill output arrays by processing CSR row by row
255        for row in 0..rows {
256            let start = row_ptr[row];
257            let end = row_ptr[row + 1];
258
259            for idx in start..end {
260                let col = col_indices[idx];
261                let val = values[idx];
262
263                // Place this element in the appropriate column
264                let pos = col_offsets[col];
265                new_row_indices[pos] = row;
266                new_values[pos] = val;
267                col_offsets[col] += 1;
268            }
269        }
270
271        // Create CSC tensor - note: need to check if CSC has a similar constructor
272        // For now, use COO as intermediate until we verify CSC API
273        let triplets: Vec<_> = (0..nnz)
274            .map(|i| (new_row_indices[i], i, new_values[i]))
275            .collect();
276        let transposed_shape = Shape::new(vec![cols, rows]);
277        let coo = patterns::triplets_to_coo(triplets, transposed_shape)?;
278        CscTensor::from_coo(&coo)
279    }
280
281    /// Direct CSC to CSR conversion via transpose
282    pub fn csc_to_csr_direct(csc: &CscTensor) -> TorshResult<CsrTensor> {
283        // For now, we'll implement this using COO as intermediate
284        // In a full implementation, we'd check for CSC accessor methods similar to CSR
285        let coo = csc.to_coo()?;
286        CsrTensor::from_coo(&coo)
287    }
288
289    /// Convert symmetric matrix representation to full matrix
290    pub fn symmetric_to_full_triplets(
291        triplets: &[(usize, usize, f32)],
292        mode: crate::symmetric::SymmetricMode,
293    ) -> Vec<(usize, usize, f32)> {
294        let mut full_triplets = Vec::with_capacity(triplets.len() * 2);
295
296        for &(row, col, val) in triplets {
297            // Always add the original element
298            full_triplets.push((row, col, val));
299
300            // Add the symmetric element if it's not on the diagonal
301            if row != col {
302                match mode {
303                    crate::symmetric::SymmetricMode::Upper => {
304                        // Upper triangular: also add (col, row)
305                        full_triplets.push((col, row, val));
306                    }
307                    crate::symmetric::SymmetricMode::Lower => {
308                        // Lower triangular: also add (col, row)
309                        full_triplets.push((col, row, val));
310                    }
311                }
312            }
313        }
314
315        full_triplets
316    }
317}
318
319/// Conversion optimization utilities
320pub mod optimization {
321    use super::*;
322
323    /// Analyze sparse tensor to provide conversion hints
324    pub fn analyze_conversion_hints(sparse: &dyn SparseTensor) -> ConversionHints {
325        let triplets = patterns::extract_triplets(sparse).unwrap_or_default();
326        let shape = sparse.shape();
327        let nnz = sparse.nnz();
328
329        if nnz == 0 {
330            return ConversionHints::default();
331        }
332
333        let rows = shape.dims()[0];
334        let cols = shape.dims()[1];
335
336        // Check if sorted
337        let is_sorted = is_triplets_sorted(&triplets);
338
339        // Check if symmetric (for square matrices only)
340        let is_symmetric = if rows == cols {
341            check_symmetry(&triplets)
342        } else {
343            false
344        };
345
346        // Check if diagonal
347        let is_diagonal = triplets.iter().all(|(r, c, _)| r == c);
348
349        // Check for uniform row sizes
350        let has_uniform_row_sizes = check_uniform_row_sizes(&triplets, rows);
351
352        // Simple block structure detection (placeholder)
353        let is_block_structured = false;
354
355        ConversionHints {
356            is_sorted,
357            is_symmetric,
358            has_uniform_row_sizes,
359            is_block_structured,
360            is_diagonal,
361        }
362    }
363
364    fn is_triplets_sorted(triplets: &[(usize, usize, f32)]) -> bool {
365        triplets
366            .windows(2)
367            .all(|w| (w[0].0, w[0].1) <= (w[1].0, w[1].1))
368    }
369
370    fn check_symmetry(triplets: &[(usize, usize, f32)]) -> bool {
371        let mut element_map: HashMap<(usize, usize), f32> = HashMap::new();
372
373        // Build map of all elements
374        for &(row, col, val) in triplets {
375            element_map.insert((row, col), val);
376        }
377
378        // Check symmetry
379        for &(row, col, val) in triplets {
380            if row != col {
381                if let Some(&sym_val) = element_map.get(&(col, row)) {
382                    if (val - sym_val).abs() > 1e-6 {
383                        return false;
384                    }
385                } else {
386                    return false;
387                }
388            }
389        }
390
391        true
392    }
393
394    fn check_uniform_row_sizes(triplets: &[(usize, usize, f32)], rows: usize) -> bool {
395        let mut row_counts = vec![0; rows];
396        for &(row, _, _) in triplets {
397            row_counts[row] += 1;
398        }
399
400        if let Some(&first_count) = row_counts.iter().find(|&&count| count > 0) {
401            row_counts
402                .iter()
403                .all(|&count| count == 0 || count == first_count)
404        } else {
405            true
406        }
407    }
408
409    /// Choose optimal conversion path based on source and target formats
410    pub fn choose_optimal_path(
411        source_format: crate::SparseFormat,
412        target_format: crate::SparseFormat,
413        hints: &ConversionHints,
414    ) -> ConversionPath {
415        use crate::SparseFormat::*;
416
417        match (source_format, target_format) {
418            // Identity conversions
419            (a, b) if a == b => ConversionPath::Identity,
420
421            // Direct optimized paths
422            (Csr, Csc) | (Csc, Csr) => ConversionPath::Direct,
423
424            // COO is efficient for most conversions from complex formats
425            (Bsr | Dia | Ell | Rle | Symmetric, _) => ConversionPath::ViaCoo,
426
427            // For other combinations, analyze hints
428            _ => {
429                if hints.is_diagonal {
430                    ConversionPath::ViaDia
431                } else if hints.is_symmetric {
432                    ConversionPath::ViaSymmetric
433                } else {
434                    ConversionPath::ViaCoo
435                }
436            }
437        }
438    }
439
440    /// Different conversion path strategies
441    #[derive(Debug, Clone, PartialEq)]
442    pub enum ConversionPath {
443        /// No conversion needed
444        Identity,
445        /// Direct conversion without intermediate format
446        Direct,
447        /// Convert via COO intermediate
448        ViaCoo,
449        /// Convert via DIA intermediate (for diagonal matrices)
450        ViaDia,
451        /// Convert via Symmetric intermediate (for symmetric matrices)
452        ViaSymmetric,
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459
460    #[test]
461    fn test_validation_functions() {
462        // Test 2D validation
463        let shape_2d = Shape::new(vec![3, 4]);
464        let shape_3d = Shape::new(vec![3, 4, 5]);
465
466        assert!(validation::validate_2d_tensor(&shape_2d, "Test").is_ok());
467        assert!(validation::validate_2d_tensor(&shape_3d, "Test").is_err());
468
469        // Test square matrix validation
470        let square_shape = Shape::new(vec![3, 3]);
471        let non_square_shape = Shape::new(vec![3, 4]);
472
473        assert!(validation::validate_square_matrix(&square_shape).is_ok());
474        assert!(validation::validate_square_matrix(&non_square_shape).is_err());
475    }
476
477    #[test]
478    fn test_triplet_processing() {
479        let triplets = vec![
480            (0, 0, 1.0),
481            (0, 0, 2.0), // Duplicate position
482            (1, 1, 3.0),
483            (0, 1, 0.0), // Zero value
484        ];
485
486        let aggregated = patterns::aggregate_duplicate_triplets(triplets);
487
488        // Should have combined (0,0) entries and filtered out zero
489        assert_eq!(aggregated.len(), 2);
490        assert!(aggregated.contains(&(0, 0, 3.0))); // 1.0 + 2.0
491        assert!(aggregated.contains(&(1, 1, 3.0)));
492    }
493
494    #[test]
495    fn test_conversion_hints() {
496        // Create a simple COO tensor for testing
497        let triplets = vec![(0, 0, 1.0), (1, 1, 2.0), (2, 2, 3.0)];
498        let shape = Shape::new(vec![3, 3]);
499        let coo = patterns::triplets_to_coo(triplets, shape).unwrap();
500
501        let hints = optimization::analyze_conversion_hints(&coo as &dyn SparseTensor);
502
503        // Diagonal matrix should be detected
504        assert!(hints.is_diagonal);
505    }
506}