1#[cfg(feature = "scipy")]
7use numpy::{PyArray1, PyReadonlyArray1};
8#[cfg(feature = "scipy")]
9use pyo3::prelude::*;
10#[cfg(feature = "scipy")]
11use pyo3::types::PyDict;
12#[cfg(feature = "scipy")]
13use pyo3::Bound;
14
15use crate::*;
16use std::collections::HashMap;
17use torsh_core::Result as TorshResult;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ScipyFormat {
22 Csr,
24 Csc,
26 Coo,
28 Bsr,
30 Dia,
32}
33
34impl From<SparseFormat> for ScipyFormat {
35 fn from(format: SparseFormat) -> Self {
36 match format {
37 SparseFormat::Coo => ScipyFormat::Coo,
38 SparseFormat::Csr => ScipyFormat::Csr,
39 SparseFormat::Csc => ScipyFormat::Csc,
40 SparseFormat::Bsr => ScipyFormat::Bsr,
41 SparseFormat::Dia => ScipyFormat::Dia,
42 SparseFormat::Ell => ScipyFormat::Csr, SparseFormat::Rle => ScipyFormat::Csr, SparseFormat::Symmetric => ScipyFormat::Csr, SparseFormat::Dsr => ScipyFormat::Csr, }
47 }
48}
49
50impl From<ScipyFormat> for SparseFormat {
51 fn from(format: ScipyFormat) -> Self {
52 match format {
53 ScipyFormat::Coo => SparseFormat::Coo,
54 ScipyFormat::Csr => SparseFormat::Csr,
55 ScipyFormat::Csc => SparseFormat::Csc,
56 ScipyFormat::Bsr => SparseFormat::Bsr,
57 ScipyFormat::Dia => SparseFormat::Dia,
58 }
59 }
60}
61
62#[derive(Debug, Clone)]
64pub struct ScipySparseData {
65 pub format: ScipyFormat,
67 pub shape: (usize, usize),
69 pub data: Vec<f64>,
71 pub indices: Vec<usize>,
73 pub indptr_or_row: Vec<usize>,
75 pub blocksize: Option<(usize, usize)>,
77 pub diagonals: Option<Vec<i32>>,
79}
80
81impl ScipySparseData {
82 pub fn new(format: ScipyFormat, shape: (usize, usize)) -> Self {
84 Self {
85 format,
86 shape,
87 data: Vec::new(),
88 indices: Vec::new(),
89 indptr_or_row: Vec::new(),
90 blocksize: None,
91 diagonals: None,
92 }
93 }
94
95 pub fn from_coo(
97 shape: (usize, usize),
98 row_indices: Vec<usize>,
99 col_indices: Vec<usize>,
100 values: Vec<f64>,
101 ) -> Self {
102 Self {
103 format: ScipyFormat::Coo,
104 shape,
105 data: values,
106 indices: col_indices,
107 indptr_or_row: row_indices,
108 blocksize: None,
109 diagonals: None,
110 }
111 }
112
113 pub fn from_csr(
115 shape: (usize, usize),
116 row_ptr: Vec<usize>,
117 col_indices: Vec<usize>,
118 values: Vec<f64>,
119 ) -> Self {
120 Self {
121 format: ScipyFormat::Csr,
122 shape,
123 data: values,
124 indices: col_indices,
125 indptr_or_row: row_ptr,
126 blocksize: None,
127 diagonals: None,
128 }
129 }
130
131 pub fn from_csc(
133 shape: (usize, usize),
134 col_ptr: Vec<usize>,
135 row_indices: Vec<usize>,
136 values: Vec<f64>,
137 ) -> Self {
138 Self {
139 format: ScipyFormat::Csc,
140 shape,
141 data: values,
142 indices: row_indices,
143 indptr_or_row: col_ptr,
144 blocksize: None,
145 diagonals: None,
146 }
147 }
148}
149
150pub struct ScipySparseIntegration;
152
153impl ScipySparseIntegration {
154 pub fn to_scipy_data(sparse: &dyn SparseTensor) -> TorshResult<ScipySparseData> {
156 let shape = sparse.shape();
157 let (rows, cols) = (shape.dims()[0], shape.dims()[1]);
158
159 match sparse.format() {
160 SparseFormat::Coo => {
161 let coo = sparse.to_coo()?;
162 let triplets = coo.triplets();
163
164 let mut row_indices = Vec::new();
165 let mut col_indices = Vec::new();
166 let mut values = Vec::new();
167
168 for (row, col, val) in triplets {
169 row_indices.push(row);
170 col_indices.push(col);
171 values.push(val as f64);
172 }
173
174 Ok(ScipySparseData::from_coo(
175 (rows, cols),
176 row_indices,
177 col_indices,
178 values,
179 ))
180 }
181 SparseFormat::Csr => {
182 let csr = sparse.to_csr()?;
183 let row_ptr = csr.row_ptr().to_vec();
184 let col_indices = csr.col_indices().to_vec();
185 let values = csr.values().iter().map(|&v| v as f64).collect();
186
187 Ok(ScipySparseData::from_csr(
188 (rows, cols),
189 row_ptr,
190 col_indices,
191 values,
192 ))
193 }
194 SparseFormat::Csc => {
195 let csc = sparse.to_csc()?;
196 let col_ptr = csc.col_ptr().to_vec();
197 let row_indices = csc.row_indices().to_vec();
198 let values = csc.values().iter().map(|&v| v as f64).collect();
199
200 Ok(ScipySparseData::from_csc(
201 (rows, cols),
202 col_ptr,
203 row_indices,
204 values,
205 ))
206 }
207 _ => {
208 let coo = sparse.to_coo()?;
210 Self::to_scipy_data(&coo)
211 }
212 }
213 }
214
215 pub fn from_scipy_data(
217 data: &ScipySparseData,
218 ) -> TorshResult<Box<dyn SparseTensor + Send + Sync>> {
219 let shape = Shape::new(vec![data.shape.0, data.shape.1]);
220
221 match data.format {
222 ScipyFormat::Coo => {
223 let mut rows = Vec::new();
224 let mut cols = Vec::new();
225 let mut values = Vec::new();
226
227 for i in 0..data.data.len() {
228 rows.push(data.indptr_or_row[i]);
229 cols.push(data.indices[i]);
230 values.push(data.data[i] as f32);
231 }
232
233 let coo = CooTensor::new(rows, cols, values, shape)?;
234 Ok(Box::new(coo))
235 }
236 ScipyFormat::Csr => {
237 let row_ptr = &data.indptr_or_row;
238 let col_indices = &data.indices;
239 let values: Vec<f32> = data.data.iter().map(|&v| v as f32).collect();
240
241 let csr = CsrTensor::from_unsorted_parts(
244 row_ptr.clone(),
245 col_indices.clone(),
246 values,
247 shape,
248 )?;
249
250 Ok(Box::new(csr))
251 }
252 ScipyFormat::Csc => {
253 let col_ptr = &data.indptr_or_row;
254 let row_indices = &data.indices;
255 let values: Vec<f32> = data.data.iter().map(|&v| v as f32).collect();
256
257 let csc = CscTensor::from_unsorted_parts(
260 col_ptr.clone(),
261 row_indices.clone(),
262 values,
263 shape,
264 )?;
265
266 Ok(Box::new(csc))
267 }
268 _ => {
269 let coo_data = ScipySparseData {
271 format: ScipyFormat::Coo,
272 ..data.clone()
273 };
274 let coo = Self::from_scipy_data(&coo_data)?;
275 convert_sparse_format(coo.as_ref(), data.format.into())
276 }
277 }
278 }
279
280 pub fn to_dict(sparse: &dyn SparseTensor) -> TorshResult<HashMap<String, Vec<f64>>> {
282 let scipy_data = Self::to_scipy_data(sparse)?;
283
284 let mut dict = HashMap::new();
285 dict.insert("data".to_string(), scipy_data.data);
286 dict.insert(
287 "indices".to_string(),
288 scipy_data.indices.iter().map(|&x| x as f64).collect(),
289 );
290 dict.insert(
291 "indptr".to_string(),
292 scipy_data.indptr_or_row.iter().map(|&x| x as f64).collect(),
293 );
294 dict.insert(
295 "shape".to_string(),
296 vec![scipy_data.shape.0 as f64, scipy_data.shape.1 as f64],
297 );
298
299 Ok(dict)
300 }
301
302 pub fn to_python_code(sparse: &dyn SparseTensor, var_name: &str) -> TorshResult<String> {
304 let scipy_data = Self::to_scipy_data(sparse)?;
305 let format_name = match scipy_data.format {
306 ScipyFormat::Coo => "coo_matrix",
307 ScipyFormat::Csr => "csr_matrix",
308 ScipyFormat::Csc => "csc_matrix",
309 ScipyFormat::Bsr => "bsr_matrix",
310 ScipyFormat::Dia => "dia_matrix",
311 };
312
313 let mut code = String::new();
314 code.push_str("import numpy as np\n");
315 code.push_str("from scipy.sparse import ");
316 code.push_str(format_name);
317 code.push_str("\n\n");
318
319 match scipy_data.format {
320 ScipyFormat::Coo => {
321 code.push_str("# COO format data\n");
322 code.push_str(&format!("row = np.array({:?})\n", scipy_data.indptr_or_row));
323 code.push_str(&format!("col = np.array({:?})\n", scipy_data.indices));
324 code.push_str(&format!("data = np.array({:?})\n", scipy_data.data));
325 code.push_str(&format!("shape = {:?}\n", scipy_data.shape));
326 code.push_str(&format!(
327 "{var_name} = {format_name}((data, (row, col)), shape=shape)\n"
328 ));
329 }
330 ScipyFormat::Csr | ScipyFormat::Csc => {
331 let ptr_name = "indptr";
332 code.push_str(&format!("# {} format data\n", format_name.to_uppercase()));
333 code.push_str(&format!("data = np.array({:?})\n", scipy_data.data));
334 code.push_str(&format!("indices = np.array({:?})\n", scipy_data.indices));
335 code.push_str(&format!(
336 "{} = np.array({:?})\n",
337 ptr_name, scipy_data.indptr_or_row
338 ));
339 code.push_str(&format!("shape = {:?}\n", scipy_data.shape));
340 code.push_str(&format!(
341 "{var_name} = {format_name}((data, indices, {ptr_name}), shape=shape)\n"
342 ));
343 }
344 _ => {
345 code.push_str(&format!(
347 "# Note: {format_name} format converted to COO for compatibility\n"
348 ));
349 code.push_str(&format!("row = np.array({:?})\n", scipy_data.indptr_or_row));
350 code.push_str(&format!("col = np.array({:?})\n", scipy_data.indices));
351 code.push_str(&format!("data = np.array({:?})\n", scipy_data.data));
352 code.push_str(&format!("shape = {:?}\n", scipy_data.shape));
353 code.push_str(&format!(
354 "{var_name} = coo_matrix((data, (row, col)), shape=shape)\n"
355 ));
356 }
357 }
358
359 Ok(code)
360 }
361}
362
363#[cfg(feature = "scipy")]
365pub mod python_bindings {
366 use super::*;
367
368 #[pyfunction]
370 pub fn torsh_to_scipy(
371 py: Python,
372 format: &str,
373 shape: (usize, usize),
374 data: Vec<f64>,
375 indices: Vec<usize>,
376 indptr: Vec<usize>,
377 ) -> PyResult<Py<PyAny>> {
378 let scipy = py.import("scipy.sparse")?;
379
380 let data_array = PyArray1::from_vec(py, data);
381 let indices_array = PyArray1::from_vec(py, indices);
382 let indptr_array = PyArray1::from_vec(py, indptr);
383
384 let args = (
385 data_array
386 .into_pyobject(py)
387 .expect("PyArray conversion should succeed"),
388 indices_array
389 .into_pyobject(py)
390 .expect("PyArray conversion should succeed"),
391 indptr_array
392 .into_pyobject(py)
393 .expect("PyArray conversion should succeed"),
394 );
395
396 let kwargs = PyDict::new(py);
397 kwargs.set_item("shape", shape)?;
398
399 let matrix_class = scipy.getattr(format)?;
400 let result = matrix_class.call(args, Some(&kwargs))?;
401
402 Ok(result.unbind())
403 }
404
405 #[pyfunction]
407 pub fn scipy_to_torsh(
408 _py: Python,
409 scipy_matrix: &Bound<PyAny>,
410 ) -> PyResult<(String, (usize, usize), Vec<f64>, Vec<usize>, Vec<usize>)> {
411 let format_attr = scipy_matrix.getattr("format")?;
413 let format: String = format_attr.extract()?;
414
415 let shape_attr = scipy_matrix.getattr("shape")?;
417 let shape: (usize, usize) = shape_attr.extract()?;
418
419 let coo_matrix = scipy_matrix.call_method0("tocoo")?;
421
422 let data_attr = coo_matrix.getattr("data")?;
424 let row_attr = coo_matrix.getattr("row")?;
425 let col_attr = coo_matrix.getattr("col")?;
426
427 let data: PyReadonlyArray1<f64> = data_attr.extract()?;
428 let row: PyReadonlyArray1<i32> = row_attr.extract()?;
429 let col: PyReadonlyArray1<i32> = col_attr.extract()?;
430
431 let data_vec = data.as_slice()?.to_vec();
432 let row_vec: Vec<usize> = row.as_slice()?.iter().map(|&x| x as usize).collect();
433 let col_vec: Vec<usize> = col.as_slice()?.iter().map(|&x| x as usize).collect();
434
435 Ok((format, shape, data_vec, col_vec, row_vec))
436 }
437}
438
439#[macro_export]
441macro_rules! to_scipy {
442 ($sparse:expr) => {
443 ScipySparseIntegration::to_scipy_data($sparse)
444 };
445 ($sparse:expr, $format:expr) => {{
446 let scipy_data = ScipySparseIntegration::to_scipy_data($sparse)?;
447 let converted = convert_sparse_format($sparse, $format)?;
448 ScipySparseIntegration::to_scipy_data(converted.as_ref())
449 }};
450}
451
452#[macro_export]
454macro_rules! from_scipy {
455 ($data:expr) => {
456 ScipySparseIntegration::from_scipy_data($data)
457 };
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463 use crate::coo::CooTensor;
464 use torsh_core::{DType, Shape};
465
466 #[test]
467 fn test_scipy_data_conversion() {
468 let shape = Shape::new(vec![3, 3]);
469 let mut coo = CooTensor::empty(shape.clone(), DType::F32).unwrap();
470
471 coo.insert(0, 0, 1.0).unwrap();
473 coo.insert(1, 1, 2.0).unwrap();
474 coo.insert(2, 2, 3.0).unwrap();
475 coo.insert(0, 2, 4.0).unwrap();
476
477 let scipy_data = ScipySparseIntegration::to_scipy_data(&coo).unwrap();
479
480 assert_eq!(scipy_data.format, ScipyFormat::Coo);
481 assert_eq!(scipy_data.shape, (3, 3));
482 assert_eq!(scipy_data.data.len(), 4);
483
484 let restored = ScipySparseIntegration::from_scipy_data(&scipy_data).unwrap();
486 assert_eq!(restored.nnz(), 4);
487 assert_eq!(restored.shape(), &shape);
488 }
489
490 #[test]
491 fn test_python_code_generation() {
492 let shape = Shape::new(vec![2, 2]);
493 let mut coo = CooTensor::empty(shape, DType::F32).unwrap();
494
495 coo.insert(0, 0, 1.0).unwrap();
496 coo.insert(1, 1, 2.0).unwrap();
497
498 let code = ScipySparseIntegration::to_python_code(&coo, "matrix").unwrap();
499
500 assert!(code.contains("import numpy as np"));
501 assert!(code.contains("from scipy.sparse import"));
502 assert!(code.contains("matrix ="));
503 }
504
505 #[test]
506 fn test_dict_conversion() {
507 let shape = Shape::new(vec![2, 2]);
508 let mut coo = CooTensor::empty(shape, DType::F32).unwrap();
509
510 coo.insert(0, 0, 1.0).unwrap();
511 coo.insert(1, 1, 2.0).unwrap();
512
513 let dict = ScipySparseIntegration::to_dict(&coo).unwrap();
514
515 assert!(dict.contains_key("data"));
516 assert!(dict.contains_key("indices"));
517 assert!(dict.contains_key("indptr"));
518 assert!(dict.contains_key("shape"));
519
520 assert_eq!(dict["shape"], vec![2.0, 2.0]);
521 assert_eq!(dict["data"].len(), 2);
522 }
523
524 #[test]
525 fn test_format_conversion() {
526 assert_eq!(ScipyFormat::from(SparseFormat::Coo), ScipyFormat::Coo);
527 assert_eq!(ScipyFormat::from(SparseFormat::Csr), ScipyFormat::Csr);
528 assert_eq!(ScipyFormat::from(SparseFormat::Csc), ScipyFormat::Csc);
529 assert_eq!(ScipyFormat::from(SparseFormat::Ell), ScipyFormat::Csr);
530
531 assert_eq!(SparseFormat::from(ScipyFormat::Coo), SparseFormat::Coo);
532 assert_eq!(SparseFormat::from(ScipyFormat::Csr), SparseFormat::Csr);
533 assert_eq!(SparseFormat::from(ScipyFormat::Csc), SparseFormat::Csc);
534 }
535
536 #[test]
537 fn test_macro_usage() {
538 let shape = Shape::new(vec![2, 2]);
539 let mut coo = CooTensor::empty(shape, DType::F32).unwrap();
540
541 coo.insert(0, 0, 1.0).unwrap();
542 coo.insert(1, 1, 2.0).unwrap();
543
544 let scipy_data = to_scipy!(&coo).unwrap();
545 assert_eq!(scipy_data.data.len(), 2);
546
547 let restored = from_scipy!(&scipy_data).unwrap();
548 assert_eq!(restored.nnz(), 2);
549 }
550}