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}