1use crate::{CooTensor, CscTensor, CsrTensor, SparseTensor, TorshResult};
7use std::collections::HashMap;
8use torsh_core::{Shape, TorshError};
9use torsh_tensor::Tensor;
10
11#[derive(Debug, Default, Clone)]
13pub struct ConversionHints {
14 pub is_sorted: bool,
16 pub is_symmetric: bool,
18 pub has_uniform_row_sizes: bool,
20 pub is_block_structured: bool,
22 pub is_diagonal: bool,
24}
25
26pub mod validation {
28 use super::*;
29
30 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 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 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 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
98pub mod patterns {
100 use super::*;
101
102 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 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 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 for _row in 0..rows {
144 for _col in 0..cols {
145 }
152 }
153
154 Ok(())
155 }
156
157 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 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 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 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_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 current.2 += next.2;
204 } else {
205 if current.2.abs() > f32::EPSILON {
207 result.push(current);
208 }
209 current = next;
210 }
211 }
212
213 if current.2.abs() > f32::EPSILON {
215 result.push(current);
216 }
217
218 result
219 }
220}
221
222pub mod direct_conversions {
224 use super::*;
225
226 pub fn csr_to_csc_direct(csr: &CsrTensor) -> TorshResult<CscTensor> {
229 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 let mut col_ptr = vec![0; cols + 1];
239 for &col in col_indices {
240 col_ptr[col + 1] += 1;
241 }
242
243 for i in 1..=cols {
245 col_ptr[i] += col_ptr[i - 1];
246 }
247
248 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 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 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 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 pub fn csc_to_csr_direct(csc: &CscTensor) -> TorshResult<CsrTensor> {
283 let coo = csc.to_coo()?;
286 CsrTensor::from_coo(&coo)
287 }
288
289 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 full_triplets.push((row, col, val));
299
300 if row != col {
302 match mode {
303 crate::symmetric::SymmetricMode::Upper => {
304 full_triplets.push((col, row, val));
306 }
307 crate::symmetric::SymmetricMode::Lower => {
308 full_triplets.push((col, row, val));
310 }
311 }
312 }
313 }
314
315 full_triplets
316 }
317}
318
319pub mod optimization {
321 use super::*;
322
323 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 let is_sorted = is_triplets_sorted(&triplets);
338
339 let is_symmetric = if rows == cols {
341 check_symmetry(&triplets)
342 } else {
343 false
344 };
345
346 let is_diagonal = triplets.iter().all(|(r, c, _)| r == c);
348
349 let has_uniform_row_sizes = check_uniform_row_sizes(&triplets, rows);
351
352 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 for &(row, col, val) in triplets {
375 element_map.insert((row, col), val);
376 }
377
378 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 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 (a, b) if a == b => ConversionPath::Identity,
420
421 (Csr, Csc) | (Csc, Csr) => ConversionPath::Direct,
423
424 (Bsr | Dia | Ell | Rle | Symmetric, _) => ConversionPath::ViaCoo,
426
427 _ => {
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 #[derive(Debug, Clone, PartialEq)]
442 pub enum ConversionPath {
443 Identity,
445 Direct,
447 ViaCoo,
449 ViaDia,
451 ViaSymmetric,
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 #[test]
461 fn test_validation_functions() {
462 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 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), (1, 1, 3.0),
483 (0, 1, 0.0), ];
485
486 let aggregated = patterns::aggregate_duplicate_triplets(triplets);
487
488 assert_eq!(aggregated.len(), 2);
490 assert!(aggregated.contains(&(0, 0, 3.0))); assert!(aggregated.contains(&(1, 1, 3.0)));
492 }
493
494 #[test]
495 fn test_conversion_hints() {
496 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 assert!(hints.is_diagonal);
505 }
506}