Skip to main content

minarrow/structs/
ndarray.rs

1//! # `NdArray`
2//!
3//! An N-dimensional container for contiguous `f32` or `f64` data.
4//!
5//! `NdArray` is Minarrow's general-purpose interchange container for numeric
6//! data received from files, sensors, networks, Python, DLPack, or other
7//! runtimes. It supports construction, indexing, views, selection, iteration,
8//! conversion, and transfer to external numerical systems.
9//!
10//! It is not intended to provide a complete numerical-computing runtime.
11//! Computational workloads should be delegated to the application's preferred
12//! kernels, BLAS implementation, or machine-learning framework.
13//!
14//! ## Shape and element type
15//!
16//! `NdArray` is generic over `T: Float`, supporting `f32` and `f64`. Rank is
17//! determined at runtime. Shapes with one to five dimensions are stored inline,
18//! avoiding a heap allocation for the common case.
19//!
20//! Shape semantics follow standard tensor conventions:
21//!
22//! - `&[]` is a rank-zero array containing one logical value.
23//! - `[0]` is a rank-one array containing no values.
24//!
25//! ## Memory layout
26//!
27//! Data is stored in compact column-major order without padding between
28//! dimensions. The complete logical array therefore occupies one contiguous
29//! buffer, and its shape and strides describe every stored element without
30//! hidden gaps.
31//!
32//! The allocation start is aligned to 64 bytes by [`Vec64`], allowing efficient
33//! SIMD operations over the flattened buffer.
34//!
35//! Per-column padding for CPU-oriented BLAS and LAPACK access is provided by
36//! [`Matrix`] instead. Converting an `NdArray` with `to_matrix` re-lays the data
37//! into that padded representation when required.
38//!
39//! NumPy and PyTorch can consume the column-major strides directly. Consumers
40//! that require row-major C-contiguous storage must create a contiguous copy,
41//! such as with `np.ascontiguousarray(...)` or `.contiguous()`.
42//!
43//! ## Null values
44//!
45//! `NdArray` does not store a null mask. When converting nullable floating-point
46//! data, nulls are represented as `NaN`. The resulting array cannot distinguish
47//! a source null from an ordinary `NaN`; interpretation is left to the consuming
48//! kernel or framework.
49//!
50//! ## Interoperability
51//!
52//! - [`Matrix`] to a two-dimensional `NdArray`: zero-copy for `f64`; the padded
53//!   column stride is preserved.
54//! - Two-dimensional `NdArray` to [`Matrix`]: re-lays data into the padded column
55//!   format for `f64`, or transfers without copying when the existing stride is
56//!   already compatible.
57//! - Two-dimensional `NdArray` to [`Table`]: copies each `f64` column into a
58//!   separate 64-byte-aligned `FloatArray`.
59//! - One-dimensional `NdArray` to [`Array`]: moves the `f64` buffer into a
60//!   `FloatArray<f64>`.
61//! - [`Table`] to `NdArray` through [`TryFrom`]: copies `f64` values and converts
62//!   nulls to `NaN`.
63//! - DLPack import and export: shares data with compatible `f32` and `f64`
64//!   consumers. Whether a transfer is zero-copy depends on ownership, alignment,
65//!   layout, and protocol constraints.
66
67use std::fmt;
68use std::fmt::{Display, Formatter};
69use std::ops::{Index, IndexMut, Range, RangeFrom, RangeFull, RangeTo};
70use std::sync::Arc;
71
72use crate::enums::error::MinarrowError;
73use crate::enums::shape_dim::ShapeDim;
74use crate::structs::buffer::Buffer;
75use crate::traits::print::print_ndarray_body;
76#[cfg(all(feature = "views", feature = "select"))]
77use crate::traits::selection::{AxisSelection, DataSelector, RowSelection};
78use crate::traits::type_unions::Float;
79use crate::traits::{concatenate::Concatenate, shape::Shape};
80use crate::{Array, ArrowType, Field, FieldArray, FloatArray, NumericArray, Table, Vec64};
81
82#[cfg(feature = "matrix")]
83use crate::structs::matrix::{Matrix, aligned_stride};
84#[cfg(feature = "views")]
85use crate::structs::views::ndarray_view::NdArrayV;
86#[cfg(feature = "dlpack")]
87use crate::ffi::dlpack::{
88    export_to_dlpack, export_to_dlpack_versioned, DLPackTensor, DLPackTensorVersioned,
89};
90
91// ****************************************************************
92// NdArray
93// ****************************************************************
94
95/// N-dimensional contiguous array of float values.
96///
97/// Backed by [`Buffer<T>`] for zero-copy interop with external memory.
98/// Compact column-major layout with a 64-byte aligned allocation start.
99///
100/// See the [module-level documentation](self) for design rationale.
101#[derive(Clone)]
102pub struct NdArray<T> {
103    pub(crate) data: Arc<Buffer<T>>,
104    pub(crate) dims: NdDims,
105    pub name: Option<String>,
106}
107
108/// Logical equality over shape and values in logical order. Strides and
109/// the name do not affect equality, matching the view and SuperNdArray types.
110impl<T: Float> PartialEq for NdArray<T> {
111    fn eq(&self, other: &Self) -> bool {
112        if self.dims.shape() != other.dims.shape() {
113            return false;
114        }
115        self.into_iter()
116            .zip(other.into_iter())
117            .all(|(a, b)| a == b)
118    }
119}
120
121// *** Construction ************************************************
122
123impl<T: Float> NdArray<T> {
124    /// Create a zeroed NdArray with the given shape, column-major strides.
125    /// `shape == &[]` creates a rank-zero array containing one scalar value;
126    /// `shape == &[0]` creates an empty rank-one array.
127    pub fn new(shape: &[usize]) -> Self {
128        let dims = NdDims::from_shape(shape);
129        let total = buffer_len(dims.shape(), dims.strides());
130        let mut v = Vec64::with_capacity(total);
131        v.0.resize(total, T::default());
132        NdArray { data: Arc::new(Buffer::from_vec64(v)), dims, name: None }
133    }
134
135    /// Create a zeroed NdArray with a name.
136    pub fn new_named(shape: &[usize], name: impl Into<String>) -> Self {
137        let mut arr = Self::new(shape);
138        arr.name = Some(name.into());
139        arr
140    }
141
142    /// Create from a flat column-major slice and shape.
143    ///
144    /// The slice holds `product(shape)` logical elements in column-major
145    /// order, matching the compact layout, so the data copies straight in.
146    /// A shape with zero axes (`&[]`) has product one and therefore requires
147    /// one value. A shape containing a zero-length axis, such as `&[0]`, has
148    /// product zero and requires no values.
149    pub fn from_slice(data: &[T], shape: &[usize]) -> Self {
150        let logical_len: usize = shape.iter().product();
151        assert_eq!(
152            data.len(), logical_len,
153            "NdArray::from_slice: data length {} does not match shape product {}",
154            data.len(), logical_len
155        );
156        let dims = NdDims::from_shape(shape);
157        NdArray {
158            data: Arc::new(Buffer::from_slice(data)),
159            dims,
160            name: None,
161        }
162    }
163
164    /// Create from an owned 64-byte aligned vector, moving it in without
165    /// a copy. The vector holds `product(shape)` logical elements in
166    /// column-major order, matching the compact layout. A shape with zero
167    /// axes (`&[]`) requires one value, while `&[0]` requires none.
168    pub fn from_vec64(data: Vec64<T>, shape: &[usize]) -> Self {
169        let logical_len: usize = shape.iter().product();
170        assert_eq!(
171            data.len(), logical_len,
172            "NdArray::from_vec64: data length {} does not match shape product {}",
173            data.len(), logical_len
174        );
175        let dims = NdDims::from_shape(shape);
176        NdArray {
177            data: Arc::new(Buffer::from_vec64(data)),
178            dims,
179            name: None,
180        }
181    }
182
183    /// Create from a pre-owned `Buffer<f64>` with explicit shape and strides.
184    ///
185    /// The buffer must already contain `buffer_len(shape, strides)` elements
186    /// in the correct strided layout.
187    pub fn from_buffer(data: Buffer<T>, shape: &[usize], strides: &[usize]) -> Self {
188        let required = buffer_len(shape, strides);
189        assert!(
190            data.len() >= required,
191            "NdArray::from_buffer: buffer has {} elements but shape requires {}",
192            data.len(), required
193        );
194        let dims = NdDims::from_shape_and_strides(shape, strides);
195        NdArray { data: Arc::new(data), dims, name: None }
196    }
197
198    /// Create an NdArray filled with a constant value.
199    pub fn fill(shape: &[usize], value: T) -> Self {
200        let dims = NdDims::from_shape(shape);
201        let total = buffer_len(dims.shape(), dims.strides());
202        let mut v = Vec64::with_capacity(total);
203        v.0.resize(total, value);
204        NdArray { data: Arc::new(Buffer::from_vec64(v)), dims, name: None }
205    }
206
207    /// Create an NdArray of ones.
208    pub fn ones(shape: &[usize]) -> Self {
209        Self::fill(shape, T::one())
210    }
211
212    /// Create a 2D identity matrix.
213    pub fn eye(n: usize) -> Self {
214        let mut arr = Self::new(&[n, n]);
215        let stride = arr.dims.strides()[1];
216        let buf = Arc::make_mut(&mut arr.data).as_mut_slice();
217        for i in 0..n {
218            buf[i * stride + i] = T::one();
219        }
220        arr
221    }
222
223    /// Create a 1D NdArray with evenly spaced values in `[start, end]`.
224    pub fn linspace(start: T, end: T, n: usize) -> Self {
225        assert!(n >= 2, "linspace requires at least 2 points");
226        let step = (end - start) / T::from(n - 1).unwrap();
227        let v: Vec64<T> = (0..n).map(|i| start + step * T::from(i).unwrap()).collect();
228        NdArray {
229            data: Arc::new(Buffer::from_vec64(v)),
230            dims: NdDims::from_shape(&[n]),
231            name: None,
232        }
233    }
234
235    /// Create a 1D NdArray with values `start, start+step, start+2*step, ...`
236    /// for `n` elements.
237    pub fn arange(start: T, step: T, n: usize) -> Self {
238        let v: Vec64<T> = (0..n).map(|i| start + step * T::from(i).unwrap()).collect();
239        NdArray {
240            data: Arc::new(Buffer::from_vec64(v)),
241            dims: NdDims::from_shape(&[n]),
242            name: None,
243        }
244    }
245
246    // *** Shape and introspection *********************************
247
248    /// Number of dimensions.
249    #[inline]
250    pub fn ndim(&self) -> usize { self.dims.ndim() }
251
252    /// Shape as a slice of dimension sizes.
253    #[inline]
254    pub fn shape(&self) -> &[usize] { self.dims.shape() }
255
256    /// Strides as a slice of element offsets per dimension.
257    #[inline]
258    pub fn strides(&self) -> &[usize] { self.dims.strides() }
259
260    /// Total number of logical elements i.e. the product of shape.
261    #[inline]
262    pub fn len(&self) -> usize { self.dims.len() }
263
264    /// Leading-axis (axis 0) observation count i.e. `shape()[0]`,
265    /// matching `SuperNdArray::n_obs` and NumPy's `len(arr)`.
266    /// Panics for a rank-zero scalar, which has no axis 0.
267    #[inline]
268    pub fn n_obs(&self) -> usize {
269        assert!(self.ndim() > 0, "n_obs() requires an axis 0");
270        self.dims.shape()[0]
271    }
272
273    /// True if any dimension is zero.
274    #[inline]
275    pub fn is_empty(&self) -> bool { self.len() == 0 }
276
277    /// True if the buffer layout matches compact column-major strides,
278    /// with no transposition or non-standard stride pattern.
279    pub fn is_contiguous(&self) -> bool {
280        let shape = self.dims.shape();
281        let strides = self.dims.strides();
282        let mut expected = 1;
283        for d in 0..shape.len() {
284            if strides[d] != expected {
285                return false;
286            }
287            expected *= shape[d];
288        }
289        true
290    }
291
292    /// True if any element is NaN.
293    pub fn has_nan(&self) -> bool {
294        self.into_iter().any(|v| v.is_nan())
295    }
296
297    /// Count of NaN elements.
298    pub fn nan_count(&self) -> usize {
299        self.into_iter().filter(|v| v.is_nan()).count()
300    }
301
302    // *** Element access ******************************************
303
304    /// Get element by N-dimensional index. Panics if out of bounds.
305    #[inline]
306    pub fn get(&self, indices: &[usize]) -> T {
307        self.data.as_slice()[self.offset_of(indices)]
308    }
309
310    /// Like `get`, but skips bounds checks.
311    ///
312    /// # Safety
313    /// The caller guarantees each index is within its dimension.
314    #[inline(always)]
315    pub unsafe fn get_unchecked(&self, indices: &[usize]) -> T {
316        let strides = self.dims.strides();
317        let mut off = 0;
318        for d in 0..indices.len() {
319            off += indices[d] * strides[d];
320        }
321        // SAFETY: in-bounds indices produce an in-bounds flat offset.
322        unsafe { *self.data.as_slice().get_unchecked(off) }
323    }
324
325    /// Set element by N-dimensional index. Panics if out of bounds.
326    /// Triggers copy-on-write when views share the buffer.
327    ///
328    /// For repeated writes, detach once and write through the slice:
329    /// ```ignore
330    /// let s1 = a.strides()[1];
331    /// let s = a.as_mut_slice();      // one copy-on-write detach
332    /// for (i, j, v) in writes {
333    ///     // SAFETY: i and j are within shape
334    ///     unsafe { *s.get_unchecked_mut(i + j * s1) = v; }
335    /// }
336    /// ```
337    #[inline]
338    pub fn set(&mut self, indices: &[usize], value: T) {
339        let off = self.offset_of(indices);
340        Arc::make_mut(&mut self.data).as_mut_slice()[off] = value;
341    }
342
343    /// Compute flat buffer offset for an N-dimensional index.
344    #[inline]
345    pub(crate) fn offset_of(&self, indices: &[usize]) -> usize {
346        offset_of_impl(indices, self.dims.shape(), self.dims.strides())
347    }
348
349    // *** Metadata ************************************************
350
351    /// Set the array name.
352    #[inline]
353    pub fn set_name(&mut self, name: impl Into<String>) {
354        self.name = Some(name.into());
355    }
356
357    /// Immutable reference to the full flat buffer.
358    #[inline]
359    pub fn as_slice(&self) -> &[T] {
360        self.data.as_slice()
361    }
362
363    /// Mutable reference to the full flat buffer. Triggers copy-on-write
364    /// when views share the buffer.
365    #[inline]
366    pub fn as_mut_slice(&mut self) -> &mut [T] {
367        Arc::make_mut(&mut self.data).as_mut_slice()
368    }
369
370    /// Fill every logical element with a value.
371    pub fn fill_with(&mut self, value: T) {
372        // For contiguous arrays, fill the whole buffer
373        if self.is_contiguous() {
374            Arc::make_mut(&mut self.data).as_mut_slice().fill(value);
375            return;
376        }
377        // Walk logical positions for non-contiguous layouts
378        let offsets: Vec<usize> = {
379            let shape = self.dims.shape();
380            let strides = self.dims.strides();
381            let mut result = Vec::with_capacity(self.len());
382            let mut indices = vec![0usize; shape.len()];
383            for _ in 0..self.len() {
384                result.push(indices.iter().zip(strides).map(|(&i, &s)| i * s).sum());
385                let mut carry = true;
386                for d in 0..shape.len() {
387                    if carry {
388                        indices[d] += 1;
389                        if indices[d] < shape[d] { carry = false; } else { indices[d] = 0; }
390                    }
391                }
392            }
393            result
394        };
395        let buf = Arc::make_mut(&mut self.data).as_mut_slice();
396        for off in offsets { buf[off] = value; }
397    }
398
399    // *** 2D axis access (column-major) ***************************
400
401    /// Immutable column slice for a 2D array.
402    /// Panics if ndim != 2, the column index is out of bounds, or axis 0
403    /// is not unit-stride i.e. column elements are not contiguous.
404    #[inline]
405    pub fn col(&self, col: usize) -> &[T] {
406        let shape = self.dims.shape();
407        assert_eq!(shape.len(), 2, "col() requires a 2D array");
408        assert!(col < shape[1], "Column index out of bounds");
409        assert_eq!(
410            self.dims.strides()[0], 1,
411            "col() requires unit stride on axis 0; call to_contiguous() first"
412        );
413        let stride = self.dims.strides()[1];
414        let start = col * stride;
415        &self.data.as_slice()[start..start + shape[0]]
416    }
417
418    /// Mutable column slice for a 2D array.
419    /// Panics if ndim != 2, the column index is out of bounds, or axis 0
420    /// is not unit-stride i.e. column elements are not contiguous.
421    /// Triggers copy-on-write if the buffer is shared.
422    #[inline]
423    pub fn col_mut(&mut self, col: usize) -> &mut [T] {
424        let shape = self.dims.shape();
425        assert_eq!(shape.len(), 2, "col_mut() requires a 2D array");
426        assert!(col < shape[1], "Column index out of bounds");
427        assert_eq!(
428            self.dims.strides()[0], 1,
429            "col_mut() requires unit stride on axis 0; call to_contiguous() first"
430        );
431        let stride = self.dims.strides()[1];
432        let n_rows = shape[0];
433        let start = col * stride;
434        &mut Arc::make_mut(&mut self.data).as_mut_slice()[start..start + n_rows]
435    }
436
437    /// All columns as immutable slices. 2D only.
438    /// Panics if ndim != 2 or axis 0 is not unit-stride.
439    pub fn columns(&self) -> Vec<&[T]> {
440        let shape = self.dims.shape();
441        assert_eq!(shape.len(), 2, "columns() requires a 2D array");
442        assert_eq!(
443            self.dims.strides()[0], 1,
444            "columns() requires unit stride on axis 0; call to_contiguous() first"
445        );
446        let stride = self.dims.strides()[1];
447        let n_rows = shape[0];
448        let buf = self.data.as_slice();
449        (0..shape[1])
450            .map(|c| &buf[c * stride..c * stride + n_rows])
451            .collect()
452    }
453
454    /// All columns as mutable slices. 2D only.
455    /// Panics if ndim != 2 or axis 0 is not unit-stride.
456    pub fn columns_mut(&mut self) -> Vec<&mut [T]> {
457        let shape = self.dims.shape();
458        assert_eq!(shape.len(), 2, "columns_mut() requires a 2D array");
459        assert_eq!(
460            self.dims.strides()[0], 1,
461            "columns_mut() requires unit stride on axis 0; call to_contiguous() first"
462        );
463        let n_rows = shape[0];
464        let n_cols = shape[1];
465        let stride = self.dims.strides()[1];
466        let ptr = Arc::make_mut(&mut self.data).as_mut_slice().as_mut_ptr();
467        let mut result = Vec::with_capacity(n_cols);
468        for c in 0..n_cols {
469            let start = c * stride;
470            // SAFETY: each slice is within bounds and non-overlapping,
471            // we have exclusive &mut access.
472            unsafe {
473                let col_ptr = ptr.add(start);
474                result.push(std::slice::from_raw_parts_mut(col_ptr, n_rows));
475            }
476        }
477        result
478    }
479
480    /// Wrap as a full `NdArrayV` view for repeated observation access.
481    ///
482    /// Call `.obs(i)` on the returned view to get individual observations.
483    /// The view holds the parent through the array's shared internal
484    /// buffer, so this is a refcount bump.
485    #[cfg(feature = "views")]
486    pub fn as_view(&self) -> NdArrayV<T> {
487        NdArrayV::from_ndarray(self.clone())
488    }
489
490    /// Zero-copy view of a single observation (axis-0 element).
491    ///
492    /// Returns an (N-1)-dimensional `NdArrayV` view. For a 2D array
493    /// with shape `[n, m]`, returns a 1D view of shape `[m]`. For 3D
494    /// `[n, m, k]`, returns 2D `[m, k]`. Requires rank 2 or higher - for
495    /// scalar access on a 1D array use `get(&[i])`.
496    ///
497    /// For repeated access in a loop, prefer `nd.as_view()` then call
498    /// `.obs()` on the view to avoid re-wrapping each time.
499    #[cfg(feature = "views")]
500    pub fn obs(&self, idx: usize) -> NdArrayV<T> {
501        self.as_view().obs(idx)
502    }
503
504    // *** BLAS/LAPACK compatibility (2D) **************************
505
506    /// Number of rows as i32. Panics if ndim != 2.
507    #[inline]
508    pub fn m(&self) -> i32 {
509        assert_eq!(self.ndim(), 2, "m() requires a 2D array");
510        self.dims.shape()[0] as i32
511    }
512
513    /// Number of columns as i32. Panics if ndim != 2.
514    #[inline]
515    pub fn n(&self) -> i32 {
516        assert_eq!(self.ndim(), 2, "n() requires a 2D array");
517        self.dims.shape()[1] as i32
518    }
519
520    /// Leading dimension for BLAS. Panics if ndim != 2.
521    #[inline]
522    pub fn lda(&self) -> i32 {
523        assert_eq!(self.ndim(), 2, "lda() requires a 2D array");
524        self.dims.strides()[1] as i32
525    }
526
527    // *** Reshape *************************************************
528
529    /// Reshape to a new shape. Returns a new NdArray with re-laid out data.
530    ///
531    /// The total number of logical elements must match. Data is copied
532    /// from logical element order into the new strided layout.
533    pub fn reshape(&self, new_shape: &[usize]) -> Result<NdArray<T>, MinarrowError> {
534        let new_len: usize = new_shape.iter().product();
535        if new_len != self.len() {
536            return Err(MinarrowError::ShapeError {
537                message: format!(
538                    "reshape: cannot reshape array of size {} into shape {:?}",
539                    self.len(), new_shape
540                ),
541            });
542        }
543        let flat: Vec64<T> = self.into_iter().collect();
544        let mut result = NdArray::from_slice(&flat, new_shape);
545        result.name = self.name.clone();
546        Ok(result)
547    }
548
549    /// Transpose by reversing the axis order. Returns a new NdArray with
550    /// re-laid out data. A 1D array copies through unchanged.
551    ///
552    /// For a zero-copy transposed view, call `as_view()` and use the
553    /// view's `transpose()`.
554    pub fn transpose(&self) -> NdArray<T> {
555        let shape = self.dims.shape();
556        let ndim = shape.len();
557        if ndim <= 1 {
558            let mut result = self.to_contiguous();
559            result.name = self.name.clone();
560            return result;
561        }
562        if ndim == 2 {
563            let (n_rows, n_cols) = (shape[0], shape[1]);
564            let new_shape = [n_cols, n_rows];
565            let mut result = NdArray::new(&new_shape);
566            let src_stride = self.dims.strides()[1];
567            let dst_stride = result.dims.strides()[1];
568            let src = self.data.as_slice();
569            let dst = Arc::make_mut(&mut result.data).as_mut_slice();
570            for r in 0..n_rows {
571                for c in 0..n_cols {
572                    dst[r * dst_stride + c] = src[c * src_stride + r];
573                }
574            }
575            result.name = self.name.clone();
576            return result;
577        }
578
579        // General N-D. Walking the result's logical positions in column-major
580        // order reads the source at the reversed index, which is the source
581        // walked with reversed strides.
582        let new_shape: Vec<usize> = shape.iter().rev().copied().collect();
583        let rev_strides: Vec<usize> = self.dims.strides().iter().rev().copied().collect();
584        let src = self.data.as_slice();
585        let total = self.len();
586        let mut buf = Vec64::with_capacity(total);
587        let mut indices = vec![0usize; ndim];
588        for _ in 0..total {
589            let offset: usize = indices.iter()
590                .zip(rev_strides.iter())
591                .map(|(&i, &s)| i * s)
592                .sum();
593            buf.push(src[offset]);
594            let mut carry = true;
595            for d in 0..ndim {
596                if carry {
597                    indices[d] += 1;
598                    if indices[d] < new_shape[d] {
599                        carry = false;
600                    } else {
601                        indices[d] = 0;
602                    }
603                }
604            }
605        }
606        NdArray {
607            data: Arc::new(Buffer::from_vec64(buf)),
608            dims: NdDims::from_shape(&new_shape),
609            name: self.name.clone(),
610        }
611    }
612
613    /// Flatten to a contiguous 1D array.
614    pub fn flatten(&self) -> NdArray<T> {
615        let flat: Vec64<T> = self.into_iter().collect();
616        let n = flat.len();
617        NdArray {
618            data: Arc::new(Buffer::from_vec64(flat)),
619            dims: NdDims::from_shape(&[n]),
620            name: self.name.clone(),
621        }
622    }
623
624    /// If the array has non-standard strides, re-lay out into default
625    /// column-major contiguous form.
626    pub fn to_contiguous(&self) -> NdArray<T> {
627        if self.is_contiguous() {
628            return self.clone();
629        }
630        let mut result = NdArray::from_slice(
631            &self.into_iter().collect::<Vec64<T>>(),
632            self.shape(),
633        );
634        result.name = self.name.clone();
635        result
636    }
637
638    // *** Slicing: arr.slice(nd![1..4, 2..5]) *************************
639
640    /// Slice this array along any combination of axes.
641    ///
642    /// Each axis takes any [`DataSelector`] - a single index collapses
643    /// that dimension, and a contiguous range keeps it. Returns a
644    /// zero-copy `NdArrayV` view that holds the parent through the
645    /// array's shared internal buffer, so this is a refcount bump,
646    /// matching `as_view` and `obs`.
647    ///
648    /// # Examples
649    /// ```ignore
650    /// arr.slice(&[&2])              // single index on 1D
651    /// arr.slice(&[&(1..4)])         // range on axis 0
652    /// arr.slice(nd![1..4, 2..5])    // range on both axes (2D)
653    /// arr.slice(nd![0..3, 2])       // range on axis 0, single on axis 1
654    /// arr.slice(nd![1, 0..4, 3])    // mixed for 3D
655    /// ```
656    #[cfg(all(feature = "views", feature = "select"))]
657    pub fn slice(&self, selection: &[&dyn DataSelector]) -> NdArrayV<T> {
658        assert_eq!(
659            selection.len(), self.ndim(),
660            "slice(): expected {} axes, got {}", self.ndim(), selection.len()
661        );
662
663        let shape = self.dims.shape();
664        let strides = self.dims.strides();
665
666        // Compute the new offset, shape, and strides
667        let mut new_offset: usize = 0;
668        let mut new_shape = Vec::with_capacity(self.ndim());
669        let mut new_strides = Vec::with_capacity(self.ndim());
670
671        for (d, sel) in selection.iter().enumerate() {
672            let (start, end, collapse) = sel.resolve_axis(shape[d]);
673            assert!(
674                end <= shape[d],
675                "slice(): end {} out of bounds for axis {} (size {})", end, d, shape[d]
676            );
677            new_offset += start * strides[d];
678            if !collapse {
679                new_shape.push(end - start);
680                new_strides.push(strides[d]);
681            }
682        }
683
684        NdArrayV::new(self.clone(), new_offset, &new_shape, &new_strides)
685    }
686
687    // *** Apply ***************************************************
688
689    /// Apply a function to every logical element, returning a new compact
690    /// array with this array's shape and name. The caller supplies the
691    /// operation; NdArray supplies logical-order traversal and materialisation.
692    pub fn apply(&self, f: impl Fn(T) -> T) -> NdArray<T> {
693        let flat: Vec64<T> = self.into_iter().map(f).collect();
694        let mut result = NdArray::from_slice(&flat, self.shape());
695        result.name = self.name.clone();
696        result
697    }
698
699    /// Apply a function to every logical element in place, with no
700    /// allocation. Copy-on-write triggers first when views share the
701    /// buffer.
702    pub fn apply_mut(&mut self, f: impl Fn(T) -> T) {
703        if self.is_contiguous() {
704            for v in Arc::make_mut(&mut self.data).as_mut_slice() {
705                *v = f(*v);
706            }
707            return;
708        }
709        // Walk logical positions for non-contiguous layouts so stride
710        // padding stays untouched.
711        let shape = self.dims.shape().to_vec();
712        let strides = self.dims.strides().to_vec();
713        let total = self.len();
714        let buf = Arc::make_mut(&mut self.data).as_mut_slice();
715        let mut indices = vec![0usize; shape.len()];
716        for _ in 0..total {
717            let offset: usize = indices.iter().zip(strides.iter()).map(|(&i, &s)| i * s).sum();
718            buf[offset] = f(buf[offset]);
719            let mut carry = true;
720            for d in 0..shape.len() {
721                if carry {
722                    indices[d] += 1;
723                    if indices[d] < shape[d] {
724                        carry = false;
725                    } else {
726                        indices[d] = 0;
727                    }
728                }
729            }
730        }
731    }
732
733    /// Apply a function to every 1D lane along the given axis, collapsing
734    /// that axis. Each lane arrives as a zero-copy [`NdArrayV`] and the
735    /// closure returns one value for it, so the output shape drops `axis`.
736    /// Requires rank 2 or higher - a 1D array is itself a single lane.
737    #[cfg(feature = "views")]
738    pub fn apply_axis(&self, axis: usize, mut f: impl FnMut(NdArrayV<T>) -> T) -> NdArray<T> {
739        let shape = self.dims.shape();
740        let strides = self.dims.strides();
741        let ndim = shape.len();
742        assert!(ndim >= 2, "apply_axis requires a 2D or higher array");
743        assert!(axis < ndim, "apply_axis: axis {} out of bounds for {}D array", axis, ndim);
744
745        let out_shape: Vec<usize> = shape
746            .iter()
747            .enumerate()
748            .filter(|(d, _)| *d != axis)
749            .map(|(_, &s)| s)
750            .collect();
751        let out_dims: Vec<usize> = (0..ndim).filter(|&d| d != axis).collect();
752        let total: usize = out_shape.iter().product();
753
754        let lane_shape = [shape[axis]];
755        let lane_strides = [strides[axis]];
756
757        // Walk the output positions in column-major order. Each position
758        // anchors one lane's base offset in the source.
759        let mut flat = Vec64::with_capacity(total);
760        let mut indices = vec![0usize; out_shape.len()];
761        for _ in 0..total {
762            let offset: usize = indices
763                .iter()
764                .zip(out_dims.iter())
765                .map(|(&i, &d)| i * strides[d])
766                .sum();
767            let lane = NdArrayV::new(self.clone(), offset, &lane_shape, &lane_strides);
768            flat.push(f(lane));
769            let mut carry = true;
770            for d in 0..out_shape.len() {
771                if carry {
772                    indices[d] += 1;
773                    if indices[d] < out_shape[d] {
774                        carry = false;
775                    } else {
776                        indices[d] = 0;
777                    }
778                }
779            }
780        }
781        let mut result = NdArray::from_slice(&flat, &out_shape);
782        result.name = self.name.clone();
783        result
784    }
785
786    // *** Conversions *********************************************
787
788    /// Export as a legacy DLPack tensor for PyTorch, NumPy, JAX, and other
789    /// compatible consumers. Shared or aliased storage copies because the
790    /// legacy protocol cannot mark the exported tensor read-only.
791    ///
792    /// Returns a `DLPackTensor` that manages the lifecycle. Drop it to
793    /// release, or call `.into_raw()` to transfer ownership to an FFI
794    /// consumer such as a PyCapsule.
795    #[cfg(feature = "dlpack")]
796    pub fn to_dlpack(self) -> DLPackTensor {
797        export_to_dlpack(self)
798    }
799
800    /// Export as a DLPack 1.x versioned tensor, carrying the spec version
801    /// and flags fields that PyTorch and JAX negotiate. The read-only
802    /// flag is set when the backing buffer is shared.
803    #[cfg(feature = "dlpack")]
804    pub fn to_dlpack_versioned(self) -> DLPackTensorVersioned {
805        export_to_dlpack_versioned(self)
806    }
807
808    // *** Parallel iteration (rayon) ******************************
809
810    /// Parallel iterator over the underlying buffer. Rayon splits
811    /// the contiguous data into chunks across threads automatically.
812    #[cfg(feature = "parallel_proc")]
813    pub fn par_iter(&self) -> rayon::slice::Iter<'_, T>
814    where
815        T: Send + Sync,
816    {
817        use rayon::prelude::*;
818        self.data.as_slice().par_iter()
819    }
820
821    /// Parallel chunks of the underlying buffer. Each chunk is a
822    /// contiguous `&[T]` slice that rayon distributes across threads.
823    #[cfg(feature = "parallel_proc")]
824    pub fn par_chunks(&self, chunk_size: usize) -> rayon::slice::Chunks<'_, T>
825    where
826        T: Send + Sync,
827    {
828        use rayon::prelude::*;
829        self.data.as_slice().par_chunks(chunk_size)
830    }
831
832    /// Iterator over axis-0 observations. Each item is the observation
833    /// index and a zero-copy `NdArrayV` view.
834    #[cfg(feature = "views")]
835    pub fn iter_obs(&self) -> impl Iterator<Item = (usize, NdArrayV<T>)> + '_ {
836        assert!(self.ndim() >= 2, "iter_obs() requires a 2D or higher array");
837        let n_obs = self.shape()[0];
838        (0..n_obs).map(move |i| (i, self.obs(i)))
839    }
840
841    /// Parallel iterator over axis-0 observations. Each item is the
842    /// observation index and a zero-copy `NdArrayV` view.
843    #[cfg(all(feature = "parallel_proc", feature = "views"))]
844    pub fn par_iter_obs(&self) -> impl rayon::iter::ParallelIterator<Item = (usize, NdArrayV<T>)> + '_
845    where
846        T: Send + Sync,
847    {
848        use rayon::prelude::*;
849        assert!(self.ndim() >= 2, "par_iter_obs() requires a 2D or higher array");
850        let n_obs = self.dims.shape()[0];
851        (0..n_obs).into_par_iter().map(move |i| (i, self.obs(i)))
852    }
853
854    /// Iterate one logical axis-0 run identified by its flattened outer
855    /// index. This composes the logical iterator for SuperNdArray without
856    /// materialising its batches.
857    pub(crate) fn iter_axis0_run(&self, run_idx: usize) -> impl ExactSizeIterator<Item = T> + '_ {
858        assert!(self.ndim() > 0, "axis-0 iteration requires an axis 0");
859        let n_runs: usize = self.shape()[1..].iter().product();
860        assert!(run_idx < n_runs, "axis-0 run {} out of bounds ({})", run_idx, n_runs);
861
862        let mut rem = run_idx;
863        let mut offset = 0;
864        for d in 1..self.ndim() {
865            offset += (rem % self.shape()[d]) * self.strides()[d];
866            rem /= self.shape()[d];
867        }
868        let stride = self.strides()[0];
869        (0..self.shape()[0]).map(move |i| self.data.as_slice()[offset + i * stride])
870    }
871}
872
873// *** f64 conversions to the Array/Table/Matrix enum boundary *****
874
875impl NdArray<f64> {
876    /// Convert a 2D NdArray to a Table.
877    ///
878    /// Each column is copied into its own 64-byte aligned `FloatArray<f64>`,
879    /// since the compact tensor layout does not place column starts on
880    /// alignment boundaries. `fields` must have exactly `n_cols` entries
881    /// when supplied. Each supplied field must describe a non-nullable
882    /// [`ArrowType::Float64`] column. `None` generates `col_0`, `col_1`,
883    /// and so on.
884    pub fn to_table(self, fields: Option<Vec<Field>>) -> Result<Table, MinarrowError> {
885        let shape = self.dims.shape();
886        if shape.len() != 2 {
887            return Err(MinarrowError::ShapeError {
888                message: format!("to_table requires a 2D array, got {}D", shape.len()),
889            });
890        }
891        let n_cols = shape[1];
892        let fields = match fields {
893            Some(fields) => {
894                if fields.len() != n_cols {
895                    return Err(MinarrowError::ShapeError {
896                        message: format!(
897                            "to_table: expected {} fields for {} columns, got {}",
898                            n_cols, n_cols, fields.len()
899                        ),
900                    });
901                }
902                for (index, field) in fields.iter().enumerate() {
903                    if field.dtype != ArrowType::Float64 {
904                        return Err(MinarrowError::TypeError {
905                            from: "Field",
906                            to: "Float64 NdArray column",
907                            message: Some(format!(
908                                "to_table: field {} ('{}') has dtype {:?}, expected Float64",
909                                index, field.name, field.dtype
910                            )),
911                        });
912                    }
913                    if field.nullable {
914                        return Err(MinarrowError::NullError {
915                            message: Some(format!(
916                                "to_table: field {} ('{}') is nullable, but NdArray columns are non-nullable",
917                                index, field.name
918                            )),
919                        });
920                    }
921                }
922                fields
923            }
924            None => (0..n_cols)
925                .map(|i| Field::new(format!("col_{}", i), ArrowType::Float64, false, None))
926                .collect(),
927        };
928        // The column copies below read unit-stride axis-0 runs, so a
929        // transposed or otherwise strided layout re-lays first.
930        let this = if self.dims.strides()[0] == 1 { self } else { self.to_contiguous() };
931        let n_rows = this.dims.shape()[0];
932        let stride = this.dims.strides()[1];
933        let name = this.name;
934        let buf = this.data.as_slice();
935
936        let mut cols = Vec::with_capacity(n_cols);
937        for (i, field) in fields.into_iter().enumerate() {
938            let col_start = i * stride;
939            let col: Buffer<f64> = Buffer::from_slice(&buf[col_start..col_start + n_rows]);
940            let float_arr = FloatArray::new(col, None);
941            let array = Array::NumericArray(NumericArray::Float64(Arc::new(float_arr)));
942            cols.push(FieldArray::new(field, array));
943        }
944
945        Ok(Table::new(name.unwrap_or_default(), Some(cols)))
946    }
947
948    /// Convert a 1D NdArray to an Array (FloatArray<f64>).
949    /// Compact, exact-length storage moves across directly. A strided array
950    /// or one with unused backing elements is materialised in logical order.
951    pub fn to_array(self) -> Result<Array, MinarrowError> {
952        if self.ndim() != 1 {
953            return Err(MinarrowError::ShapeError {
954                message: format!("to_array requires a 1D array, got {}D", self.ndim()),
955            });
956        }
957        let buffer = if self.dims.strides()[0] == 1 && self.data.len() == self.len() {
958            Arc::try_unwrap(self.data).unwrap_or_else(|arc| (*arc).clone())
959        } else {
960            Buffer::from_vec64((&self).into_iter().collect())
961        };
962        let float_arr = FloatArray::new(buffer, None);
963        Ok(Array::NumericArray(NumericArray::Float64(Arc::new(float_arr))))
964    }
965
966    /// Convert a 2D NdArray to a Matrix.
967    ///
968    /// Matrix pads each column to a 64-byte boundary for BLAS/LAPACK and
969    /// SIMD access, so compact tensor data is re-laid out into the padded
970    /// form. An array whose stride already matches the padded layout, such
971    /// as one built from a Matrix, moves across zero-copy.
972    #[cfg(feature = "matrix")]
973    pub fn to_matrix(self) -> Result<Matrix, MinarrowError> {
974        let shape = self.dims.shape();
975        if shape.len() != 2 {
976            return Err(MinarrowError::ShapeError {
977                message: format!("to_matrix requires a 2D array, got {}D", shape.len()),
978            });
979        }
980        let n_rows = shape[0];
981        let n_cols = shape[1];
982        let strides = self.dims.strides();
983
984        if strides[0] == 1 && strides[1] == aligned_stride(n_rows) {
985            return Ok(Matrix {
986                n_rows,
987                n_cols,
988                stride: strides[1],
989                data: Arc::try_unwrap(self.data).unwrap_or_else(|arc| (*arc).clone()),
990                name: self.name,
991            });
992        }
993
994        let name = self.name.clone();
995        let compact: Vec64<f64> = self.into_iter().collect();
996        Ok(Matrix::from_f64_unaligned(&compact, n_rows, n_cols, name))
997    }
998}
999
1000// ****************************************************************
1001// NdDims - internal dimension storage
1002// ****************************************************************
1003
1004/// Internal storage for shape and strides.
1005///
1006/// Inline arrays for 1D-5D to avoid heap allocation; the `Dn` variant
1007/// handles 6+ dimensions via boxed slices.
1008#[derive(Clone, PartialEq)]
1009pub(crate) enum NdDims {
1010    D1 { shape: [usize; 1], strides: [usize; 1] },
1011    D2 { shape: [usize; 2], strides: [usize; 2] },
1012    D3 { shape: [usize; 3], strides: [usize; 3] },
1013    D4 { shape: [usize; 4], strides: [usize; 4] },
1014    D5 { shape: [usize; 5], strides: [usize; 5] },
1015    Dn { shape: Box<[usize]>, strides: Box<[usize]> },
1016}
1017
1018impl NdDims {
1019    /// Build dims from a shape slice, computing compact column-major
1020    /// strides.
1021    pub(crate) fn from_shape(shape: &[usize]) -> Self {
1022        let strides = col_major_strides(shape);
1023        Self::from_shape_and_strides(shape, &strides)
1024    }
1025
1026    /// Build dims from explicit shape and strides.
1027    pub(crate) fn from_shape_and_strides(shape: &[usize], strides: &[usize]) -> Self {
1028        assert_eq!(
1029            shape.len(),
1030            strides.len(),
1031            "NdArray: shape rank {} does not match strides rank {}",
1032            shape.len(),
1033            strides.len()
1034        );
1035        match shape.len() {
1036            1 => NdDims::D1 {
1037                shape: [shape[0]],
1038                strides: [strides[0]],
1039            },
1040            2 => NdDims::D2 {
1041                shape: [shape[0], shape[1]],
1042                strides: [strides[0], strides[1]],
1043            },
1044            3 => NdDims::D3 {
1045                shape: [shape[0], shape[1], shape[2]],
1046                strides: [strides[0], strides[1], strides[2]],
1047            },
1048            4 => NdDims::D4 {
1049                shape: [shape[0], shape[1], shape[2], shape[3]],
1050                strides: [strides[0], strides[1], strides[2], strides[3]],
1051            },
1052            5 => NdDims::D5 {
1053                shape: [shape[0], shape[1], shape[2], shape[3], shape[4]],
1054                strides: [strides[0], strides[1], strides[2], strides[3], strides[4]],
1055            },
1056            _ => NdDims::Dn {
1057                shape: shape.into(),
1058                strides: strides.into(),
1059            },
1060        }
1061    }
1062
1063    /// Number of dimensions.
1064    #[inline]
1065    pub(crate) fn ndim(&self) -> usize {
1066        match self {
1067            NdDims::D1 { .. } => 1,
1068            NdDims::D2 { .. } => 2,
1069            NdDims::D3 { .. } => 3,
1070            NdDims::D4 { .. } => 4,
1071            NdDims::D5 { .. } => 5,
1072            NdDims::Dn { shape, .. } => shape.len(),
1073        }
1074    }
1075
1076    /// Shape as a slice.
1077    #[inline]
1078    pub(crate) fn shape(&self) -> &[usize] {
1079        match self {
1080            NdDims::D1 { shape, .. } => shape,
1081            NdDims::D2 { shape, .. } => shape,
1082            NdDims::D3 { shape, .. } => shape,
1083            NdDims::D4 { shape, .. } => shape,
1084            NdDims::D5 { shape, .. } => shape,
1085            NdDims::Dn { shape, .. } => shape,
1086        }
1087    }
1088
1089    /// Strides as a slice.
1090    #[inline]
1091    pub(crate) fn strides(&self) -> &[usize] {
1092        match self {
1093            NdDims::D1 { strides, .. } => strides,
1094            NdDims::D2 { strides, .. } => strides,
1095            NdDims::D3 { strides, .. } => strides,
1096            NdDims::D4 { strides, .. } => strides,
1097            NdDims::D5 { strides, .. } => strides,
1098            NdDims::Dn { strides, .. } => strides,
1099        }
1100    }
1101
1102    /// Total logical element count i.e. the product of all dimensions.
1103    #[inline]
1104    pub(crate) fn len(&self) -> usize {
1105        self.shape().iter().product()
1106    }
1107}
1108
1109// ****************************************************************
1110// Axis selection input
1111// ****************************************************************
1112
1113/// Build an axis-selection slice from mixed indices and ranges.
1114///
1115/// Each entry is any [`DataSelector`](crate::traits::selection::DataSelector) -
1116/// a single index collapses the dimension, and a contiguous range keeps it.
1117///
1118/// # Example
1119/// ```ignore
1120/// arr.slice(nd![0..3, 2, 1..4])
1121/// ```
1122#[macro_export]
1123macro_rules! nd {
1124    ($($sel:expr),+ $(,)?) => {
1125        &[$(&$sel as &dyn $crate::traits::selection::DataSelector),+]
1126    };
1127}
1128
1129// ****************************************************************
1130// Stride computation
1131// ****************************************************************
1132
1133/// Compute compact column-major strides with no inter-dimension padding.
1134///
1135/// For a shape `[a, b, c]`, the strides are `[1, a, a * b]`. The buffer is
1136/// fully contiguous, so DLPack consumers receive a contiguous tensor and range
1137/// indexing over the outermost axis reads logical data with no gaps. The
1138/// backing allocation start remains 64-byte aligned through `Vec64`.
1139pub(crate) fn col_major_strides(shape: &[usize]) -> Vec<usize> {
1140    let mut strides = Vec::with_capacity(shape.len());
1141    if shape.is_empty() {
1142        return strides;
1143    }
1144    strides.push(1);
1145    for d in 1..shape.len() {
1146        strides.push(strides[d - 1] * shape[d - 1]);
1147    }
1148    strides
1149}
1150
1151/// Total buffer length required for a given shape and strides.
1152pub(crate) fn buffer_len(shape: &[usize], strides: &[usize]) -> usize {
1153    if shape.iter().any(|&d| d == 0) {
1154        return 0;
1155    }
1156    // The last element is at sum((shape[d]-1) * strides[d]) for all d.
1157    // Buffer must hold one past that.
1158    let max_offset: usize = shape.iter()
1159        .zip(strides.iter())
1160        .map(|(&s, &st)| (s - 1) * st)
1161        .sum();
1162    max_offset + 1
1163}
1164
1165// ****************************************************************
1166// IntoIterator
1167// ****************************************************************
1168
1169/// Iterating an NdArray yields `T` values in column-major order,
1170/// walking contiguous runs along axis 0 (the innermost dimension)
1171/// and advancing through higher dimensions. Each column/slice is
1172/// a sequential cache-friendly read with no per-element arithmetic.
1173impl<'a, T: Float> IntoIterator for &'a NdArray<T> {
1174    type Item = T;
1175    type IntoIter = NdArrayIter<'a, T>;
1176
1177    #[inline]
1178    fn into_iter(self) -> NdArrayIter<'a, T> {
1179        let shape = self.dims.shape();
1180        let strides = self.dims.strides();
1181        if shape.is_empty() {
1182            return NdArrayIter {
1183                buf: self.data.as_slice(),
1184                n_inner: 1,
1185                inner_stride: 1,
1186                run_offsets: vec![0],
1187                run_idx: 0,
1188                inner_idx: 0,
1189                total: 1,
1190                yielded: 0,
1191            };
1192        }
1193        let n_inner = shape[0];
1194
1195        // Number of contiguous runs = product of all dims except axis 0
1196        let n_runs: usize = shape[1..].iter().product();
1197
1198        // Build the starting offset of each contiguous run.
1199        // For 1D there is one run at offset 0.
1200        // For 2D these are just [0, stride1, 2*stride1, ...].
1201        // For N-D we walk the outer indices in column-major order.
1202        let mut run_offsets = Vec::with_capacity(n_runs);
1203        if shape.len() <= 1 {
1204            run_offsets.push(0);
1205        } else {
1206            let outer_shape = &shape[1..];
1207            let outer_strides = &strides[1..];
1208            let mut outer_indices = vec![0usize; outer_shape.len()];
1209            for _ in 0..n_runs {
1210                let off: usize = outer_indices.iter()
1211                    .zip(outer_strides.iter())
1212                    .map(|(&i, &s)| i * s)
1213                    .sum();
1214                run_offsets.push(off);
1215                // Advance outer indices (column-major)
1216                let mut carry = true;
1217                for d in 0..outer_shape.len() {
1218                    if carry {
1219                        outer_indices[d] += 1;
1220                        if outer_indices[d] < outer_shape[d] {
1221                            carry = false;
1222                        } else {
1223                            outer_indices[d] = 0;
1224                        }
1225                    }
1226                }
1227            }
1228        }
1229
1230        NdArrayIter {
1231            buf: self.data.as_slice(),
1232            n_inner,
1233            inner_stride: strides[0],
1234            run_offsets,
1235            run_idx: 0,
1236            inner_idx: 0,
1237            total: self.len(),
1238            yielded: 0,
1239        }
1240    }
1241}
1242
1243/// Consuming iterator - collects logical elements then iterates.
1244impl<T: Float> IntoIterator for NdArray<T> {
1245    type Item = T;
1246    type IntoIter = std::vec::IntoIter<T>;
1247
1248    #[inline]
1249    fn into_iter(self) -> std::vec::IntoIter<T> {
1250        let v: Vec<T> = (&self).into_iter().collect();
1251        v.into_iter()
1252    }
1253}
1254
1255/// Iterator over NdArray elements in column-major order.
1256///
1257/// When `inner_stride` is 1 (the normal case), walks contiguous runs
1258/// along axis 0 with sequential memory reads. When `inner_stride` > 1
1259/// (e.g. after collapsing axis 0 via slicing), steps through strided
1260/// elements within each run.
1261pub struct NdArrayIter<'a, T> {
1262    pub(crate) buf: &'a [T],
1263    pub(crate) n_inner: usize,
1264    pub(crate) inner_stride: usize,
1265    pub(crate) run_offsets: Vec<usize>,
1266    pub(crate) run_idx: usize,
1267    pub(crate) inner_idx: usize,
1268    pub(crate) total: usize,
1269    pub(crate) yielded: usize,
1270}
1271
1272impl<'a, T: Float> Iterator for NdArrayIter<'a, T> {
1273    type Item = T;
1274
1275    #[inline]
1276    fn next(&mut self) -> Option<T> {
1277        if self.yielded >= self.total { return None; }
1278
1279        let val = self.buf[self.run_offsets[self.run_idx] + self.inner_idx * self.inner_stride];
1280        self.yielded += 1;
1281        self.inner_idx += 1;
1282        if self.inner_idx >= self.n_inner {
1283            self.inner_idx = 0;
1284            self.run_idx += 1;
1285        }
1286        Some(val)
1287    }
1288
1289    #[inline]
1290    fn size_hint(&self) -> (usize, Option<usize>) {
1291        let r = self.total - self.yielded;
1292        (r, Some(r))
1293    }
1294}
1295
1296impl<'a, T: Float> ExactSizeIterator for NdArrayIter<'a, T> {}
1297
1298// ****************************************************************
1299// Internal helpers
1300// ****************************************************************
1301
1302/// Compute flat buffer offset for an N-dimensional index.
1303/// Panics on rank mismatch or an out-of-bounds index, in release
1304/// builds included, since a wrong offset can silently read or write
1305/// another element's slot.
1306#[inline]
1307pub(crate) fn offset_of_impl(indices: &[usize], shape: &[usize], strides: &[usize]) -> usize {
1308    assert_eq!(
1309        indices.len(),
1310        shape.len(),
1311        "NdArray: {} indices for a {}D array",
1312        indices.len(),
1313        shape.len()
1314    );
1315    let mut offset = 0;
1316    for d in 0..shape.len() {
1317        assert!(
1318            indices[d] < shape[d],
1319            "NdArray: index {} out of bounds for dim {} (size {})",
1320            indices[d], d, shape[d]
1321        );
1322        offset += indices[d] * strides[d];
1323    }
1324    offset
1325}
1326
1327// ****************************************************************
1328// Trait implementations
1329// ****************************************************************
1330
1331impl<T: Float> Shape for NdArray<T> {
1332    fn shape(&self) -> ShapeDim {
1333        match self.dims.ndim() {
1334            0 => ShapeDim::Rank0(1),
1335            1 => ShapeDim::Rank1(self.dims.shape()[0]),
1336            2 => ShapeDim::Rank2 {
1337                rows: self.dims.shape()[0],
1338                cols: self.dims.shape()[1],
1339            },
1340            _ => ShapeDim::RankN(self.dims.shape().to_vec()),
1341        }
1342    }
1343}
1344
1345impl<T: Float> Concatenate for NdArray<T> {
1346    /// Concatenate along axis 0. All other dimensions must match.
1347    fn concat(self, other: Self) -> Result<Self, MinarrowError> {
1348        let s1 = self.dims.shape();
1349        let s2 = other.dims.shape();
1350        if s1.len() != s2.len() {
1351            return Err(MinarrowError::IncompatibleTypeError {
1352                from: "NdArray",
1353                to: "NdArray",
1354                message: Some(format!(
1355                    "Cannot concatenate {}D and {}D arrays", s1.len(), s2.len()
1356                )),
1357            });
1358        }
1359        if s1.is_empty() {
1360            return Err(MinarrowError::ShapeError {
1361                message: "Cannot concatenate rank-zero arrays along axis 0".to_string(),
1362            });
1363        }
1364        for d in 1..s1.len() {
1365            if s1[d] != s2[d] {
1366                return Err(MinarrowError::IncompatibleTypeError {
1367                    from: "NdArray",
1368                    to: "NdArray",
1369                    message: Some(format!(
1370                        "Dimension {} mismatch: {} vs {}", d, s1[d], s2[d]
1371                    )),
1372                });
1373            }
1374        }
1375
1376        let mut new_shape: Vec<usize> = s1.to_vec();
1377        new_shape[0] += s2[0];
1378
1379        // The result name joins both sides, matching Table's concat.
1380        let name = match (&self.name, &other.name) {
1381            (Some(a), Some(b)) => Some(format!("{}+{}", a, b)),
1382            (Some(a), None) => Some(a.clone()),
1383            (None, Some(b)) => Some(b.clone()),
1384            (None, None) => None,
1385        };
1386
1387        // 1D: plain append
1388        if s1.len() == 1 {
1389            let mut flat = Vec64::with_capacity(self.len() + other.len());
1390            flat.extend(&self);
1391            flat.extend(&other);
1392            let mut result = NdArray::from_slice(&flat, &new_shape);
1393            result.name = name;
1394            return Ok(result);
1395        }
1396
1397        // Fast path for contiguous 2D: interleave columns with memcpy
1398        if s1.len() == 2 && self.dims.strides()[0] == 1 && other.dims.strides()[0] == 1 {
1399            let new_dims = NdDims::from_shape(&new_shape);
1400            let new_stride = new_dims.strides()[1];
1401            let total = buffer_len(&new_shape, new_dims.strides());
1402            let mut buf = Vec64::with_capacity(total);
1403            buf.0.resize(total, T::default());
1404            let dst = buf.as_mut_slice();
1405            for c in 0..s1[1] {
1406                let dst_start = c * new_stride;
1407                dst[dst_start..dst_start + s1[0]].copy_from_slice(self.col(c));
1408                dst[dst_start + s1[0]..dst_start + s1[0] + s2[0]].copy_from_slice(other.col(c));
1409            }
1410            return Ok(NdArray {
1411                data: Arc::new(Buffer::from_vec64(buf)),
1412                dims: new_dims,
1413                name,
1414            });
1415        }
1416
1417        // General case: interleave the axis-0 run of each operand per
1418        // outer index, walking both sources in column-major logical order.
1419        let n1 = s1[0];
1420        let n2 = s2[0];
1421        let run_len = n1 + n2;
1422        let n_runs: usize = s1[1..].iter().product();
1423        let new_dims = NdDims::from_shape(&new_shape);
1424        let total = buffer_len(&new_shape, new_dims.strides());
1425        let mut buf = Vec64::with_capacity(total);
1426        buf.0.resize(total, T::default());
1427        let dst = buf.as_mut_slice();
1428        let mut it_a = (&self).into_iter();
1429        let mut it_b = (&other).into_iter();
1430        for r in 0..n_runs {
1431            let base = r * run_len;
1432            for i in 0..n1 {
1433                dst[base + i] = it_a.next().unwrap();
1434            }
1435            for i in 0..n2 {
1436                dst[base + n1 + i] = it_b.next().unwrap();
1437            }
1438        }
1439        Ok(NdArray {
1440            data: Arc::new(Buffer::from_vec64(buf)),
1441            dims: new_dims,
1442            name,
1443        })
1444    }
1445}
1446
1447// *** Axis selection: arr.s(nd![1..4, 2]) *************************
1448
1449/// Selection across every axis at once, delegating to `slice`. Single
1450/// indices collapse their dimension, and contiguous ranges keep it.
1451/// Zero-copy.
1452#[cfg(all(feature = "views", feature = "select"))]
1453impl<T: Float> AxisSelection for NdArray<T> {
1454    type View = NdArrayV<T>;
1455
1456    fn s(&self, selection: &[&dyn DataSelector]) -> NdArrayV<T> {
1457        self.slice(selection)
1458    }
1459
1460    fn get_axis_count(&self) -> usize {
1461        self.ndim()
1462    }
1463}
1464
1465// *** Row selection: arr.r(0..10) *********************************
1466
1467/// Axis-0 observation selection. Contiguous ranges return a zero-copy
1468/// window view. Index arrays gather the selected observations into an
1469/// owned array wrapped in a full view, matching `Table`'s behaviour.
1470#[cfg(all(feature = "views", feature = "select"))]
1471impl<T: Float> RowSelection for NdArray<T> {
1472    type View = NdArrayV<T>;
1473
1474    fn r<S: DataSelector>(&self, selection: S) -> NdArrayV<T> {
1475        assert!(self.ndim() > 0, "row selection requires an axis 0");
1476        let n_obs = self.shape()[0];
1477        let indices = selection.resolve_indices(n_obs);
1478        if selection.is_contiguous() {
1479            let start = indices.first().copied().unwrap_or(0);
1480            let ranges: Vec<Range<usize>> = std::iter::once(start..start + indices.len())
1481                .chain(self.shape()[1..].iter().map(|&n| 0..n))
1482                .collect();
1483            let refs: Vec<&dyn DataSelector> = ranges.iter().map(|r| r as _).collect();
1484            return self.slice(&refs);
1485        }
1486        NdArrayV::from_ndarray(gather_obs_impl(
1487            &indices,
1488            self.shape(),
1489            self.name.clone(),
1490            |idx| self.get(idx),
1491        ))
1492    }
1493
1494    fn get_row_count(&self) -> usize {
1495        self.n_obs()
1496    }
1497}
1498
1499/// Materialise selected axis-0 observations into a compact owned array.
1500/// Walks the output positions in column-major order, reading each source
1501/// element through the provided accessor, so any stride layout gathers
1502/// correctly.
1503#[cfg(all(feature = "views", feature = "select"))]
1504pub(crate) fn gather_obs_impl<T: Float>(
1505    indices: &[usize],
1506    shape: &[usize],
1507    name: Option<String>,
1508    get: impl Fn(&[usize]) -> T,
1509) -> NdArray<T> {
1510    let mut out_shape = shape.to_vec();
1511    out_shape[0] = indices.len();
1512    let total: usize = out_shape.iter().product();
1513
1514    let mut flat = Vec64::with_capacity(total);
1515    let ndim = shape.len();
1516    let mut idx = vec![0usize; ndim];
1517    let inner_runs: usize = shape[1..].iter().product::<usize>().max(1);
1518    for _ in 0..inner_runs {
1519        for &obs in indices {
1520            idx[0] = obs;
1521            flat.push(get(&idx));
1522        }
1523        // Advance the inner multi-index in column-major order.
1524        let mut carry = true;
1525        for d in 1..ndim {
1526            if carry {
1527                idx[d] += 1;
1528                if idx[d] < shape[d] {
1529                    carry = false;
1530                } else {
1531                    idx[d] = 0;
1532                }
1533            }
1534        }
1535    }
1536    let mut result = NdArray::from_slice(&flat, &out_shape);
1537    result.name = name;
1538    result
1539}
1540
1541// *** Bracket indexing: arr[col][row] ******************************
1542
1543/// `arr[i]` selects along the outermost stored axis.
1544///
1545/// For 1D, returns a single-element slice.
1546/// For 2D (column-major), `arr[col]` returns the contiguous column
1547/// as `&[f64]`, so `arr[col][row]` gives `&f64`.
1548impl<T: Float> Index<usize> for NdArray<T> {
1549    type Output = [T];
1550
1551    #[inline]
1552    fn index(&self, idx: usize) -> &[T] {
1553        let shape = self.dims.shape();
1554        let strides = self.dims.strides();
1555        match shape.len() {
1556            0 => panic!("NdArray: a rank-zero array has no axis to index with usize"),
1557            1 => {
1558                assert!(idx < shape[0], "NdArray: index {} out of bounds (size {})", idx, shape[0]);
1559                &self.data.as_slice()[idx..idx + 1]
1560            }
1561            2 => {
1562                assert!(idx < shape[1], "NdArray: column {} out of bounds (n_cols {})", idx, shape[1]);
1563                let start = idx * strides[1];
1564                &self.data.as_slice()[start..start + shape[0]]
1565            }
1566            n => {
1567                // Index the outermost axis (last), return the contiguous inner slab
1568                assert!(
1569                    self.is_contiguous(),
1570                    "outermost-axis indexing on 3D+ requires a contiguous layout, use slice() for strided access"
1571                );
1572                let last = n - 1;
1573                assert!(idx < shape[last], "index out of bounds for axis {}", last);
1574                let start = idx * strides[last];
1575                &self.data.as_slice()[start..start + strides[last]]
1576            }
1577        }
1578    }
1579}
1580
1581impl<T: Float> IndexMut<usize> for NdArray<T> {
1582    #[inline]
1583    fn index_mut(&mut self, idx: usize) -> &mut [T] {
1584        let shape = self.dims.shape().to_vec();
1585        let strides = self.dims.strides().to_vec();
1586        match shape.len() {
1587            0 => panic!("NdArray: a rank-zero array has no axis to index with usize"),
1588            1 => {
1589                assert!(idx < shape[0], "NdArray: index {} out of bounds (size {})", idx, shape[0]);
1590                &mut Arc::make_mut(&mut self.data).as_mut_slice()[idx..idx + 1]
1591            }
1592            2 => {
1593                assert!(idx < shape[1], "NdArray: column {} out of bounds (n_cols {})", idx, shape[1]);
1594                let start = idx * strides[1];
1595                let n_rows = shape[0];
1596                &mut Arc::make_mut(&mut self.data).as_mut_slice()[start..start + n_rows]
1597            }
1598            n => {
1599                assert!(
1600                    self.is_contiguous(),
1601                    "outermost-axis indexing on 3D+ requires a contiguous layout, use slice() for strided access"
1602                );
1603                let last = n - 1;
1604                assert!(idx < shape[last], "index out of bounds for axis {}", last);
1605                let start = idx * strides[last];
1606                &mut Arc::make_mut(&mut self.data).as_mut_slice()[start..start + strides[last]]
1607            }
1608        }
1609    }
1610}
1611
1612// *** Range indexing: arr[1..4] ************************************
1613
1614/// `arr[start..end]` selects a contiguous range along the outermost axis.
1615///
1616/// For 1D, returns the element slice directly.
1617/// For 2D and above, selects a range of outermost-axis entries as one
1618/// contiguous slab of logical data. Requires a contiguous layout, since a
1619/// padded or transposed stride pattern has no gap-free slab to return.
1620/// Non-contiguous arrays panic with guidance to use `slice()`.
1621impl<T: Float> Index<Range<usize>> for NdArray<T> {
1622    type Output = [T];
1623
1624    #[inline]
1625    fn index(&self, range: Range<usize>) -> &[T] {
1626        let shape = self.dims.shape();
1627        let strides = self.dims.strides();
1628        match shape.len() {
1629            0 => panic!("NdArray: a rank-zero array has no axis to range-index"),
1630            1 => &self.data.as_slice()[range],
1631            _ => {
1632                assert!(
1633                    self.is_contiguous(),
1634                    "range indexing requires a contiguous layout, use slice() for strided access"
1635                );
1636                let last = shape.len() - 1;
1637                assert!(range.end <= shape[last], "NdArray: range end {} out of bounds (size {})", range.end, shape[last]);
1638                let start = range.start * strides[last];
1639                let end = range.end * strides[last];
1640                &self.data.as_slice()[start..end]
1641            }
1642        }
1643    }
1644}
1645
1646impl<T: Float> IndexMut<Range<usize>> for NdArray<T> {
1647    #[inline]
1648    fn index_mut(&mut self, range: Range<usize>) -> &mut [T] {
1649        let shape = self.dims.shape().to_vec();
1650        let strides = self.dims.strides().to_vec();
1651        match shape.len() {
1652            0 => panic!("NdArray: a rank-zero array has no axis to range-index"),
1653            1 => &mut Arc::make_mut(&mut self.data).as_mut_slice()[range],
1654            _ => {
1655                assert!(
1656                    self.is_contiguous(),
1657                    "range indexing requires a contiguous layout, use slice() for strided access"
1658                );
1659                let last = shape.len() - 1;
1660                assert!(range.end <= shape[last], "NdArray: range end {} out of bounds (size {})", range.end, shape[last]);
1661                let start = range.start * strides[last];
1662                let end = range.end * strides[last];
1663                &mut Arc::make_mut(&mut self.data).as_mut_slice()[start..end]
1664            }
1665        }
1666    }
1667}
1668
1669impl<T: Float> Index<RangeFrom<usize>> for NdArray<T> {
1670    type Output = [T];
1671
1672    #[inline]
1673    fn index(&self, range: RangeFrom<usize>) -> &[T] {
1674        assert!(self.ndim() > 0, "NdArray: a rank-zero array has no axis to range-index");
1675        let last_dim = self.dims.shape().len() - 1;
1676        let end = self.dims.shape()[last_dim];
1677        &self[range.start..end]
1678    }
1679}
1680
1681impl<T: Float> Index<RangeTo<usize>> for NdArray<T> {
1682    type Output = [T];
1683
1684    #[inline]
1685    fn index(&self, range: RangeTo<usize>) -> &[T] {
1686        &self[0..range.end]
1687    }
1688}
1689
1690impl<T: Float> Index<RangeFull> for NdArray<T> {
1691    type Output = [T];
1692
1693    #[inline]
1694    fn index(&self, _: RangeFull) -> &[T] {
1695        assert!(self.ndim() > 0, "NdArray: a rank-zero array has no axis to range-index");
1696        let last_dim = self.dims.shape().len() - 1;
1697        let end = self.dims.shape()[last_dim];
1698        &self[0..end]
1699    }
1700}
1701
1702// *** Tuple indexing **********************************************
1703
1704impl<T: Float> Index<()> for NdArray<T> {
1705    type Output = T;
1706    #[inline]
1707    fn index(&self, (): ()) -> &T {
1708        &self.data.as_slice()[self.offset_of(&[])]
1709    }
1710}
1711
1712impl<T: Float> Index<(usize,)> for NdArray<T> {
1713    type Output = T;
1714    #[inline]
1715    fn index(&self, (i,): (usize,)) -> &T {
1716        &self.data.as_slice()[self.offset_of(&[i])]
1717    }
1718}
1719
1720impl<T: Float> Index<(usize, usize)> for NdArray<T> {
1721    type Output = T;
1722    #[inline]
1723    fn index(&self, (i, j): (usize, usize)) -> &T {
1724        &self.data.as_slice()[self.offset_of(&[i, j])]
1725    }
1726}
1727
1728impl<T: Float> Index<(usize, usize, usize)> for NdArray<T> {
1729    type Output = T;
1730    #[inline]
1731    fn index(&self, (i, j, k): (usize, usize, usize)) -> &T {
1732        &self.data.as_slice()[self.offset_of(&[i, j, k])]
1733    }
1734}
1735
1736impl<T: Float> Index<(usize, usize, usize, usize)> for NdArray<T> {
1737    type Output = T;
1738    #[inline]
1739    fn index(&self, (i, j, k, l): (usize, usize, usize, usize)) -> &T {
1740        &self.data.as_slice()[self.offset_of(&[i, j, k, l])]
1741    }
1742}
1743
1744impl<T: Float> Index<(usize, usize, usize, usize, usize)> for NdArray<T> {
1745    type Output = T;
1746    #[inline]
1747    fn index(&self, (i, j, k, l, m): (usize, usize, usize, usize, usize)) -> &T {
1748        &self.data.as_slice()[self.offset_of(&[i, j, k, l, m])]
1749    }
1750}
1751
1752impl<T: Float> IndexMut<(usize,)> for NdArray<T> {
1753    #[inline]
1754    fn index_mut(&mut self, (i,): (usize,)) -> &mut T {
1755        let off = self.offset_of(&[i]);
1756        &mut Arc::make_mut(&mut self.data).as_mut_slice()[off]
1757    }
1758}
1759
1760impl<T: Float> IndexMut<()> for NdArray<T> {
1761    #[inline]
1762    fn index_mut(&mut self, (): ()) -> &mut T {
1763        let off = self.offset_of(&[]);
1764        &mut Arc::make_mut(&mut self.data).as_mut_slice()[off]
1765    }
1766}
1767
1768impl<T: Float> IndexMut<(usize, usize)> for NdArray<T> {
1769    #[inline]
1770    fn index_mut(&mut self, (i, j): (usize, usize)) -> &mut T {
1771        let off = self.offset_of(&[i, j]);
1772        &mut Arc::make_mut(&mut self.data).as_mut_slice()[off]
1773    }
1774}
1775
1776impl<T: Float> IndexMut<(usize, usize, usize)> for NdArray<T> {
1777    #[inline]
1778    fn index_mut(&mut self, (i, j, k): (usize, usize, usize)) -> &mut T {
1779        let off = self.offset_of(&[i, j, k]);
1780        &mut Arc::make_mut(&mut self.data).as_mut_slice()[off]
1781    }
1782}
1783
1784impl<T: Float> IndexMut<(usize, usize, usize, usize)> for NdArray<T> {
1785    #[inline]
1786    fn index_mut(&mut self, (i, j, k, l): (usize, usize, usize, usize)) -> &mut T {
1787        let off = self.offset_of(&[i, j, k, l]);
1788        &mut Arc::make_mut(&mut self.data).as_mut_slice()[off]
1789    }
1790}
1791
1792impl<T: Float> IndexMut<(usize, usize, usize, usize, usize)> for NdArray<T> {
1793    #[inline]
1794    fn index_mut(&mut self, (i, j, k, l, m): (usize, usize, usize, usize, usize)) -> &mut T {
1795        let off = self.offset_of(&[i, j, k, l, m]);
1796        &mut Arc::make_mut(&mut self.data).as_mut_slice()[off]
1797    }
1798}
1799
1800// *** From conversions ********************************************
1801
1802/// 1D from a flat slice.
1803impl<T: Float> From<&[T]> for NdArray<T> {
1804    fn from(data: &[T]) -> Self {
1805        NdArray {
1806            data: Arc::new(Buffer::from_slice(data)),
1807            dims: NdDims::from_shape(&[data.len()]),
1808            name: None,
1809        }
1810    }
1811}
1812
1813/// 1D from owned Vec64.
1814impl<T: Float> From<Vec64<T>> for NdArray<T> {
1815    fn from(v: Vec64<T>) -> Self {
1816        let n = v.len();
1817        NdArray {
1818            data: Arc::new(Buffer::from_vec64(v)),
1819            dims: NdDims::from_shape(&[n]),
1820            name: None,
1821        }
1822    }
1823}
1824
1825/// 2D from column vectors.
1826impl<T: Float> From<&[Vec<T>]> for NdArray<T> {
1827    fn from(columns: &[Vec<T>]) -> Self {
1828        let n_cols = columns.len();
1829        if n_cols == 0 {
1830            return NdArray::new(&[0, 0]);
1831        }
1832        let n_rows = columns[0].len();
1833        for col in columns {
1834            assert_eq!(col.len(), n_rows, "Column length mismatch");
1835        }
1836        let shape = [n_rows, n_cols];
1837        let dims = NdDims::from_shape(&shape);
1838        let stride = dims.strides()[1];
1839        let total = buffer_len(&shape, dims.strides());
1840        let mut buf = Vec64::with_capacity(total);
1841        buf.0.resize(total, T::default());
1842        for (c, col) in columns.iter().enumerate() {
1843            let start = c * stride;
1844            buf.as_mut_slice()[start..start + n_rows].copy_from_slice(col);
1845        }
1846        NdArray { data: Arc::new(Buffer::from_vec64(buf)), dims, name: None }
1847    }
1848}
1849
1850/// 2D from FloatArray columns.
1851impl<T: Float> From<&[FloatArray<T>]> for NdArray<T> {
1852    fn from(columns: &[FloatArray<T>]) -> Self {
1853        let n_cols = columns.len();
1854        if n_cols == 0 {
1855            return NdArray::new(&[0, 0]);
1856        }
1857        let n_rows = columns[0].data.len();
1858        for col in columns {
1859            assert_eq!(col.data.len(), n_rows, "Column length mismatch");
1860        }
1861        let shape = [n_rows, n_cols];
1862        let dims = NdDims::from_shape(&shape);
1863        let stride = dims.strides()[1];
1864        let total = buffer_len(&shape, dims.strides());
1865        let mut buf = Vec64::with_capacity(total);
1866        buf.0.resize(total, T::default());
1867        for (c, col) in columns.iter().enumerate() {
1868            let start = c * stride;
1869            buf.as_mut_slice()[start..start + n_rows].copy_from_slice(col.data.as_slice());
1870        }
1871        NdArray { data: Arc::new(Buffer::from_vec64(buf)), dims, name: None }
1872    }
1873}
1874
1875/// From Matrix - zero-copy, moves the Buffer straight across. The Matrix's
1876/// padded column stride carries through, so the resulting array reports
1877/// non-contiguous. Call `to_contiguous` to re-lay out compactly.
1878#[cfg(feature = "matrix")]
1879impl From<Matrix> for NdArray<f64> {
1880    fn from(mat: Matrix) -> Self {
1881        let shape = [mat.n_rows, mat.n_cols];
1882        let strides = [1, mat.stride];
1883        NdArray {
1884            data: Arc::new(mat.data),
1885            dims: NdDims::from_shape_and_strides(&shape, &strides),
1886            name: mat.name,
1887        }
1888    }
1889}
1890
1891/// TryFrom Table - extracts numeric columns, converts nulls to NaN.
1892impl TryFrom<&Table> for NdArray<f64> {
1893    type Error = MinarrowError;
1894
1895    fn try_from(table: &Table) -> Result<Self, Self::Error> {
1896        let n_cols = table.n_cols();
1897        let n_rows = table.n_rows;
1898        if n_cols == 0 {
1899            return Ok(NdArray::new(&[0, 0]));
1900        }
1901
1902        let shape = [n_rows, n_cols];
1903        let dims = NdDims::from_shape(&shape);
1904        let stride = dims.strides()[1];
1905        let total = buffer_len(&shape, dims.strides());
1906        let mut buf = Vec64::with_capacity(total);
1907        buf.0.resize(total, 0.0);
1908
1909        for (col_idx, fa) in table.cols.iter().enumerate() {
1910            let numeric = fa.array.try_num().map_err(|_| MinarrowError::TypeError {
1911                from: "non-numeric",
1912                to: "Float64",
1913                message: Some(format!("column {} is not numeric", col_idx)),
1914            })?;
1915            let f64_arr = numeric.try_f64()?;
1916            if f64_arr.data.len() != n_rows {
1917                return Err(MinarrowError::ColumnLengthMismatch {
1918                    col: col_idx,
1919                    expected: n_rows,
1920                    found: f64_arr.data.len(),
1921                });
1922            }
1923
1924            let start = col_idx * stride;
1925            let src = f64_arr.data.as_slice();
1926            let dst = &mut buf.as_mut_slice()[start..start + n_rows];
1927
1928            // Copy data, converting nulls to NaN
1929            match f64_arr.null_mask.as_ref() {
1930                Some(mask) => {
1931                    for i in 0..n_rows {
1932                        dst[i] = if mask.get(i) { src[i] } else { f64::NAN };
1933                    }
1934                }
1935                None => dst.copy_from_slice(src),
1936            }
1937        }
1938
1939        let name = if table.name.is_empty() { None } else { Some(table.name.clone()) };
1940        Ok(NdArray { data: Arc::new(Buffer::from_vec64(buf)), dims, name })
1941    }
1942}
1943
1944impl TryFrom<Table> for NdArray<f64> {
1945    type Error = MinarrowError;
1946    fn try_from(table: Table) -> Result<Self, Self::Error> {
1947        NdArray::try_from(&table)
1948    }
1949}
1950
1951// *** Debug *******************************************************
1952
1953impl<T: Float> fmt::Debug for NdArray<T> {
1954    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1955        write!(
1956            f, "NdArray{}: {:?} [{}D, col-major]",
1957            self.name.as_deref().map_or(String::new(), |n| format!(" '{}'", n)),
1958            self.dims.shape(),
1959            self.ndim(),
1960        )?;
1961        if self.ndim() == 0 {
1962            write!(f, "\n{:8.4}", self.get(&[]).to_f64().unwrap_or(f64::NAN))?;
1963        } else if self.ndim() == 2 {
1964            let shape = self.dims.shape();
1965            let max_rows = shape[0].min(6);
1966            let max_cols = shape[1].min(8);
1967            for r in 0..max_rows {
1968                write!(f, "\n[")?;
1969                for c in 0..max_cols {
1970                    write!(f, " {:8.4}", self.get(&[r, c]).to_f64().unwrap_or(f64::NAN))?;
1971                    if c < max_cols - 1 { write!(f, ",")?; }
1972                }
1973                if shape[1] > 8 { write!(f, " ...")?; }
1974                write!(f, " ]")?;
1975            }
1976            if shape[0] > 6 { write!(f, "\n...")?; }
1977        } else if self.ndim() == 1 {
1978            let n = self.dims.shape()[0].min(10);
1979            write!(f, "\n[")?;
1980            for i in 0..n {
1981                write!(f, " {:8.4}", self.get(&[i]).to_f64().unwrap_or(f64::NAN))?;
1982                if i < n - 1 { write!(f, ",")?; }
1983            }
1984            if self.dims.shape()[0] > 10 { write!(f, " ...")?; }
1985            write!(f, " ]")?;
1986        }
1987        Ok(())
1988    }
1989}
1990
1991impl<T: Float + Display> Display for NdArray<T> {
1992    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1993        let shape = self.shape();
1994        let elem = std::any::type_name::<T>();
1995        let dims = if shape.is_empty() {
1996            String::from("scalar")
1997        } else {
1998            shape.iter().map(|d| d.to_string()).collect::<Vec<_>>().join(" × ")
1999        };
2000        match &self.name {
2001            Some(name) => writeln!(f, "NdArray \"{}\" [{}, {}]", name, dims, elem)?,
2002            None => writeln!(f, "NdArray [{}, {}]", dims, elem)?,
2003        }
2004        print_ndarray_body(f, shape, |index| self.get(index))
2005    }
2006}
2007
2008impl TryFrom<NdArray<f64>> for Table {
2009    type Error = MinarrowError;
2010
2011    /// Presents the leading axis as rows and the trailing axis as columns,
2012    /// naming the columns by position. Shapes that do not lay out as a
2013    /// table report the reason.
2014    fn try_from(value: NdArray<f64>) -> Result<Self, Self::Error> {
2015        value.to_table(None)
2016    }
2017}
2018
2019impl TryFrom<NdArray<f64>> for Array {
2020    type Error = MinarrowError;
2021
2022    /// Reads a one-dimensional array as a single column.
2023    fn try_from(value: NdArray<f64>) -> Result<Self, Self::Error> {
2024        value.to_array()
2025    }
2026}
2027
2028#[cfg(feature = "matrix")]
2029impl TryFrom<NdArray<f64>> for Matrix {
2030    type Error = MinarrowError;
2031
2032    /// Reads a two-dimensional array as a contiguous matrix.
2033    fn try_from(value: NdArray<f64>) -> Result<Self, Self::Error> {
2034        value.to_matrix()
2035    }
2036}
2037
2038#[cfg(test)]
2039mod tests {
2040    use super::*;
2041    use crate::StringArray;
2042    use crate::structs::bitmask::Bitmask;
2043
2044    // *** Row selection and apply *************************************
2045
2046    #[cfg(all(feature = "views", feature = "select"))]
2047    #[test]
2048    fn axis_selection_rank10() {
2049        let shape = [3, 4, 2, 5, 3, 2, 4, 3, 2, 5];
2050        let len: usize = shape.iter().product();
2051        let data: Vec<f64> = (0..len).map(|i| i as f64).collect();
2052        let a = NdArray::from_slice(&data, &shape);
2053
2054        // Mixed ranges, single indices, and a full range across all ten axes.
2055        let v = a.s(nd![1..3, 0..2, 1, 2..5, .., 0..1, 1..3, 2, 0..2, 3..5]);
2056        assert_eq!(v.shape(), &[2, 2, 3, 3, 1, 2, 2, 2]);
2057        assert_eq!(
2058            v.get(&[0, 0, 0, 0, 0, 0, 0, 0]),
2059            a.get(&[1, 0, 1, 2, 0, 0, 1, 2, 0, 3])
2060        );
2061        assert_eq!(
2062            v.get(&[1, 1, 2, 2, 0, 1, 1, 1]),
2063            a.get(&[2, 1, 1, 4, 2, 0, 2, 2, 1, 4])
2064        );
2065    }
2066
2067    #[cfg(all(feature = "views", feature = "select"))]
2068    #[test]
2069    fn axis_selection_runtime_rank() {
2070        // Selections built at runtime for ranks beyond literal syntax.
2071        let mut shape = vec![1usize; 100];
2072        shape[0] = 3;
2073        shape[10] = 4;
2074        shape[50] = 5;
2075        shape[99] = 2;
2076        let len: usize = shape.iter().product();
2077        let data: Vec<f64> = (0..len).map(|i| i as f64).collect();
2078        let a = NdArray::from_slice(&data, &shape);
2079
2080        // Full range on every axis, then narrow three and collapse one.
2081        let mut sels: Vec<Box<dyn DataSelector>> =
2082            shape.iter().map(|&n| Box::new(0..n) as Box<dyn DataSelector>).collect();
2083        sels[0] = Box::new(1..3);
2084        sels[10] = Box::new(2usize);
2085        sels[50] = Box::new(1..4);
2086        let refs: Vec<&dyn DataSelector> = sels.iter().map(|s| s.as_ref()).collect();
2087
2088        let v = a.s(&refs);
2089        assert_eq!(v.ndim(), 99);
2090        assert_eq!(v.shape()[0], 2);
2091        assert_eq!(v.shape()[49], 3);
2092        assert_eq!(v.shape()[98], 2);
2093
2094        let mut view_idx = vec![0usize; 99];
2095        view_idx[0] = 1;
2096        view_idx[49] = 2;
2097        view_idx[98] = 1;
2098        let mut source_idx = vec![0usize; 100];
2099        source_idx[0] = 2;
2100        source_idx[10] = 2;
2101        source_idx[50] = 3;
2102        source_idx[99] = 1;
2103        assert_eq!(v.get(&view_idx), a.get(&source_idx));
2104    }
2105
2106    #[cfg(all(feature = "views", feature = "select"))]
2107    #[test]
2108    fn axis_selection_trait() {
2109        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
2110        // Range keeps the axis, index collapses it.
2111        let v = a.s(nd![1..3, 1]);
2112        assert_eq!(v.shape(), &[2]);
2113        assert_eq!(v.get(&[0]), 5.0);
2114        assert_eq!(v.get(&[1]), 6.0);
2115        // Selection composes on the view.
2116        let sub = a.s(nd![0..3, 0..2]).s(nd![2, 0..2]);
2117        assert_eq!(sub.shape(), &[2]);
2118        assert_eq!(sub.get(&[0]), 3.0);
2119        assert_eq!(sub.get(&[1]), 6.0);
2120        assert_eq!(a.get_axis_count(), 2);
2121        assert_eq!(sub.get_axis_count(), 1);
2122    }
2123
2124    #[cfg(all(feature = "views", feature = "select"))]
2125    #[test]
2126    fn row_selection_contiguous() {
2127        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
2128        let v = a.r(1..3);
2129        assert_eq!(v.shape(), &[2, 2]);
2130        assert_eq!(v.get(&[0, 0]), 2.0);
2131        assert_eq!(v.get(&[1, 1]), 6.0);
2132        // The row alias selects a single observation.
2133        let single = a.row(2);
2134        assert_eq!(single.shape(), &[1, 2]);
2135        assert_eq!(single.get(&[0, 1]), 6.0);
2136    }
2137
2138    #[cfg(all(feature = "views", feature = "select"))]
2139    #[test]
2140    fn row_selection_gathers_indices() {
2141        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
2142        let v = a.r(&[2, 0]);
2143        assert_eq!(v.shape(), &[2, 2]);
2144        // Gathered rows follow selection order.
2145        assert_eq!(v.get(&[0, 0]), 3.0);
2146        assert_eq!(v.get(&[0, 1]), 6.0);
2147        assert_eq!(v.get(&[1, 0]), 1.0);
2148        assert_eq!(v.get(&[1, 1]), 4.0);
2149    }
2150
2151    #[test]
2152    fn apply_maps_elements() {
2153        let mut a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
2154        a.set_name("m");
2155        let b = a.apply(|x| x * 10.0);
2156        assert_eq!(b.get(&[1, 1]), 40.0);
2157        assert_eq!(b.name.as_deref(), Some("m"));
2158        // The source is untouched.
2159        assert_eq!(a.get(&[1, 1]), 4.0);
2160    }
2161
2162    #[test]
2163    fn apply_mut_in_place() {
2164        let mut a = NdArray::from_slice(&[1.0, 2.0, 3.0], &[3]);
2165        a.apply_mut(|x| x + 0.5);
2166        assert_eq!((&a).into_iter().collect::<Vec<f64>>(), vec![1.5, 2.5, 3.5]);
2167    }
2168
2169    #[cfg(feature = "matrix")]
2170    #[test]
2171    fn apply_mut_non_contiguous_touches_logical_only() {
2172        // A Matrix-imported array carries stride padding. The logical walk
2173        // mutates only real elements.
2174        let mat = Matrix::from_f64_unaligned(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 3, 2, None);
2175        let mut a = NdArray::from(mat);
2176        assert!(!a.is_contiguous());
2177        a.apply_mut(|x| x * 2.0);
2178        assert_eq!(a.get(&[0, 0]), 2.0);
2179        assert_eq!(a.get(&[2, 1]), 12.0);
2180    }
2181
2182    #[cfg(feature = "views")]
2183    #[test]
2184    fn apply_axis_collapses_axis() {
2185        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
2186        // Sum each column lane (axis 0) - output shape [2].
2187        let col_sums = a.apply_axis(0, |lane| (&lane).into_iter().sum());
2188        assert_eq!(col_sums.shape(), &[2]);
2189        assert_eq!(col_sums.get(&[0]), 6.0);
2190        assert_eq!(col_sums.get(&[1]), 15.0);
2191        // Sum each row lane (axis 1) - output shape [3].
2192        let row_sums = a.apply_axis(1, |lane| (&lane).into_iter().sum());
2193        assert_eq!(row_sums.shape(), &[3]);
2194        assert_eq!(row_sums.get(&[0]), 5.0);
2195        assert_eq!(row_sums.get(&[2]), 9.0);
2196    }
2197
2198    #[cfg(feature = "views")]
2199    #[test]
2200    fn apply_axis_3d() {
2201        let data: Vec<f64> = (1..=24).map(|x| x as f64).collect();
2202        let a = NdArray::from_slice(&data, &[2, 3, 4]);
2203        let maxes = a.apply_axis(1, |lane| {
2204            (&lane).into_iter().fold(f64::MIN, f64::max)
2205        });
2206        assert_eq!(maxes.shape(), &[2, 4]);
2207        // Lane over axis 1 at [0, .., 0] holds values at [0,j,0].
2208        assert_eq!(maxes.get(&[0, 0]), a.get(&[0, 2, 0]));
2209        assert_eq!(maxes.get(&[1, 3]), a.get(&[1, 2, 3]));
2210    }
2211
2212    // ****************************************************************
2213    // Construction
2214    // ****************************************************************
2215
2216    #[test]
2217    fn new_zeroed_1d() {
2218        let a = NdArray::<f64>::new(&[5]);
2219        assert_eq!(a.ndim(), 1);
2220        assert_eq!(a.shape(), &[5]);
2221        assert_eq!(a.len(), 5);
2222        assert!(!a.is_empty());
2223        for v in &a { assert_eq!(v, 0.0); }
2224    }
2225
2226    #[test]
2227    fn new_zeroed_2d() {
2228        let a = NdArray::<f64>::new(&[3, 4]);
2229        assert_eq!(a.ndim(), 2);
2230        assert_eq!(a.shape(), &[3, 4]);
2231        assert_eq!(a.len(), 12);
2232        for v in &a { assert_eq!(v, 0.0); }
2233    }
2234
2235    #[test]
2236    fn new_zeroed_3d() {
2237        let a = NdArray::<f64>::new(&[2, 3, 4]);
2238        assert_eq!(a.ndim(), 3);
2239        assert_eq!(a.shape(), &[2, 3, 4]);
2240        assert_eq!(a.len(), 24);
2241    }
2242
2243    #[test]
2244    fn new_zeroed_5d() {
2245        let a = NdArray::<f64>::new(&[2, 3, 4, 5, 6]);
2246        assert_eq!(a.ndim(), 5);
2247        assert_eq!(a.len(), 720);
2248    }
2249
2250    #[test]
2251    fn new_zeroed_6d_heap() {
2252        let a = NdArray::<f64>::new(&[2, 3, 2, 2, 2, 2]);
2253        assert_eq!(a.ndim(), 6);
2254        assert_eq!(a.len(), 96);
2255    }
2256
2257    #[test]
2258    fn new_named() {
2259        let a = NdArray::<f64>::new_named(&[3, 3], "covariance");
2260        assert_eq!(a.name.as_deref(), Some("covariance"));
2261    }
2262
2263    #[test]
2264    fn from_slice_1d() {
2265        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
2266        let a = NdArray::from_slice(&data, &[5]);
2267        assert_eq!(a.len(), 5);
2268        let vals: Vec<f64> = (&a).into_iter().collect();
2269        assert_eq!(vals, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
2270    }
2271
2272    #[test]
2273    fn from_slice_2d_column_major() {
2274        let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
2275        let a = NdArray::from_slice(&data, &[3, 2]);
2276        assert_eq!(a.get(&[0, 0]), 1.0);
2277        assert_eq!(a.get(&[1, 0]), 2.0);
2278        assert_eq!(a.get(&[2, 0]), 3.0);
2279        assert_eq!(a.get(&[0, 1]), 4.0);
2280        assert_eq!(a.get(&[1, 1]), 5.0);
2281        assert_eq!(a.get(&[2, 1]), 6.0);
2282    }
2283
2284    #[test]
2285    fn fill_and_ones() {
2286        let a = NdArray::fill(&[2, 3], 7.0);
2287        assert_eq!(a.len(), 6);
2288        for v in &a { assert_eq!(v, 7.0); }
2289
2290        let b = NdArray::<f64>::ones(&[4]);
2291        for v in &b { assert_eq!(v, 1.0); }
2292    }
2293
2294    #[test]
2295    fn eye_identity() {
2296        let a = NdArray::<f64>::eye(3);
2297        assert_eq!(a.shape(), &[3, 3]);
2298        assert_eq!(a.get(&[0, 0]), 1.0);
2299        assert_eq!(a.get(&[1, 1]), 1.0);
2300        assert_eq!(a.get(&[2, 2]), 1.0);
2301        assert_eq!(a.get(&[0, 1]), 0.0);
2302        assert_eq!(a.get(&[1, 0]), 0.0);
2303    }
2304
2305    #[test]
2306    fn linspace_basic() {
2307        let a = NdArray::<f64>::linspace(0.0, 1.0, 5);
2308        assert_eq!(a.shape(), &[5]);
2309        assert_eq!(a.get(&[0]), 0.0);
2310        assert_eq!(a.get(&[4]), 1.0);
2311        assert!((a.get(&[2]) - 0.5).abs() < 1e-15);
2312    }
2313
2314    #[test]
2315    fn arange_basic() {
2316        let a = NdArray::arange(0.0, 0.5, 4);
2317        assert_eq!(a.shape(), &[4]);
2318        assert_eq!(a.get(&[0]), 0.0);
2319        assert_eq!(a.get(&[1]), 0.5);
2320        assert_eq!(a.get(&[2]), 1.0);
2321        assert_eq!(a.get(&[3]), 1.5);
2322    }
2323
2324    #[test]
2325    fn from_vec64_moves_data() {
2326        let data: Vec64<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0].into();
2327        let a = NdArray::from_vec64(data, &[3, 2]);
2328        assert_eq!(a.shape(), &[3, 2]);
2329        assert_eq!(a.get(&[2, 1]), 6.0);
2330    }
2331
2332    #[cfg(feature = "views")]
2333    #[test]
2334    fn iter_obs_walks_observations() {
2335        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
2336        let rows: Vec<(usize, Vec<f64>)> = a
2337            .iter_obs()
2338            .map(|(i, v)| (i, (&v).into_iter().collect()))
2339            .collect();
2340        assert_eq!(rows.len(), 3);
2341        assert_eq!(rows[0], (0, vec![1.0, 4.0]));
2342        assert_eq!(rows[2], (2, vec![3.0, 6.0]));
2343    }
2344
2345    #[test]
2346    fn equality_ignores_name() {
2347        let a = NdArray::from_slice(&[1.0, 2.0], &[2]);
2348        let mut b = a.clone();
2349        b.name = Some("named".to_string());
2350        assert_eq!(a, b);
2351    }
2352
2353    #[test]
2354    fn name_survives_reshape_and_concat() {
2355        let mut a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0], &[4]);
2356        a.name = Some("px".to_string());
2357        assert_eq!(a.reshape(&[2, 2]).unwrap().name.as_deref(), Some("px"));
2358
2359        let mut b = NdArray::from_slice(&[5.0, 6.0], &[2]);
2360        b.name = Some("qty".to_string());
2361        let joined = a.concat(b).unwrap();
2362        assert_eq!(joined.name.as_deref(), Some("px+qty"));
2363    }
2364
2365    // ****************************************************************
2366    // Element access and indexing
2367    // ****************************************************************
2368
2369    #[test]
2370    fn get_set_1d() {
2371        let mut a = NdArray::new(&[3]);
2372        a.set(&[0], 10.0);
2373        a.set(&[1], 20.0);
2374        a.set(&[2], 30.0);
2375        assert_eq!(a.get(&[0]), 10.0);
2376        assert_eq!(a.get(&[1]), 20.0);
2377        assert_eq!(a.get(&[2]), 30.0);
2378    }
2379
2380    #[test]
2381    fn tuple_index_1d() {
2382        let a = NdArray::from_slice(&[10.0, 20.0, 30.0], &[3]);
2383        assert_eq!(a[(0,)], 10.0);
2384        assert_eq!(a[(1,)], 20.0);
2385        assert_eq!(a[(2,)], 30.0);
2386    }
2387
2388    #[test]
2389    fn tuple_index_2d() {
2390        let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
2391        let a = NdArray::from_slice(&data, &[3, 2]);
2392        assert_eq!(a[(0, 0)], 1.0);
2393        assert_eq!(a[(2, 0)], 3.0);
2394        assert_eq!(a[(0, 1)], 4.0);
2395        assert_eq!(a[(2, 1)], 6.0);
2396    }
2397
2398    #[test]
2399    fn tuple_index_3d() {
2400        let data: Vec<f64> = (1..=12).map(|x| x as f64).collect();
2401        let a = NdArray::from_slice(&data, &[2, 3, 2]);
2402        assert_eq!(a[(0, 0, 0)], 1.0);
2403        assert_eq!(a[(1, 0, 0)], 2.0);
2404        assert_eq!(a[(0, 1, 0)], 3.0);
2405        assert_eq!(a[(1, 1, 0)], 4.0);
2406        assert_eq!(a[(0, 2, 0)], 5.0);
2407        assert_eq!(a[(1, 2, 0)], 6.0);
2408        assert_eq!(a[(0, 0, 1)], 7.0);
2409        assert_eq!(a[(1, 2, 1)], 12.0);
2410    }
2411
2412    #[test]
2413    fn index_mut_2d() {
2414        let mut a = NdArray::new(&[2, 2]);
2415        a[(0, 0)] = 1.0;
2416        a[(1, 0)] = 2.0;
2417        a[(0, 1)] = 3.0;
2418        a[(1, 1)] = 4.0;
2419        assert_eq!(a[(0, 0)], 1.0);
2420        assert_eq!(a[(1, 1)], 4.0);
2421    }
2422
2423    // ****************************************************************
2424    // Iteration
2425    // ****************************************************************
2426
2427    #[test]
2428    fn iter_1d() {
2429        let a = NdArray::from_slice(&[10.0, 20.0, 30.0], &[3]);
2430        let vals: Vec<f64> = (&a).into_iter().collect();
2431        assert_eq!(vals, vec![10.0, 20.0, 30.0]);
2432    }
2433
2434    #[test]
2435    fn iter_2d_column_major_order() {
2436        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
2437        let vals: Vec<f64> = (&a).into_iter().collect();
2438        assert_eq!(vals, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
2439    }
2440
2441    #[test]
2442    fn iter_2d_compact_layout() {
2443        // Compact strides place columns back to back, so iteration reads
2444        // the buffer straight through.
2445        let data: Vec<f64> = (1..=30).map(|x| x as f64).collect();
2446        let a = NdArray::from_slice(&data, &[10, 3]);
2447        assert_eq!(a.strides()[1], 10);
2448        let vals: Vec<f64> = (&a).into_iter().collect();
2449        assert_eq!(vals.len(), 30);
2450        assert_eq!(&vals[..10], &data[..10]);
2451        assert_eq!(&vals[10..20], &data[10..20]);
2452        assert_eq!(&vals[20..30], &data[20..30]);
2453    }
2454
2455    #[test]
2456    fn iter_3d_column_major_order() {
2457        let data: Vec<f64> = (1..=24).map(|x| x as f64).collect();
2458        let a = NdArray::from_slice(&data, &[2, 3, 4]);
2459        let vals: Vec<f64> = (&a).into_iter().collect();
2460        assert_eq!(vals.len(), 24);
2461        assert_eq!(vals, data);
2462    }
2463
2464    #[test]
2465    fn iter_exact_size() {
2466        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
2467        let iter = (&a).into_iter();
2468        assert_eq!(iter.len(), 6);
2469    }
2470
2471    #[test]
2472    fn consuming_into_iter() {
2473        let a = NdArray::from_slice(&[1.0, 2.0, 3.0], &[3]);
2474        let vals: Vec<f64> = a.into_iter().collect();
2475        assert_eq!(vals, vec![1.0, 2.0, 3.0]);
2476    }
2477
2478    // ****************************************************************
2479    // Shape introspection
2480    // ****************************************************************
2481
2482    #[test]
2483    fn rank_zero_scalar_semantics() {
2484        let mut a = NdArray::from_slice(&[5.0], &[]);
2485        assert_eq!(a.ndim(), 0);
2486        assert!(a.shape().is_empty());
2487        assert!(a.strides().is_empty());
2488        assert_eq!(a.len(), 1);
2489        assert!(!a.is_empty());
2490        assert!(a.is_contiguous());
2491        assert_eq!(a[()], 5.0);
2492        assert_eq!((&a).into_iter().collect::<Vec<_>>(), vec![5.0]);
2493        assert_eq!(Shape::shape(&a), ShapeDim::Rank0(1));
2494
2495        a[()] = 7.0;
2496        assert_eq!(a[()], 7.0);
2497        assert!(a.transpose().shape().is_empty());
2498        assert_eq!(a.reshape(&[1]).unwrap().get(&[0]), 7.0);
2499    }
2500
2501    #[cfg(all(feature = "views", feature = "select"))]
2502    #[test]
2503    fn selecting_every_axis_can_produce_a_scalar_view() {
2504        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
2505        let scalar = a.slice(&[&1usize, &1usize]);
2506        assert!(scalar.shape().is_empty());
2507        assert!(scalar.strides().is_empty());
2508        assert_eq!(scalar.len(), 1);
2509        assert_eq!(scalar.get(&[]), 4.0);
2510        assert_eq!((&scalar).into_iter().collect::<Vec<_>>(), vec![4.0]);
2511        assert_eq!(Shape::shape(&scalar), ShapeDim::Rank0(1));
2512    }
2513
2514    #[test]
2515    fn is_contiguous_default() {
2516        let a = NdArray::<f64>::new(&[3, 4]);
2517        assert!(a.is_contiguous());
2518    }
2519
2520    #[test]
2521    fn shape_trait_1d() {
2522        let a = NdArray::<f64>::new(&[5]);
2523        assert_eq!(Shape::shape(&a), ShapeDim::Rank1(5));
2524    }
2525
2526    #[test]
2527    fn shape_trait_2d() {
2528        let a = NdArray::<f64>::new(&[3, 4]);
2529        assert_eq!(Shape::shape(&a), ShapeDim::Rank2 { rows: 3, cols: 4 });
2530    }
2531
2532    #[test]
2533    fn shape_trait_3d() {
2534        let a = NdArray::<f64>::new(&[2, 3, 4]);
2535        assert_eq!(Shape::shape(&a), ShapeDim::RankN(vec![2, 3, 4]));
2536    }
2537
2538    // ****************************************************************
2539    // NaN handling
2540    // ****************************************************************
2541
2542    #[test]
2543    fn has_nan_false() {
2544        let a = NdArray::from_slice(&[1.0, 2.0, 3.0], &[3]);
2545        assert!(!a.has_nan());
2546        assert_eq!(a.nan_count(), 0);
2547    }
2548
2549    #[test]
2550    fn has_nan_true() {
2551        let a = NdArray::from_slice(&[1.0, f64::NAN, 3.0], &[3]);
2552        assert!(a.has_nan());
2553        assert_eq!(a.nan_count(), 1);
2554    }
2555
2556    #[test]
2557    fn has_nan_2d() {
2558        let a = NdArray::from_slice(&[1.0, 2.0, f64::NAN, 4.0, 5.0, f64::NAN], &[3, 2]);
2559        assert!(a.has_nan());
2560        assert_eq!(a.nan_count(), 2);
2561    }
2562
2563    // ****************************************************************
2564    // 2D axis access
2565    // ****************************************************************
2566
2567    #[test]
2568    fn col_access() {
2569        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
2570        assert_eq!(a.col(0), &[1.0, 2.0, 3.0]);
2571        assert_eq!(a.col(1), &[4.0, 5.0, 6.0]);
2572    }
2573
2574    #[test]
2575    fn col_mut_access() {
2576        let mut a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
2577        a.col_mut(0)[1] = 99.0;
2578        assert_eq!(a.col(0), &[1.0, 99.0, 3.0]);
2579    }
2580
2581    #[test]
2582    fn columns_access() {
2583        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
2584        let cols = a.columns();
2585        assert_eq!(cols.len(), 2);
2586        assert_eq!(cols[0], &[1.0, 2.0, 3.0]);
2587        assert_eq!(cols[1], &[4.0, 5.0, 6.0]);
2588    }
2589
2590    #[test]
2591    fn columns_mut_access() {
2592        let mut a = NdArray::new(&[3, 2]);
2593        {
2594            let mut cols = a.columns_mut();
2595            cols[0].copy_from_slice(&[1.0, 2.0, 3.0]);
2596            cols[1].copy_from_slice(&[4.0, 5.0, 6.0]);
2597        }
2598        assert_eq!(a.col(0), &[1.0, 2.0, 3.0]);
2599        assert_eq!(a.col(1), &[4.0, 5.0, 6.0]);
2600    }
2601
2602    #[cfg(feature = "views")]
2603    #[test]
2604    fn obs_access() {
2605        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
2606        let v = a.as_view();
2607        let obs0: Vec<f64> = (&v.obs(0)).into_iter().collect();
2608        let obs1: Vec<f64> = (&v.obs(1)).into_iter().collect();
2609        let obs2: Vec<f64> = (&v.obs(2)).into_iter().collect();
2610        assert_eq!(obs0, vec![1.0, 4.0]);
2611        assert_eq!(obs1, vec![2.0, 5.0]);
2612        assert_eq!(obs2, vec![3.0, 6.0]);
2613
2614        // Single-shot .obs() on NdArray also works
2615        assert_eq!((&a.obs(1)).into_iter().collect::<Vec<f64>>(), vec![2.0, 5.0]);
2616
2617        // 3D obs returns a 2D view
2618        let b = NdArray::from_slice(&(1..=24).map(|x| x as f64).collect::<Vec<_>>(), &[2, 3, 4]);
2619        let obs0_3d = b.obs(0);
2620        assert_eq!(obs0_3d.shape(), &[3, 4]);
2621    }
2622
2623    #[test]
2624    fn col_access_2d() {
2625        let data: Vec<f64> = (1..=20).map(|x| x as f64).collect();
2626        let a = NdArray::from_slice(&data, &[10, 2]);
2627        assert_eq!(a.col(0).len(), 10);
2628        assert_eq!(a.col(1).len(), 10);
2629        assert_eq!(a.col(0), &data[..10]);
2630        assert_eq!(a.col(1), &data[10..20]);
2631    }
2632
2633    // ****************************************************************
2634    // BLAS compatibility
2635    // ****************************************************************
2636
2637    #[test]
2638    fn blas_params() {
2639        let a = NdArray::<f64>::new(&[10, 5]);
2640        assert_eq!(a.m(), 10);
2641        assert_eq!(a.n(), 5);
2642        assert_eq!(a.lda(), 10);
2643    }
2644
2645    #[test]
2646    fn blas_params_aligned_rows() {
2647        let a = NdArray::<f64>::new(&[8, 3]);
2648        assert_eq!(a.m(), 8);
2649        assert_eq!(a.n(), 3);
2650        assert_eq!(a.lda(), 8);
2651    }
2652
2653    // ****************************************************************
2654    // Compact strides
2655    // ****************************************************************
2656
2657    #[test]
2658    fn compact_strides_2d() {
2659        for n_rows in 1..=20 {
2660            let a = NdArray::<f64>::new(&[n_rows, 3]);
2661            assert_eq!(a.strides()[1], n_rows);
2662            assert!(a.is_contiguous());
2663        }
2664    }
2665
2666    #[test]
2667    fn compact_strides_3d() {
2668        let a = NdArray::<f64>::new(&[10, 3, 4]);
2669        let strides = a.strides();
2670        assert_eq!(strides[0], 1);
2671        assert_eq!(strides[1], 10);
2672        assert_eq!(strides[2], 10 * 3);
2673        assert!(a.is_contiguous());
2674    }
2675
2676    // ****************************************************************
2677    // Reshape and transform
2678    // ****************************************************************
2679
2680    #[test]
2681    fn reshape_1d_to_2d() {
2682        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[6]);
2683        let b = a.reshape(&[3, 2]).unwrap();
2684        assert_eq!(b.shape(), &[3, 2]);
2685        assert_eq!(b.get(&[0, 0]), 1.0);
2686        assert_eq!(b.get(&[1, 0]), 2.0);
2687        assert_eq!(b.get(&[2, 0]), 3.0);
2688        assert_eq!(b.get(&[0, 1]), 4.0);
2689    }
2690
2691    #[test]
2692    fn reshape_size_mismatch() {
2693        let a = NdArray::from_slice(&[1.0, 2.0, 3.0], &[3]);
2694        assert!(a.reshape(&[2, 2]).is_err());
2695    }
2696
2697    #[test]
2698    fn transpose_2d() {
2699        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
2700        let t = a.transpose();
2701        assert_eq!(t.shape(), &[2, 3]);
2702        assert_eq!(t.get(&[0, 0]), 1.0);
2703        assert_eq!(t.get(&[1, 0]), 4.0);
2704        assert_eq!(t.get(&[0, 1]), 2.0);
2705        assert_eq!(t.get(&[1, 1]), 5.0);
2706        assert_eq!(t.get(&[0, 2]), 3.0);
2707        assert_eq!(t.get(&[1, 2]), 6.0);
2708    }
2709
2710    #[test]
2711    fn transpose_3d_reverses_axes() {
2712        let data: Vec<f64> = (1..=24).map(|x| x as f64).collect();
2713        let a = NdArray::from_slice(&data, &[2, 3, 4]);
2714        let t = a.transpose();
2715        assert_eq!(t.shape(), &[4, 3, 2]);
2716        assert!(t.is_contiguous());
2717        // Every element lands at its reversed index.
2718        for i in 0..2 {
2719            for j in 0..3 {
2720                for k in 0..4 {
2721                    assert_eq!(t.get(&[k, j, i]), a.get(&[i, j, k]));
2722                }
2723            }
2724        }
2725    }
2726
2727    #[test]
2728    fn transpose_1d_copies_through() {
2729        let a = NdArray::from_slice(&[1.0, 2.0, 3.0], &[3]);
2730        let t = a.transpose();
2731        assert_eq!(t.shape(), &[3]);
2732        assert_eq!(t, a);
2733    }
2734
2735    #[test]
2736    fn flatten() {
2737        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
2738        let flat = a.flatten();
2739        assert_eq!(flat.shape(), &[6]);
2740        let vals: Vec<f64> = (&flat).into_iter().collect();
2741        assert_eq!(vals, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
2742    }
2743
2744    #[test]
2745    fn to_contiguous_noop() {
2746        let a = NdArray::from_slice(&[1.0, 2.0, 3.0], &[3]);
2747        let b = a.to_contiguous();
2748        assert_eq!(a, b);
2749    }
2750
2751    #[test]
2752    fn fill_with_contiguous() {
2753        let mut a = NdArray::new(&[3, 2]);
2754        a.fill_with(42.0);
2755        for v in &a { assert_eq!(v, 42.0); }
2756    }
2757
2758    // ****************************************************************
2759    // From conversions
2760    // ****************************************************************
2761
2762    #[test]
2763    fn from_f64_slice() {
2764        let a = NdArray::from(&[1.0, 2.0, 3.0][..]);
2765        assert_eq!(a.ndim(), 1);
2766        assert_eq!(a.len(), 3);
2767        assert_eq!(a[(0,)], 1.0);
2768    }
2769
2770    #[test]
2771    fn from_vec64() {
2772        let v: Vec64<f64> = vec![10.0, 20.0].into_iter().collect();
2773        let a = NdArray::from(v);
2774        assert_eq!(a.ndim(), 1);
2775        assert_eq!(a[(0,)], 10.0);
2776        assert_eq!(a[(1,)], 20.0);
2777    }
2778
2779    #[test]
2780    fn from_column_vecs() {
2781        let cols = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
2782        let a = NdArray::from(cols.as_slice());
2783        assert_eq!(a.shape(), &[3, 2]);
2784        assert_eq!(a.col(0), &[1.0, 2.0, 3.0]);
2785        assert_eq!(a.col(1), &[4.0, 5.0, 6.0]);
2786    }
2787
2788    #[test]
2789    fn from_float_arrays() {
2790        let c0 = FloatArray::from_slice(&[1.0, 2.0]);
2791        let c1 = FloatArray::from_slice(&[3.0, 4.0]);
2792        let a = NdArray::from([c0, c1].as_slice());
2793        assert_eq!(a.shape(), &[2, 2]);
2794        assert_eq!(a.col(0), &[1.0, 2.0]);
2795        assert_eq!(a.col(1), &[3.0, 4.0]);
2796    }
2797
2798    #[test]
2799    fn from_buffer_explicit_strides() {
2800        let mut buf = Vec64::with_capacity(16);
2801        buf.0.resize(16, 0.0);
2802        buf[0] = 1.0; buf[1] = 2.0; buf[2] = 3.0;
2803        buf[8] = 4.0; buf[9] = 5.0; buf[10] = 6.0;
2804        let a = NdArray::from_buffer(Buffer::from_vec64(buf), &[3, 2], &[1, 8]);
2805        assert_eq!(a.col(0), &[1.0, 2.0, 3.0]);
2806        assert_eq!(a.col(1), &[4.0, 5.0, 6.0]);
2807    }
2808
2809    // ****************************************************************
2810    // Matrix interop
2811    // ****************************************************************
2812
2813    #[cfg(feature = "matrix")]
2814    #[test]
2815    fn from_matrix() {
2816        let mat = Matrix::from_f64_unaligned(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 3, 2, Some("m".into()));
2817        let a = NdArray::from(mat);
2818        assert_eq!(a.shape(), &[3, 2]);
2819        assert_eq!(a.name.as_deref(), Some("m"));
2820        assert_eq!(a.col(0), &[1.0, 2.0, 3.0]);
2821        assert_eq!(a.col(1), &[4.0, 5.0, 6.0]);
2822    }
2823
2824    #[cfg(feature = "matrix")]
2825    #[test]
2826    fn to_matrix_roundtrip() {
2827        let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
2828        let a = NdArray::from_slice(&data, &[3, 2]);
2829        let mat = a.to_matrix().unwrap();
2830        assert_eq!(mat.n_rows, 3);
2831        assert_eq!(mat.n_cols, 2);
2832        assert_eq!(mat.col(0), &[1.0, 2.0, 3.0]);
2833        assert_eq!(mat.col(1), &[4.0, 5.0, 6.0]);
2834    }
2835
2836    #[cfg(feature = "matrix")]
2837    #[test]
2838    fn to_matrix_non_2d_fails() {
2839        let a = NdArray::new(&[5]);
2840        assert!(a.to_matrix().is_err());
2841    }
2842
2843    // ****************************************************************
2844    // Table interop
2845    // ****************************************************************
2846
2847    fn make_numeric_table() -> Table {
2848        let c0 = FieldArray::from_arr("x", Array::NumericArray(
2849            NumericArray::Float64(Arc::new(FloatArray::from_slice(&[1.0, 2.0, 3.0])))
2850        ));
2851        let c1 = FieldArray::from_arr("y", Array::NumericArray(
2852            NumericArray::Float64(Arc::new(FloatArray::from_slice(&[4.0, 5.0, 6.0])))
2853        ));
2854        Table::new("data".to_string(), Some(vec![c0, c1]))
2855    }
2856
2857    #[test]
2858    fn try_from_table() {
2859        let table = make_numeric_table();
2860        let a = NdArray::try_from(&table).unwrap();
2861        assert_eq!(a.shape(), &[3, 2]);
2862        assert_eq!(a.col(0), &[1.0, 2.0, 3.0]);
2863        assert_eq!(a.col(1), &[4.0, 5.0, 6.0]);
2864        assert_eq!(a.name.as_deref(), Some("data"));
2865    }
2866
2867    #[test]
2868    fn try_from_table_with_nulls_converts_to_nan() {
2869        let mut mask = Bitmask::new_set_all(3, true);
2870        mask.set(1, false);
2871        let arr = FloatArray::new(Buffer::from_slice(&[10.0, 0.0, 30.0]), Some(mask));
2872        let c0 = FieldArray::from_arr("v", Array::NumericArray(
2873            NumericArray::Float64(Arc::new(arr))
2874        ));
2875        let table = Table::new("nulls".to_string(), Some(vec![c0]));
2876        let a = NdArray::try_from(&table).unwrap();
2877        assert_eq!(a.get(&[0, 0]), 10.0);
2878        assert!(a.get(&[1, 0]).is_nan());
2879        assert_eq!(a.get(&[2, 0]), 30.0);
2880    }
2881
2882    #[test]
2883    fn try_from_table_coerces_unparseable_text_to_nan() {
2884        // TryFrom<&Table> uses the library's lenient numeric cast. Text that
2885        // does not parse as a number coerces to nulls, which surface as NaN
2886        // in the contiguous NdArray rather than failing the conversion.
2887        let c0 = FieldArray::from_arr("name", Array::from_string32(
2888            StringArray::from_slice(&["a", "b"])
2889        ));
2890        let table = Table::new("text".to_string(), Some(vec![c0]));
2891        let a = NdArray::try_from(&table).unwrap();
2892        assert_eq!(a.shape(), &[2, 1]);
2893        assert!(a.get(&[0, 0]).is_nan());
2894        assert!(a.get(&[1, 0]).is_nan());
2895    }
2896
2897    #[test]
2898    fn to_table_roundtrip() {
2899        let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
2900        let a = NdArray::from_slice(&data, &[3, 2]);
2901        let fields = vec![
2902            Field::new("x", ArrowType::Float64, false, None),
2903            Field::new("y", ArrowType::Float64, false, None),
2904        ];
2905        let table = a.to_table(Some(fields)).unwrap();
2906        assert_eq!(table.n_rows(), 3);
2907        assert_eq!(table.n_cols(), 2);
2908        assert_eq!(table.col_names(), vec!["x", "y"]);
2909        let col0 = table.cols[0].array.num().f64();
2910        assert_eq!(col0.data.as_slice(), &[1.0, 2.0, 3.0]);
2911    }
2912
2913    #[test]
2914    fn to_table_generated_names() {
2915        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
2916        let table = a.to_table(None).unwrap();
2917        assert_eq!(table.col_names(), vec!["col_0", "col_1"]);
2918    }
2919
2920    #[test]
2921    fn to_table_rejects_incompatible_field_metadata() {
2922        let wrong_dtype = NdArray::from_slice(&[1.0, 2.0], &[2, 1]).to_table(Some(vec![
2923            Field::new("value", ArrowType::Float32, false, None),
2924        ]));
2925        assert!(matches!(wrong_dtype, Err(MinarrowError::TypeError { .. })));
2926
2927        let nullable = NdArray::from_slice(&[1.0, 2.0], &[2, 1]).to_table(Some(vec![
2928            Field::new("value", ArrowType::Float64, true, None),
2929        ]));
2930        assert!(matches!(nullable, Err(MinarrowError::NullError { .. })));
2931    }
2932
2933    #[test]
2934    fn to_table_non_2d_fails() {
2935        let a = NdArray::new(&[5]);
2936        assert!(a.to_table(None).is_err());
2937    }
2938
2939    #[test]
2940    fn to_array_1d() {
2941        let a = NdArray::from_slice(&[1.0, 2.0, 3.0], &[3]);
2942        let arr = a.to_array().unwrap();
2943        let f = arr.num().f64();
2944        assert_eq!(f.data.as_slice(), &[1.0, 2.0, 3.0]);
2945    }
2946
2947    #[test]
2948    fn to_array_materialises_strided_logical_values() {
2949        let a = NdArray::from_buffer(
2950            Buffer::from_slice(&[1.0, 99.0, 2.0, 99.0, 3.0]),
2951            &[3],
2952            &[2],
2953        );
2954        let arr = a.to_array().unwrap();
2955        assert_eq!(arr.num().f64().data.as_slice(), &[1.0, 2.0, 3.0]);
2956    }
2957
2958    #[test]
2959    fn to_array_ignores_unused_backing_elements() {
2960        let a = NdArray::from_buffer(
2961            Buffer::from_slice(&[1.0, 2.0, 3.0, 99.0]),
2962            &[3],
2963            &[1],
2964        );
2965        let arr = a.to_array().unwrap();
2966        assert_eq!(arr.num().f64().data.as_slice(), &[1.0, 2.0, 3.0]);
2967    }
2968
2969    #[test]
2970    fn to_array_non_1d_fails() {
2971        let a = NdArray::new(&[2, 3]);
2972        assert!(a.to_array().is_err());
2973    }
2974
2975    // ****************************************************************
2976    // Concatenate
2977    // ****************************************************************
2978
2979    #[test]
2980    fn concat_1d() {
2981        let a = NdArray::from_slice(&[1.0, 2.0], &[2]);
2982        let b = NdArray::from_slice(&[3.0, 4.0, 5.0], &[3]);
2983        let c = a.concat(b).unwrap();
2984        assert_eq!(c.shape(), &[5]);
2985        let vals: Vec<f64> = (&c).into_iter().collect();
2986        assert_eq!(vals, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
2987    }
2988
2989    #[test]
2990    fn concat_2d() {
2991        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
2992        let b = NdArray::from_slice(&[5.0, 6.0, 7.0, 8.0, 9.0, 10.0], &[3, 2]);
2993        let c = a.concat(b).unwrap();
2994        assert_eq!(c.shape(), &[5, 2]);
2995        assert_eq!(c.col(0), &[1.0, 2.0, 5.0, 6.0, 7.0]);
2996        assert_eq!(c.col(1), &[3.0, 4.0, 8.0, 9.0, 10.0]);
2997    }
2998
2999    #[test]
3000    fn concat_dimension_mismatch_fails() {
3001        let a = NdArray::<f64>::new(&[3, 2]);
3002        let b = NdArray::new(&[3, 3]);
3003        assert!(a.concat(b).is_err());
3004    }
3005
3006    #[test]
3007    fn concat_rank_mismatch_fails() {
3008        let a = NdArray::<f64>::new(&[3]);
3009        let b = NdArray::new(&[3, 2]);
3010        assert!(a.concat(b).is_err());
3011    }
3012
3013    // ****************************************************************
3014    // Clone and PartialEq
3015    // ****************************************************************
3016
3017    #[test]
3018    fn clone_and_eq() {
3019        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
3020        let b = a.clone();
3021        assert_eq!(a, b);
3022    }
3023
3024    #[test]
3025    fn ne_different_data() {
3026        let a = NdArray::from_slice(&[1.0, 2.0], &[2]);
3027        let b = NdArray::from_slice(&[1.0, 3.0], &[2]);
3028        assert_ne!(a, b);
3029    }
3030
3031    #[test]
3032    fn ne_different_shape() {
3033        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0], &[4]);
3034        let b = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
3035        assert_ne!(a, b);
3036    }
3037
3038    // ****************************************************************
3039    // Debug formatting
3040    // ****************************************************************
3041
3042    #[test]
3043    fn debug_1d() {
3044        let a = NdArray::from_slice(&[1.0, 2.0, 3.0], &[3]);
3045        let s = format!("{:?}", a);
3046        assert!(s.contains("[3]"));
3047        assert!(s.contains("1D"));
3048    }
3049
3050    #[test]
3051    fn debug_2d_named() {
3052        let a = NdArray::<f64>::new_named(&[2, 3], "test");
3053        let s = format!("{:?}", a);
3054        assert!(s.contains("'test'"));
3055        assert!(s.contains("[2, 3]"));
3056        assert!(s.contains("2D"));
3057    }
3058
3059    // ****************************************************************
3060    // Edge cases
3061    // ****************************************************************
3062
3063    #[test]
3064    fn empty_array() {
3065        let a = NdArray::new(&[0, 5]);
3066        assert!(a.is_empty());
3067        assert_eq!(a.len(), 0);
3068        let vals: Vec<f64> = (&a).into_iter().collect();
3069        assert!(vals.is_empty());
3070    }
3071
3072    #[test]
3073    fn single_element() {
3074        let a = NdArray::from_slice(&[42.0], &[1]);
3075        assert_eq!(a.len(), 1);
3076        assert_eq!(a[(0,)], 42.0);
3077        let vals: Vec<f64> = (&a).into_iter().collect();
3078        assert_eq!(vals, vec![42.0]);
3079    }
3080
3081    #[test]
3082    fn single_element_2d() {
3083        let a = NdArray::from_slice(&[42.0], &[1, 1]);
3084        assert_eq!(a[(0, 0)], 42.0);
3085    }
3086
3087    #[test]
3088    fn large_array_iteration_count() {
3089        let n = 1000;
3090        let a = NdArray::<f64>::ones(&[n, 100]);
3091        let count = (&a).into_iter().count();
3092        assert_eq!(count, n * 100);
3093    }
3094
3095    // ****************************************************************
3096    // Bracket indexing: arr[col][row]
3097    // ****************************************************************
3098
3099    #[test]
3100    fn bracket_index_1d() {
3101        let a = NdArray::from_slice(&[10.0, 20.0, 30.0], &[3]);
3102        assert_eq!(a[0], [10.0]);
3103        assert_eq!(a[2], [30.0]);
3104    }
3105
3106    #[test]
3107    fn bracket_index_2d_column() {
3108        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3109        assert_eq!(a[0], [1.0, 2.0, 3.0]);
3110        assert_eq!(a[1], [4.0, 5.0, 6.0]);
3111    }
3112
3113    #[test]
3114    fn bracket_index_2d_chained() {
3115        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3116        // arr[col][row]
3117        assert_eq!(a[0][0], 1.0);
3118        assert_eq!(a[0][2], 3.0);
3119        assert_eq!(a[1][0], 4.0);
3120        assert_eq!(a[1][2], 6.0);
3121    }
3122
3123    #[test]
3124    fn bracket_index_mut_2d() {
3125        let mut a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3126        a[0][1] = 99.0;
3127        assert_eq!(a[0][1], 99.0);
3128        assert_eq!(a[0], [1.0, 99.0, 3.0]);
3129    }
3130
3131    // ****************************************************************
3132    // Range indexing: arr[1..3]
3133    // ****************************************************************
3134
3135    #[test]
3136    fn range_index_1d() {
3137        let a = NdArray::from_slice(&[10.0, 20.0, 30.0, 40.0], &[4]);
3138        assert_eq!(a[1..3], [20.0, 30.0]);
3139    }
3140
3141    #[test]
3142    fn range_index_2d_columns() {
3143        // Selecting a range of columns returns the exact logical data,
3144        // since the compact layout has no padding between columns.
3145        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3146        let slab = &a[0..2];
3147        assert_eq!(slab, &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
3148        let col1 = &a[1..2];
3149        assert_eq!(col1, &[4.0, 5.0, 6.0]);
3150    }
3151
3152    #[cfg(feature = "matrix")]
3153    #[test]
3154    #[should_panic(expected = "contiguous")]
3155    fn range_index_non_contiguous_panics() {
3156        // A Matrix-imported array carries the padded stride, so range
3157        // indexing has no gap-free slab to return and panics.
3158        let mat = Matrix::from_f64_unaligned(
3159            &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 3, 2, None,
3160        );
3161        let a = NdArray::from(mat);
3162        if a.is_contiguous() {
3163            // Padding only appears when rows are off the alignment boundary,
3164            // which holds for 3 rows. Guard the premise.
3165            panic!("premise failed: expected non-contiguous import");
3166        }
3167        let _ = &a[0..2];
3168    }
3169
3170    #[cfg(feature = "matrix")]
3171    #[test]
3172    fn to_matrix_repacks_compact_layout() {
3173        // Compact tensor data re-lays into Matrix's padded column layout.
3174        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3175        let mat = a.to_matrix().unwrap();
3176        assert_eq!(mat.n_rows, 3);
3177        assert_eq!(mat.n_cols, 2);
3178        assert_eq!(mat.stride, 8);
3179        assert_eq!(&mat.data.as_slice()[..3], &[1.0, 2.0, 3.0]);
3180        assert_eq!(&mat.data.as_slice()[8..11], &[4.0, 5.0, 6.0]);
3181    }
3182
3183    #[cfg(feature = "matrix")]
3184    #[test]
3185    fn matrix_roundtrip_via_contiguous() {
3186        // Matrix -> NdArray is zero-copy with the padded stride carried
3187        // through. to_contiguous compacts, and to_matrix re-pads.
3188        let mat = Matrix::from_f64_unaligned(
3189            &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 3, 2, None,
3190        );
3191        let a = NdArray::from(mat);
3192        assert!(!a.is_contiguous());
3193        assert_eq!(a.get(&[2, 1]), 6.0);
3194        let compact = a.to_contiguous();
3195        assert!(compact.is_contiguous());
3196        assert_eq!(&compact[0..2], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
3197        let back = compact.to_matrix().unwrap();
3198        assert_eq!(back.stride, 8);
3199        assert_eq!(&back.data.as_slice()[8..11], &[4.0, 5.0, 6.0]);
3200    }
3201
3202    #[test]
3203    fn range_from_index() {
3204        let a = NdArray::from_slice(&[10.0, 20.0, 30.0, 40.0], &[4]);
3205        assert_eq!(a[2..], [30.0, 40.0]);
3206    }
3207
3208    #[test]
3209    fn range_to_index() {
3210        let a = NdArray::from_slice(&[10.0, 20.0, 30.0, 40.0], &[4]);
3211        assert_eq!(a[..2], [10.0, 20.0]);
3212    }
3213
3214    #[test]
3215    fn range_full_index() {
3216        let a = NdArray::from_slice(&[10.0, 20.0, 30.0], &[3]);
3217        assert_eq!(a[..], [10.0, 20.0, 30.0]);
3218    }
3219
3220    // ****************************************************************
3221    // Slicing: arr.slice(nd![1..4, 2..5])
3222    // ****************************************************************
3223
3224    #[cfg(feature = "views")]
3225    #[test]
3226    fn slice_1d_single_index() {
3227        let a = NdArray::from_slice(&[10.0, 20.0, 30.0], &[3]);
3228        let v = a.slice(&[&1]);
3229        assert!(v.shape().is_empty());
3230        assert_eq!(v[()], 20.0);
3231    }
3232
3233    #[cfg(feature = "views")]
3234    #[test]
3235    fn slice_1d_range() {
3236        let a = NdArray::from_slice(&[10.0, 20.0, 30.0, 40.0], &[4]);
3237        let v = a.slice(&[&(1..3)]);
3238        assert_eq!(v.shape(), &[2]);
3239        assert_eq!(v[(0,)], 20.0);
3240        assert_eq!(v[(1,)], 30.0);
3241    }
3242
3243    #[cfg(feature = "views")]
3244    #[test]
3245    fn slice_2d_row_range_single_col() {
3246        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3247        // Rows 0..2 of column 1
3248        let v = a.slice(nd![0..2, 1]);
3249        assert_eq!(v.shape(), &[2]);
3250        assert_eq!(v[(0,)], 4.0);
3251        assert_eq!(v[(1,)], 5.0);
3252    }
3253
3254    #[cfg(feature = "views")]
3255    #[test]
3256    fn slice_2d_single_row_col_range() {
3257        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3258        // Row 1, columns 0..2 - collapses row axis
3259        let v = a.slice(nd![1, 0..2]);
3260        assert_eq!(v.shape(), &[2]);
3261        // Should get row 1 values: a[(1,0)]=2.0, a[(1,1)]=5.0
3262        let vals: Vec<f64> = (&v).into_iter().collect();
3263        assert_eq!(vals, vec![2.0, 5.0]);
3264    }
3265
3266    #[cfg(feature = "views")]
3267    #[test]
3268    fn slice_2d_both_ranges() {
3269        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3270        // Rows 0..2, columns 0..2 - sub-matrix
3271        let v = a.slice(nd![0..2, 0..2]);
3272        assert_eq!(v.shape(), &[2, 2]);
3273        assert_eq!(v[(0, 0)], 1.0);
3274        assert_eq!(v[(1, 0)], 2.0);
3275        assert_eq!(v[(0, 1)], 4.0);
3276        assert_eq!(v[(1, 1)], 5.0);
3277    }
3278
3279    #[cfg(feature = "views")]
3280    #[test]
3281    fn slice_2d_both_indices_scalar() {
3282        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3283        // Single element as a rank-zero scalar view
3284        let v = a.slice(nd![2, 1]);
3285        assert!(v.shape().is_empty());
3286        assert_eq!(v[()], 6.0);
3287    }
3288
3289    #[cfg(feature = "views")]
3290    #[test]
3291    fn slice_3d_mixed() {
3292        // 2x3x4 array
3293        let data: Vec<f64> = (1..=24).map(|x| x as f64).collect();
3294        let a = NdArray::from_slice(&data, &[2, 3, 4]);
3295        // All rows, column 1, slices 0..2
3296        let v = a.slice(nd![0..2, 1, 0..2]);
3297        assert_eq!(v.shape(), &[2, 2]);
3298        // a[(0,1,0)]=3, a[(1,1,0)]=4, a[(0,1,1)]=9, a[(1,1,1)]=10
3299        assert_eq!(v[(0, 0)], 3.0);
3300        assert_eq!(v[(1, 0)], 4.0);
3301        assert_eq!(v[(0, 1)], 9.0);
3302        assert_eq!(v[(1, 1)], 10.0);
3303    }
3304
3305    #[cfg(feature = "views")]
3306    #[test]
3307    fn slice_with_nd_macro() {
3308        let data: Vec<f64> = (1..=24).map(|x| x as f64).collect();
3309        let a = NdArray::from_slice(&data, &[2, 3, 4]);
3310        let v = a.slice(nd![0..2, 0..3, 0..4]);
3311        assert_eq!(v.shape(), &[2, 3, 4]);
3312        assert_eq!(v.len(), 24);
3313    }
3314
3315    #[cfg(feature = "views")]
3316    #[test]
3317    fn slice_preserves_data_through_iteration() {
3318        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3319        let v = a.slice(nd![1..3, 0..2]);
3320        // Sub-matrix: rows 1..3, cols 0..2
3321        let vals: Vec<f64> = (&v).into_iter().collect();
3322        assert_eq!(vals, vec![2.0, 3.0, 5.0, 6.0]);
3323    }
3324
3325    #[cfg(feature = "views")]
3326    #[test]
3327    fn slice_column_window() {
3328        // Slice rows 2..5 from column 1 of a [10, 2] array
3329        let data: Vec<f64> = (1..=20).map(|x| x as f64).collect();
3330        let a = NdArray::from_slice(&data, &[10, 2]);
3331        let v = a.slice(nd![2..5, 1]);
3332        assert_eq!(v.shape(), &[3]);
3333        assert_eq!(v[(0,)], 13.0);
3334        assert_eq!(v[(1,)], 14.0);
3335        assert_eq!(v[(2,)], 15.0);
3336    }
3337
3338    // ****************************************************************
3339    // Bounds and panic contracts
3340    // ****************************************************************
3341
3342    #[test]
3343    #[should_panic(expected = "Column index out of bounds")]
3344    fn col_out_of_bounds_panics() {
3345        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3346        let _ = a.col(2);
3347    }
3348
3349    #[test]
3350    #[should_panic(expected = "indices for a 2D array")]
3351    fn get_rank_mismatch_panics() {
3352        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
3353        let _ = a.get(&[0]);
3354    }
3355
3356    #[test]
3357    #[should_panic(expected = "out of bounds for dim")]
3358    fn get_index_out_of_bounds_panics() {
3359        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
3360        let _ = a.get(&[2, 0]);
3361    }
3362
3363    #[test]
3364    #[should_panic(expected = "out of bounds for dim")]
3365    fn set_out_of_bounds_panics() {
3366        let mut a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
3367        a.set(&[0, 5], 9.0);
3368    }
3369
3370    #[cfg(all(feature = "views", feature = "select"))]
3371    #[test]
3372    #[should_panic(expected = "expected 2 axes, got 1")]
3373    fn slice_wrong_axis_count_panics() {
3374        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3375        let _ = a.slice(nd![0..2]);
3376    }
3377
3378    #[cfg(all(feature = "views", feature = "select"))]
3379    #[test]
3380    #[should_panic(expected = "range 0..100 out of bounds")]
3381    fn slice_range_out_of_bounds_panics() {
3382        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3383        let _ = a.slice(nd![0..100, 0..2]);
3384    }
3385
3386    #[cfg(all(feature = "views", feature = "select"))]
3387    #[test]
3388    #[should_panic(expected = "index 5 out of bounds")]
3389    fn slice_single_index_out_of_bounds_panics() {
3390        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3391        let _ = a.slice(nd![5, 0..2]);
3392    }
3393
3394    #[test]
3395    #[should_panic(expected = "at least 2 points")]
3396    fn linspace_requires_two_points() {
3397        let _ = NdArray::<f64>::linspace(0.0, 1.0, 1);
3398    }
3399
3400    #[cfg(all(feature = "views", feature = "select"))]
3401    #[test]
3402    fn slice_one_element_index_array_collapses() {
3403        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3404        let idx: &[usize] = &[1];
3405        let v = a.slice(nd![idx, 0..2]);
3406        assert_eq!(v.shape(), &[2]);
3407        assert_eq!(v[(0,)], 2.0);
3408        assert_eq!(v[(1,)], 5.0);
3409    }
3410
3411    #[test]
3412    fn get_unchecked_matches_get() {
3413        let data: Vec<f64> = (1..=24).map(|x| x as f64).collect();
3414        let a = NdArray::from_slice(&data, &[2, 3, 4]);
3415        for i in 0..2 {
3416            for j in 0..3 {
3417                for k in 0..4 {
3418                    let idx = [i, j, k];
3419                    // SAFETY: indices are within shape
3420                    assert_eq!(unsafe { a.get_unchecked(&idx) }, a.get(&idx));
3421                }
3422            }
3423        }
3424    }
3425
3426    // ****************************************************************
3427    // Concatenate correctness
3428    // ****************************************************************
3429
3430    #[test]
3431    fn concat_3d_interleaves_axis0() {
3432        let da: Vec<f64> = (1..=8).map(|x| x as f64).collect();
3433        let db: Vec<f64> = (9..=16).map(|x| x as f64).collect();
3434        let a = NdArray::from_slice(&da, &[2, 2, 2]);
3435        let b = NdArray::from_slice(&db, &[2, 2, 2]);
3436        let c = a.clone().concat(b.clone()).unwrap();
3437        assert_eq!(c.shape(), &[4, 2, 2]);
3438        for i in 0..4 {
3439            for j in 0..2 {
3440                for k in 0..2 {
3441                    let expected = if i < 2 {
3442                        a.get(&[i, j, k])
3443                    } else {
3444                        b.get(&[i - 2, j, k])
3445                    };
3446                    assert_eq!(c.get(&[i, j, k]), expected);
3447                }
3448            }
3449        }
3450    }
3451
3452    #[test]
3453    fn concat_non_contiguous_operand() {
3454        // Row-major strides on the first operand exercise the general
3455        // interleave path.
3456        let a = NdArray::from_buffer(
3457            Buffer::from_slice(&[1.0, 2.0, 3.0, 4.0]),
3458            &[2, 2],
3459            &[2, 1],
3460        );
3461        assert!(!a.is_contiguous());
3462        let b = NdArray::from_slice(&[5.0, 6.0, 7.0, 8.0], &[2, 2]);
3463        let c = a.concat(b).unwrap();
3464        assert_eq!(c.shape(), &[4, 2]);
3465        assert_eq!(c.col(0), &[1.0, 3.0, 5.0, 6.0]);
3466        assert_eq!(c.col(1), &[2.0, 4.0, 7.0, 8.0]);
3467    }
3468
3469    // ****************************************************************
3470    // Copy-on-write and strided mutation
3471    // ****************************************************************
3472
3473    #[cfg(feature = "views")]
3474    #[test]
3475    fn set_after_view_copy_on_write() {
3476        let mut a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
3477        let v = a.as_view();
3478        a.set(&[0, 0], 99.0);
3479        // The write detached the array's buffer, so the view keeps the
3480        // original value.
3481        assert_eq!(a.get(&[0, 0]), 99.0);
3482        assert_eq!(v.get(&[0, 0]), 1.0);
3483    }
3484
3485    #[test]
3486    fn fill_with_non_contiguous_logical_only() {
3487        // Padded column stride leaves gaps between columns in the buffer.
3488        let mut buf = Vec64::with_capacity(11);
3489        buf.0.resize(11, 0.0);
3490        let mut a = NdArray::from_buffer(Buffer::from_vec64(buf), &[3, 2], &[1, 8]);
3491        assert!(!a.is_contiguous());
3492        a.fill_with(7.0);
3493        for v in &a { assert_eq!(v, 7.0); }
3494        // Padding between the columns stays untouched.
3495        assert_eq!(a.as_slice()[3], 0.0);
3496        assert_eq!(a.as_slice()[7], 0.0);
3497    }
3498
3499    // ****************************************************************
3500    // Range shorthand on 2D
3501    // ****************************************************************
3502
3503    #[test]
3504    fn range_from_index_2d() {
3505        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3506        assert_eq!(&a[1..], &[4.0, 5.0, 6.0]);
3507    }
3508
3509    #[test]
3510    fn range_to_index_2d() {
3511        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3512        assert_eq!(&a[..1], &[1.0, 2.0, 3.0]);
3513    }
3514
3515    #[test]
3516    fn range_full_index_2d() {
3517        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3518        assert_eq!(&a[..], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
3519    }
3520
3521    // ****************************************************************
3522    // Row window and size estimate
3523    // ****************************************************************
3524
3525    #[cfg(all(feature = "views", feature = "select"))]
3526    #[test]
3527    fn row_selection_single_index_window() {
3528        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3529        assert_eq!(a.n_obs(), 3);
3530        let v = a.r(1usize);
3531        assert_eq!(v.shape(), &[1, 2]);
3532        assert_eq!(v.get(&[0, 0]), 2.0);
3533        assert_eq!(v.get(&[0, 1]), 5.0);
3534        // A window keeps the full source buffer rather than gathering a copy.
3535        assert_eq!(v.source.len(), a.len());
3536        // SAFETY: indices are within the window shape
3537        assert_eq!(unsafe { v.get_unchecked(&[0, 1]) }, 5.0);
3538    }
3539
3540    #[cfg(feature = "size")]
3541    #[test]
3542    fn est_bytes_covers_buffer() {
3543        use crate::traits::byte_size::ByteSize;
3544        let a = NdArray::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
3545        assert!(a.est_bytes() >= 6 * size_of::<f64>());
3546    }
3547}