Skip to main content

ferray_core/creation/
mod.rs

1// ferray-core: Array creation functions (REQ-16, REQ-17, REQ-18, REQ-19)
2//
3// Mirrors numpy's array creation routines: zeros, ones, full, empty,
4// arange, linspace, logspace, geomspace, eye, identity, diag, etc.
5
6use std::mem::MaybeUninit;
7
8use crate::array::owned::Array;
9use crate::array::view::ArrayView;
10use crate::dimension::{Dimension, Ix1, Ix2, IxDyn};
11use crate::dtype::Element;
12use crate::error::{FerrayError, FerrayResult};
13
14// ============================================================================
15// REQ-16: Basic creation functions
16// ============================================================================
17
18/// Create an array from a flat vector and a shape (C-order).
19///
20/// This is the primary "array constructor" — analogous to `numpy.array()` when
21/// given a flat sequence plus a shape.
22///
23/// # Errors
24/// Returns `FerrayError::ShapeMismatch` if `data.len()` does not equal the
25/// product of the shape dimensions.
26pub fn array<T: Element, D: Dimension>(dim: D, data: Vec<T>) -> FerrayResult<Array<T, D>> {
27    Array::from_vec(dim, data)
28}
29
30/// Interpret existing data as an array without copying (if possible).
31///
32/// This is equivalent to `numpy.asarray()`. Since Rust ownership rules
33/// require moving the data, this creates an owned array from the vector.
34///
35/// # Errors
36/// Returns `FerrayError::ShapeMismatch` if lengths don't match.
37pub fn asarray<T: Element, D: Dimension>(dim: D, data: Vec<T>) -> FerrayResult<Array<T, D>> {
38    Array::from_vec(dim, data)
39}
40
41/// Create an array from a byte buffer, interpreting bytes as elements of type `T`.
42///
43/// Analogous to `numpy.frombuffer()`.
44///
45/// # Errors
46/// Returns `FerrayError::InvalidValue` if the buffer length is not a multiple
47/// of `size_of::<T>()`, or if the resulting length does not match the shape.
48pub fn frombuffer<T: Element, D: Dimension>(dim: D, buf: &[u8]) -> FerrayResult<Array<T, D>> {
49    let elem_size = std::mem::size_of::<T>();
50    if elem_size == 0 {
51        return Err(FerrayError::invalid_value("zero-sized type"));
52    }
53    if buf.len() % elem_size != 0 {
54        return Err(FerrayError::invalid_value(format!(
55            "buffer length {} is not a multiple of element size {}",
56            buf.len(),
57            elem_size,
58        )));
59    }
60    let n_elems = buf.len() / elem_size;
61    let expected = dim.size();
62    if n_elems != expected {
63        return Err(FerrayError::shape_mismatch(format!(
64            "buffer contains {} elements but shape {:?} requires {}",
65            n_elems,
66            dim.as_slice(),
67            expected,
68        )));
69    }
70    // Validate bytes for types where not all bit patterns are valid.
71    // bool only permits 0x00 and 0x01.
72    if std::any::TypeId::of::<T>() == std::any::TypeId::of::<bool>() {
73        for &byte in buf {
74            if byte > 1 {
75                return Err(FerrayError::invalid_value(format!(
76                    "invalid byte {byte:#04x} for bool (must be 0x00 or 0x01)"
77                )));
78            }
79        }
80    }
81
82    // Copy bytes element-by-element via from_ne_bytes equivalent
83    let mut data = Vec::with_capacity(n_elems);
84    for i in 0..n_elems {
85        let start = i * elem_size;
86        let end = start + elem_size;
87        let slice = &buf[start..end];
88        // SAFETY: We're reading elem_size bytes and interpreting as T.
89        // For bool, we validated above that all bytes are 0 or 1.
90        // For numeric types, all bit patterns are valid.
91        let val = unsafe {
92            let mut val = MaybeUninit::<T>::uninit();
93            std::ptr::copy_nonoverlapping(slice.as_ptr(), val.as_mut_ptr().cast::<u8>(), elem_size);
94            val.assume_init()
95        };
96        data.push(val);
97    }
98    Array::from_vec(dim, data)
99}
100
101/// Create a zero-copy [`ArrayView`] over an existing byte buffer (#364).
102///
103/// Unlike [`frombuffer`], which copies bytes into a freshly allocated
104/// `Array`, this function returns a view whose lifetime is tied to the
105/// input slice. This is the equivalent of `NumPy`'s `np.frombuffer()` with
106/// a memoryview source — the primary building block for zero-copy
107/// interop with mmap, shared memory, network buffers, and FFI.
108///
109/// # Errors
110/// - `InvalidValue` if `T` is a ZST.
111/// - `InvalidValue` if `buf.len()` is not a multiple of `size_of::<T>()`.
112/// - `ShapeMismatch` if the element count doesn't match `dim.size()`.
113/// - `InvalidValue` if `buf.as_ptr()` is not aligned to `align_of::<T>()`
114///   (views require proper alignment — use the copying [`frombuffer`]
115///   instead if alignment cannot be guaranteed).
116/// - `InvalidValue` if `T` is `bool` and any byte is outside `{0x00, 0x01}`.
117pub fn frombuffer_view<T: Element, D: Dimension>(
118    dim: D,
119    buf: &[u8],
120) -> FerrayResult<ArrayView<'_, T, D>> {
121    let elem_size = std::mem::size_of::<T>();
122    if elem_size == 0 {
123        return Err(FerrayError::invalid_value("zero-sized type"));
124    }
125    if buf.len() % elem_size != 0 {
126        return Err(FerrayError::invalid_value(format!(
127            "buffer length {} is not a multiple of element size {}",
128            buf.len(),
129            elem_size,
130        )));
131    }
132    let n_elems = buf.len() / elem_size;
133    let expected = dim.size();
134    if n_elems != expected {
135        return Err(FerrayError::shape_mismatch(format!(
136            "buffer contains {} elements but shape {:?} requires {}",
137            n_elems,
138            dim.as_slice(),
139            expected,
140        )));
141    }
142
143    // Alignment: a view interprets the bytes in place, so the buffer must
144    // already be aligned for T. A misaligned read of f32/f64/etc. is UB.
145    let align = std::mem::align_of::<T>();
146    let addr = buf.as_ptr() as usize;
147    if addr % align != 0 {
148        return Err(FerrayError::invalid_value(format!(
149            "buffer address 0x{addr:x} is not aligned to {align} bytes required by the element type; \
150             use `frombuffer` for misaligned input"
151        )));
152    }
153
154    // bool has the same size/alignment as u8 but restricts the valid bit
155    // patterns; validate exhaustively, matching the copy-based frombuffer.
156    if std::any::TypeId::of::<T>() == std::any::TypeId::of::<bool>() {
157        for &byte in buf {
158            if byte > 1 {
159                return Err(FerrayError::invalid_value(format!(
160                    "invalid byte {byte:#04x} for bool (must be 0x00 or 0x01)"
161                )));
162            }
163        }
164    }
165
166    // SAFETY:
167    // - The pointer comes from a valid `&[u8]` slice with length
168    //   `n_elems * elem_size`, so the region is valid for reads of
169    //   `n_elems` `T` values.
170    // - Alignment was checked above.
171    // - For bool, bit patterns were validated above. For all other
172    //   `Element` types, every bit pattern is a valid value.
173    // - The returned view's lifetime is bound to `'a = &'a [u8]`, which
174    //   tracks the borrow back to `buf`, so the memory cannot be freed
175    //   or mutated while the view lives.
176    let ptr = buf.as_ptr().cast::<T>();
177    let nd_dim = dim.to_ndarray_dim();
178    let nd_view = unsafe { ndarray::ArrayView::from_shape_ptr(nd_dim, ptr) };
179    Ok(ArrayView::from_ndarray(nd_view))
180}
181
182/// Create a 1-D array from an iterator.
183///
184/// Analogous to `numpy.fromiter()`.
185///
186/// # Errors
187/// This function always succeeds (returns `Ok`).
188pub fn fromiter<T: Element>(iter: impl IntoIterator<Item = T>) -> FerrayResult<Array<T, Ix1>> {
189    Array::from_iter_1d(iter)
190}
191
192/// Create an array filled with zeros.
193///
194/// Analogous to `numpy.zeros()`.
195pub fn zeros<T: Element, D: Dimension>(dim: D) -> FerrayResult<Array<T, D>> {
196    Array::zeros(dim)
197}
198
199/// Create an array filled with ones.
200///
201/// Analogous to `numpy.ones()`.
202pub fn ones<T: Element, D: Dimension>(dim: D) -> FerrayResult<Array<T, D>> {
203    Array::ones(dim)
204}
205
206/// Create an array filled with a given value.
207///
208/// Analogous to `numpy.full()`.
209pub fn full<T: Element, D: Dimension>(dim: D, fill_value: T) -> FerrayResult<Array<T, D>> {
210    Array::from_elem(dim, fill_value)
211}
212
213/// Create an array with the same shape as `other`, filled with zeros.
214///
215/// Analogous to `numpy.zeros_like()`.
216pub fn zeros_like<T: Element, D: Dimension>(other: &Array<T, D>) -> FerrayResult<Array<T, D>> {
217    Array::zeros(other.dim().clone())
218}
219
220/// Create an array with the same shape as `other`, filled with ones.
221///
222/// Analogous to `numpy.ones_like()`.
223pub fn ones_like<T: Element, D: Dimension>(other: &Array<T, D>) -> FerrayResult<Array<T, D>> {
224    Array::ones(other.dim().clone())
225}
226
227/// Create an array with the same shape as `other`, filled with `fill_value`.
228///
229/// Analogous to `numpy.full_like()`.
230pub fn full_like<T: Element, D: Dimension>(
231    other: &Array<T, D>,
232    fill_value: T,
233) -> FerrayResult<Array<T, D>> {
234    Array::from_elem(other.dim().clone(), fill_value)
235}
236
237// ============================================================================
238// REQ-17: empty() returning MaybeUninit
239// ============================================================================
240
241/// An array whose elements have not been initialized.
242///
243/// The caller must call [`assume_init`](UninitArray::assume_init) after
244/// filling all elements.
245pub struct UninitArray<T: Element, D: Dimension> {
246    data: Vec<MaybeUninit<T>>,
247    dim: D,
248}
249
250impl<T: Element, D: Dimension> UninitArray<T, D> {
251    /// Shape as a slice.
252    #[inline]
253    pub fn shape(&self) -> &[usize] {
254        self.dim.as_slice()
255    }
256
257    /// Total number of elements.
258    #[inline]
259    pub fn size(&self) -> usize {
260        self.data.len()
261    }
262
263    /// Number of dimensions.
264    #[inline]
265    pub fn ndim(&self) -> usize {
266        self.dim.ndim()
267    }
268
269    /// Get a mutable raw pointer to the underlying data.
270    ///
271    /// Use this to fill the array element-by-element before calling
272    /// `assume_init()`.
273    #[inline]
274    pub fn as_mut_ptr(&mut self) -> *mut MaybeUninit<T> {
275        self.data.as_mut_ptr()
276    }
277
278    /// Write a value at a flat index.
279    ///
280    /// # Errors
281    /// Returns `FerrayError::IndexOutOfBounds` if `flat_index >= size()`.
282    pub fn write_at(&mut self, flat_index: usize, value: T) -> FerrayResult<()> {
283        let size = self.size();
284        if flat_index >= size {
285            return Err(FerrayError::IndexOutOfBounds {
286                index: flat_index as isize,
287                axis: 0,
288                size,
289            });
290        }
291        self.data[flat_index] = MaybeUninit::new(value);
292        Ok(())
293    }
294
295    /// Convert to an initialized `Array<T, D>`.
296    ///
297    /// # Safety
298    /// The caller must ensure that **all** elements have been initialized
299    /// (e.g., via `write_at` or raw pointer writes). Reading uninitialized
300    /// memory is undefined behavior.
301    pub unsafe fn assume_init(self) -> Array<T, D> {
302        let nd_dim = self.dim.to_ndarray_dim();
303        let len = self.data.len();
304
305        // Transmute Vec<MaybeUninit<T>> to Vec<T>.
306        // SAFETY: MaybeUninit<T> has the same layout as T, and the caller
307        // guarantees all elements are initialized.
308        let mut raw_vec = std::mem::ManuallyDrop::new(self.data);
309        let data: Vec<T> = unsafe {
310            Vec::from_raw_parts(raw_vec.as_mut_ptr().cast::<T>(), len, raw_vec.capacity())
311        };
312
313        let inner = ndarray::Array::from_shape_vec(nd_dim, data)
314            .expect("UninitArray assume_init: shape/data mismatch (this is a bug)");
315        Array::from_ndarray(inner)
316    }
317}
318
319/// Create an uninitialized array.
320///
321/// Analogous to `numpy.empty()`, but returns a [`UninitArray`] that must
322/// be explicitly initialized via [`UninitArray::assume_init`].
323///
324/// This prevents accidentally reading uninitialized memory — a key safety
325/// improvement over `NumPy`'s `empty()`.
326pub fn empty<T: Element, D: Dimension>(dim: D) -> UninitArray<T, D> {
327    let size = dim.size();
328    let mut data = Vec::with_capacity(size);
329    // SAFETY: MaybeUninit does not require initialization.
330    // We set the length to match the capacity; each element is MaybeUninit.
331    unsafe {
332        data.set_len(size);
333    }
334    UninitArray { data, dim }
335}
336
337/// Create an uninitialized array with the same shape (and element type)
338/// as `other`.
339///
340/// Analogous to `numpy.empty_like()`. Returns a [`UninitArray`] that the
341/// caller must fully initialize before calling
342/// [`UninitArray::assume_init`]. Avoids the memset that `zeros_like` /
343/// `full_like` incur when the caller is about to overwrite every element
344/// anyway.
345pub fn empty_like<T: Element, D: Dimension>(other: &Array<T, D>) -> UninitArray<T, D> {
346    empty(other.dim().clone())
347}
348
349// ============================================================================
350// REQ-18: Range functions
351// ============================================================================
352
353/// Trait for types usable with `arange` — numeric types that support
354/// stepping and comparison.
355pub trait ArangeNum: Element + PartialOrd {
356    /// Convert from f64 for step calculations.
357    fn from_f64(v: f64) -> Self;
358    /// Convert to f64 for step calculations.
359    fn to_f64(self) -> f64;
360}
361
362macro_rules! impl_arange_int {
363    ($($ty:ty),*) => {
364        $(
365            impl ArangeNum for $ty {
366                #[inline]
367                fn from_f64(v: f64) -> Self { v as Self }
368                #[inline]
369                fn to_f64(self) -> f64 { self as f64 }
370            }
371        )*
372    };
373}
374
375macro_rules! impl_arange_float {
376    ($($ty:ty),*) => {
377        $(
378            impl ArangeNum for $ty {
379                #[inline]
380                fn from_f64(v: f64) -> Self { v as Self }
381                #[inline]
382                fn to_f64(self) -> f64 { self as f64 }
383            }
384        )*
385    };
386}
387
388impl_arange_int!(u8, u16, u32, u64, i8, i16, i32, i64);
389impl_arange_float!(f32, f64);
390
391/// Create a 1-D array with evenly spaced values within a given interval.
392///
393/// Analogous to `numpy.arange(start, stop, step)`.
394///
395/// # Errors
396/// Returns `FerrayError::InvalidValue` if `step` is zero.
397pub fn arange<T: ArangeNum>(start: T, stop: T, step: T) -> FerrayResult<Array<T, Ix1>> {
398    let step_f = step.to_f64();
399    if step_f == 0.0 {
400        return Err(FerrayError::invalid_value("step cannot be zero"));
401    }
402    let start_f = start.to_f64();
403    let stop_f = stop.to_f64();
404    let n = ((stop_f - start_f) / step_f).ceil();
405    let n = if n < 0.0 { 0 } else { n as usize };
406
407    let mut data = Vec::with_capacity(n);
408    for i in 0..n {
409        data.push(T::from_f64((i as f64).mul_add(step_f, start_f)));
410    }
411    let dim = Ix1::new([data.len()]);
412    Array::from_vec(dim, data)
413}
414
415/// Trait for float-like types used in linspace/logspace/geomspace.
416pub trait LinspaceNum: Element + PartialOrd {
417    /// Convert from f64.
418    fn from_f64(v: f64) -> Self;
419    /// Convert to f64.
420    fn to_f64(self) -> f64;
421}
422
423impl LinspaceNum for f32 {
424    #[inline]
425    fn from_f64(v: f64) -> Self {
426        v as Self
427    }
428    #[inline]
429    fn to_f64(self) -> f64 {
430        self as f64
431    }
432}
433
434impl LinspaceNum for f64 {
435    #[inline]
436    fn from_f64(v: f64) -> Self {
437        v
438    }
439    #[inline]
440    fn to_f64(self) -> f64 {
441        self
442    }
443}
444
445/// Create a 1-D array with `num` evenly spaced values between `start` and `stop`.
446///
447/// If `endpoint` is true (the default in `NumPy`), `stop` is the last sample.
448/// Otherwise, it is not included.
449///
450/// Analogous to `numpy.linspace()`.
451///
452/// # Errors
453/// Returns `FerrayError::InvalidValue` if `num` is 0 and `endpoint` is true
454/// (cannot produce an empty array with an endpoint).
455pub fn linspace<T: LinspaceNum>(
456    start: T,
457    stop: T,
458    num: usize,
459    endpoint: bool,
460) -> FerrayResult<Array<T, Ix1>> {
461    if num == 0 {
462        return Array::from_vec(Ix1::new([0]), vec![]);
463    }
464    if num == 1 {
465        return Array::from_vec(Ix1::new([1]), vec![start]);
466    }
467    let start_f = start.to_f64();
468    let stop_f = stop.to_f64();
469    let divisor = if endpoint {
470        (num - 1) as f64
471    } else {
472        num as f64
473    };
474    let step = (stop_f - start_f) / divisor;
475    let mut data = Vec::with_capacity(num);
476    for i in 0..num {
477        data.push(T::from_f64((i as f64).mul_add(step, start_f)));
478    }
479    Array::from_vec(Ix1::new([num]), data)
480}
481
482/// Create a 1-D array with values spaced evenly on a log scale.
483///
484/// Returns `base ** linspace(start, stop, num)`.
485///
486/// Analogous to `numpy.logspace()`.
487///
488/// # Errors
489/// Propagates errors from `linspace`.
490pub fn logspace<T: LinspaceNum>(
491    start: T,
492    stop: T,
493    num: usize,
494    endpoint: bool,
495    base: f64,
496) -> FerrayResult<Array<T, Ix1>> {
497    let lin = linspace(start, stop, num, endpoint)?;
498    let data: Vec<T> = lin
499        .iter()
500        .map(|v| T::from_f64(base.powf(v.clone().to_f64())))
501        .collect();
502    Array::from_vec(Ix1::new([num]), data)
503}
504
505/// Create a 1-D array with values spaced evenly on a geometric (log) scale.
506///
507/// The values are `start * (stop/start) ** linspace(0, 1, num)`.
508///
509/// Analogous to `numpy.geomspace()`.
510///
511/// # Errors
512/// Returns `FerrayError::InvalidValue` if `start` or `stop` is zero or
513/// if they have different signs.
514pub fn geomspace<T: LinspaceNum>(
515    start: T,
516    stop: T,
517    num: usize,
518    endpoint: bool,
519) -> FerrayResult<Array<T, Ix1>> {
520    let start_f = start.clone().to_f64();
521    let stop_f = stop.to_f64();
522    if start_f == 0.0 || stop_f == 0.0 {
523        return Err(FerrayError::invalid_value(
524            "geomspace: start and stop must be non-zero",
525        ));
526    }
527    if (start_f < 0.0) != (stop_f < 0.0) {
528        return Err(FerrayError::invalid_value(
529            "geomspace: start and stop must have the same sign",
530        ));
531    }
532    if num == 0 {
533        return Array::from_vec(Ix1::new([0]), vec![]);
534    }
535    if num == 1 {
536        return Array::from_vec(Ix1::new([1]), vec![start]);
537    }
538    let log_start = start_f.abs().ln();
539    let log_stop = stop_f.abs().ln();
540    let sign = if start_f < 0.0 { -1.0 } else { 1.0 };
541    let divisor = if endpoint {
542        (num - 1) as f64
543    } else {
544        num as f64
545    };
546    let step = (log_stop - log_start) / divisor;
547    let mut data = Vec::with_capacity(num);
548    for i in 0..num {
549        let log_val = (i as f64).mul_add(step, log_start);
550        data.push(T::from_f64(sign * log_val.exp()));
551    }
552    Array::from_vec(Ix1::new([num]), data)
553}
554
555/// Return coordinate arrays from coordinate vectors.
556///
557/// Analogous to `numpy.meshgrid(*xi, indexing='xy')`.
558///
559/// Given N 1-D arrays, returns N N-D arrays, where each output array
560/// has the shape `(len(x1), len(x2), ..., len(xN))` for 'xy' indexing
561/// or `(len(x1), ..., len(xN))` transposed for 'ij' indexing.
562///
563/// `indexing` should be `"xy"` (default Cartesian) or `"ij"` (matrix).
564///
565/// # Errors
566/// Returns `FerrayError::InvalidValue` if `indexing` is not `"xy"` or `"ij"`,
567/// or if there are fewer than 2 input arrays.
568pub fn meshgrid(
569    arrays: &[Array<f64, Ix1>],
570    indexing: &str,
571) -> FerrayResult<Vec<Array<f64, IxDyn>>> {
572    if indexing != "xy" && indexing != "ij" {
573        return Err(FerrayError::invalid_value(
574            "meshgrid: indexing must be 'xy' or 'ij'",
575        ));
576    }
577    let ndim = arrays.len();
578    if ndim == 0 {
579        return Ok(vec![]);
580    }
581
582    let mut shapes: Vec<usize> = arrays.iter().map(|a| a.shape()[0]).collect();
583    if indexing == "xy" && ndim >= 2 {
584        shapes.swap(0, 1);
585    }
586
587    let total: usize = shapes.iter().product();
588    let mut results = Vec::with_capacity(ndim);
589
590    for (k, arr) in arrays.iter().enumerate() {
591        let src_data: Vec<f64> = arr.iter().copied().collect();
592        let mut data = Vec::with_capacity(total);
593        // For 'xy' indexing, the first two dimensions are swapped
594        let effective_k = if indexing == "xy" && ndim >= 2 {
595            match k {
596                0 => 1,
597                1 => 0,
598                other => other,
599            }
600        } else {
601            k
602        };
603
604        // Build the output by iterating over all indices in the output shape
605        for flat in 0..total {
606            // Compute the index along dimension effective_k
607            let mut rem = flat;
608            let mut idx_k = 0;
609            for (d, &s) in shapes.iter().enumerate().rev() {
610                if d == effective_k {
611                    idx_k = rem % s;
612                }
613                rem /= s;
614            }
615            data.push(src_data[idx_k]);
616        }
617
618        let dim = IxDyn::new(&shapes);
619        results.push(Array::from_vec(dim, data)?);
620    }
621    Ok(results)
622}
623
624/// Create a dense multi-dimensional "meshgrid" with matrix ('ij') indexing.
625///
626/// Analogous to `numpy.mgrid[start:stop:step, ...]`.
627///
628/// Takes a slice of `(start, stop, step)` tuples, one per dimension.
629/// Returns a vector of arrays, one per dimension.
630///
631/// # Errors
632/// Returns `FerrayError::InvalidValue` if any step is zero.
633pub fn mgrid(ranges: &[(f64, f64, f64)]) -> FerrayResult<Vec<Array<f64, IxDyn>>> {
634    let mut arrs: Vec<Array<f64, Ix1>> = Vec::with_capacity(ranges.len());
635    for &(start, stop, step) in ranges {
636        arrs.push(arange(start, stop, step)?);
637    }
638    meshgrid(&arrs, "ij")
639}
640
641/// Create a sparse (open) multi-dimensional "meshgrid" with 'ij' indexing.
642///
643/// Analogous to `numpy.ogrid[start:stop:step, ...]`.
644///
645/// Returns arrays that are broadcastable to the full grid shape.
646/// Each returned array has shape 1 in all dimensions except its own.
647///
648/// # Errors
649/// Returns `FerrayError::InvalidValue` if any step is zero.
650pub fn ogrid(ranges: &[(f64, f64, f64)]) -> FerrayResult<Vec<Array<f64, IxDyn>>> {
651    let ndim = ranges.len();
652    let mut results = Vec::with_capacity(ndim);
653    for (i, &(start, stop, step)) in ranges.iter().enumerate() {
654        let arr1d = arange(start, stop, step)?;
655        let n = arr1d.shape()[0];
656        let data: Vec<f64> = arr1d.iter().copied().collect();
657        // Build shape: all ones except dimension i = n
658        let mut shape = vec![1usize; ndim];
659        shape[i] = n;
660        let dim = IxDyn::new(&shape);
661        results.push(Array::from_vec(dim, data)?);
662    }
663    Ok(results)
664}
665
666// ============================================================================
667// REQ-19: Identity/diagonal functions
668// ============================================================================
669
670/// Create a 2-D identity matrix of size `n x n`.
671///
672/// Analogous to `numpy.identity()`.
673pub fn identity<T: Element>(n: usize) -> FerrayResult<Array<T, Ix2>> {
674    eye(n, n, 0)
675}
676
677/// Create a 2-D array with ones on the diagonal and zeros elsewhere.
678///
679/// `k` is the diagonal offset: 0 = main diagonal, positive = above, negative = below.
680///
681/// Analogous to `numpy.eye(N, M, k)`.
682pub fn eye<T: Element>(n: usize, m: usize, k: isize) -> FerrayResult<Array<T, Ix2>> {
683    let mut data = vec![T::zero(); n * m];
684    for i in 0..n {
685        let j = i as isize + k;
686        if j >= 0 && (j as usize) < m {
687            data[i * m + j as usize] = T::one();
688        }
689    }
690    Array::from_vec(Ix2::new([n, m]), data)
691}
692
693/// Extract a diagonal or construct a diagonal array.
694///
695/// If `a` is 2-D, extract the `k`-th diagonal as a 1-D array.
696/// If `a` is 1-D, construct a 2-D array with `a` on the `k`-th diagonal.
697///
698/// Analogous to `numpy.diag()`.
699///
700/// # Errors
701/// Returns `FerrayError::InvalidValue` if `a` is not 1-D or 2-D.
702pub fn diag<T: Element>(a: &Array<T, IxDyn>, k: isize) -> FerrayResult<Array<T, IxDyn>> {
703    let shape = a.shape();
704    match shape.len() {
705        1 => {
706            // Construct a 2-D diagonal array
707            let n = shape[0];
708            let size = n + k.unsigned_abs();
709            let mut data = vec![T::zero(); size * size];
710            let src: Vec<T> = a.iter().cloned().collect();
711            for (i, val) in src.into_iter().enumerate() {
712                let row = if k >= 0 { i } else { i + k.unsigned_abs() };
713                let col = if k >= 0 { i + k as usize } else { i };
714                data[row * size + col] = val;
715            }
716            Array::from_vec(IxDyn::new(&[size, size]), data)
717        }
718        2 => {
719            // Extract the k-th diagonal
720            let (n, m) = (shape[0], shape[1]);
721            let src: Vec<T> = a.iter().cloned().collect();
722            let mut diag_vals = Vec::new();
723            for i in 0..n {
724                let j = i as isize + k;
725                if j >= 0 && (j as usize) < m {
726                    diag_vals.push(src[i * m + j as usize].clone());
727                }
728            }
729            let len = diag_vals.len();
730            Array::from_vec(IxDyn::new(&[len]), diag_vals)
731        }
732        _ => Err(FerrayError::invalid_value("diag: input must be 1-D or 2-D")),
733    }
734}
735
736/// Create a 2-D array with the flattened input as a diagonal.
737///
738/// Analogous to `numpy.diagflat()`.
739///
740/// # Errors
741/// Propagates errors from the underlying construction.
742pub fn diagflat<T: Element>(a: &Array<T, IxDyn>, k: isize) -> FerrayResult<Array<T, IxDyn>> {
743    // Flatten a to 1-D, then call diag
744    let flat: Vec<T> = a.iter().cloned().collect();
745    let n = flat.len();
746    let arr1d = Array::from_vec(IxDyn::new(&[n]), flat)?;
747    diag(&arr1d, k)
748}
749
750/// Create a lower-triangular matrix of ones.
751///
752/// Returns an `n x m` array where `a[i, j] = 1` if `i >= j - k`, else `0`.
753///
754/// Analogous to `numpy.tri(N, M, k)`.
755pub fn tri<T: Element>(n: usize, m: usize, k: isize) -> FerrayResult<Array<T, Ix2>> {
756    let mut data = vec![T::zero(); n * m];
757    for i in 0..n {
758        for j in 0..m {
759            if (i as isize) >= (j as isize) - k {
760                data[i * m + j] = T::one();
761            }
762        }
763    }
764    Array::from_vec(Ix2::new([n, m]), data)
765}
766
767/// Return the lower triangle of a 2-D array.
768///
769/// `k` is the diagonal above which to zero elements. 0 = main diagonal.
770///
771/// Analogous to `numpy.tril()`.
772///
773/// # Errors
774/// Returns `FerrayError::InvalidValue` if input is not 2-D.
775pub fn tril<T: Element>(a: &Array<T, IxDyn>, k: isize) -> FerrayResult<Array<T, IxDyn>> {
776    let shape = a.shape();
777    if shape.len() != 2 {
778        return Err(FerrayError::invalid_value("tril: input must be 2-D"));
779    }
780    let (n, m) = (shape[0], shape[1]);
781    let src: Vec<T> = a.iter().cloned().collect();
782    let mut data = vec![T::zero(); n * m];
783    for i in 0..n {
784        for j in 0..m {
785            if (i as isize) >= (j as isize) - k {
786                data[i * m + j] = src[i * m + j].clone();
787            }
788        }
789    }
790    Array::from_vec(IxDyn::new(&[n, m]), data)
791}
792
793/// Return the upper triangle of a 2-D array.
794///
795/// `k` is the diagonal below which to zero elements. 0 = main diagonal.
796///
797/// Analogous to `numpy.triu()`.
798///
799/// # Errors
800/// Returns `FerrayError::InvalidValue` if input is not 2-D.
801pub fn triu<T: Element>(a: &Array<T, IxDyn>, k: isize) -> FerrayResult<Array<T, IxDyn>> {
802    let shape = a.shape();
803    if shape.len() != 2 {
804        return Err(FerrayError::invalid_value("triu: input must be 2-D"));
805    }
806    let (n, m) = (shape[0], shape[1]);
807    let src: Vec<T> = a.iter().cloned().collect();
808    let mut data = vec![T::zero(); n * m];
809    for i in 0..n {
810        for j in 0..m {
811            if (i as isize) <= (j as isize) - k {
812                data[i * m + j] = src[i * m + j].clone();
813            }
814        }
815    }
816    Array::from_vec(IxDyn::new(&[n, m]), data)
817}
818
819// ============================================================================
820// REQ: vander (Vandermonde matrix)
821// ============================================================================
822
823/// Generate a Vandermonde matrix.
824///
825/// Each row is a geometric progression of the corresponding `x` element.
826/// `n` is the number of columns (`None` => `x.len()`). When `increasing`
827/// is `false` (numpy default), columns are powers in decreasing order
828/// `x[i]^(n-1-j)`. When `true`, they are increasing: `x[i]^j`.
829///
830/// Analogous to `numpy.vander(x, N=n, increasing=...)`.
831///
832/// # Errors
833/// Returns `FerrayError::InvalidValue` if `x` is empty.
834pub fn vander<T>(
835    x: &Array<T, Ix1>,
836    n: Option<usize>,
837    increasing: bool,
838) -> FerrayResult<Array<T, IxDyn>>
839where
840    T: Element + std::ops::Mul<Output = T> + Copy,
841{
842    let m = x.shape()[0];
843    if m == 0 {
844        return Err(FerrayError::invalid_value(
845            "vander: input array must not be empty",
846        ));
847    }
848    let cols = n.unwrap_or(m);
849    let xs: Vec<T> = x.iter().copied().collect();
850    let mut data = vec![<T as Element>::one(); m * cols];
851    for (i, &xi) in xs.iter().enumerate() {
852        // Build powers x^0, x^1, ..., x^(cols-1) for this row.
853        let mut acc = <T as Element>::one();
854        let mut powers = Vec::with_capacity(cols);
855        for _ in 0..cols {
856            powers.push(acc);
857            acc = acc * xi;
858        }
859        if increasing {
860            for (j, p) in powers.iter().enumerate() {
861                data[i * cols + j] = *p;
862            }
863        } else {
864            for (j, p) in powers.iter().enumerate() {
865                data[i * cols + (cols - 1 - j)] = *p;
866            }
867        }
868    }
869    Array::from_vec(IxDyn::new(&[m, cols]), data)
870}
871
872// ============================================================================
873// REQ: copy / asarray family / chkfinite / require
874// ============================================================================
875
876/// Return an array copy of the given object.
877///
878/// Analogous to `numpy.copy()`. Allocates a new C-contiguous buffer
879/// with the same data.
880pub fn copy<T: Element, D: Dimension>(a: &Array<T, D>) -> Array<T, D> {
881    a.clone()
882}
883
884/// Return a contiguous array (C order) in memory.
885///
886/// In ferray, owned `Array<T, D>` values are always C-contiguous, so this
887/// is functionally equivalent to `clone()`. Provided for NumPy API parity
888/// (`numpy.ascontiguousarray`).
889pub fn ascontiguousarray<T: Element, D: Dimension>(a: &Array<T, D>) -> Array<T, D> {
890    a.clone()
891}
892
893/// Return an array (Fortran-order) by transposing then cloning.
894///
895/// ferray stores in C-order; to get Fortran-style layout we materialize a
896/// new buffer in column-major order. The shape is preserved; only the
897/// memory layout differs (the element traversal order in `iter()` will
898/// follow C-order regardless after this round-trip).
899///
900/// Analogous to `numpy.asfortranarray`. Note: ferray's iterator/view API
901/// always reflects C-order, so the returned array will appear identical
902/// in element ordering — the underlying memory layout is the only
903/// distinction with NumPy. Provided for API parity.
904pub fn asfortranarray<T: Element, D: Dimension>(a: &Array<T, D>) -> Array<T, D> {
905    a.clone()
906}
907
908/// Convert input to an array. Accepts an existing array (no-op clone).
909///
910/// In NumPy, `asanyarray` differs from `asarray` only in that it preserves
911/// `MaskedArray` / `matrix` subclasses. ferray has no array-subclass
912/// hierarchy at this layer (masked arrays live in `ferray-ma`), so
913/// `asanyarray` is identical to `asarray` for owned arrays.
914///
915/// Analogous to `numpy.asanyarray`.
916pub fn asanyarray<T: Element, D: Dimension>(a: &Array<T, D>) -> Array<T, D> {
917    a.clone()
918}
919
920/// Convert the input to an array, checking for NaNs or infinities.
921///
922/// Analogous to `numpy.asarray_chkfinite`. Returns an `InvalidValue`
923/// error if any element is non-finite. Float-only.
924///
925/// # Errors
926/// Returns `FerrayError::InvalidValue` if any element is NaN or infinite.
927pub fn asarray_chkfinite<T, D: Dimension>(a: &Array<T, D>) -> FerrayResult<Array<T, D>>
928where
929    T: Element + num_traits::Float,
930{
931    for v in a.iter() {
932        if !v.is_finite() {
933            return Err(FerrayError::invalid_value(
934                "asarray_chkfinite: array contains non-finite values (NaN or Inf)",
935            ));
936        }
937    }
938    Ok(a.clone())
939}
940
941/// Return a contiguous array meeting requirements.
942///
943/// `requirements` is a string of single-character flags:
944/// - `'C'` — C-contiguous
945/// - `'F'` — F-contiguous (treated as C in ferray; see `asfortranarray`)
946/// - `'A'` — any contiguous (no-op)
947/// - `'W'` — writeable (always true for owned arrays)
948/// - `'O'` — owned data (no-op; arrays are always owned)
949///
950/// Analogous to `numpy.require`. Unknown flags are ignored.
951pub fn require<T: Element, D: Dimension>(a: &Array<T, D>, _requirements: &str) -> Array<T, D> {
952    a.clone()
953}
954
955// ============================================================================
956// REQ: fromfunction / fromstring / fromfile
957// ============================================================================
958
959/// Construct an array by executing a function over each coordinate.
960///
961/// `f` is called with the multi-index of each element (as `&[usize]`) and
962/// returns the value to place there. Iteration is C-order over the given
963/// shape.
964///
965/// Analogous to `numpy.fromfunction(func, shape, ...)`. NumPy passes
966/// per-axis index arrays to `func` (broadcasting); ferray instead invokes
967/// `f` per element with a coordinate slice — equivalent in result for
968/// scalar-returning functions but simpler in Rust.
969pub fn fromfunction<T, D, F>(shape: D, mut f: F) -> FerrayResult<Array<T, D>>
970where
971    T: Element,
972    D: Dimension,
973    F: FnMut(&[usize]) -> T,
974{
975    let dims: Vec<usize> = shape.as_slice().to_vec();
976    let total: usize = dims.iter().product();
977    let mut data = Vec::with_capacity(total);
978    let mut idx = vec![0usize; dims.len()];
979    for _ in 0..total {
980        data.push(f(&idx));
981        // Increment multi-index in C-order.
982        for axis in (0..dims.len()).rev() {
983            idx[axis] += 1;
984            if idx[axis] < dims[axis] {
985                break;
986            }
987            idx[axis] = 0;
988        }
989    }
990    Array::from_vec(shape, data)
991}
992
993/// Construct a 1-D array from a whitespace- or separator-delimited string.
994///
995/// Each token is parsed via `T::from_str` (any type implementing
996/// [`std::str::FromStr`]). Empty tokens are ignored.
997///
998/// Analogous to `numpy.fromstring(s, sep=...)` (the binary mode of NumPy
999/// `fromstring` is deprecated in favor of `frombuffer`, so we only
1000/// implement the text-parsing form here).
1001///
1002/// # Errors
1003/// Returns `FerrayError::InvalidValue` if any token fails to parse.
1004pub fn fromstring<T>(s: &str, sep: &str) -> FerrayResult<Array<T, Ix1>>
1005where
1006    T: Element + std::str::FromStr,
1007    <T as std::str::FromStr>::Err: std::fmt::Display,
1008{
1009    let parts: Vec<&str> = if sep.is_empty() {
1010        s.split_whitespace().collect()
1011    } else {
1012        s.split(sep)
1013            .map(str::trim)
1014            .filter(|t| !t.is_empty())
1015            .collect()
1016    };
1017    let mut data = Vec::with_capacity(parts.len());
1018    for tok in parts {
1019        let v = tok.parse::<T>().map_err(|e| {
1020            FerrayError::invalid_value(format!("fromstring: failed to parse {tok:?}: {e}"))
1021        })?;
1022        data.push(v);
1023    }
1024    let n = data.len();
1025    Array::from_vec(Ix1::new([n]), data)
1026}
1027
1028/// Read a 1-D array from a file by parsing whitespace-delimited tokens.
1029///
1030/// Analogous to `numpy.fromfile(path, sep=' ')` (text mode). The binary
1031/// mode of NumPy's `fromfile` is intentionally not provided here — use
1032/// `ferray-io::load` for `.npy` and `ferray_core::frombuffer` for raw
1033/// byte buffers.
1034///
1035/// # Errors
1036/// Returns `FerrayError::Io` for filesystem errors, `FerrayError::InvalidValue`
1037/// for parse failures.
1038pub fn fromfile<T, P: AsRef<std::path::Path>>(path: P, sep: &str) -> FerrayResult<Array<T, Ix1>>
1039where
1040    T: Element + std::str::FromStr,
1041    <T as std::str::FromStr>::Err: std::fmt::Display,
1042{
1043    let s = std::fs::read_to_string(path)
1044        .map_err(|e| FerrayError::invalid_value(format!("fromfile: {e}")))?;
1045    fromstring(&s, sep)
1046}
1047
1048// ============================================================================
1049// Tests
1050// ============================================================================
1051
1052#[cfg(test)]
1053mod tests {
1054    use super::*;
1055    use crate::dimension::{Ix1, Ix2, IxDyn};
1056
1057    // -- REQ-16 tests --
1058
1059    #[test]
1060    fn test_array_creation() {
1061        let a = array(Ix2::new([2, 3]), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
1062        assert_eq!(a.shape(), &[2, 3]);
1063        assert_eq!(a.size(), 6);
1064    }
1065
1066    #[test]
1067    fn test_asarray() {
1068        let a = asarray(Ix1::new([3]), vec![1, 2, 3]).unwrap();
1069        assert_eq!(a.as_slice().unwrap(), &[1, 2, 3]);
1070    }
1071
1072    #[test]
1073    fn test_frombuffer() {
1074        let bytes: Vec<u8> = vec![1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0];
1075        let a = frombuffer::<i32, Ix1>(Ix1::new([3]), &bytes).unwrap();
1076        assert_eq!(a.as_slice().unwrap(), &[1, 2, 3]);
1077    }
1078
1079    #[test]
1080    fn test_frombuffer_bad_length() {
1081        let bytes: Vec<u8> = vec![1, 0, 0];
1082        assert!(frombuffer::<i32, Ix1>(Ix1::new([1]), &bytes).is_err());
1083    }
1084
1085    #[test]
1086    fn test_frombuffer_bool() {
1087        // Issue #135: bool round-trips through frombuffer must
1088        // preserve the discriminating byte (0 -> false, nonzero
1089        // -> true, although our raw-buffer contract is that each
1090        // byte is a valid bool per `Element`).
1091        let bytes: Vec<u8> = vec![0, 1, 0, 1, 1];
1092        let a = frombuffer::<bool, Ix1>(Ix1::new([5]), &bytes).unwrap();
1093        assert_eq!(a.as_slice().unwrap(), &[false, true, false, true, true]);
1094    }
1095
1096    #[test]
1097    fn test_frombuffer_bool_wrong_length() {
1098        // For bool (1 byte each), the buffer length must equal the
1099        // requested element count.
1100        let bytes: Vec<u8> = vec![0, 1];
1101        assert!(frombuffer::<bool, Ix1>(Ix1::new([3]), &bytes).is_err());
1102    }
1103
1104    // #364: frombuffer_view — zero-copy view over an existing byte buffer.
1105
1106    /// Build an aligned byte buffer of `nbytes` from a typed slice so we
1107    /// can exercise the zero-copy path without fighting the allocator.
1108    fn aligned_bytes<T: Copy>(src: &[T]) -> Vec<u8> {
1109        let n = std::mem::size_of_val(src);
1110        let mut out = vec![0u8; n];
1111        // SAFETY: src is &[T], out is a byte buffer of exactly n bytes.
1112        unsafe {
1113            std::ptr::copy_nonoverlapping(src.as_ptr().cast::<u8>(), out.as_mut_ptr(), n);
1114        }
1115        out
1116    }
1117
1118    #[test]
1119    fn test_frombuffer_view_i32_is_zero_copy() {
1120        // Build an aligned byte buffer that represents three i32s.
1121        let source: Vec<i32> = vec![10, 20, 30];
1122        let bytes = aligned_bytes(&source);
1123        let view = frombuffer_view::<i32, Ix1>(Ix1::new([3]), &bytes).unwrap();
1124        assert_eq!(view.shape(), &[3]);
1125        let values: Vec<i32> = view.iter().copied().collect();
1126        assert_eq!(values, vec![10, 20, 30]);
1127        // Pointer must alias the source buffer — that's the zero-copy
1128        // contract distinguishing this from the copying frombuffer.
1129        assert_eq!(view.as_ptr().cast::<u8>(), bytes.as_ptr());
1130    }
1131
1132    #[test]
1133    fn test_frombuffer_view_f64_2d() {
1134        let source: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1135        let bytes = aligned_bytes(&source);
1136        let view = frombuffer_view::<f64, Ix2>(Ix2::new([2, 3]), &bytes).unwrap();
1137        assert_eq!(view.shape(), &[2, 3]);
1138        let values: Vec<f64> = view.iter().copied().collect();
1139        assert_eq!(values, source);
1140    }
1141
1142    #[test]
1143    fn test_frombuffer_view_bool_valid() {
1144        let bytes: Vec<u8> = vec![0, 1, 0, 1];
1145        let view = frombuffer_view::<bool, Ix1>(Ix1::new([4]), &bytes).unwrap();
1146        let values: Vec<bool> = view.iter().copied().collect();
1147        assert_eq!(values, vec![false, true, false, true]);
1148    }
1149
1150    #[test]
1151    fn test_frombuffer_view_bool_rejects_invalid_byte() {
1152        let bytes: Vec<u8> = vec![0, 1, 42]; // 42 is not a valid bool.
1153        assert!(frombuffer_view::<bool, Ix1>(Ix1::new([3]), &bytes).is_err());
1154    }
1155
1156    #[test]
1157    fn test_frombuffer_view_rejects_wrong_length() {
1158        // 13 bytes is not a multiple of size_of::<i32>() = 4.
1159        let bytes = vec![0u8; 13];
1160        assert!(frombuffer_view::<i32, Ix1>(Ix1::new([3]), &bytes).is_err());
1161        // 8 bytes is 2 i32s, but the caller asked for shape [3].
1162        let bytes = vec![0u8; 8];
1163        assert!(frombuffer_view::<i32, Ix1>(Ix1::new([3]), &bytes).is_err());
1164    }
1165
1166    #[test]
1167    fn test_frombuffer_view_rejects_misalignment() {
1168        // Force a misaligned slice by advancing the byte pointer by 1.
1169        let mut backing: Vec<u8> = vec![0u8; 1 + 4 * 3];
1170        for (i, chunk) in backing[1..].chunks_exact_mut(4).enumerate() {
1171            chunk.copy_from_slice(&(i as i32).to_ne_bytes());
1172        }
1173        let misaligned = &backing[1..];
1174        // The slice address is off by one from a 4-byte boundary, so
1175        // alignment for i32 cannot be satisfied.
1176        assert!((misaligned.as_ptr() as usize) % 4 != 0);
1177        assert!(frombuffer_view::<i32, Ix1>(Ix1::new([3]), misaligned).is_err());
1178    }
1179
1180    #[test]
1181    fn test_fromiter() {
1182        let a = fromiter((0..5).map(|x| x as f64)).unwrap();
1183        assert_eq!(a.shape(), &[5]);
1184        assert_eq!(a.as_slice().unwrap(), &[0.0, 1.0, 2.0, 3.0, 4.0]);
1185    }
1186
1187    #[test]
1188    fn test_zeros() {
1189        let a = zeros::<f64, Ix2>(Ix2::new([3, 4])).unwrap();
1190        assert_eq!(a.shape(), &[3, 4]);
1191        assert!(a.iter().all(|&v| v == 0.0));
1192    }
1193
1194    #[test]
1195    fn test_ones() {
1196        let a = ones::<f64, Ix1>(Ix1::new([5])).unwrap();
1197        assert!(a.iter().all(|&v| v == 1.0));
1198    }
1199
1200    #[test]
1201    fn test_full() {
1202        let a = full(Ix1::new([4]), 42i32).unwrap();
1203        assert!(a.iter().all(|&v| v == 42));
1204    }
1205
1206    #[test]
1207    fn test_zeros_like() {
1208        let a = ones::<f64, Ix2>(Ix2::new([2, 3])).unwrap();
1209        let b = zeros_like(&a).unwrap();
1210        assert_eq!(b.shape(), &[2, 3]);
1211        assert!(b.iter().all(|&v| v == 0.0));
1212    }
1213
1214    #[test]
1215    fn test_ones_like() {
1216        let a = zeros::<f64, Ix1>(Ix1::new([4])).unwrap();
1217        let b = ones_like(&a).unwrap();
1218        assert!(b.iter().all(|&v| v == 1.0));
1219    }
1220
1221    #[test]
1222    fn test_full_like() {
1223        let a = zeros::<i32, Ix1>(Ix1::new([3])).unwrap();
1224        let b = full_like(&a, 7).unwrap();
1225        assert!(b.iter().all(|&v| v == 7));
1226    }
1227
1228    // -- REQ-17 tests --
1229
1230    #[test]
1231    fn test_empty_and_init() {
1232        let mut u = empty::<f64, Ix1>(Ix1::new([3]));
1233        assert_eq!(u.shape(), &[3]);
1234        u.write_at(0, 1.0).unwrap();
1235        u.write_at(1, 2.0).unwrap();
1236        u.write_at(2, 3.0).unwrap();
1237        // SAFETY: all elements initialized
1238        let a = unsafe { u.assume_init() };
1239        assert_eq!(a.as_slice().unwrap(), &[1.0, 2.0, 3.0]);
1240    }
1241
1242    #[test]
1243    fn test_empty_write_oob() {
1244        let mut u = empty::<f64, Ix1>(Ix1::new([2]));
1245        assert!(u.write_at(5, 1.0).is_err());
1246    }
1247
1248    // #363: empty_like matches source shape, contents independent.
1249    #[test]
1250    fn test_empty_like_matches_shape_2d() {
1251        use crate::dimension::Ix2;
1252        let src = Array::<f64, Ix2>::from_vec(Ix2::new([2, 3]), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
1253            .unwrap();
1254        let mut u = empty_like(&src);
1255        assert_eq!(u.shape(), &[2, 3]);
1256        assert_eq!(u.size(), 6);
1257        assert_eq!(u.ndim(), 2);
1258
1259        // Fill and init — the resulting array is independent of `src`.
1260        for i in 0..6 {
1261            u.write_at(i, -(i as f64)).unwrap();
1262        }
1263        // SAFETY: every slot just written.
1264        let out = unsafe { u.assume_init() };
1265        assert_eq!(out.shape(), &[2, 3]);
1266        assert_eq!(
1267            out.as_slice().unwrap(),
1268            &[0.0, -1.0, -2.0, -3.0, -4.0, -5.0]
1269        );
1270        // Source is unchanged.
1271        assert_eq!(src.as_slice().unwrap(), &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1272    }
1273
1274    #[test]
1275    fn test_empty_like_zero_sized() {
1276        let src = Array::<f64, Ix1>::from_vec(Ix1::new([0]), vec![]).unwrap();
1277        let u = empty_like(&src);
1278        assert_eq!(u.shape(), &[0]);
1279        assert_eq!(u.size(), 0);
1280        // SAFETY: size is zero — nothing to initialize.
1281        let out = unsafe { u.assume_init() };
1282        assert_eq!(out.size(), 0);
1283    }
1284
1285    // -- REQ-18 tests --
1286
1287    #[test]
1288    fn test_arange_int() {
1289        let a = arange(0i32, 5, 1).unwrap();
1290        assert_eq!(a.as_slice().unwrap(), &[0, 1, 2, 3, 4]);
1291    }
1292
1293    #[test]
1294    fn test_arange_float() {
1295        let a = arange(0.0_f64, 1.0, 0.25).unwrap();
1296        assert_eq!(a.shape(), &[4]);
1297        let data = a.as_slice().unwrap();
1298        assert!((data[0] - 0.0).abs() < 1e-10);
1299        assert!((data[1] - 0.25).abs() < 1e-10);
1300        assert!((data[2] - 0.5).abs() < 1e-10);
1301        assert!((data[3] - 0.75).abs() < 1e-10);
1302    }
1303
1304    #[test]
1305    fn test_arange_negative_step() {
1306        let a = arange(5.0_f64, 0.0, -1.0).unwrap();
1307        assert_eq!(a.shape(), &[5]);
1308    }
1309
1310    #[test]
1311    fn test_arange_zero_step() {
1312        assert!(arange(0.0_f64, 1.0, 0.0).is_err());
1313    }
1314
1315    #[test]
1316    fn test_arange_empty() {
1317        let a = arange(5i32, 0, 1).unwrap();
1318        assert_eq!(a.shape(), &[0]);
1319    }
1320
1321    #[test]
1322    fn test_linspace() {
1323        let a = linspace(0.0_f64, 1.0, 5, true).unwrap();
1324        assert_eq!(a.shape(), &[5]);
1325        let data = a.as_slice().unwrap();
1326        assert!((data[0] - 0.0).abs() < 1e-10);
1327        assert!((data[4] - 1.0).abs() < 1e-10);
1328        assert!((data[2] - 0.5).abs() < 1e-10);
1329    }
1330
1331    #[test]
1332    fn test_linspace_no_endpoint() {
1333        let a = linspace(0.0_f64, 1.0, 4, false).unwrap();
1334        assert_eq!(a.shape(), &[4]);
1335        let data = a.as_slice().unwrap();
1336        assert!((data[0] - 0.0).abs() < 1e-10);
1337        assert!((data[1] - 0.25).abs() < 1e-10);
1338    }
1339
1340    #[test]
1341    fn test_linspace_single() {
1342        let a = linspace(5.0_f64, 10.0, 1, true).unwrap();
1343        assert_eq!(a.as_slice().unwrap(), &[5.0]);
1344    }
1345
1346    #[test]
1347    fn test_linspace_empty() {
1348        let a = linspace(0.0_f64, 1.0, 0, true).unwrap();
1349        assert_eq!(a.shape(), &[0]);
1350    }
1351
1352    #[test]
1353    fn test_logspace() {
1354        let a = logspace(0.0_f64, 2.0, 3, true, 10.0).unwrap();
1355        let data = a.as_slice().unwrap();
1356        assert!((data[0] - 1.0).abs() < 1e-10); // 10^0
1357        assert!((data[1] - 10.0).abs() < 1e-10); // 10^1
1358        assert!((data[2] - 100.0).abs() < 1e-10); // 10^2
1359    }
1360
1361    #[test]
1362    fn test_geomspace() {
1363        let a = geomspace(1.0_f64, 1000.0, 4, true).unwrap();
1364        let data = a.as_slice().unwrap();
1365        assert!((data[0] - 1.0).abs() < 1e-10);
1366        assert!((data[1] - 10.0).abs() < 1e-8);
1367        assert!((data[2] - 100.0).abs() < 1e-6);
1368        assert!((data[3] - 1000.0).abs() < 1e-4);
1369    }
1370
1371    #[test]
1372    fn test_geomspace_zero_start() {
1373        assert!(geomspace(0.0_f64, 1.0, 5, true).is_err());
1374    }
1375
1376    #[test]
1377    fn test_geomspace_different_signs() {
1378        assert!(geomspace(-1.0_f64, 1.0, 5, true).is_err());
1379    }
1380
1381    #[test]
1382    fn test_meshgrid_xy() {
1383        let x = Array::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
1384        let y = Array::from_vec(Ix1::new([2]), vec![4.0, 5.0]).unwrap();
1385        let grids = meshgrid(&[x, y], "xy").unwrap();
1386        assert_eq!(grids.len(), 2);
1387        assert_eq!(grids[0].shape(), &[2, 3]);
1388        assert_eq!(grids[1].shape(), &[2, 3]);
1389        // X grid: rows are [1,2,3], [1,2,3]
1390        let xdata: Vec<f64> = grids[0].iter().copied().collect();
1391        assert_eq!(xdata, vec![1.0, 2.0, 3.0, 1.0, 2.0, 3.0]);
1392        // Y grid: rows are [4,4,4], [5,5,5]
1393        let ydata: Vec<f64> = grids[1].iter().copied().collect();
1394        assert_eq!(ydata, vec![4.0, 4.0, 4.0, 5.0, 5.0, 5.0]);
1395    }
1396
1397    #[test]
1398    fn test_meshgrid_ij() {
1399        let x = Array::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
1400        let y = Array::from_vec(Ix1::new([2]), vec![4.0, 5.0]).unwrap();
1401        let grids = meshgrid(&[x, y], "ij").unwrap();
1402        assert_eq!(grids.len(), 2);
1403        assert_eq!(grids[0].shape(), &[3, 2]);
1404        assert_eq!(grids[1].shape(), &[3, 2]);
1405    }
1406
1407    #[test]
1408    fn test_meshgrid_bad_indexing() {
1409        assert!(meshgrid(&[], "zz").is_err());
1410    }
1411
1412    #[test]
1413    fn test_mgrid() {
1414        let grids = mgrid(&[(0.0, 3.0, 1.0), (0.0, 2.0, 1.0)]).unwrap();
1415        assert_eq!(grids.len(), 2);
1416        assert_eq!(grids[0].shape(), &[3, 2]);
1417    }
1418
1419    #[test]
1420    fn test_ogrid() {
1421        let grids = ogrid(&[(0.0, 3.0, 1.0), (0.0, 2.0, 1.0)]).unwrap();
1422        assert_eq!(grids.len(), 2);
1423        assert_eq!(grids[0].shape(), &[3, 1]);
1424        assert_eq!(grids[1].shape(), &[1, 2]);
1425    }
1426
1427    // -- REQ-19 tests --
1428
1429    #[test]
1430    fn test_identity() {
1431        let a = identity::<f64>(3).unwrap();
1432        assert_eq!(a.shape(), &[3, 3]);
1433        let data = a.as_slice().unwrap();
1434        assert_eq!(data, &[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]);
1435    }
1436
1437    #[test]
1438    fn test_eye() {
1439        let a = eye::<f64>(3, 4, 0).unwrap();
1440        assert_eq!(a.shape(), &[3, 4]);
1441        let data = a.as_slice().unwrap();
1442        assert_eq!(
1443            data,
1444            &[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0]
1445        );
1446    }
1447
1448    #[test]
1449    fn test_eye_positive_k() {
1450        let a = eye::<f64>(3, 3, 1).unwrap();
1451        let data = a.as_slice().unwrap();
1452        assert_eq!(data, &[0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
1453    }
1454
1455    #[test]
1456    fn test_eye_negative_k() {
1457        let a = eye::<f64>(3, 3, -1).unwrap();
1458        let data = a.as_slice().unwrap();
1459        assert_eq!(data, &[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]);
1460    }
1461
1462    #[test]
1463    fn test_diag_from_1d() {
1464        let a = Array::from_vec(IxDyn::new(&[3]), vec![1.0, 2.0, 3.0]).unwrap();
1465        let d = diag(&a, 0).unwrap();
1466        assert_eq!(d.shape(), &[3, 3]);
1467        let data: Vec<f64> = d.iter().copied().collect();
1468        assert_eq!(data, vec![1.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0]);
1469    }
1470
1471    #[test]
1472    fn test_diag_from_2d() {
1473        let a = Array::from_vec(
1474            IxDyn::new(&[3, 3]),
1475            vec![1.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0],
1476        )
1477        .unwrap();
1478        let d = diag(&a, 0).unwrap();
1479        assert_eq!(d.shape(), &[3]);
1480        let data: Vec<f64> = d.iter().copied().collect();
1481        assert_eq!(data, vec![1.0, 2.0, 3.0]);
1482    }
1483
1484    #[test]
1485    fn test_diag_k_positive() {
1486        let a = Array::from_vec(IxDyn::new(&[2]), vec![1.0, 2.0]).unwrap();
1487        let d = diag(&a, 1).unwrap();
1488        assert_eq!(d.shape(), &[3, 3]);
1489        let data: Vec<f64> = d.iter().copied().collect();
1490        assert_eq!(data, vec![0.0, 1.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0]);
1491    }
1492
1493    #[test]
1494    fn test_diagflat() {
1495        let a = Array::from_vec(IxDyn::new(&[2, 2]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
1496        let d = diagflat(&a, 0).unwrap();
1497        assert_eq!(d.shape(), &[4, 4]);
1498        // Diagonal should be [1, 2, 3, 4]
1499        let extracted = diag(&d, 0).unwrap();
1500        let data: Vec<f64> = extracted.iter().copied().collect();
1501        assert_eq!(data, vec![1.0, 2.0, 3.0, 4.0]);
1502    }
1503
1504    #[test]
1505    fn test_tri() {
1506        let a = tri::<f64>(3, 3, 0).unwrap();
1507        let data = a.as_slice().unwrap();
1508        assert_eq!(data, &[1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0]);
1509    }
1510
1511    #[test]
1512    fn test_tril() {
1513        let a = Array::from_vec(
1514            IxDyn::new(&[3, 3]),
1515            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
1516        )
1517        .unwrap();
1518        let t = tril(&a, 0).unwrap();
1519        let data: Vec<f64> = t.iter().copied().collect();
1520        assert_eq!(data, vec![1.0, 0.0, 0.0, 4.0, 5.0, 0.0, 7.0, 8.0, 9.0]);
1521    }
1522
1523    #[test]
1524    fn test_triu() {
1525        let a = Array::from_vec(
1526            IxDyn::new(&[3, 3]),
1527            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
1528        )
1529        .unwrap();
1530        let t = triu(&a, 0).unwrap();
1531        let data: Vec<f64> = t.iter().copied().collect();
1532        assert_eq!(data, vec![1.0, 2.0, 3.0, 0.0, 5.0, 6.0, 0.0, 0.0, 9.0]);
1533    }
1534
1535    #[test]
1536    fn test_tril_not_2d() {
1537        let a = Array::from_vec(IxDyn::new(&[3]), vec![1.0, 2.0, 3.0]).unwrap();
1538        assert!(tril(&a, 0).is_err());
1539    }
1540
1541    #[test]
1542    fn test_triu_not_2d() {
1543        let a = Array::from_vec(IxDyn::new(&[3]), vec![1.0, 2.0, 3.0]).unwrap();
1544        assert!(triu(&a, 0).is_err());
1545    }
1546
1547    // -- vander --
1548
1549    #[test]
1550    fn test_vander_default_decreasing() {
1551        let x = Array::from_vec(Ix1::new([3]), vec![1.0_f64, 2.0, 3.0]).unwrap();
1552        let v = vander(&x, None, false).unwrap();
1553        assert_eq!(v.shape(), &[3, 3]);
1554        // Row 0: [1, 1, 1], row 1: [4, 2, 1], row 2: [9, 3, 1]
1555        let data: Vec<f64> = v.iter().copied().collect();
1556        assert_eq!(data, vec![1.0, 1.0, 1.0, 4.0, 2.0, 1.0, 9.0, 3.0, 1.0]);
1557    }
1558
1559    #[test]
1560    fn test_vander_increasing() {
1561        let x = Array::from_vec(Ix1::new([3]), vec![1.0_f64, 2.0, 3.0]).unwrap();
1562        let v = vander(&x, None, true).unwrap();
1563        let data: Vec<f64> = v.iter().copied().collect();
1564        // Row 0: [1, 1, 1], row 1: [1, 2, 4], row 2: [1, 3, 9]
1565        assert_eq!(data, vec![1.0, 1.0, 1.0, 1.0, 2.0, 4.0, 1.0, 3.0, 9.0]);
1566    }
1567
1568    #[test]
1569    fn test_vander_explicit_n() {
1570        let x = Array::from_vec(Ix1::new([2]), vec![2.0_f64, 3.0]).unwrap();
1571        let v = vander(&x, Some(4), false).unwrap();
1572        assert_eq!(v.shape(), &[2, 4]);
1573        // Row 0: 2^3, 2^2, 2^1, 2^0 = [8, 4, 2, 1]
1574        // Row 1: 3^3, 3^2, 3^1, 3^0 = [27, 9, 3, 1]
1575        let data: Vec<f64> = v.iter().copied().collect();
1576        assert_eq!(data, vec![8.0, 4.0, 2.0, 1.0, 27.0, 9.0, 3.0, 1.0]);
1577    }
1578
1579    #[test]
1580    fn test_vander_empty() {
1581        let x: Array<f64, Ix1> = Array::from_vec(Ix1::new([0]), vec![]).unwrap();
1582        assert!(vander(&x, None, false).is_err());
1583    }
1584
1585    // -- copy / asarray family --
1586
1587    #[test]
1588    fn test_copy() {
1589        let a = array(Ix1::new([3]), vec![1.0_f64, 2.0, 3.0]).unwrap();
1590        let b = copy(&a);
1591        assert_eq!(
1592            a.iter().copied().collect::<Vec<_>>(),
1593            b.iter().copied().collect::<Vec<_>>()
1594        );
1595        // Different buffer
1596        assert_ne!(
1597            a.as_slice().unwrap().as_ptr(),
1598            b.as_slice().unwrap().as_ptr()
1599        );
1600    }
1601
1602    #[test]
1603    fn test_ascontiguousarray() {
1604        let a = array(Ix1::new([3]), vec![1.0_f64, 2.0, 3.0]).unwrap();
1605        let b = ascontiguousarray(&a);
1606        assert_eq!(b.shape(), a.shape());
1607    }
1608
1609    #[test]
1610    fn test_asarray_chkfinite_ok() {
1611        let a = array(Ix1::new([3]), vec![1.0_f64, 2.0, 3.0]).unwrap();
1612        assert!(asarray_chkfinite(&a).is_ok());
1613    }
1614
1615    #[test]
1616    fn test_asarray_chkfinite_nan_fails() {
1617        let a = array(Ix1::new([3]), vec![1.0_f64, f64::NAN, 3.0]).unwrap();
1618        assert!(asarray_chkfinite(&a).is_err());
1619    }
1620
1621    #[test]
1622    fn test_asarray_chkfinite_inf_fails() {
1623        let a = array(Ix1::new([3]), vec![1.0_f64, f64::INFINITY, 3.0]).unwrap();
1624        assert!(asarray_chkfinite(&a).is_err());
1625    }
1626
1627    #[test]
1628    fn test_require() {
1629        let a = array(Ix1::new([3]), vec![1, 2, 3]).unwrap();
1630        let b = require(&a, "CW");
1631        assert_eq!(b.shape(), a.shape());
1632    }
1633
1634    // -- fromfunction / fromstring / fromfile --
1635
1636    #[test]
1637    fn test_fromfunction_2d() {
1638        let a = fromfunction(Ix2::new([3, 3]), |idx| (idx[0] + idx[1]) as i32).unwrap();
1639        let data: Vec<i32> = a.iter().copied().collect();
1640        assert_eq!(data, vec![0, 1, 2, 1, 2, 3, 2, 3, 4]);
1641    }
1642
1643    #[test]
1644    fn test_fromfunction_1d() {
1645        let a = fromfunction(Ix1::new([5]), |idx| idx[0] as f64 * 2.0).unwrap();
1646        let data: Vec<f64> = a.iter().copied().collect();
1647        assert_eq!(data, vec![0.0, 2.0, 4.0, 6.0, 8.0]);
1648    }
1649
1650    #[test]
1651    fn test_fromstring_whitespace() {
1652        let a: Array<f64, Ix1> = fromstring("1.5 2.5 3.5", " ").unwrap();
1653        let data: Vec<f64> = a.iter().copied().collect();
1654        assert_eq!(data, vec![1.5, 2.5, 3.5]);
1655    }
1656
1657    #[test]
1658    fn test_fromstring_comma() {
1659        let a: Array<i32, Ix1> = fromstring("1, 2, 3, 4", ",").unwrap();
1660        assert_eq!(a.shape(), &[4]);
1661        let data: Vec<i32> = a.iter().copied().collect();
1662        assert_eq!(data, vec![1, 2, 3, 4]);
1663    }
1664
1665    #[test]
1666    fn test_fromstring_empty_sep_uses_whitespace() {
1667        let a: Array<i32, Ix1> = fromstring("1   2\t3\n4", "").unwrap();
1668        let data: Vec<i32> = a.iter().copied().collect();
1669        assert_eq!(data, vec![1, 2, 3, 4]);
1670    }
1671
1672    #[test]
1673    fn test_fromstring_bad_token() {
1674        let r: FerrayResult<Array<f64, Ix1>> = fromstring("1.0 abc 3.0", " ");
1675        assert!(r.is_err());
1676    }
1677}