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