Skip to main content

minarrow/structs/views/
ndarray_view.rs

1//! # **NdArrayV** - *Zero-copy view into an NdArray*
2//!
3//! Holds a clone of the parent `NdArray`, kept alive through the array's
4//! shared internal buffer, plus its own offset and dimension metadata.
5//! Views can have different shapes and strides from the parent, enabling
6//! slicing, axis selection, transposition, and axis permutation without
7//! copying data.
8
9use std::fmt;
10use std::fmt::{Display, Formatter};
11use std::ops::Index;
12
13use crate::enums::error::MinarrowError;
14use crate::enums::shape_dim::ShapeDim;
15use crate::structs::ndarray::{NdArray, NdArrayIter, NdDims, offset_of_impl};
16use crate::traits::print::print_ndarray_body;
17#[cfg(feature = "select")]
18use crate::structs::ndarray::gather_obs_impl;
19#[cfg(feature = "select")]
20use crate::traits::selection::{AxisSelection, DataSelector, RowSelection};
21#[cfg(feature = "select")]
22use std::ops::Range;
23#[cfg(feature = "dlpack")]
24use crate::ffi::dlpack::{
25    export_view_to_dlpack, export_view_to_dlpack_versioned, DLPackTensor, DLPackTensorVersioned,
26};
27use crate::traits::shape::Shape;
28use crate::traits::type_unions::Float;
29use crate::Vec64;
30
31#[cfg(feature = "matrix")]
32use crate::structs::matrix::Matrix;
33
34/// Zero-copy view into an [`NdArray`].
35///
36/// Holds a clone of the parent `NdArray`, kept alive through the array's
37/// shared internal buffer, with its own offset and dimension metadata.
38/// This enables slicing, axis selection, transposition, and axis
39/// permutation without copying the underlying data.
40#[derive(Clone)]
41pub struct NdArrayV<T> {
42    pub(crate) source: NdArray<T>,
43    pub(crate) offset: usize,
44    pub(crate) dims: NdDims,
45}
46
47impl<T: Float> NdArrayV<T> {
48    /// Create a view over an NdArray with the given offset and dimensions.
49    /// Panics when the rank metadata or reachable buffer span is invalid.
50    pub fn new(source: NdArray<T>, offset: usize, shape: &[usize], strides: &[usize]) -> Self {
51        Self::try_new(source, offset, shape, strides)
52            .unwrap_or_else(|e| panic!("NdArrayV::new: {}", e))
53    }
54
55    /// Checked view construction over an NdArray.
56    ///
57    /// Validates rank metadata, arithmetic overflow, and that every logical
58    /// element reachable through `shape` and `strides` lies in the source
59    /// buffer. A shape with zero axes (`shape == &[]`) denotes one scalar
60    /// value. A shape containing a zero-length axis, such as `&[0]`, denotes
61    /// no values and may use an offset one past the buffer end.
62    pub fn try_new(
63        source: NdArray<T>,
64        offset: usize,
65        shape: &[usize],
66        strides: &[usize],
67    ) -> Result<Self, MinarrowError> {
68        if shape.len() != strides.len() {
69            return Err(MinarrowError::ShapeError {
70                message: format!(
71                    "view shape rank {} does not match strides rank {}",
72                    shape.len(), strides.len()
73                ),
74            });
75        }
76
77        let span = if shape.contains(&0) {
78            0
79        } else {
80            let max_offset = shape.iter().zip(strides.iter()).try_fold(
81                0usize,
82                |acc, (&dim, &stride)| {
83                    (dim - 1)
84                        .checked_mul(stride)
85                        .and_then(|extent| acc.checked_add(extent))
86                },
87            ).ok_or_else(|| MinarrowError::ShapeError {
88                message: format!("view shape {:?} with strides {:?} overflows", shape, strides),
89            })?;
90            max_offset.checked_add(1).ok_or_else(|| MinarrowError::ShapeError {
91                message: format!("view shape {:?} with strides {:?} overflows", shape, strides),
92            })?
93        };
94        let end = offset.checked_add(span).ok_or_else(|| MinarrowError::ShapeError {
95            message: format!("view offset {} plus span {} overflows", offset, span),
96        })?;
97        let buffer_len = source.data.as_slice().len();
98        if end > buffer_len || (span == 0 && offset > buffer_len) {
99            return Err(MinarrowError::IndexError(format!(
100                "view span [{}, {}) exceeds source buffer length {}",
101                offset, end, buffer_len
102            )));
103        }
104
105        Ok(NdArrayV {
106            source,
107            offset,
108            dims: NdDims::from_shape_and_strides(shape, strides),
109        })
110    }
111
112    /// Create a full view over an NdArray with the same shape and strides.
113    pub fn from_ndarray(source: NdArray<T>) -> Self {
114        let dims = source.dims.clone();
115        NdArrayV { source, offset: 0, dims }
116    }
117
118    /// Number of dimensions.
119    #[inline]
120    pub fn ndim(&self) -> usize { self.dims.ndim() }
121
122    /// Shape as a slice.
123    #[inline]
124    pub fn shape(&self) -> &[usize] { self.dims.shape() }
125
126    /// Strides as a slice.
127    #[inline]
128    pub fn strides(&self) -> &[usize] { self.dims.strides() }
129
130    /// Total logical element count.
131    #[inline]
132    pub fn len(&self) -> usize { self.dims.len() }
133
134    /// True if empty.
135    #[inline]
136    pub fn is_empty(&self) -> bool { self.len() == 0 }
137
138    /// Get element by N-dimensional index.
139    #[inline]
140    pub fn get(&self, indices: &[usize]) -> T {
141        let off = self.offset_of(indices);
142        self.source.data.as_slice()[off]
143    }
144
145    /// Like `get`, but skips bounds checks.
146    ///
147    /// # Safety
148    /// The caller guarantees each index is within its dimension.
149    #[inline(always)]
150    pub unsafe fn get_unchecked(&self, indices: &[usize]) -> T {
151        let strides = self.dims.strides();
152        let mut off = self.offset;
153        for d in 0..indices.len() {
154            off += indices[d] * strides[d];
155        }
156        // SAFETY: in-bounds indices produce an in-bounds flat offset.
157        unsafe { *self.source.data.as_slice().get_unchecked(off) }
158    }
159
160    /// Compute flat offset.
161    #[inline]
162    fn offset_of(&self, indices: &[usize]) -> usize {
163        self.offset + offset_of_impl(indices, self.dims.shape(), self.dims.strides())
164    }
165
166    /// Immutable column slice for 2D views.
167    /// Panics if ndim != 2, the column index is out of bounds, or axis 0
168    /// is not unit-stride i.e. column elements are not contiguous, as after
169    /// `transpose`, `permute_axes`, or `swap_axes`. Materialise with
170    /// `to_ndarray()` first for those.
171    #[inline]
172    pub fn col(&self, col: usize) -> &[T] {
173        let shape = self.dims.shape();
174        assert_eq!(shape.len(), 2, "col() requires a 2D view");
175        assert!(col < shape[1], "Column index out of bounds");
176        assert_eq!(
177            self.dims.strides()[0], 1,
178            "col() requires unit stride on axis 0; materialise with to_ndarray() first"
179        );
180        let stride = self.dims.strides()[1];
181        let start = self.offset + col * stride;
182        &self.source.data.as_slice()[start..start + shape[0]]
183    }
184
185    /// All columns as slices. 2D only.
186    /// Panics if ndim != 2 or axis 0 is not unit-stride.
187    pub fn columns(&self) -> Vec<&[T]> {
188        let shape = self.dims.shape();
189        assert_eq!(shape.len(), 2, "columns() requires a 2D view");
190        assert_eq!(
191            self.dims.strides()[0], 1,
192            "columns() requires unit stride on axis 0; materialise with to_ndarray() first"
193        );
194        let stride = self.dims.strides()[1];
195        let n_rows = shape[0];
196        let buf = self.source.data.as_slice();
197        (0..shape[1])
198            .map(|c| &buf[self.offset + c * stride..self.offset + c * stride + n_rows])
199            .collect()
200    }
201
202    // *** BLAS/LAPACK compatibility (2D) **************************
203
204    /// BLAS row count. 2D only.
205    #[inline]
206    pub fn m(&self) -> i32 {
207        assert_eq!(self.ndim(), 2, "m() requires a 2D view");
208        self.dims.shape()[0] as i32
209    }
210
211    /// BLAS column count. 2D only.
212    #[inline]
213    pub fn n(&self) -> i32 {
214        assert_eq!(self.ndim(), 2, "n() requires a 2D view");
215        self.dims.shape()[1] as i32
216    }
217
218    /// BLAS leading dimension. 2D only.
219    /// Panics unless axis 0 is unit-stride - BLAS requires column
220    /// elements to be contiguous, which a transposed or permuted view
221    /// does not satisfy. Materialise with `to_ndarray()` first.
222    #[inline]
223    pub fn lda(&self) -> i32 {
224        assert_eq!(self.ndim(), 2, "lda() requires a 2D view");
225        assert_eq!(
226            self.dims.strides()[0], 1,
227            "lda() requires unit stride on axis 0; materialise with to_ndarray() first"
228        );
229        self.dims.strides()[1] as i32
230    }
231
232    // *** Slicing *************************************************
233
234    /// Zero-copy view of a single observation (axis-0 element).
235    ///
236    /// Returns an (N-1)-dimensional view. For a 2D view with shape
237    /// `[n, m]`, returns a 1D view of shape `[m]`. For 3D `[n, m, k]`,
238    /// returns 2D `[m, k]`. Requires rank 2 or higher - a 1D view has no
239    /// sub-array observations, so scalar access goes through `get(&[i])`.
240    pub fn obs(&self, idx: usize) -> NdArrayV<T> {
241        let shape = self.dims.shape();
242        let strides = self.dims.strides();
243        assert!(
244            shape.len() >= 2,
245            "obs() requires a 2D or higher view, use get(&[i]) for scalar access on 1D"
246        );
247        assert!(idx < shape[0], "obs: index {} out of bounds for axis 0 (size {})", idx, shape[0]);
248
249        let new_offset = self.offset + idx * strides[0];
250        NdArrayV::new(self.source.clone(), new_offset, &shape[1..], &strides[1..])
251    }
252
253    /// Slice this view, producing a sub-view. Each axis takes any
254    /// [`DataSelector`] - a single index collapses that dimension, and a
255    /// contiguous range keeps it. Zero-copy - shares the same backing
256    /// buffer, just adjusts offset and dims.
257    #[cfg(feature = "select")]
258    pub fn slice(&self, selection: &[&dyn DataSelector]) -> NdArrayV<T> {
259        let shape = self.dims.shape();
260        let strides = self.dims.strides();
261        assert_eq!(
262            selection.len(), shape.len(),
263            "slice(): expected {} axes, got {}", shape.len(), selection.len()
264        );
265
266        let mut new_offset = self.offset;
267        let mut new_shape = Vec::with_capacity(shape.len());
268        let mut new_strides = Vec::with_capacity(shape.len());
269
270        for (d, sel) in selection.iter().enumerate() {
271            let (start, end, collapse) = sel.resolve_axis(shape[d]);
272            assert!(
273                end <= shape[d],
274                "slice(): end {} out of bounds for axis {} (size {})", end, d, shape[d]
275            );
276            new_offset += start * strides[d];
277            if !collapse {
278                new_shape.push(end - start);
279                new_strides.push(strides[d]);
280            }
281        }
282
283        NdArrayV::new(self.source.clone(), new_offset, &new_shape, &new_strides)
284    }
285
286    // *** Axis manipulation ***************************************
287
288    /// Transposed view with the axis order reversed. Zero-copy - only
289    /// the shape and stride metadata reorder. A 1D view returns itself
290    /// unchanged.
291    pub fn transpose(&self) -> NdArrayV<T> {
292        let shape: Vec<usize> = self.dims.shape().iter().rev().copied().collect();
293        let strides: Vec<usize> = self.dims.strides().iter().rev().copied().collect();
294        NdArrayV::new(self.source.clone(), self.offset, &shape, &strides)
295    }
296
297    /// View with axes reordered by the given permutation. Zero-copy.
298    ///
299    /// `perm[d]` names the source axis that becomes axis `d` of the
300    /// result. Panics unless `perm` is a permutation of `0..ndim`.
301    pub fn permute_axes(&self, perm: &[usize]) -> NdArrayV<T> {
302        let shape = self.dims.shape();
303        let strides = self.dims.strides();
304        let ndim = shape.len();
305        assert_eq!(
306            perm.len(), ndim,
307            "permute_axes: expected {} axes, got {}", ndim, perm.len()
308        );
309        let mut seen = vec![false; ndim];
310        for &ax in perm {
311            assert!(ax < ndim, "permute_axes: axis {} out of bounds for {}D view", ax, ndim);
312            assert!(!seen[ax], "permute_axes: axis {} repeated", ax);
313            seen[ax] = true;
314        }
315        let new_shape: Vec<usize> = perm.iter().map(|&ax| shape[ax]).collect();
316        let new_strides: Vec<usize> = perm.iter().map(|&ax| strides[ax]).collect();
317        NdArrayV::new(self.source.clone(), self.offset, &new_shape, &new_strides)
318    }
319
320    /// View with two axes swapped. Zero-copy.
321    pub fn swap_axes(&self, a: usize, b: usize) -> NdArrayV<T> {
322        let ndim = self.dims.ndim();
323        assert!(
324            a < ndim && b < ndim,
325            "swap_axes: axes ({}, {}) out of bounds for {}D view", a, b, ndim
326        );
327        let mut shape = self.dims.shape().to_vec();
328        let mut strides = self.dims.strides().to_vec();
329        shape.swap(a, b);
330        strides.swap(a, b);
331        NdArrayV::new(self.source.clone(), self.offset, &shape, &strides)
332    }
333
334    // *** Conversions *********************************************
335
336    /// Export this view as a legacy DLPack tensor without copying. The
337    /// window offset carries through DLPack's `byte_offset` field, and
338    /// the view strides carry as element strides.
339    #[cfg(feature = "dlpack")]
340    pub fn to_dlpack(self) -> DLPackTensor {
341        export_view_to_dlpack(self)
342    }
343
344    /// Export this view as a DLPack 1.x versioned tensor without
345    /// copying. The read-only flag is set whenever another reference to
346    /// the source buffer exists.
347    #[cfg(feature = "dlpack")]
348    pub fn to_dlpack_versioned(self) -> DLPackTensorVersioned {
349        export_view_to_dlpack_versioned(self)
350    }
351
352    // *** Materialisation *****************************************
353
354    /// Materialise this view as an owned NdArray.
355    pub fn to_ndarray(&self) -> NdArray<T> {
356        let flat: Vec64<T> = self.into_iter().collect();
357        let mut arr = NdArray::from_slice(&flat, self.dims.shape());
358        arr.name = self.source.name.clone();
359        arr
360    }
361
362    // *** Parallel iteration (rayon) ******************************
363
364    /// Parallel iterator over axis-0 observations. Each item is the
365    /// observation index and a zero-copy `NdArrayV` view.
366    #[cfg(feature = "parallel_proc")]
367    pub fn par_iter_obs(&self) -> impl rayon::iter::ParallelIterator<Item = (usize, NdArrayV<T>)> + '_
368    where
369        T: Send + Sync,
370    {
371        use rayon::prelude::*;
372        assert!(self.ndim() >= 2, "par_iter_obs() requires a 2D or higher view");
373        let n_obs = self.dims.shape()[0];
374        (0..n_obs).into_par_iter().map(move |i| (i, self.obs(i)))
375    }
376
377    /// Iterate one logical axis-0 run identified by its flattened outer
378    /// index. This composes the logical iterator for SuperNdArrayV without
379    /// materialising its slices.
380    pub(crate) fn iter_axis0_run(&self, run_idx: usize) -> impl ExactSizeIterator<Item = T> + '_ {
381        assert!(self.ndim() > 0, "axis-0 iteration requires an axis 0");
382        let n_runs: usize = self.shape()[1..].iter().product();
383        assert!(run_idx < n_runs, "axis-0 run {} out of bounds ({})", run_idx, n_runs);
384
385        let mut rem = run_idx;
386        let mut offset = self.offset;
387        for d in 1..self.ndim() {
388            offset += (rem % self.shape()[d]) * self.strides()[d];
389            rem /= self.shape()[d];
390        }
391        let stride = self.strides()[0];
392        (0..self.shape()[0]).map(move |i| self.source.data.as_slice()[offset + i * stride])
393    }
394}
395
396/// Materialise a 2D view as a Matrix.
397#[cfg(feature = "matrix")]
398impl NdArrayV<f64> {
399    /// Materialise a 2D view as a Matrix.
400    pub fn to_matrix(&self) -> Result<Matrix, MinarrowError> {
401        self.to_ndarray().to_matrix()
402    }
403}
404
405impl<T: Float> NdArrayV<T> {
406    /// Apply a function to every logical element, materialising a new
407    /// compact [`NdArray`] with this view's shape and the parent's name.
408    pub fn apply(&self, f: impl Fn(T) -> T) -> NdArray<T> {
409        let flat: Vec64<T> = self.into_iter().map(f).collect();
410        let mut result = NdArray::from_slice(&flat, self.dims.shape());
411        result.name = self.source.name.clone();
412        result
413    }
414}
415
416// *** Axis selection: view.s(nd![1..4, 2]) ************************
417
418/// Selection across every axis at once, delegating to `slice`. Single
419/// indices collapse their dimension, and contiguous ranges keep it.
420/// Zero-copy.
421#[cfg(feature = "select")]
422impl<T: Float> AxisSelection for NdArrayV<T> {
423    type View = NdArrayV<T>;
424
425    fn s(&self, selection: &[&dyn DataSelector]) -> NdArrayV<T> {
426        self.slice(selection)
427    }
428
429    fn get_axis_count(&self) -> usize {
430        self.ndim()
431    }
432}
433
434// *** Row selection: view.r(0..10) ********************************
435
436/// Axis-0 observation selection over a view. Contiguous ranges narrow
437/// the window zero-copy. Index arrays gather the selected observations
438/// into an owned array wrapped in a full view.
439#[cfg(feature = "select")]
440impl<T: Float> RowSelection for NdArrayV<T> {
441    type View = NdArrayV<T>;
442
443    fn r<S: DataSelector>(&self, selection: S) -> NdArrayV<T> {
444        assert!(self.ndim() > 0, "row selection requires an axis 0");
445        let n_obs = self.dims.shape()[0];
446        let indices = selection.resolve_indices(n_obs);
447        if selection.is_contiguous() {
448            let start = indices.first().copied().unwrap_or(0);
449            let ranges: Vec<Range<usize>> = std::iter::once(start..start + indices.len())
450                .chain(self.dims.shape()[1..].iter().map(|&n| 0..n))
451                .collect();
452            let refs: Vec<&dyn DataSelector> = ranges.iter().map(|r| r as _).collect();
453            return self.slice(&refs);
454        }
455        NdArrayV::from_ndarray(gather_obs_impl(
456            &indices,
457            self.dims.shape(),
458            self.source.name.clone(),
459            |idx| self.get(idx),
460        ))
461    }
462
463    fn get_row_count(&self) -> usize {
464        assert!(self.ndim() > 0, "row count requires an axis 0");
465        self.dims.shape()[0]
466    }
467}
468
469// *** IntoIterator ************************************************
470
471/// Iterating a view works the same as iterating an NdArray: contiguous
472/// runs along axis 0, cache-friendly, no per-element offset arithmetic.
473impl<'a, T: Float> IntoIterator for &'a NdArrayV<T> {
474    type Item = T;
475    type IntoIter = NdArrayIter<'a, T>;
476
477    fn into_iter(self) -> NdArrayIter<'a, T> {
478        let shape = self.dims.shape();
479        let strides = self.dims.strides();
480        if shape.is_empty() {
481            return NdArrayIter {
482                buf: self.source.data.as_slice(),
483                n_inner: 1,
484                inner_stride: 1,
485                run_offsets: vec![self.offset],
486                run_idx: 0,
487                inner_idx: 0,
488                total: 1,
489                yielded: 0,
490            };
491        }
492        let n_inner = shape[0];
493        let n_runs: usize = shape[1..].iter().product();
494
495        let mut run_offsets = Vec::with_capacity(n_runs);
496        if shape.len() <= 1 {
497            run_offsets.push(self.offset);
498        } else {
499            let outer_shape = &shape[1..];
500            let outer_strides = &strides[1..];
501            let mut outer_indices = vec![0usize; outer_shape.len()];
502            for _ in 0..n_runs {
503                let off: usize = self.offset + outer_indices.iter()
504                    .zip(outer_strides.iter())
505                    .map(|(&i, &s)| i * s)
506                    .sum::<usize>();
507                run_offsets.push(off);
508                let mut carry = true;
509                for d in 0..outer_shape.len() {
510                    if carry {
511                        outer_indices[d] += 1;
512                        if outer_indices[d] < outer_shape[d] {
513                            carry = false;
514                        } else {
515                            outer_indices[d] = 0;
516                        }
517                    }
518                }
519            }
520        }
521
522        NdArrayIter {
523            buf: self.source.data.as_slice(),
524            n_inner,
525            inner_stride: strides[0],
526            run_offsets,
527            run_idx: 0,
528            inner_idx: 0,
529            total: self.len(),
530            yielded: 0,
531        }
532    }
533}
534
535// *** Trait implementations ***************************************
536
537impl<T: Float> Shape for NdArrayV<T> {
538    fn shape(&self) -> ShapeDim {
539        match self.dims.ndim() {
540            0 => ShapeDim::Rank0(1),
541            1 => ShapeDim::Rank1(self.dims.shape()[0]),
542            2 => ShapeDim::Rank2 {
543                rows: self.dims.shape()[0],
544                cols: self.dims.shape()[1],
545            },
546            _ => ShapeDim::RankN(self.dims.shape().to_vec()),
547        }
548    }
549}
550
551impl<T: Float> PartialEq for NdArrayV<T> {
552    fn eq(&self, other: &Self) -> bool {
553        if self.dims.shape() != other.dims.shape() { return false; }
554        self.into_iter()
555            .zip(other.into_iter())
556            .all(|(a, b)| a == b)
557    }
558}
559
560// *** Tuple indexing **********************************************
561
562impl<T: Float> Index<()> for NdArrayV<T> {
563    type Output = T;
564    #[inline]
565    fn index(&self, (): ()) -> &T {
566        &self.source.data.as_slice()[self.offset_of(&[])]
567    }
568}
569
570impl<T: Float> Index<(usize,)> for NdArrayV<T> {
571    type Output = T;
572    #[inline]
573    fn index(&self, (i,): (usize,)) -> &T {
574        &self.source.data.as_slice()[self.offset_of(&[i])]
575    }
576}
577
578impl<T: Float> Index<(usize, usize)> for NdArrayV<T> {
579    type Output = T;
580    #[inline]
581    fn index(&self, (i, j): (usize, usize)) -> &T {
582        &self.source.data.as_slice()[self.offset_of(&[i, j])]
583    }
584}
585
586impl<T: Float> Index<(usize, usize, usize)> for NdArrayV<T> {
587    type Output = T;
588    #[inline]
589    fn index(&self, (i, j, k): (usize, usize, usize)) -> &T {
590        &self.source.data.as_slice()[self.offset_of(&[i, j, k])]
591    }
592}
593
594impl<T: Float> Index<(usize, usize, usize, usize)> for NdArrayV<T> {
595    type Output = T;
596    #[inline]
597    fn index(&self, (i, j, k, l): (usize, usize, usize, usize)) -> &T {
598        &self.source.data.as_slice()[self.offset_of(&[i, j, k, l])]
599    }
600}
601
602impl<T: Float> Index<(usize, usize, usize, usize, usize)> for NdArrayV<T> {
603    type Output = T;
604    #[inline]
605    fn index(&self, (i, j, k, l, m): (usize, usize, usize, usize, usize)) -> &T {
606        &self.source.data.as_slice()[self.offset_of(&[i, j, k, l, m])]
607    }
608}
609
610// *** Debug *******************************************************
611
612impl<T: Float> fmt::Debug for NdArrayV<T> {
613    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
614        write!(
615            f, "NdArrayV: {:?} [{}D, offset={}]",
616            self.dims.shape(), self.ndim(), self.offset,
617        )
618    }
619}
620
621impl<T: Float + Display> Display for NdArrayV<T> {
622    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
623        let shape = self.shape();
624        let elem = std::any::type_name::<T>();
625        let dims = if shape.is_empty() {
626            String::from("scalar")
627        } else {
628            shape.iter().map(|d| d.to_string()).collect::<Vec<_>>().join(" × ")
629        };
630        match &self.source.name {
631            Some(name) => writeln!(
632                f,
633                "NdArrayView \"{}\" [{}, {}] (offset: {})",
634                name, dims, elem, self.offset
635            )?,
636            None => writeln!(f, "NdArrayView [{}, {}] (offset: {})", dims, elem, self.offset)?,
637        }
638        print_ndarray_body(f, shape, |index| self.get(index))
639    }
640}
641
642impl<T: Float> From<NdArrayV<T>> for NdArray<T> {
643    /// Materialises the viewed window as an owned contiguous array.
644    fn from(value: NdArrayV<T>) -> Self {
645        value.to_ndarray()
646    }
647}
648
649#[cfg(test)]
650mod tests {
651    use super::*;
652    use crate::enums::shape_dim::ShapeDim;
653
654    #[test]
655    fn from_ndarray() {
656        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
657        let v = NdArrayV::from_ndarray(a);
658        assert_eq!(v.shape(), &[3, 2]);
659        assert_eq!(v.len(), 6);
660        assert_eq!(v[(0, 0)], 1.0);
661        assert_eq!(v[(2, 1)], 6.0);
662    }
663
664    #[test]
665    fn try_new_validates_and_constructs_view() {
666        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0], &[4]);
667        let v = NdArrayV::try_new(a, 1, &[2], &[1]).unwrap();
668        assert_eq!(v.shape(), &[2]);
669        assert_eq!(v.get(&[0]), 2.0);
670        assert_eq!(v.get(&[1]), 3.0);
671    }
672
673    #[test]
674    fn try_new_accepts_rank_zero_scalar_view() {
675        let a = NdArray::from_slice(&[10.0, 20.0], &[2]);
676        let v = NdArrayV::try_new(a, 1, &[], &[]).unwrap();
677        assert!(v.shape().is_empty());
678        assert!(v.strides().is_empty());
679        assert_eq!(v.len(), 1);
680        assert_eq!(v[()], 20.0);
681        assert_eq!((&v).into_iter().collect::<Vec<_>>(), vec![20.0]);
682        assert_eq!(Shape::shape(&v), ShapeDim::Rank0(1));
683    }
684
685    #[test]
686    fn try_new_rejects_invalid_rank_and_span() {
687        let a = NdArray::<f64>::new(&[4]);
688        assert!(NdArrayV::try_new(a.clone(), 0, &[2, 2], &[1]).is_err());
689        assert!(NdArrayV::try_new(a.clone(), 3, &[2], &[1]).is_err());
690        assert!(NdArrayV::try_new(a, usize::MAX, &[2], &[1]).is_err());
691    }
692
693    #[test]
694    fn try_new_accepts_empty_view_at_buffer_end() {
695        let a = NdArray::<f64>::new(&[4]);
696        let v = NdArrayV::try_new(a, 4, &[0], &[1]).unwrap();
697        assert!(v.is_empty());
698    }
699
700    #[test]
701    #[should_panic(expected = "NdArrayV::new")]
702    fn new_panics_on_invalid_span() {
703        let a = NdArray::<f64>::new(&[4]);
704        let _ = NdArrayV::new(a, 3, &[2], &[1]);
705    }
706
707    #[test]
708    fn col_access() {
709        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
710        let v = NdArrayV::from_ndarray(a);
711        assert_eq!(v.col(0), &[1.0, 2.0, 3.0]);
712        assert_eq!(v.col(1), &[4.0, 5.0, 6.0]);
713    }
714
715    #[test]
716    fn columns() {
717        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
718        let v = NdArrayV::from_ndarray(a);
719        let cols = v.columns();
720        assert_eq!(cols.len(), 2);
721        assert_eq!(cols[0], &[1.0, 2.0, 3.0]);
722        assert_eq!(cols[1], &[4.0, 5.0, 6.0]);
723    }
724
725    #[test]
726    #[cfg(feature = "select")]
727    fn row_selection_on_view() {
728        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
729        let v = a.as_view();
730        // Contiguous narrows zero-copy.
731        let sub = v.r(1..3);
732        assert_eq!(sub.shape(), &[2, 2]);
733        assert_eq!(sub.get(&[0, 1]), 5.0);
734        // Index arrays gather in order, and the source is unaffected.
735        let picked = v.r(&[2, 0]);
736        assert_eq!(picked.get(&[0, 0]), 3.0);
737        assert_eq!(picked.get(&[1, 1]), 4.0);
738    }
739
740    #[test]
741    fn apply_on_view() {
742        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
743        let out = a.as_view().apply(|x| x + 1.0);
744        assert_eq!(out.shape(), &[2, 2]);
745        assert_eq!(out.get(&[1, 1]), 5.0);
746    }
747
748    #[test]
749    fn obs_access() {
750        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
751        let v = NdArrayV::from_ndarray(a);
752        let obs0: Vec<f64> = (&v.obs(0)).into_iter().collect();
753        let obs2: Vec<f64> = (&v.obs(2)).into_iter().collect();
754        assert_eq!(obs0, vec![1.0, 4.0]);
755        assert_eq!(obs2, vec![3.0, 6.0]);
756    }
757
758    #[test]
759    fn iteration() {
760        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
761        let v = NdArrayV::from_ndarray(a);
762        let vals: Vec<f64> = (&v).into_iter().collect();
763        assert_eq!(vals, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
764    }
765
766    #[test]
767    fn iteration_2d() {
768        let data: Vec<f64> = (1..=20).map(|x| x as f64).collect();
769        let a = NdArray::from_slice(&data, &[10, 2]);
770        let v = NdArrayV::from_ndarray(a);
771        let vals: Vec<f64> = (&v).into_iter().collect();
772        assert_eq!(vals.len(), 20);
773        assert_eq!(&vals[..10], &data[..10]);
774        assert_eq!(&vals[10..], &data[10..]);
775    }
776
777    #[test]
778    fn with_offset() {
779        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
780        let stride = a.strides()[1];
781        // View into column 1 only as a 1D view
782        let v = NdArrayV::new(a.clone(), stride, &[3], &[1]);
783        assert_eq!(v.shape(), &[3]);
784        assert_eq!(v[(0,)], 4.0);
785        assert_eq!(v[(1,)], 5.0);
786        assert_eq!(v[(2,)], 6.0);
787    }
788
789    #[test]
790    fn to_ndarray() {
791        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
792        let v = NdArrayV::from_ndarray(a);
793        let b = v.to_ndarray();
794        assert_eq!(b.shape(), &[3, 2]);
795        assert_eq!(b.col(0), &[1.0, 2.0, 3.0]);
796    }
797
798    #[test]
799    fn transpose_2d_view() {
800        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
801        let t = NdArrayV::from_ndarray(a).transpose();
802        assert_eq!(t.shape(), &[2, 3]);
803        assert_eq!(t[(0, 0)], 1.0);
804        assert_eq!(t[(1, 0)], 4.0);
805        assert_eq!(t[(0, 2)], 3.0);
806        assert_eq!(t[(1, 2)], 6.0);
807        // Materialising the transposed view matches the owned transpose.
808        let owned = t.to_ndarray();
809        assert_eq!(owned.shape(), &[2, 3]);
810        assert_eq!(owned.get(&[1, 1]), 5.0);
811    }
812
813    #[test]
814    fn transpose_3d_view_matches_materialised() {
815        let data: Vec<f64> = (1..=24).map(|x| x as f64).collect();
816        let a = NdArray::from_slice(&data, &[2, 3, 4]);
817        let t = a.as_view().transpose();
818        assert_eq!(t.shape(), &[4, 3, 2]);
819        let materialised = a.transpose();
820        for i in 0..4 {
821            for j in 0..3 {
822                for k in 0..2 {
823                    assert_eq!(t[(i, j, k)], materialised.get(&[i, j, k]));
824                }
825            }
826        }
827    }
828
829    #[test]
830    fn permute_axes_view() {
831        let data: Vec<f64> = (1..=24).map(|x| x as f64).collect();
832        let a = NdArray::from_slice(&data, &[2, 3, 4]);
833        let p = a.as_view().permute_axes(&[2, 0, 1]);
834        assert_eq!(p.shape(), &[4, 2, 3]);
835        for i in 0..2 {
836            for j in 0..3 {
837                for k in 0..4 {
838                    assert_eq!(p[(k, i, j)], a.get(&[i, j, k]));
839                }
840            }
841        }
842    }
843
844    #[test]
845    #[should_panic(expected = "permute_axes")]
846    fn permute_axes_rejects_repeat() {
847        let a = NdArray::<f64>::new(&[2, 3, 4]);
848        let _ = a.as_view().permute_axes(&[0, 0, 1]);
849    }
850
851    #[test]
852    fn swap_axes_view() {
853        let data: Vec<f64> = (1..=24).map(|x| x as f64).collect();
854        let a = NdArray::from_slice(&data, &[2, 3, 4]);
855        let s = a.as_view().swap_axes(0, 2);
856        assert_eq!(s.shape(), &[4, 3, 2]);
857        assert_eq!(s[(3, 2, 1)], a.get(&[1, 2, 3]));
858        assert_eq!(s[(0, 0, 0)], a.get(&[0, 0, 0]));
859    }
860
861    #[test]
862    #[should_panic(expected = "obs()")]
863    fn obs_on_1d_panics() {
864        let a = NdArray::from_slice(&[1.0, 2.0, 3.0], &[3]);
865        let _ = a.as_view().obs(1);
866    }
867
868    #[cfg(feature = "matrix")]
869    #[test]
870    fn to_matrix() {
871        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
872        let v = NdArrayV::from_ndarray(a);
873        let mat = v.to_matrix().unwrap();
874        assert_eq!(mat.n_rows, 3);
875        assert_eq!(mat.n_cols, 2);
876    }
877
878    #[test]
879    fn blas_params() {
880        let a = NdArray::<f64>::new(&[10, 5]);
881        let v = NdArrayV::from_ndarray(a);
882        assert_eq!(v.m(), 10);
883        assert_eq!(v.n(), 5);
884        assert_eq!(v.lda(), 10);
885    }
886
887    #[test]
888    fn eq() {
889        let a = NdArray::from_slice(&[1.0, 2.0, 3.0], &[3]);
890        let v1 = NdArrayV::from_ndarray(a.clone());
891        let v2 = NdArrayV::from_ndarray(a);
892        assert_eq!(v1, v2);
893    }
894
895    #[test]
896    fn shape_trait() {
897        let a = NdArray::<f64>::new(&[3, 4]);
898        let v = NdArrayV::from_ndarray(a);
899        assert_eq!(Shape::shape(&v), ShapeDim::Rank2 { rows: 3, cols: 4 });
900    }
901
902    #[test]
903    #[should_panic(expected = "unit stride")]
904    fn col_on_transposed_view_panics() {
905        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
906        let t = a.as_view().transpose();
907        let _ = t.col(0);
908    }
909
910    #[test]
911    #[should_panic(expected = "unit stride")]
912    fn columns_on_transposed_view_panics() {
913        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
914        let t = a.as_view().transpose();
915        let _ = t.columns();
916    }
917
918    #[test]
919    #[should_panic(expected = "unit stride")]
920    fn lda_on_transposed_view_panics() {
921        let a = NdArray::<f64>::new(&[3, 2]);
922        let t = a.as_view().transpose();
923        let _ = t.lda();
924    }
925
926    #[test]
927    #[should_panic(expected = "index 3 out of bounds for axis 0")]
928    fn obs_out_of_bounds_panics() {
929        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
930        let _ = a.as_view().obs(3);
931    }
932
933    #[test]
934    #[should_panic(expected = "swap_axes: axes (0, 3) out of bounds")]
935    fn swap_axes_out_of_bounds_panics() {
936        let a = NdArray::<f64>::new(&[2, 3, 4]);
937        let _ = a.as_view().swap_axes(0, 3);
938    }
939
940    #[test]
941    #[should_panic(expected = "permute_axes: axis 3 out of bounds")]
942    fn permute_axes_out_of_bounds_axis_panics() {
943        let a = NdArray::<f64>::new(&[2, 3, 4]);
944        let _ = a.as_view().permute_axes(&[0, 1, 3]);
945    }
946
947    #[test]
948    fn get_unchecked_matches_get() {
949        let data: Vec<f64> = (1..=24).map(|x| x as f64).collect();
950        let a = NdArray::from_slice(&data, &[2, 3, 4]);
951        let t = a.as_view().transpose();
952        for i in 0..4 {
953            for j in 0..3 {
954                for k in 0..2 {
955                    let idx = [i, j, k];
956                    // SAFETY: indices are within shape
957                    assert_eq!(unsafe { t.get_unchecked(&idx) }, t.get(&idx));
958                }
959            }
960        }
961    }
962}