Skip to main content

oxiblas_ndarray/
conversions.rs

1//! Conversion utilities between ndarray and OxiBLAS types.
2//!
3//! This module provides two distinct kinds of conversions between
4//! ndarray's `Array2`/`ArrayView2`/`ArrayViewMut2` and OxiBLAS's
5//! `Mat`/`MatRef`/`MatMut` types, and it is important not to confuse them:
6//!
7//! - **Genuinely zero-copy views**: [`array_view_to_mat_ref`],
8//!   [`array_view_to_mat_ref_or_transposed`], [`array_view_mut_to_mat_mut`],
9//!   [`array_viewd_to_mat_ref`], [`array_viewd_to_mat_ref_or_transposed`],
10//!   and [`array_view_mutd_to_mat_mut`] wrap the source array's *existing*
11//!   buffer in a borrowing `MatRef`/`MatMut` - no allocation, no element
12//!   copy - whenever the array is contiguous along one axis with a
13//!   non-negative stride. They return `None` for layouts that cannot be
14//!   represented this way (non-contiguous, or a reversed/negative-stride
15//!   axis, which `MatRef`/`MatMut` do not support).
16//! - **Copying conversions**: [`array2_to_mat`], [`array2_into_mat`],
17//!   [`arrayd_to_mat`], [`arrayd_into_mat`], [`mat_to_array2`],
18//!   [`mat_to_array2_c`], [`mat_ref_to_array2`], [`mat_to_arrayd`], and
19//!   [`mat_ref_to_arrayd`] always allocate a brand-new buffer and copy
20//!   every element. `Mat`'s storage (`AlignedVec`) is always a distinct,
21//!   cache-line-aligned allocation with a possibly-padded column stride
22//!   (see `oxiblas_matrix::Mat`'s memory layout docs), so it can never
23//!   adopt/reuse an `ndarray::Array2`'s `Vec`-backed buffer - regardless
24//!   of whether the source array happens to already be column-major. If
25//!   you need a real zero-copy path, work with `MatRef`/`MatMut` via the
26//!   view conversions above instead of `Mat`.
27
28use ndarray::{
29    Array1, Array2, ArrayD, ArrayView1, ArrayView2, ArrayViewD, ArrayViewMut1, ArrayViewMut2,
30    ArrayViewMutD, IxDyn, ShapeBuilder,
31};
32use oxiblas_core::scalar::Field;
33use oxiblas_matrix::{Mat, MatMut, MatRef};
34
35// =============================================================================
36// Array2 <-> Mat Conversions
37// =============================================================================
38
39/// Converts an ndarray Array2 to an OxiBLAS Mat by copying every element.
40///
41/// # Notes
42///
43/// This is **always a copying conversion**, regardless of whether `arr` is
44/// column-major, row-major, or otherwise strided: `Mat` allocates its own
45/// cache-line-aligned, potentially row-padded column-major buffer (see
46/// `oxiblas_matrix::Mat`), which is structurally incompatible with
47/// `ndarray`'s `Vec`-backed storage, so there is no layout for which
48/// `arr`'s buffer could be reused. If you need a real zero-copy view
49/// instead, use [`array_view_to_mat_ref`] (or
50/// [`array_view_to_mat_ref_or_transposed`]), which borrow `arr`'s existing
51/// buffer via `MatRef` with no allocation and no element copy whenever the
52/// layout allows it.
53pub fn array2_to_mat<T: Field + Clone>(arr: &Array2<T>) -> Mat<T>
54where
55    T: bytemuck::Zeroable,
56{
57    let (nrows, ncols) = arr.dim();
58    let mut mat = Mat::zeros(nrows, ncols);
59    for i in 0..nrows {
60        for j in 0..ncols {
61            mat[(i, j)] = arr[[i, j]];
62        }
63    }
64    mat
65}
66
67/// Converts an ndarray Array2 to an OxiBLAS Mat, consuming the array.
68///
69/// # Notes
70///
71/// Despite taking `arr` by value, this **still copies every element**: it
72/// is not more efficient than [`array2_to_mat`], for any layout of `arr`.
73/// `Mat`'s backing `AlignedVec` is always a fresh, cache-line-aligned
74/// allocation (potentially with column padding for SIMD alignment) built
75/// through a different allocator path than `ndarray`'s `Vec`, so `arr`'s
76/// buffer can never be moved into the returned `Mat`. `arr` is consumed
77/// (and dropped) purely so callers don't have to hold onto it after this
78/// call; prefer [`array2_to_mat`] if you still need `arr` afterwards.
79pub fn array2_into_mat<T: Field + Clone>(arr: Array2<T>) -> Mat<T>
80where
81    T: bytemuck::Zeroable,
82{
83    array2_to_mat(&arr)
84}
85
86/// Converts an OxiBLAS Mat to an ndarray Array2.
87///
88/// Creates a column-major (Fortran order) Array2.
89pub fn mat_to_array2<T: Field + Clone>(mat: &Mat<T>) -> Array2<T> {
90    let (nrows, ncols) = mat.shape();
91    // Create in Fortran order for efficient conversion
92    Array2::from_shape_fn((nrows, ncols).f(), |(i, j)| mat[(i, j)])
93}
94
95/// Converts an OxiBLAS MatRef to an ndarray Array2.
96///
97/// Creates a column-major (Fortran order) Array2.
98pub fn mat_ref_to_array2<T: Field + Clone>(mat: MatRef<'_, T>) -> Array2<T> {
99    let (nrows, ncols) = (mat.nrows(), mat.ncols());
100    Array2::from_shape_fn((nrows, ncols).f(), |(i, j)| mat[(i, j)])
101}
102
103/// Converts an OxiBLAS Mat to a row-major ndarray Array2.
104pub fn mat_to_array2_c<T: Field + Clone>(mat: &Mat<T>) -> Array2<T> {
105    let (nrows, ncols) = mat.shape();
106    Array2::from_shape_fn((nrows, ncols), |(i, j)| mat[(i, j)])
107}
108
109// =============================================================================
110// ArrayD <-> Mat Conversions (Dynamic Dimension)
111// =============================================================================
112
113/// Converts an ndarray ArrayD (dynamic dimension) to an OxiBLAS Mat.
114///
115/// # Panics
116/// Panics if the array is not 2-dimensional.
117///
118/// # Example
119/// ```
120/// use ndarray::{ArrayD, IxDyn};
121/// use oxiblas_ndarray::conversions::arrayd_to_mat;
122///
123/// let arr = ArrayD::from_shape_fn(IxDyn(&[3, 4]), |idx| (idx[0] * 4 + idx[1]) as f64);
124/// let mat = arrayd_to_mat(&arr);
125/// assert_eq!(mat.shape(), (3, 4));
126/// ```
127pub fn arrayd_to_mat<T: Field + Clone>(arr: &ArrayD<T>) -> Mat<T>
128where
129    T: bytemuck::Zeroable,
130{
131    assert_eq!(
132        arr.ndim(),
133        2,
134        "ArrayD must be 2-dimensional for matrix conversion"
135    );
136    let shape = arr.shape();
137    let nrows = shape[0];
138    let ncols = shape[1];
139
140    let mut mat = Mat::zeros(nrows, ncols);
141    for i in 0..nrows {
142        for j in 0..ncols {
143            mat[(i, j)] = arr[[i, j].as_ref()];
144        }
145    }
146    mat
147}
148
149/// Converts an ndarray ArrayD to an OxiBLAS Mat, consuming the array.
150///
151/// # Panics
152/// Panics if the array is not 2-dimensional.
153pub fn arrayd_into_mat<T: Field + Clone>(arr: ArrayD<T>) -> Mat<T>
154where
155    T: bytemuck::Zeroable,
156{
157    arrayd_to_mat(&arr)
158}
159
160/// Converts an OxiBLAS Mat to an ndarray ArrayD.
161///
162/// Creates a column-major (Fortran order) ArrayD.
163pub fn mat_to_arrayd<T: Field + Clone>(mat: &Mat<T>) -> ArrayD<T> {
164    let (nrows, ncols) = mat.shape();
165    let mut arr = ArrayD::from_elem(IxDyn(&[nrows, ncols]), T::zero());
166    for i in 0..nrows {
167        for j in 0..ncols {
168            arr[[i, j].as_ref()] = mat[(i, j)];
169        }
170    }
171    arr
172}
173
174/// Converts an OxiBLAS MatRef to an ndarray ArrayD.
175pub fn mat_ref_to_arrayd<T: Field + Clone>(mat: MatRef<'_, T>) -> ArrayD<T> {
176    let (nrows, ncols) = (mat.nrows(), mat.ncols());
177    let mut arr = ArrayD::from_elem(IxDyn(&[nrows, ncols]), T::zero());
178    for i in 0..nrows {
179        for j in 0..ncols {
180            arr[[i, j].as_ref()] = mat[(i, j)];
181        }
182    }
183    arr
184}
185
186/// Converts an ArrayD to an Array2.
187///
188/// # Panics
189/// Panics if the array is not 2-dimensional.
190pub fn arrayd_to_array2<T: Clone>(arr: &ArrayD<T>) -> Array2<T> {
191    assert_eq!(arr.ndim(), 2, "ArrayD must be 2-dimensional");
192    let shape = arr.shape();
193    Array2::from_shape_fn((shape[0], shape[1]), |(i, j)| arr[[i, j].as_ref()].clone())
194}
195
196/// Converts an Array2 to an ArrayD.
197///
198/// Works correctly for empty arrays (`nrows == 0` and/or `ncols == 0`):
199/// this clones `arr` and reinterprets its dimension type via
200/// [`ArrayBase::into_dyn`](ndarray::ArrayBase::into_dyn), which never
201/// indexes into `arr`, so there is no "template element" that would be
202/// out of bounds on an empty array.
203pub fn array2_to_arrayd<T: Clone>(arr: &Array2<T>) -> ArrayD<T> {
204    arr.clone().into_dyn()
205}
206
207/// Converts a (possibly negative) `ndarray` stride into the non-negative
208/// stride representation that `MatRef`/`MatMut` require.
209///
210/// `oxiblas_matrix::MatRef`/`MatMut` store their stride as a plain `usize`
211/// and have no concept of a reversed/negative-stride axis. `ndarray`
212/// views produced by operations such as `.slice(s![..;-1, ..])` or
213/// `.invert_axis(...)` report a negative stride for the reversed axis;
214/// casting that directly with `as usize` would wrap around into an
215/// enormous positive value that later corrupts pointer arithmetic
216/// (`ptr.add(...)`) - safe code silently invoking undefined behavior.
217///
218/// Returns `None` for negative strides so callers can honestly report the
219/// layout as unsupported (falling back to a copying conversion) instead
220/// of ever performing that unchecked cast.
221#[inline]
222fn non_negative_stride(stride: isize) -> Option<usize> {
223    usize::try_from(stride).ok()
224}
225
226// =============================================================================
227// ArrayViewD -> MatRef Conversion
228// =============================================================================
229
230/// Creates a MatRef view from an ndarray ArrayViewD.
231///
232/// # Returns
233/// - `Some(MatRef)` if the array is 2D and in column-major order
234/// - `None` if the array is not 2D or layout is incompatible
235pub fn array_viewd_to_mat_ref<'a, T: Field>(arr: &'a ArrayViewD<'a, T>) -> Option<MatRef<'a, T>> {
236    if arr.ndim() != 2 {
237        return None;
238    }
239
240    let shape = arr.shape();
241    let nrows = shape[0];
242    let ncols = shape[1];
243    let strides = arr.strides();
244
245    // Check for column-major order: row stride = 1
246    if strides[0] == 1 {
247        let col_stride = non_negative_stride(strides[1])?;
248        let ptr = arr.as_ptr();
249        // SAFETY: ptr comes from arr.as_ptr() and is valid for the lifetime 'a
250        // borrowed from arr; nrows/ncols come from arr.shape() and col_stride is the
251        // verified non-negative column stride from arr.strides(), so the resulting
252        // MatRef addresses only elements within the source array's allocation.
253        Some(unsafe { MatRef::new(ptr, nrows, ncols, col_stride) })
254    } else {
255        None
256    }
257}
258
259/// Creates a MatRef view from an ndarray ArrayViewD, handling row-major layout.
260///
261/// # Returns
262/// - `Some((MatRef, false))` if the array is 2D and column-major
263/// - `Some((MatRef, true))` if the array is 2D and row-major (MatRef is transposed)
264/// - `None` if the array is not 2D or layout is incompatible
265pub fn array_viewd_to_mat_ref_or_transposed<'a, T: Field>(
266    arr: &'a ArrayViewD<'a, T>,
267) -> Option<(MatRef<'a, T>, bool)> {
268    if arr.ndim() != 2 {
269        return None;
270    }
271
272    let shape = arr.shape();
273    let nrows = shape[0];
274    let ncols = shape[1];
275    let strides = arr.strides();
276
277    if strides[0] == 1 {
278        // Column-major
279        let col_stride = non_negative_stride(strides[1])?;
280        let ptr = arr.as_ptr();
281        // SAFETY: ptr comes from arr.as_ptr() and is valid for the lifetime 'a
282        // borrowed from arr; nrows/ncols come from arr.shape() and col_stride is the
283        // verified non-negative column stride from arr.strides().
284        Some((unsafe { MatRef::new(ptr, nrows, ncols, col_stride) }, false))
285    } else if strides[1] == 1 {
286        // Row-major: treat as transposed column-major
287        let row_stride = non_negative_stride(strides[0])?;
288        let ptr = arr.as_ptr();
289        // SAFETY: ptr is valid for lifetime 'a borrowed from arr; ncols/nrows are
290        // swapped to describe the transposed view and row_stride is the verified
291        // non-negative stride for that transposed traversal.
292        Some((unsafe { MatRef::new(ptr, ncols, nrows, row_stride) }, true))
293    } else {
294        None
295    }
296}
297
298// =============================================================================
299// ArrayViewMutD -> MatMut Conversion
300// =============================================================================
301
302/// Creates a MatMut view from an ndarray ArrayViewMutD.
303///
304/// # Returns
305/// - `Some(MatMut)` if the array is 2D and in column-major order
306/// - `None` if the array is not 2D or layout is incompatible
307pub fn array_view_mutd_to_mat_mut<'a, T: Field>(
308    arr: &'a mut ArrayViewMutD<'a, T>,
309) -> Option<MatMut<'a, T>> {
310    if arr.ndim() != 2 {
311        return None;
312    }
313
314    let shape = arr.shape();
315    let nrows = shape[0];
316    let ncols = shape[1];
317    let strides = arr.strides();
318
319    if strides[0] == 1 {
320        let col_stride = non_negative_stride(strides[1])?;
321        let ptr = arr.as_mut_ptr();
322        // SAFETY: `strides[0] == 1` and `col_stride` is the (non-negative)
323        // column stride of a live `ArrayViewMut`, so ndarray guarantees every
324        // `(i, j)` with `i < nrows`, `j < ncols` maps to a distinct,
325        // initialized, aligned element inside the array's allocation; the
326        // exclusive borrow keeps it alive and unaliased for `'a`.
327        Some(unsafe { MatMut::new(ptr, nrows, ncols, col_stride) })
328    } else {
329        None
330    }
331}
332
333// =============================================================================
334// ArrayView2 -> MatRef Zero-Copy Conversion
335// =============================================================================
336
337/// Creates a MatRef view from an ndarray ArrayView2.
338///
339/// # Returns
340/// - `Some(MatRef)` if the array is in column-major (Fortran) order
341/// - `None` if the array layout is incompatible
342///
343/// # Safety
344/// The returned MatRef borrows from the ArrayView2.
345pub fn array_view_to_mat_ref<'a, T: Field>(arr: &'a ArrayView2<'a, T>) -> Option<MatRef<'a, T>> {
346    let (nrows, ncols) = arr.dim();
347    let strides = arr.strides();
348
349    // Check for column-major order: row stride = 1
350    if strides[0] == 1 {
351        let col_stride = non_negative_stride(strides[1])?;
352        let ptr = arr.as_ptr();
353        // SAFETY: ptr comes from arr.as_ptr() and is valid for the lifetime 'a
354        // borrowed from arr; nrows/ncols come from arr.dim() and col_stride is the
355        // verified non-negative column stride from arr.strides().
356        Some(unsafe { MatRef::new(ptr, nrows, ncols, col_stride) })
357    } else {
358        None
359    }
360}
361
362/// Creates a MatRef view from an ndarray ArrayView2, handling row-major layout
363/// by returning a transposed view if needed.
364///
365/// # Returns
366/// - `(MatRef, false)` if the array is column-major
367/// - `(MatRef, true)` if the array is row-major (MatRef is transposed)
368/// - `None` if the array layout is incompatible (non-contiguous)
369pub fn array_view_to_mat_ref_or_transposed<'a, T: Field>(
370    arr: &'a ArrayView2<'a, T>,
371) -> Option<(MatRef<'a, T>, bool)> {
372    let (nrows, ncols) = arr.dim();
373    let strides = arr.strides();
374
375    if strides[0] == 1 {
376        // Column-major
377        let col_stride = non_negative_stride(strides[1])?;
378        let ptr = arr.as_ptr();
379        // SAFETY: ptr comes from arr.as_ptr() and is valid for the lifetime 'a
380        // borrowed from arr; nrows/ncols come from arr.dim() and col_stride is the
381        // verified non-negative column stride from arr.strides().
382        Some((unsafe { MatRef::new(ptr, nrows, ncols, col_stride) }, false))
383    } else if strides[1] == 1 {
384        // Row-major: treat as transposed column-major
385        let row_stride = non_negative_stride(strides[0])?;
386        let ptr = arr.as_ptr();
387        // Return transposed dimensions
388        // SAFETY: ptr is valid for lifetime 'a borrowed from arr; ncols/nrows are
389        // swapped to describe the transposed view and row_stride is the verified
390        // non-negative stride for that transposed traversal.
391        Some((unsafe { MatRef::new(ptr, ncols, nrows, row_stride) }, true))
392    } else {
393        // Non-contiguous
394        None
395    }
396}
397
398// =============================================================================
399// ArrayViewMut2 -> MatMut Zero-Copy Conversion
400// =============================================================================
401
402/// Creates a MatMut view from an ndarray ArrayViewMut2.
403///
404/// # Returns
405/// - `Some(MatMut)` if the array is in column-major (Fortran) order
406/// - `None` if the array layout is incompatible
407pub fn array_view_mut_to_mat_mut<'a, T: Field>(
408    arr: &'a mut ArrayViewMut2<'a, T>,
409) -> Option<MatMut<'a, T>> {
410    let (nrows, ncols) = arr.dim();
411    let strides = arr.strides();
412
413    if strides[0] == 1 {
414        let col_stride = non_negative_stride(strides[1])?;
415        let ptr = arr.as_mut_ptr();
416        // SAFETY: `strides[0] == 1` and `col_stride` is the (non-negative)
417        // column stride of a live `ArrayViewMut`, so ndarray guarantees every
418        // `(i, j)` with `i < nrows`, `j < ncols` maps to a distinct,
419        // initialized, aligned element inside the array's allocation; the
420        // exclusive borrow keeps it alive and unaliased for `'a`.
421        Some(unsafe { MatMut::new(ptr, nrows, ncols, col_stride) })
422    } else {
423        None
424    }
425}
426
427// =============================================================================
428// 1D Array Conversions (for vectors)
429// =============================================================================
430
431/// Converts an ndarray Array1 to a Vec.
432pub fn array1_to_vec<T: Clone>(arr: &Array1<T>) -> Vec<T> {
433    arr.iter().cloned().collect()
434}
435
436/// Converts a slice to an ndarray Array1.
437pub fn slice_to_array1<T: Clone>(slice: &[T]) -> Array1<T> {
438    Array1::from_vec(slice.to_vec())
439}
440
441/// Gets a slice from an ArrayView1 if contiguous.
442pub fn array_view1_as_slice<'a, T>(arr: &'a ArrayView1<'a, T>) -> Option<&'a [T]> {
443    arr.as_slice()
444}
445
446/// Gets a mutable slice from an ArrayViewMut1 if contiguous.
447pub fn array_view1_as_slice_mut<'a, T>(arr: &'a mut ArrayViewMut1<'a, T>) -> Option<&'a mut [T]> {
448    arr.as_slice_mut()
449}
450
451// =============================================================================
452// Helper Functions
453// =============================================================================
454
455/// Creates a column-major Array2 (Fortran order).
456///
457/// This is the preferred layout for OxiBLAS operations as it allows
458/// zero-copy conversions.
459pub fn zeros_f<T: Clone + Default>(nrows: usize, ncols: usize) -> Array2<T> {
460    Array2::from_shape_fn((nrows, ncols).f(), |_| T::default())
461}
462
463/// Creates a column-major Array2 filled with a value.
464pub fn filled_f<T: Clone>(nrows: usize, ncols: usize, value: T) -> Array2<T> {
465    Array2::from_shape_fn((nrows, ncols).f(), |_| value.clone())
466}
467
468/// Checks if an Array2 is in column-major (Fortran) order.
469pub fn is_column_major<T>(arr: &Array2<T>) -> bool {
470    let strides = arr.strides();
471    let (nrows, _) = arr.dim();
472    strides[0] == 1 && strides[1] == nrows as isize
473}
474
475/// Checks if an Array2 is in row-major (C) order.
476pub fn is_row_major<T>(arr: &Array2<T>) -> bool {
477    let strides = arr.strides();
478    let (_, ncols) = arr.dim();
479    strides[0] == ncols as isize && strides[1] == 1
480}
481
482/// Converts a row-major Array2 to column-major.
483pub fn to_column_major<T: Clone + Default>(arr: &Array2<T>) -> Array2<T> {
484    let (nrows, ncols) = arr.dim();
485    let mut result = zeros_f(nrows, ncols);
486    for i in 0..nrows {
487        for j in 0..ncols {
488            result[[i, j]] = arr[[i, j]].clone();
489        }
490    }
491    result
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497    use ndarray::Array2;
498
499    #[test]
500    fn test_array2_to_mat_rowmajor() {
501        let arr = Array2::from_shape_fn((3, 4), |(i, j)| (i * 4 + j) as f64);
502        let mat = array2_to_mat(&arr);
503
504        assert_eq!(mat.shape(), (3, 4));
505        for i in 0..3 {
506            for j in 0..4 {
507                assert_eq!(mat[(i, j)], arr[[i, j]]);
508            }
509        }
510    }
511
512    #[test]
513    fn test_array2_to_mat_colmajor() {
514        let arr: Array2<f64> = Array2::from_shape_fn((3, 4).f(), |(i, j)| (i * 4 + j) as f64);
515        assert!(is_column_major(&arr));
516
517        let mat = array2_to_mat(&arr);
518        assert_eq!(mat.shape(), (3, 4));
519        for i in 0..3 {
520            for j in 0..4 {
521                assert_eq!(mat[(i, j)], arr[[i, j]]);
522            }
523        }
524    }
525
526    #[test]
527    fn test_mat_to_array2() {
528        let mat: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]]);
529        let arr = mat_to_array2(&mat);
530
531        assert_eq!(arr.dim(), (2, 3));
532        assert_eq!(arr[[0, 0]], 1.0);
533        assert_eq!(arr[[1, 2]], 6.0);
534    }
535
536    #[test]
537    fn test_roundtrip() {
538        let original = Array2::from_shape_fn((5, 7), |(i, j)| (i * 7 + j) as f64);
539        let mat = array2_to_mat(&original);
540        let recovered = mat_to_array2(&mat);
541
542        for i in 0..5 {
543            for j in 0..7 {
544                assert!((original[[i, j]] - recovered[[i, j]]).abs() < 1e-15);
545            }
546        }
547    }
548
549    #[test]
550    fn test_is_column_major() {
551        let col_major: Array2<f64> = Array2::zeros((3, 4).f());
552        let row_major: Array2<f64> = Array2::zeros((3, 4));
553
554        assert!(is_column_major(&col_major));
555        assert!(!is_column_major(&row_major));
556        assert!(is_row_major(&row_major));
557        assert!(!is_row_major(&col_major));
558    }
559
560    #[test]
561    fn test_to_column_major() {
562        let row_major = Array2::from_shape_fn((3, 4), |(i, j)| (i * 4 + j) as f64);
563        let col_major = to_column_major(&row_major);
564
565        assert!(is_column_major(&col_major));
566        for i in 0..3 {
567            for j in 0..4 {
568                assert_eq!(row_major[[i, j]], col_major[[i, j]]);
569            }
570        }
571    }
572
573    #[test]
574    fn test_array_view_to_mat_ref_or_transposed() {
575        // Column-major
576        let col_major: Array2<f64> = Array2::from_shape_fn((3, 4).f(), |(i, j)| (i * 4 + j) as f64);
577        let view = col_major.view();
578        let (mat_ref, transposed) = array_view_to_mat_ref_or_transposed(&view).unwrap();
579        assert!(!transposed);
580        assert_eq!(mat_ref.shape(), (3, 4));
581
582        // Row-major
583        let row_major: Array2<f64> = Array2::from_shape_fn((3, 4), |(i, j)| (i * 4 + j) as f64);
584        let view = row_major.view();
585        let (mat_ref, transposed) = array_view_to_mat_ref_or_transposed(&view).unwrap();
586        assert!(transposed);
587        // Transposed: original is 3x4, so MatRef should be 4x3
588        assert_eq!(mat_ref.shape(), (4, 3));
589    }
590
591    // =========================================================================
592    // ArrayD (Dynamic Dimension) Tests
593    // =========================================================================
594
595    #[test]
596    fn test_arrayd_to_mat() {
597        let arr = ArrayD::from_shape_fn(IxDyn(&[3, 4]), |idx| (idx[0] * 4 + idx[1]) as f64);
598        let mat = arrayd_to_mat(&arr);
599
600        assert_eq!(mat.shape(), (3, 4));
601        for i in 0..3 {
602            for j in 0..4 {
603                assert_eq!(mat[(i, j)], arr[[i, j].as_ref()]);
604            }
605        }
606    }
607
608    #[test]
609    fn test_mat_to_arrayd() {
610        let mat: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]]);
611        let arr = mat_to_arrayd(&mat);
612
613        assert_eq!(arr.ndim(), 2);
614        assert_eq!(arr.shape(), &[2, 3]);
615        assert_eq!(arr[[0, 0].as_ref()], 1.0);
616        assert_eq!(arr[[1, 2].as_ref()], 6.0);
617    }
618
619    #[test]
620    fn test_arrayd_roundtrip() {
621        let original = ArrayD::from_shape_fn(IxDyn(&[5, 7]), |idx| (idx[0] * 7 + idx[1]) as f64);
622        let mat = arrayd_to_mat(&original);
623        let recovered = mat_to_arrayd(&mat);
624
625        assert_eq!(recovered.shape(), original.shape());
626        for i in 0..5 {
627            for j in 0..7 {
628                assert!((original[[i, j].as_ref()] - recovered[[i, j].as_ref()]).abs() < 1e-15);
629            }
630        }
631    }
632
633    #[test]
634    fn test_arrayd_to_array2() {
635        let arr_d = ArrayD::from_shape_fn(IxDyn(&[3, 4]), |idx| (idx[0] * 4 + idx[1]) as f64);
636        let arr_2 = arrayd_to_array2(&arr_d);
637
638        assert_eq!(arr_2.dim(), (3, 4));
639        for i in 0..3 {
640            for j in 0..4 {
641                assert_eq!(arr_2[[i, j]], arr_d[[i, j].as_ref()]);
642            }
643        }
644    }
645
646    #[test]
647    fn test_array2_to_arrayd() {
648        let arr_2: Array2<f64> = Array2::from_shape_fn((3, 4), |(i, j)| (i * 4 + j) as f64);
649        let arr_d = array2_to_arrayd(&arr_2);
650
651        assert_eq!(arr_d.ndim(), 2);
652        assert_eq!(arr_d.shape(), &[3, 4]);
653        for i in 0..3 {
654            for j in 0..4 {
655                assert_eq!(arr_d[[i, j].as_ref()], arr_2[[i, j]]);
656            }
657        }
658    }
659
660    #[test]
661    fn test_array2_to_arrayd_empty_does_not_panic() {
662        // Regression test: array2_to_arrayd used to index arr[[0, 0]] as a
663        // "template" element before checking for emptiness, which panicked
664        // on any Array2 with a zero dimension.
665        let empty_rows: Array2<f64> = Array2::from_shape_fn((0, 4), |_| 0.0);
666        let arr_d = array2_to_arrayd(&empty_rows);
667        assert_eq!(arr_d.shape(), &[0, 4]);
668
669        let empty_cols: Array2<f64> = Array2::from_shape_fn((3, 0), |_| 0.0);
670        let arr_d = array2_to_arrayd(&empty_cols);
671        assert_eq!(arr_d.shape(), &[3, 0]);
672
673        let empty_both: Array2<f64> = Array2::from_shape_fn((0, 0), |_| 0.0);
674        let arr_d = array2_to_arrayd(&empty_both);
675        assert_eq!(arr_d.shape(), &[0, 0]);
676    }
677
678    #[test]
679    fn test_array_viewd_to_mat_ref() {
680        let arr = ArrayD::from_shape_fn(IxDyn(&[3, 4]), |idx| (idx[0] * 4 + idx[1]) as f64);
681        let view = arr.view();
682
683        // Default ndarray layout is C-order (row-major), so column-major view should fail
684        // unless we specifically create it that way
685        let result = array_viewd_to_mat_ref(&view);
686        // Row-major, so this should return None
687        assert!(result.is_none());
688    }
689
690    #[test]
691    fn test_array_viewd_to_mat_ref_or_transposed() {
692        // Row-major ArrayD
693        let arr = ArrayD::from_shape_fn(IxDyn(&[3, 4]), |idx| (idx[0] * 4 + idx[1]) as f64);
694        let view = arr.view();
695
696        let result = array_viewd_to_mat_ref_or_transposed(&view);
697        assert!(result.is_some());
698        let (mat_ref, transposed) = result.unwrap();
699        assert!(transposed); // Row-major should be transposed
700        assert_eq!(mat_ref.shape(), (4, 3)); // Transposed dimensions
701    }
702
703    #[test]
704    #[should_panic(expected = "2-dimensional")]
705    fn test_arrayd_to_mat_wrong_dim() {
706        let arr = ArrayD::from_shape_fn(IxDyn(&[2, 3, 4]), |idx| idx[0] as f64);
707        let _ = arrayd_to_mat(&arr);
708    }
709
710    #[test]
711    fn test_array_viewd_wrong_dim() {
712        // 3D array
713        let arr = ArrayD::from_shape_fn(IxDyn(&[2, 3, 4]), |idx| idx[0] as f64);
714        let view = arr.view();
715
716        // Should return None for non-2D arrays
717        assert!(array_viewd_to_mat_ref(&view).is_none());
718        assert!(array_viewd_to_mat_ref_or_transposed(&view).is_none());
719    }
720
721    // =========================================================================
722    // Negative-stride rejection tests (see `non_negative_stride`)
723    // =========================================================================
724    //
725    // Regression tests: the view-based *-to-mat_ref/mat_mut conversions used
726    // to cast a possibly-negative ndarray stride to `usize` unchecked. For a
727    // reversed-axis view that cast silently wraps around into an enormous
728    // bogus stride, which is safe-code UB the moment it feeds pointer
729    // arithmetic. They must now report such layouts as `None` instead.
730
731    #[test]
732    fn test_array_view_to_mat_ref_rejects_negative_stride() {
733        use ndarray::s;
734
735        // Column-major 3x4 array: strides = [1, 3]. Reversing the column
736        // axis flips strides[1] negative while strides[0] stays 1.
737        let col_major: Array2<f64> = Array2::from_shape_fn((3, 4).f(), |(i, j)| (i * 4 + j) as f64);
738        let reversed = col_major.slice(s![.., ..;-1]);
739        assert_eq!(reversed.strides()[0], 1);
740        assert!(reversed.strides()[1] < 0);
741
742        assert!(array_view_to_mat_ref(&reversed).is_none());
743        assert!(array_view_to_mat_ref_or_transposed(&reversed).is_none());
744    }
745
746    #[test]
747    fn test_array_view_mut_to_mat_mut_rejects_negative_stride() {
748        use ndarray::s;
749
750        let mut col_major: Array2<f64> =
751            Array2::from_shape_fn((3, 4).f(), |(i, j)| (i * 4 + j) as f64);
752        let mut reversed = col_major.slice_mut(s![.., ..;-1]);
753        assert_eq!(reversed.strides()[0], 1);
754        assert!(reversed.strides()[1] < 0);
755
756        assert!(array_view_mut_to_mat_mut(&mut reversed).is_none());
757    }
758
759    #[test]
760    fn test_array_viewd_to_mat_ref_rejects_negative_stride() {
761        use ndarray::s;
762
763        // Build the negative-stride view on a statically 2D array first
764        // (so the `s![]` macro produces a fixed Ix2 output dimension), then
765        // erase the dimension to IxDyn while preserving the strides/layout.
766        let col_major: Array2<f64> = Array2::from_shape_fn((3, 4).f(), |(i, j)| (i * 4 + j) as f64);
767        let reversed = col_major.slice(s![.., ..;-1]).into_dyn();
768        assert_eq!(reversed.strides()[0], 1);
769        assert!(reversed.strides()[1] < 0);
770
771        assert!(array_viewd_to_mat_ref(&reversed).is_none());
772        assert!(array_viewd_to_mat_ref_or_transposed(&reversed).is_none());
773    }
774}