oxiblas_ndarray/
conversions.rs

1//! Conversion utilities between ndarray and OxiBLAS types.
2//!
3//! This module provides zero-copy and copying conversions between
4//! ndarray's `Array2`, `ArrayView2`, `ArrayViewMut2` and OxiBLAS's
5//! `Mat`, `MatRef`, `MatMut` types.
6
7use ndarray::{
8    Array1, Array2, ArrayD, ArrayView1, ArrayView2, ArrayViewD, ArrayViewMut1, ArrayViewMut2,
9    ArrayViewMutD, IxDyn, ShapeBuilder,
10};
11use oxiblas_core::scalar::Field;
12use oxiblas_matrix::{Mat, MatMut, MatRef};
13
14// =============================================================================
15// Array2 <-> Mat Conversions
16// =============================================================================
17
18/// Converts an ndarray Array2 to an OxiBLAS Mat.
19///
20/// This checks if the array is in column-major order for zero-copy conversion.
21/// If not, it performs a copy.
22pub fn array2_to_mat<T: Field + Clone>(arr: &Array2<T>) -> Mat<T>
23where
24    T: bytemuck::Zeroable,
25{
26    let (nrows, ncols) = arr.dim();
27    let strides = arr.strides();
28
29    // Check for column-major (Fortran) order
30    if strides[0] == 1 && strides[1] == nrows as isize {
31        // Zero-copy path: array is already column-major
32        let mut mat = Mat::zeros(nrows, ncols);
33        // Element-by-element copy
34        for i in 0..nrows {
35            for j in 0..ncols {
36                mat[(i, j)] = arr[[i, j]];
37            }
38        }
39        mat
40    } else {
41        // Row-major or other order, need element-by-element copy
42        let mut mat = Mat::zeros(nrows, ncols);
43        for i in 0..nrows {
44            for j in 0..ncols {
45                mat[(i, j)] = arr[[i, j]];
46            }
47        }
48        mat
49    }
50}
51
52/// Converts an ndarray Array2 to an OxiBLAS Mat, consuming the array.
53///
54/// This is more efficient when the array is column-major as it can
55/// potentially reuse the underlying storage.
56pub fn array2_into_mat<T: Field + Clone>(arr: Array2<T>) -> Mat<T>
57where
58    T: bytemuck::Zeroable,
59{
60    // For now, just delegate to the reference version
61    // Future optimization: could potentially use into_raw_vec() for zero-copy
62    array2_to_mat(&arr)
63}
64
65/// Converts an OxiBLAS Mat to an ndarray Array2.
66///
67/// Creates a column-major (Fortran order) Array2.
68pub fn mat_to_array2<T: Field + Clone>(mat: &Mat<T>) -> Array2<T> {
69    let (nrows, ncols) = mat.shape();
70    // Create in Fortran order for efficient conversion
71    Array2::from_shape_fn((nrows, ncols).f(), |(i, j)| mat[(i, j)])
72}
73
74/// Converts an OxiBLAS MatRef to an ndarray Array2.
75///
76/// Creates a column-major (Fortran order) Array2.
77pub fn mat_ref_to_array2<T: Field + Clone>(mat: MatRef<'_, T>) -> Array2<T> {
78    let (nrows, ncols) = (mat.nrows(), mat.ncols());
79    Array2::from_shape_fn((nrows, ncols).f(), |(i, j)| mat[(i, j)])
80}
81
82/// Converts an OxiBLAS Mat to a row-major ndarray Array2.
83pub fn mat_to_array2_c<T: Field + Clone>(mat: &Mat<T>) -> Array2<T> {
84    let (nrows, ncols) = mat.shape();
85    Array2::from_shape_fn((nrows, ncols), |(i, j)| mat[(i, j)])
86}
87
88// =============================================================================
89// ArrayD <-> Mat Conversions (Dynamic Dimension)
90// =============================================================================
91
92/// Converts an ndarray ArrayD (dynamic dimension) to an OxiBLAS Mat.
93///
94/// # Panics
95/// Panics if the array is not 2-dimensional.
96///
97/// # Example
98/// ```
99/// use ndarray::{ArrayD, IxDyn};
100/// use oxiblas_ndarray::conversions::arrayd_to_mat;
101///
102/// let arr = ArrayD::from_shape_fn(IxDyn(&[3, 4]), |idx| (idx[0] * 4 + idx[1]) as f64);
103/// let mat = arrayd_to_mat(&arr);
104/// assert_eq!(mat.shape(), (3, 4));
105/// ```
106pub fn arrayd_to_mat<T: Field + Clone>(arr: &ArrayD<T>) -> Mat<T>
107where
108    T: bytemuck::Zeroable,
109{
110    assert_eq!(
111        arr.ndim(),
112        2,
113        "ArrayD must be 2-dimensional for matrix conversion"
114    );
115    let shape = arr.shape();
116    let nrows = shape[0];
117    let ncols = shape[1];
118
119    let mut mat = Mat::zeros(nrows, ncols);
120    for i in 0..nrows {
121        for j in 0..ncols {
122            mat[(i, j)] = arr[[i, j].as_ref()];
123        }
124    }
125    mat
126}
127
128/// Converts an ndarray ArrayD to an OxiBLAS Mat, consuming the array.
129///
130/// # Panics
131/// Panics if the array is not 2-dimensional.
132pub fn arrayd_into_mat<T: Field + Clone>(arr: ArrayD<T>) -> Mat<T>
133where
134    T: bytemuck::Zeroable,
135{
136    arrayd_to_mat(&arr)
137}
138
139/// Converts an OxiBLAS Mat to an ndarray ArrayD.
140///
141/// Creates a column-major (Fortran order) ArrayD.
142pub fn mat_to_arrayd<T: Field + Clone>(mat: &Mat<T>) -> ArrayD<T> {
143    let (nrows, ncols) = mat.shape();
144    let mut arr = ArrayD::from_elem(IxDyn(&[nrows, ncols]), T::zero());
145    for i in 0..nrows {
146        for j in 0..ncols {
147            arr[[i, j].as_ref()] = mat[(i, j)];
148        }
149    }
150    arr
151}
152
153/// Converts an OxiBLAS MatRef to an ndarray ArrayD.
154pub fn mat_ref_to_arrayd<T: Field + Clone>(mat: MatRef<'_, T>) -> ArrayD<T> {
155    let (nrows, ncols) = (mat.nrows(), mat.ncols());
156    let mut arr = ArrayD::from_elem(IxDyn(&[nrows, ncols]), T::zero());
157    for i in 0..nrows {
158        for j in 0..ncols {
159            arr[[i, j].as_ref()] = mat[(i, j)];
160        }
161    }
162    arr
163}
164
165/// Converts an ArrayD to an Array2.
166///
167/// # Panics
168/// Panics if the array is not 2-dimensional.
169pub fn arrayd_to_array2<T: Clone>(arr: &ArrayD<T>) -> Array2<T> {
170    assert_eq!(arr.ndim(), 2, "ArrayD must be 2-dimensional");
171    let shape = arr.shape();
172    Array2::from_shape_fn((shape[0], shape[1]), |(i, j)| arr[[i, j].as_ref()].clone())
173}
174
175/// Converts an Array2 to an ArrayD.
176pub fn array2_to_arrayd<T: Clone>(arr: &Array2<T>) -> ArrayD<T> {
177    let (nrows, ncols) = arr.dim();
178    let mut result = ArrayD::from_elem(IxDyn(&[nrows, ncols]), arr[[0, 0]].clone());
179    for i in 0..nrows {
180        for j in 0..ncols {
181            result[[i, j].as_ref()] = arr[[i, j]].clone();
182        }
183    }
184    result
185}
186
187// =============================================================================
188// ArrayViewD -> MatRef Conversion
189// =============================================================================
190
191/// Creates a MatRef view from an ndarray ArrayViewD.
192///
193/// # Returns
194/// - `Some(MatRef)` if the array is 2D and in column-major order
195/// - `None` if the array is not 2D or layout is incompatible
196pub fn array_viewd_to_mat_ref<'a, T: Field>(arr: &'a ArrayViewD<'a, T>) -> Option<MatRef<'a, T>> {
197    if arr.ndim() != 2 {
198        return None;
199    }
200
201    let shape = arr.shape();
202    let nrows = shape[0];
203    let ncols = shape[1];
204    let strides = arr.strides();
205
206    // Check for column-major order: row stride = 1
207    if strides[0] == 1 {
208        let col_stride = strides[1] as usize;
209        let ptr = arr.as_ptr();
210        Some(MatRef::new(ptr, nrows, ncols, col_stride))
211    } else {
212        None
213    }
214}
215
216/// Creates a MatRef view from an ndarray ArrayViewD, handling row-major layout.
217///
218/// # Returns
219/// - `Some((MatRef, false))` if the array is 2D and column-major
220/// - `Some((MatRef, true))` if the array is 2D and row-major (MatRef is transposed)
221/// - `None` if the array is not 2D or layout is incompatible
222pub fn array_viewd_to_mat_ref_or_transposed<'a, T: Field>(
223    arr: &'a ArrayViewD<'a, T>,
224) -> Option<(MatRef<'a, T>, bool)> {
225    if arr.ndim() != 2 {
226        return None;
227    }
228
229    let shape = arr.shape();
230    let nrows = shape[0];
231    let ncols = shape[1];
232    let strides = arr.strides();
233
234    if strides[0] == 1 {
235        // Column-major
236        let col_stride = strides[1] as usize;
237        let ptr = arr.as_ptr();
238        Some((MatRef::new(ptr, nrows, ncols, col_stride), false))
239    } else if strides[1] == 1 {
240        // Row-major: treat as transposed column-major
241        let row_stride = strides[0] as usize;
242        let ptr = arr.as_ptr();
243        Some((MatRef::new(ptr, ncols, nrows, row_stride), true))
244    } else {
245        None
246    }
247}
248
249// =============================================================================
250// ArrayViewMutD -> MatMut Conversion
251// =============================================================================
252
253/// Creates a MatMut view from an ndarray ArrayViewMutD.
254///
255/// # Returns
256/// - `Some(MatMut)` if the array is 2D and in column-major order
257/// - `None` if the array is not 2D or layout is incompatible
258pub fn array_view_mutd_to_mat_mut<'a, T: Field>(
259    arr: &'a mut ArrayViewMutD<'a, T>,
260) -> Option<MatMut<'a, T>> {
261    if arr.ndim() != 2 {
262        return None;
263    }
264
265    let shape = arr.shape();
266    let nrows = shape[0];
267    let ncols = shape[1];
268    let strides = arr.strides();
269
270    if strides[0] == 1 {
271        let col_stride = strides[1] as usize;
272        let ptr = arr.as_mut_ptr();
273        Some(MatMut::new(ptr, nrows, ncols, col_stride))
274    } else {
275        None
276    }
277}
278
279// =============================================================================
280// ArrayView2 -> MatRef Zero-Copy Conversion
281// =============================================================================
282
283/// Creates a MatRef view from an ndarray ArrayView2.
284///
285/// # Returns
286/// - `Some(MatRef)` if the array is in column-major (Fortran) order
287/// - `None` if the array layout is incompatible
288///
289/// # Safety
290/// The returned MatRef borrows from the ArrayView2.
291pub fn array_view_to_mat_ref<'a, T: Field>(arr: &'a ArrayView2<'a, T>) -> Option<MatRef<'a, T>> {
292    let (nrows, ncols) = arr.dim();
293    let strides = arr.strides();
294
295    // Check for column-major order: row stride = 1
296    if strides[0] == 1 {
297        let col_stride = strides[1] as usize;
298        let ptr = arr.as_ptr();
299        Some(MatRef::new(ptr, nrows, ncols, col_stride))
300    } else {
301        None
302    }
303}
304
305/// Creates a MatRef view from an ndarray ArrayView2, handling row-major layout
306/// by returning a transposed view if needed.
307///
308/// # Returns
309/// - `(MatRef, false)` if the array is column-major
310/// - `(MatRef, true)` if the array is row-major (MatRef is transposed)
311/// - `None` if the array layout is incompatible (non-contiguous)
312pub fn array_view_to_mat_ref_or_transposed<'a, T: Field>(
313    arr: &'a ArrayView2<'a, T>,
314) -> Option<(MatRef<'a, T>, bool)> {
315    let (nrows, ncols) = arr.dim();
316    let strides = arr.strides();
317
318    if strides[0] == 1 {
319        // Column-major
320        let col_stride = strides[1] as usize;
321        let ptr = arr.as_ptr();
322        Some((MatRef::new(ptr, nrows, ncols, col_stride), false))
323    } else if strides[1] == 1 {
324        // Row-major: treat as transposed column-major
325        let row_stride = strides[0] as usize;
326        let ptr = arr.as_ptr();
327        // Return transposed dimensions
328        Some((MatRef::new(ptr, ncols, nrows, row_stride), true))
329    } else {
330        // Non-contiguous
331        None
332    }
333}
334
335// =============================================================================
336// ArrayViewMut2 -> MatMut Zero-Copy Conversion
337// =============================================================================
338
339/// Creates a MatMut view from an ndarray ArrayViewMut2.
340///
341/// # Returns
342/// - `Some(MatMut)` if the array is in column-major (Fortran) order
343/// - `None` if the array layout is incompatible
344pub fn array_view_mut_to_mat_mut<'a, T: Field>(
345    arr: &'a mut ArrayViewMut2<'a, T>,
346) -> Option<MatMut<'a, T>> {
347    let (nrows, ncols) = arr.dim();
348    let strides = arr.strides();
349
350    if strides[0] == 1 {
351        let col_stride = strides[1] as usize;
352        let ptr = arr.as_mut_ptr();
353        Some(MatMut::new(ptr, nrows, ncols, col_stride))
354    } else {
355        None
356    }
357}
358
359// =============================================================================
360// 1D Array Conversions (for vectors)
361// =============================================================================
362
363/// Converts an ndarray Array1 to a Vec.
364pub fn array1_to_vec<T: Clone>(arr: &Array1<T>) -> Vec<T> {
365    arr.iter().cloned().collect()
366}
367
368/// Converts a slice to an ndarray Array1.
369pub fn slice_to_array1<T: Clone>(slice: &[T]) -> Array1<T> {
370    Array1::from_vec(slice.to_vec())
371}
372
373/// Gets a slice from an ArrayView1 if contiguous.
374pub fn array_view1_as_slice<'a, T>(arr: &'a ArrayView1<'a, T>) -> Option<&'a [T]> {
375    arr.as_slice()
376}
377
378/// Gets a mutable slice from an ArrayViewMut1 if contiguous.
379pub fn array_view1_as_slice_mut<'a, T>(arr: &'a mut ArrayViewMut1<'a, T>) -> Option<&'a mut [T]> {
380    arr.as_slice_mut()
381}
382
383// =============================================================================
384// Helper Functions
385// =============================================================================
386
387/// Creates a column-major Array2 (Fortran order).
388///
389/// This is the preferred layout for OxiBLAS operations as it allows
390/// zero-copy conversions.
391pub fn zeros_f<T: Clone + Default>(nrows: usize, ncols: usize) -> Array2<T> {
392    Array2::from_shape_fn((nrows, ncols).f(), |_| T::default())
393}
394
395/// Creates a column-major Array2 filled with a value.
396pub fn filled_f<T: Clone>(nrows: usize, ncols: usize, value: T) -> Array2<T> {
397    Array2::from_shape_fn((nrows, ncols).f(), |_| value.clone())
398}
399
400/// Checks if an Array2 is in column-major (Fortran) order.
401pub fn is_column_major<T>(arr: &Array2<T>) -> bool {
402    let strides = arr.strides();
403    let (nrows, _) = arr.dim();
404    strides[0] == 1 && strides[1] == nrows as isize
405}
406
407/// Checks if an Array2 is in row-major (C) order.
408pub fn is_row_major<T>(arr: &Array2<T>) -> bool {
409    let strides = arr.strides();
410    let (_, ncols) = arr.dim();
411    strides[0] == ncols as isize && strides[1] == 1
412}
413
414/// Converts a row-major Array2 to column-major.
415pub fn to_column_major<T: Clone + Default>(arr: &Array2<T>) -> Array2<T> {
416    let (nrows, ncols) = arr.dim();
417    let mut result = zeros_f(nrows, ncols);
418    for i in 0..nrows {
419        for j in 0..ncols {
420            result[[i, j]] = arr[[i, j]].clone();
421        }
422    }
423    result
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429    use ndarray::Array2;
430
431    #[test]
432    fn test_array2_to_mat_rowmajor() {
433        let arr = Array2::from_shape_fn((3, 4), |(i, j)| (i * 4 + j) as f64);
434        let mat = array2_to_mat(&arr);
435
436        assert_eq!(mat.shape(), (3, 4));
437        for i in 0..3 {
438            for j in 0..4 {
439                assert_eq!(mat[(i, j)], arr[[i, j]]);
440            }
441        }
442    }
443
444    #[test]
445    fn test_array2_to_mat_colmajor() {
446        let arr: Array2<f64> = Array2::from_shape_fn((3, 4).f(), |(i, j)| (i * 4 + j) as f64);
447        assert!(is_column_major(&arr));
448
449        let mat = array2_to_mat(&arr);
450        assert_eq!(mat.shape(), (3, 4));
451        for i in 0..3 {
452            for j in 0..4 {
453                assert_eq!(mat[(i, j)], arr[[i, j]]);
454            }
455        }
456    }
457
458    #[test]
459    fn test_mat_to_array2() {
460        let mat: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]]);
461        let arr = mat_to_array2(&mat);
462
463        assert_eq!(arr.dim(), (2, 3));
464        assert_eq!(arr[[0, 0]], 1.0);
465        assert_eq!(arr[[1, 2]], 6.0);
466    }
467
468    #[test]
469    fn test_roundtrip() {
470        let original = Array2::from_shape_fn((5, 7), |(i, j)| (i * 7 + j) as f64);
471        let mat = array2_to_mat(&original);
472        let recovered = mat_to_array2(&mat);
473
474        for i in 0..5 {
475            for j in 0..7 {
476                assert!((original[[i, j]] - recovered[[i, j]]).abs() < 1e-15);
477            }
478        }
479    }
480
481    #[test]
482    fn test_is_column_major() {
483        let col_major: Array2<f64> = Array2::zeros((3, 4).f());
484        let row_major: Array2<f64> = Array2::zeros((3, 4));
485
486        assert!(is_column_major(&col_major));
487        assert!(!is_column_major(&row_major));
488        assert!(is_row_major(&row_major));
489        assert!(!is_row_major(&col_major));
490    }
491
492    #[test]
493    fn test_to_column_major() {
494        let row_major = Array2::from_shape_fn((3, 4), |(i, j)| (i * 4 + j) as f64);
495        let col_major = to_column_major(&row_major);
496
497        assert!(is_column_major(&col_major));
498        for i in 0..3 {
499            for j in 0..4 {
500                assert_eq!(row_major[[i, j]], col_major[[i, j]]);
501            }
502        }
503    }
504
505    #[test]
506    fn test_array_view_to_mat_ref_or_transposed() {
507        // Column-major
508        let col_major: Array2<f64> = Array2::from_shape_fn((3, 4).f(), |(i, j)| (i * 4 + j) as f64);
509        let view = col_major.view();
510        let (mat_ref, transposed) = array_view_to_mat_ref_or_transposed(&view).unwrap();
511        assert!(!transposed);
512        assert_eq!(mat_ref.shape(), (3, 4));
513
514        // Row-major
515        let row_major: Array2<f64> = Array2::from_shape_fn((3, 4), |(i, j)| (i * 4 + j) as f64);
516        let view = row_major.view();
517        let (mat_ref, transposed) = array_view_to_mat_ref_or_transposed(&view).unwrap();
518        assert!(transposed);
519        // Transposed: original is 3x4, so MatRef should be 4x3
520        assert_eq!(mat_ref.shape(), (4, 3));
521    }
522
523    // =========================================================================
524    // ArrayD (Dynamic Dimension) Tests
525    // =========================================================================
526
527    #[test]
528    fn test_arrayd_to_mat() {
529        let arr = ArrayD::from_shape_fn(IxDyn(&[3, 4]), |idx| (idx[0] * 4 + idx[1]) as f64);
530        let mat = arrayd_to_mat(&arr);
531
532        assert_eq!(mat.shape(), (3, 4));
533        for i in 0..3 {
534            for j in 0..4 {
535                assert_eq!(mat[(i, j)], arr[[i, j].as_ref()]);
536            }
537        }
538    }
539
540    #[test]
541    fn test_mat_to_arrayd() {
542        let mat: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]]);
543        let arr = mat_to_arrayd(&mat);
544
545        assert_eq!(arr.ndim(), 2);
546        assert_eq!(arr.shape(), &[2, 3]);
547        assert_eq!(arr[[0, 0].as_ref()], 1.0);
548        assert_eq!(arr[[1, 2].as_ref()], 6.0);
549    }
550
551    #[test]
552    fn test_arrayd_roundtrip() {
553        let original = ArrayD::from_shape_fn(IxDyn(&[5, 7]), |idx| (idx[0] * 7 + idx[1]) as f64);
554        let mat = arrayd_to_mat(&original);
555        let recovered = mat_to_arrayd(&mat);
556
557        assert_eq!(recovered.shape(), original.shape());
558        for i in 0..5 {
559            for j in 0..7 {
560                assert!((original[[i, j].as_ref()] - recovered[[i, j].as_ref()]).abs() < 1e-15);
561            }
562        }
563    }
564
565    #[test]
566    fn test_arrayd_to_array2() {
567        let arr_d = ArrayD::from_shape_fn(IxDyn(&[3, 4]), |idx| (idx[0] * 4 + idx[1]) as f64);
568        let arr_2 = arrayd_to_array2(&arr_d);
569
570        assert_eq!(arr_2.dim(), (3, 4));
571        for i in 0..3 {
572            for j in 0..4 {
573                assert_eq!(arr_2[[i, j]], arr_d[[i, j].as_ref()]);
574            }
575        }
576    }
577
578    #[test]
579    fn test_array2_to_arrayd() {
580        let arr_2: Array2<f64> = Array2::from_shape_fn((3, 4), |(i, j)| (i * 4 + j) as f64);
581        let arr_d = array2_to_arrayd(&arr_2);
582
583        assert_eq!(arr_d.ndim(), 2);
584        assert_eq!(arr_d.shape(), &[3, 4]);
585        for i in 0..3 {
586            for j in 0..4 {
587                assert_eq!(arr_d[[i, j].as_ref()], arr_2[[i, j]]);
588            }
589        }
590    }
591
592    #[test]
593    fn test_array_viewd_to_mat_ref() {
594        let arr = ArrayD::from_shape_fn(IxDyn(&[3, 4]), |idx| (idx[0] * 4 + idx[1]) as f64);
595        let view = arr.view();
596
597        // Default ndarray layout is C-order (row-major), so column-major view should fail
598        // unless we specifically create it that way
599        let result = array_viewd_to_mat_ref(&view);
600        // Row-major, so this should return None
601        assert!(result.is_none());
602    }
603
604    #[test]
605    fn test_array_viewd_to_mat_ref_or_transposed() {
606        // Row-major ArrayD
607        let arr = ArrayD::from_shape_fn(IxDyn(&[3, 4]), |idx| (idx[0] * 4 + idx[1]) as f64);
608        let view = arr.view();
609
610        let result = array_viewd_to_mat_ref_or_transposed(&view);
611        assert!(result.is_some());
612        let (mat_ref, transposed) = result.unwrap();
613        assert!(transposed); // Row-major should be transposed
614        assert_eq!(mat_ref.shape(), (4, 3)); // Transposed dimensions
615    }
616
617    #[test]
618    #[should_panic(expected = "2-dimensional")]
619    fn test_arrayd_to_mat_wrong_dim() {
620        let arr = ArrayD::from_shape_fn(IxDyn(&[2, 3, 4]), |idx| idx[0] as f64);
621        let _ = arrayd_to_mat(&arr);
622    }
623
624    #[test]
625    fn test_array_viewd_wrong_dim() {
626        // 3D array
627        let arr = ArrayD::from_shape_fn(IxDyn(&[2, 3, 4]), |idx| idx[0] as f64);
628        let view = arr.view();
629
630        // Should return None for non-2D arrays
631        assert!(array_viewd_to_mat_ref(&view).is_none());
632        assert!(array_viewd_to_mat_ref_or_transposed(&view).is_none());
633    }
634}