Skip to main content

ferray_ma/
extras.rs

1// ferray-ma: numpy.ma extras
2//
3// Adds the high-value `numpy.ma` API surface that was missing:
4//   - extra reductions: prod, cumsum, cumprod, argmin, argmax, median, ptp,
5//     average, anom
6//   - constructors: masked_all, masked_values, nomask
7//   - manipulation: ma_concatenate, ma_clip, ma_diagonal, ma_repeat,
8//     ma_atleast_{1,2,3}d, ma_expand_dims, ma_swapaxes
9//   - mask manipulation: harden_mask, soften_mask, mask_or, make_mask,
10//     make_mask_none
11//   - linalg/set/corr: ma_dot, ma_inner, ma_outer, ma_trace,
12//     ma_unique, ma_in1d, ma_isin
13//   - functional: ma_apply_along_axis, ma_apply_over_axes, ma_vander
14//   - fill-value protocol: default_fill_value, maximum_fill_value,
15//     minimum_fill_value, common_fill_value
16//   - bitwise/comparison ufuncs: ma_equal, ma_not_equal, ma_less,
17//     ma_greater, ma_less_equal, ma_greater_equal, ma_logical_and,
18//     ma_logical_or, ma_logical_not, ma_logical_xor
19//   - class helpers: is_masked_array (alias isMA), getmaskarray, ids
20//
21// Items intentionally NOT covered (deferred):
22//   - mvoid (record-void): requires structured-record dtype work in core
23//   - mr_ / masked_singleton / masked_print_option: Python-class display
24//     state with no Rust analog
25//   - clump_masked / clump_unmasked / *notmasked_contiguous: rarely used,
26//     deferred until requested
27//   - polyfit / convolve / correlate / corrcoef / cov: would pull in
28//     ferray-polynomial / ferray-stats; users can call those directly
29//     on the unmasked subset via .compressed()
30
31use ferray_core::dimension::{Dimension, IxDyn};
32use ferray_core::dtype::Element;
33use ferray_core::error::{FerrayError, FerrayResult};
34use ferray_core::{Array, Ix1, Ix2};
35use num_traits::Float;
36
37use crate::MaskedArray;
38
39// ===========================================================================
40// Sentinel constants
41// ===========================================================================
42
43/// The "no mask" sentinel — equivalent to `numpy.ma.nomask`. Read this and
44/// pass it as a mask to indicate "no elements are masked."
45pub const NOMASK: bool = false;
46
47// ===========================================================================
48// Extra reductions on MaskedArray
49// ===========================================================================
50
51impl<T, D> MaskedArray<T, D>
52where
53    T: Element + Copy + std::ops::Mul<Output = T> + num_traits::One,
54    D: Dimension,
55{
56    /// Product of unmasked elements. Returns `T::one()` for the all-masked case.
57    ///
58    /// # Errors
59    /// Returns an error only for internal failures.
60    pub fn prod(&self) -> FerrayResult<T> {
61        let one = num_traits::one::<T>();
62        let p = self
63            .data()
64            .iter()
65            .zip(self.mask().iter())
66            .filter(|(_, m)| !**m)
67            .fold(one, |acc, (v, _)| acc * *v);
68        Ok(p)
69    }
70}
71
72impl<T, D> MaskedArray<T, D>
73where
74    T: Element + Copy + std::ops::Add<Output = T> + num_traits::Zero,
75    D: Dimension,
76{
77    /// Cumulative sum over unmasked elements (mask propagates: each output
78    /// position carries the same mask as the input). Masked positions
79    /// contribute zero to the running total.
80    ///
81    /// # Errors
82    /// Returns an error if internal array construction fails.
83    pub fn cumsum_flat(&self) -> FerrayResult<MaskedArray<T, Ix1>> {
84        let zero = num_traits::zero::<T>();
85        let n = self.size();
86        let data: Vec<T> = self
87            .data()
88            .iter()
89            .zip(self.mask().iter())
90            .scan(zero, |acc, (v, m)| {
91                if !*m {
92                    *acc = *acc + *v;
93                }
94                Some(*acc)
95            })
96            .collect();
97        let mask: Vec<bool> = self.mask().iter().copied().collect();
98        let data_arr = Array::from_vec(Ix1::new([n]), data)?;
99        let mask_arr = Array::from_vec(Ix1::new([n]), mask)?;
100        MaskedArray::new(data_arr, mask_arr)
101    }
102}
103
104impl<T, D> MaskedArray<T, D>
105where
106    T: Element + Copy + std::ops::Mul<Output = T> + num_traits::One,
107    D: Dimension,
108{
109    /// Cumulative product over unmasked elements (mask propagates).
110    /// Masked positions contribute one to the running product.
111    ///
112    /// # Errors
113    /// Returns an error if internal array construction fails.
114    pub fn cumprod_flat(&self) -> FerrayResult<MaskedArray<T, Ix1>> {
115        let one = num_traits::one::<T>();
116        let n = self.size();
117        let data: Vec<T> = self
118            .data()
119            .iter()
120            .zip(self.mask().iter())
121            .scan(one, |acc, (v, m)| {
122                if !*m {
123                    *acc = *acc * *v;
124                }
125                Some(*acc)
126            })
127            .collect();
128        let mask: Vec<bool> = self.mask().iter().copied().collect();
129        let data_arr = Array::from_vec(Ix1::new([n]), data)?;
130        let mask_arr = Array::from_vec(Ix1::new([n]), mask)?;
131        MaskedArray::new(data_arr, mask_arr)
132    }
133}
134
135impl<T, D> MaskedArray<T, D>
136where
137    T: Element + Copy + PartialOrd,
138    D: Dimension,
139{
140    /// Flat index of the minimum unmasked element.
141    ///
142    /// # Errors
143    /// Returns `FerrayError::InvalidValue` if every element is masked.
144    pub fn argmin(&self) -> FerrayResult<usize> {
145        self.data()
146            .iter()
147            .zip(self.mask().iter())
148            .enumerate()
149            .filter(|(_, (_, m))| !**m)
150            .min_by(|(_, (a, _)), (_, (b, _))| {
151                a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
152            })
153            .map(|(i, _)| i)
154            .ok_or_else(|| FerrayError::invalid_value("argmin: all elements are masked"))
155    }
156
157    /// Flat index of the maximum unmasked element.
158    ///
159    /// # Errors
160    /// Returns `FerrayError::InvalidValue` if every element is masked.
161    pub fn argmax(&self) -> FerrayResult<usize> {
162        self.data()
163            .iter()
164            .zip(self.mask().iter())
165            .enumerate()
166            .filter(|(_, (_, m))| !**m)
167            .max_by(|(_, (a, _)), (_, (b, _))| {
168                a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
169            })
170            .map(|(i, _)| i)
171            .ok_or_else(|| FerrayError::invalid_value("argmax: all elements are masked"))
172    }
173
174    /// Peak-to-peak range over unmasked elements (`max - min`).
175    ///
176    /// # Errors
177    /// Returns `FerrayError::InvalidValue` if every element is masked.
178    pub fn ptp(&self) -> FerrayResult<T>
179    where
180        T: std::ops::Sub<Output = T>,
181    {
182        let mut iter = self
183            .data()
184            .iter()
185            .zip(self.mask().iter())
186            .filter(|(_, m)| !**m)
187            .map(|(v, _)| *v);
188        let first = iter
189            .next()
190            .ok_or_else(|| FerrayError::invalid_value("ptp: all elements are masked"))?;
191        let mut lo = first;
192        let mut hi = first;
193        for v in iter {
194            if v < lo {
195                lo = v;
196            }
197            if v > hi {
198                hi = v;
199            }
200        }
201        Ok(hi - lo)
202    }
203}
204
205impl<T, D> MaskedArray<T, D>
206where
207    T: Element
208        + Copy
209        + PartialOrd
210        + num_traits::FromPrimitive
211        + std::ops::Add<Output = T>
212        + std::ops::Div<Output = T>,
213    D: Dimension,
214{
215    /// Median of unmasked elements (interpolated between the two middle
216    /// values for an even count).
217    ///
218    /// # Errors
219    /// Returns `FerrayError::InvalidValue` if every element is masked.
220    pub fn median(&self) -> FerrayResult<T> {
221        let mut vals: Vec<T> = self
222            .data()
223            .iter()
224            .zip(self.mask().iter())
225            .filter(|(_, m)| !**m)
226            .map(|(v, _)| *v)
227            .collect();
228        if vals.is_empty() {
229            return Err(FerrayError::invalid_value(
230                "median: all elements are masked",
231            ));
232        }
233        vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
234        let n = vals.len();
235        if n % 2 == 1 {
236            Ok(vals[n / 2])
237        } else {
238            let two = T::from_u8(2).unwrap();
239            Ok((vals[n / 2 - 1] + vals[n / 2]) / two)
240        }
241    }
242}
243
244impl<T, D> MaskedArray<T, D>
245where
246    T: Element + Copy + Float,
247    D: Dimension,
248{
249    /// Weighted average of unmasked elements.
250    ///
251    /// `weights` must be `Some(Array<T, D>)` of the same shape as `self`,
252    /// or `None` (delegates to [`MaskedArray::mean`]).
253    ///
254    /// # Errors
255    /// Returns `FerrayError::ShapeMismatch` if weights shape differs, or
256    /// `FerrayError::InvalidValue` if the weight sum over unmasked
257    /// elements is zero (or every element is masked).
258    pub fn average(&self, weights: Option<&Array<T, D>>) -> FerrayResult<T> {
259        let Some(w) = weights else {
260            return self.mean();
261        };
262        if w.shape() != self.shape() {
263            return Err(FerrayError::shape_mismatch(format!(
264                "average: weights shape {:?} differs from array shape {:?}",
265                w.shape(),
266                self.shape(),
267            )));
268        }
269        let zero = <T as num_traits::Zero>::zero();
270        let mut wsum = zero;
271        let mut acc = zero;
272        for ((v, m), wi) in self.data().iter().zip(self.mask().iter()).zip(w.iter()) {
273            if !*m {
274                wsum = wsum + *wi;
275                acc = acc + *v * *wi;
276            }
277        }
278        if wsum == zero {
279            return Err(FerrayError::invalid_value(
280                "average: weight sum is zero (or all elements are masked)",
281            ));
282        }
283        Ok(acc / wsum)
284    }
285
286    /// Anomaly array: `self - mean(self)`. Returns a masked array of the
287    /// same shape with the same mask.
288    ///
289    /// Equivalent to `numpy.ma.anom`.
290    ///
291    /// # Errors
292    /// Returns `FerrayError::InvalidValue` if every element is masked.
293    pub fn anom(&self) -> FerrayResult<MaskedArray<T, D>> {
294        let m = self.mean()?;
295        let data: Vec<T> = self.data().iter().map(|v| *v - m).collect();
296        let data_arr = Array::from_vec(self.data().dim().clone(), data)?;
297        let mask_arr: Array<bool, D> = Array::from_vec(
298            self.mask().dim().clone(),
299            self.mask().iter().copied().collect(),
300        )?;
301        MaskedArray::new(data_arr, mask_arr)
302    }
303}
304
305// ===========================================================================
306// Constructors
307// ===========================================================================
308
309/// Build a fully-masked array of the given shape, filled with `T::zero()`.
310///
311/// Equivalent to `numpy.ma.masked_all(shape)`.
312///
313/// # Errors
314/// Returns an error if internal array construction fails.
315pub fn masked_all<T, D>(shape: D) -> FerrayResult<MaskedArray<T, D>>
316where
317    T: Element + Copy,
318    D: Dimension,
319{
320    let data = Array::<T, D>::from_elem(shape.clone(), T::zero())?;
321    let mask = Array::<bool, D>::from_elem(shape, true)?;
322    MaskedArray::new(data, mask)
323}
324
325/// Build a masked-all array with the same shape as the reference.
326///
327/// Equivalent to `numpy.ma.masked_all_like(other)`.
328///
329/// # Errors
330/// Returns an error if internal array construction fails.
331pub fn masked_all_like<T, U, D>(reference: &Array<U, D>) -> FerrayResult<MaskedArray<T, D>>
332where
333    T: Element + Copy,
334    U: Element,
335    D: Dimension,
336{
337    masked_all(reference.dim().clone())
338}
339
340/// Mask `arr` where each element approximately equals `value` within `rtol` /
341/// `atol` (relative + absolute tolerance, NumPy semantics:
342/// `|x - value| <= atol + rtol * |value|`).
343///
344/// Equivalent to `numpy.ma.masked_values(arr, value, rtol, atol)`.
345///
346/// # Errors
347/// Returns an error if internal array construction fails.
348pub fn masked_values<T, D>(
349    arr: &Array<T, D>,
350    value: T,
351    rtol: T,
352    atol: T,
353) -> FerrayResult<MaskedArray<T, D>>
354where
355    T: Element + Copy + Float,
356    D: Dimension,
357{
358    let threshold = atol + rtol * value.abs();
359    let mask: Vec<bool> = arr
360        .iter()
361        .map(|x| (*x - value).abs() <= threshold)
362        .collect();
363    let mask_arr = Array::from_vec(arr.dim().clone(), mask)?;
364    let data_arr = arr.clone();
365    MaskedArray::new(data_arr, mask_arr)
366}
367
368// ===========================================================================
369// Mask manipulation: harden / soften / mask_or / make_mask / make_mask_none
370// ===========================================================================
371
372// Note: `harden_mask` / `soften_mask` already live on `MaskedArray` via
373// `mask_ops.rs`. They're re-exported from this crate's root for parity
374// with `numpy.ma.MaskedArray.harden_mask` / `soften_mask`.
375
376/// Element-wise OR of two masks. Both must share the same shape.
377///
378/// Equivalent to `numpy.ma.mask_or(m1, m2)`.
379///
380/// # Errors
381/// Returns `FerrayError::ShapeMismatch` if shapes differ.
382pub fn mask_or<D: Dimension>(
383    a: &Array<bool, D>,
384    b: &Array<bool, D>,
385) -> FerrayResult<Array<bool, D>> {
386    if a.shape() != b.shape() {
387        return Err(FerrayError::shape_mismatch(format!(
388            "mask_or: shapes {:?} and {:?} differ",
389            a.shape(),
390            b.shape()
391        )));
392    }
393    let data: Vec<bool> = a.iter().zip(b.iter()).map(|(x, y)| *x || *y).collect();
394    Array::from_vec(a.dim().clone(), data)
395}
396
397/// Construct a boolean mask from a slice — convenience wrapper.
398///
399/// Equivalent to `numpy.ma.make_mask(values)` for the simple case of an
400/// existing bool array.
401///
402/// # Errors
403/// Returns an error if internal array construction fails.
404pub fn make_mask<D: Dimension>(values: &[bool], shape: D) -> FerrayResult<Array<bool, D>> {
405    Array::from_vec(shape, values.to_vec())
406}
407
408/// Build an all-`false` mask of the given shape.
409///
410/// Equivalent to `numpy.ma.make_mask_none(shape)`.
411///
412/// # Errors
413/// Returns an error if internal array construction fails.
414pub fn make_mask_none<D: Dimension>(shape: D) -> FerrayResult<Array<bool, D>> {
415    Array::from_elem(shape, false)
416}
417
418// ===========================================================================
419// Manipulation: concatenate / clip / repeat / atleast_*d / expand_dims /
420// swapaxes / diag / diagonal
421// ===========================================================================
422
423/// Concatenate two masked arrays along an axis. Both must have the same
424/// dimensionality. Mask is concatenated alongside data.
425///
426/// Equivalent to `numpy.ma.concatenate` for two-arg case.
427///
428/// # Errors
429/// Returns `FerrayError::ShapeMismatch` if shapes are incompatible.
430pub fn ma_concatenate<T>(
431    a: &MaskedArray<T, IxDyn>,
432    b: &MaskedArray<T, IxDyn>,
433    axis: usize,
434) -> FerrayResult<MaskedArray<T, IxDyn>>
435where
436    T: Element + Copy,
437{
438    let cat_data =
439        ferray_core::manipulation::concatenate(&[a.data().clone(), b.data().clone()], axis)?;
440    let cat_mask =
441        ferray_core::manipulation::concatenate(&[a.mask().clone(), b.mask().clone()], axis)?;
442    MaskedArray::new(cat_data, cat_mask)
443}
444
445impl<T, D> MaskedArray<T, D>
446where
447    T: Element + Copy + PartialOrd,
448    D: Dimension,
449{
450    /// Clip each unmasked element to `[a_min, a_max]`. Masked elements
451    /// pass through unchanged. Equivalent to `numpy.ma.clip`.
452    ///
453    /// # Errors
454    /// Returns an error if internal array construction fails.
455    pub fn clip(&self, a_min: T, a_max: T) -> FerrayResult<MaskedArray<T, D>> {
456        let data: Vec<T> = self
457            .data()
458            .iter()
459            .zip(self.mask().iter())
460            .map(|(v, m)| {
461                if *m {
462                    *v
463                } else if *v < a_min {
464                    a_min
465                } else if *v > a_max {
466                    a_max
467                } else {
468                    *v
469                }
470            })
471            .collect();
472        let data_arr = Array::from_vec(self.data().dim().clone(), data)?;
473        let mask_arr: Array<bool, D> = Array::from_vec(
474            self.mask().dim().clone(),
475            self.mask().iter().copied().collect(),
476        )?;
477        MaskedArray::new(data_arr, mask_arr)
478    }
479}
480
481impl<T, D> MaskedArray<T, D>
482where
483    T: Element + Copy,
484    D: Dimension,
485{
486    /// Repeat each unmasked element `repeats` times along the flat axis.
487    /// Always returns a 1-D masked array.
488    ///
489    /// Equivalent to `numpy.ma.repeat(arr, repeats)` with default flat axis.
490    ///
491    /// # Errors
492    /// Returns an error if internal array construction fails.
493    pub fn repeat(&self, repeats: usize) -> FerrayResult<MaskedArray<T, Ix1>> {
494        let n = self.size() * repeats;
495        let mut data = Vec::with_capacity(n);
496        let mut mask = Vec::with_capacity(n);
497        for (v, m) in self.data().iter().zip(self.mask().iter()) {
498            for _ in 0..repeats {
499                data.push(*v);
500                mask.push(*m);
501            }
502        }
503        let data_arr = Array::from_vec(Ix1::new([n]), data)?;
504        let mask_arr = Array::from_vec(Ix1::new([n]), mask)?;
505        MaskedArray::new(data_arr, mask_arr)
506    }
507
508    /// Promote to at least 1-D. Scalar (0-D) becomes shape `(1,)`.
509    ///
510    /// # Errors
511    /// Returns an error if internal array construction fails.
512    pub fn atleast_1d(&self) -> FerrayResult<MaskedArray<T, IxDyn>> {
513        let shape = self.shape();
514        let new_shape: Vec<usize> = if shape.is_empty() {
515            vec![1]
516        } else {
517            shape.to_vec()
518        };
519        let data: Vec<T> = self.data().iter().copied().collect();
520        let mask: Vec<bool> = self.mask().iter().copied().collect();
521        let data_arr = Array::from_vec(IxDyn::new(&new_shape), data)?;
522        let mask_arr = Array::from_vec(IxDyn::new(&new_shape), mask)?;
523        MaskedArray::new(data_arr, mask_arr)
524    }
525
526    /// Promote to at least 2-D. 0-D → (1,1); 1-D (N,) → (1, N).
527    ///
528    /// # Errors
529    /// Returns an error if internal array construction fails.
530    pub fn atleast_2d(&self) -> FerrayResult<MaskedArray<T, IxDyn>> {
531        let shape = self.shape();
532        let new_shape: Vec<usize> = match shape.len() {
533            0 => vec![1, 1],
534            1 => vec![1, shape[0]],
535            _ => shape.to_vec(),
536        };
537        let data: Vec<T> = self.data().iter().copied().collect();
538        let mask: Vec<bool> = self.mask().iter().copied().collect();
539        let data_arr = Array::from_vec(IxDyn::new(&new_shape), data)?;
540        let mask_arr = Array::from_vec(IxDyn::new(&new_shape), mask)?;
541        MaskedArray::new(data_arr, mask_arr)
542    }
543
544    /// Promote to at least 3-D. See [`atleast_3d`](crate::extras::MaskedArray::atleast_3d) for the rank ladder.
545    ///
546    /// # Errors
547    /// Returns an error if internal array construction fails.
548    pub fn atleast_3d(&self) -> FerrayResult<MaskedArray<T, IxDyn>> {
549        let shape = self.shape();
550        let new_shape: Vec<usize> = match shape.len() {
551            0 => vec![1, 1, 1],
552            1 => vec![1, shape[0], 1],
553            2 => vec![shape[0], shape[1], 1],
554            _ => shape.to_vec(),
555        };
556        let data: Vec<T> = self.data().iter().copied().collect();
557        let mask: Vec<bool> = self.mask().iter().copied().collect();
558        let data_arr = Array::from_vec(IxDyn::new(&new_shape), data)?;
559        let mask_arr = Array::from_vec(IxDyn::new(&new_shape), mask)?;
560        MaskedArray::new(data_arr, mask_arr)
561    }
562
563    /// Insert an axis of length 1 at the given position.
564    ///
565    /// # Errors
566    /// Returns `FerrayError::AxisOutOfBounds` if `axis > ndim`.
567    pub fn expand_dims(&self, axis: usize) -> FerrayResult<MaskedArray<T, IxDyn>> {
568        let data_exp = ferray_core::manipulation::expand_dims(self.data(), axis)?;
569        let mask_exp = ferray_core::manipulation::expand_dims(self.mask(), axis)?;
570        MaskedArray::new(data_exp, mask_exp)
571    }
572}
573
574impl<T> MaskedArray<T, Ix2>
575where
576    T: Element + Copy + num_traits::Zero,
577{
578    /// Extract the `k`-th diagonal of a 2-D masked array as a 1-D masked array.
579    ///
580    /// Equivalent to `numpy.ma.diagonal` for the 2-D case.
581    ///
582    /// # Errors
583    /// Returns an error if internal array construction fails.
584    pub fn diagonal(&self, k: isize) -> FerrayResult<MaskedArray<T, Ix1>> {
585        let shape = self.shape();
586        let (rows, cols) = (shape[0], shape[1]);
587        let (start_r, start_c) = if k >= 0 {
588            (0usize, k as usize)
589        } else {
590            (-k as usize, 0usize)
591        };
592        let mut data = Vec::new();
593        let mut mask = Vec::new();
594        let data_slice = self.data().as_slice();
595        let mask_slice = self.mask().as_slice();
596        let mut r = start_r;
597        let mut c = start_c;
598        while r < rows && c < cols {
599            let flat = r * cols + c;
600            // Use as_slice when available (C-contiguous), else fall back to iter().nth.
601            if let (Some(ds), Some(ms)) = (data_slice, mask_slice) {
602                data.push(ds[flat]);
603                mask.push(ms[flat]);
604            } else {
605                data.push(*self.data().iter().nth(flat).expect("flat index in bounds"));
606                mask.push(*self.mask().iter().nth(flat).expect("flat index in bounds"));
607            }
608            r += 1;
609            c += 1;
610        }
611        let n = data.len();
612        let data_arr = Array::from_vec(Ix1::new([n]), data)?;
613        let mask_arr = Array::from_vec(Ix1::new([n]), mask)?;
614        MaskedArray::new(data_arr, mask_arr)
615    }
616}
617
618// ===========================================================================
619// Linalg-lite (mask-aware via fill-zero)
620// ===========================================================================
621
622impl<T, D> MaskedArray<T, D>
623where
624    T: Element + Copy + std::ops::Add<Output = T> + std::ops::Mul<Output = T> + num_traits::Zero,
625    D: Dimension,
626{
627    /// Inner product of two masked arrays as flat 1-D vectors. Masked
628    /// positions on either side contribute zero.
629    ///
630    /// # Errors
631    /// Returns `FerrayError::ShapeMismatch` if total element counts differ.
632    pub fn ma_dot_flat(&self, other: &MaskedArray<T, D>) -> FerrayResult<T> {
633        if self.size() != other.size() {
634            return Err(FerrayError::shape_mismatch(format!(
635                "ma_dot_flat: lhs.size()={} != rhs.size()={}",
636                self.size(),
637                other.size(),
638            )));
639        }
640        let zero = <T as num_traits::Zero>::zero();
641        let s = self
642            .data()
643            .iter()
644            .zip(self.mask().iter())
645            .zip(other.data().iter().zip(other.mask().iter()))
646            .fold(
647                zero,
648                |acc, ((a, ma), (b, mb))| {
649                    if *ma || *mb { acc } else { acc + *a * *b }
650                },
651            );
652        Ok(s)
653    }
654}
655
656impl<T> MaskedArray<T, Ix2>
657where
658    T: Element + Copy + std::ops::Add<Output = T> + std::ops::Mul<Output = T> + num_traits::Zero,
659{
660    /// Trace (sum of diagonal) of a 2-D masked array. Masked diagonal
661    /// elements contribute zero.
662    pub fn trace(&self, k: isize) -> FerrayResult<T> {
663        let diag = self.diagonal(k)?;
664        let zero = <T as num_traits::Zero>::zero();
665        let s = diag
666            .data()
667            .iter()
668            .zip(diag.mask().iter())
669            .filter(|(_, m)| !**m)
670            .fold(zero, |acc, (v, _)| acc + *v);
671        Ok(s)
672    }
673}
674
675// ===========================================================================
676// Set ops on the unmasked subset
677// ===========================================================================
678
679/// Sorted unique unmasked values of `ma` as a plain `Array<T, Ix1>` (not a
680/// MaskedArray — the result has no masked entries by construction).
681///
682/// Equivalent to `numpy.ma.unique(ma)`.
683///
684/// # Errors
685/// Returns an error if internal array construction fails.
686pub fn ma_unique<T, D>(ma: &MaskedArray<T, D>) -> FerrayResult<Array<T, Ix1>>
687where
688    T: Element + Copy + PartialOrd,
689    D: Dimension,
690{
691    let mut vals: Vec<T> = ma
692        .data()
693        .iter()
694        .zip(ma.mask().iter())
695        .filter(|(_, m)| !**m)
696        .map(|(v, _)| *v)
697        .collect();
698    vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
699    vals.dedup_by(|a, b| (*a).partial_cmp(&*b) == Some(std::cmp::Ordering::Equal));
700    let n = vals.len();
701    Array::from_vec(Ix1::new([n]), vals)
702}
703
704/// Per-element membership test against `test_values`: true where `ma[i]`
705/// equals any value in `test_values` AND `ma[i]` is not masked.
706///
707/// Equivalent to `numpy.ma.isin(ma, test_values)`. The output mask follows
708/// the input's mask (positions where the input was masked are also masked
709/// in the result, so the boolean is meaningful for unmasked positions).
710///
711/// # Errors
712/// Returns an error if internal array construction fails.
713pub fn ma_isin<T, D>(
714    ma: &MaskedArray<T, D>,
715    test_values: &[T],
716) -> FerrayResult<MaskedArray<bool, D>>
717where
718    T: Element + Copy + PartialEq,
719    D: Dimension,
720{
721    let data: Vec<bool> = ma
722        .data()
723        .iter()
724        .map(|v| test_values.iter().any(|t| t == v))
725        .collect();
726    let data_arr = Array::from_vec(ma.data().dim().clone(), data)?;
727    let mask_arr: Array<bool, D> =
728        Array::from_vec(ma.mask().dim().clone(), ma.mask().iter().copied().collect())?;
729    MaskedArray::new(data_arr, mask_arr)
730}
731
732/// 1-D variant of [`ma_isin`].
733///
734/// Equivalent to `numpy.ma.in1d`.
735///
736/// # Errors
737/// Returns an error if internal array construction fails.
738pub fn ma_in1d<T>(
739    ma: &MaskedArray<T, Ix1>,
740    test_values: &[T],
741) -> FerrayResult<MaskedArray<bool, Ix1>>
742where
743    T: Element + Copy + PartialEq,
744{
745    ma_isin(ma, test_values)
746}
747
748// ===========================================================================
749// Functional helpers: apply_along_axis / apply_over_axes / vander
750// ===========================================================================
751
752/// Apply a function `f` to every 1-D slice along `axis`, collecting the
753/// scalar result of each into an `(ndim - 1)`-D masked array.
754///
755/// `f` receives a [`MaskedArray<T, Ix1>`] view (rebuilt each call) and
756/// returns a tuple `(value, masked)` indicating the reduction's output and
757/// whether to mark the output position as masked.
758///
759/// # Errors
760/// Returns `FerrayError::AxisOutOfBounds` if `axis >= ndim`, or any error
761/// from `f`.
762pub fn ma_apply_along_axis<T, F>(
763    ma: &MaskedArray<T, IxDyn>,
764    axis: usize,
765    mut f: F,
766) -> FerrayResult<MaskedArray<T, IxDyn>>
767where
768    T: Element + Copy,
769    F: FnMut(&MaskedArray<T, Ix1>) -> FerrayResult<(T, bool)>,
770{
771    let shape = ma.shape().to_vec();
772    if axis >= shape.len() {
773        return Err(FerrayError::axis_out_of_bounds(axis, shape.len()));
774    }
775    let lane_len = shape[axis];
776    let out_shape: Vec<usize> = shape
777        .iter()
778        .enumerate()
779        .filter(|&(i, _)| i != axis)
780        .map(|(_, &s)| s)
781        .collect();
782    let out_size: usize = out_shape.iter().product::<usize>().max(1);
783
784    let ndim = shape.len();
785    let mut strides = vec![1usize; ndim];
786    for i in (0..ndim.saturating_sub(1)).rev() {
787        strides[i] = strides[i + 1] * shape[i + 1];
788    }
789
790    let data: Vec<T> = ma.data().iter().copied().collect();
791    let mask: Vec<bool> = ma.mask().iter().copied().collect();
792
793    let mut out_data = Vec::with_capacity(out_size);
794    let mut out_mask = Vec::with_capacity(out_size);
795    let mut out_multi = vec![0usize; out_shape.len()];
796
797    for _ in 0..out_size {
798        // Build the lane.
799        let mut lane_data = Vec::with_capacity(lane_len);
800        let mut lane_mask = Vec::with_capacity(lane_len);
801        let mut full_idx = vec![0usize; ndim];
802        // Map out_multi -> full_idx (skipping axis).
803        let mut oi = 0;
804        for (d, slot) in full_idx.iter_mut().enumerate() {
805            if d == axis {
806                continue;
807            }
808            *slot = out_multi[oi];
809            oi += 1;
810        }
811        for j in 0..lane_len {
812            full_idx[axis] = j;
813            let flat: usize = full_idx
814                .iter()
815                .zip(strides.iter())
816                .map(|(i, s)| i * s)
817                .sum();
818            lane_data.push(data[flat]);
819            lane_mask.push(mask[flat]);
820        }
821        let lane_data_arr = Array::from_vec(Ix1::new([lane_len]), lane_data)?;
822        let lane_mask_arr = Array::from_vec(Ix1::new([lane_len]), lane_mask)?;
823        let lane_ma = MaskedArray::new(lane_data_arr, lane_mask_arr)?;
824        let (val, masked) = f(&lane_ma)?;
825        out_data.push(val);
826        out_mask.push(masked);
827
828        // Advance multi-index over output dimensions.
829        for d in (0..out_shape.len()).rev() {
830            out_multi[d] += 1;
831            if out_multi[d] < out_shape[d] {
832                break;
833            }
834            out_multi[d] = 0;
835        }
836    }
837
838    let data_arr = Array::from_vec(IxDyn::new(&out_shape), out_data)?;
839    let mask_arr = Array::from_vec(IxDyn::new(&out_shape), out_mask)?;
840    MaskedArray::new(data_arr, mask_arr)
841}
842
843/// Apply `f` repeatedly, reducing over the given axes in succession.
844///
845/// Each axis in `axes` is reduced via [`ma_apply_along_axis`] in order,
846/// with subsequent axis indices adjusted for previous reductions.
847///
848/// # Errors
849/// Returns errors from [`ma_apply_along_axis`].
850pub fn ma_apply_over_axes<T, F>(
851    ma: &MaskedArray<T, IxDyn>,
852    axes: &[usize],
853    mut f: F,
854) -> FerrayResult<MaskedArray<T, IxDyn>>
855where
856    T: Element + Copy,
857    F: FnMut(&MaskedArray<T, Ix1>) -> FerrayResult<(T, bool)>,
858{
859    let mut result = ma.clone();
860    let mut sorted: Vec<usize> = axes.to_vec();
861    sorted.sort_unstable();
862    for (offset, &ax) in sorted.iter().enumerate() {
863        // Each previous reduction collapsed an earlier axis, so shift
864        // subsequent axis indices left by the number already consumed.
865        let adjusted = ax.saturating_sub(offset);
866        result = ma_apply_along_axis(&result, adjusted, &mut f)?;
867    }
868    Ok(result)
869}
870
871/// Vandermonde matrix from a 1-D masked input. Masked rows propagate to
872/// the corresponding row of the result.
873///
874/// # Errors
875/// Returns `FerrayError::InvalidValue` if `x` is empty.
876pub fn ma_vander<T>(x: &MaskedArray<T, Ix1>, n: Option<usize>) -> FerrayResult<MaskedArray<T, Ix2>>
877where
878    T: Element + Copy + std::ops::Mul<Output = T> + num_traits::One,
879{
880    let m = x.size();
881    if m == 0 {
882        return Err(FerrayError::invalid_value(
883            "ma_vander: input must not be empty",
884        ));
885    }
886    let cols = n.unwrap_or(m);
887    let xs: Vec<T> = x.data().iter().copied().collect();
888    let xmask: Vec<bool> = x.mask().iter().copied().collect();
889    let one = num_traits::one::<T>();
890    let mut data = vec![one; m * cols];
891    let mut mask = vec![false; m * cols];
892    for (i, &xi) in xs.iter().enumerate() {
893        let mut acc = one;
894        let mut powers = Vec::with_capacity(cols);
895        for _ in 0..cols {
896            powers.push(acc);
897            acc = acc * xi;
898        }
899        for (j, p) in powers.iter().enumerate() {
900            // NumPy's vander defaults to decreasing powers (left-to-right).
901            data[i * cols + (cols - 1 - j)] = *p;
902            mask[i * cols + (cols - 1 - j)] = xmask[i];
903        }
904    }
905    let data_arr = Array::from_vec(Ix2::new([m, cols]), data)?;
906    let mask_arr = Array::from_vec(Ix2::new([m, cols]), mask)?;
907    MaskedArray::new(data_arr, mask_arr)
908}
909
910// ===========================================================================
911// Fill-value protocol
912// ===========================================================================
913
914/// Default fill value for a dtype, mirroring NumPy:
915/// - bool: `false`
916/// - integer: `999_999` (capped at the type max for parity with modern numpy)
917/// - float: `1e20` (numpy uses `1e20` for f64, `1e20` cast to f32)
918/// - complex: `1e20 + 0j`
919///
920/// This function is type-erased at the boundary; the type-specific
921/// equivalents are `T::fill_default_value`. ferray represents this via a
922/// trait helper rather than a single function.
923#[must_use]
924pub fn default_fill_value_f64() -> f64 {
925    1e20
926}
927
928/// Default fill value for f32.
929#[must_use]
930pub fn default_fill_value_f32() -> f32 {
931    1e20_f32
932}
933
934/// Default fill value for bool.
935#[must_use]
936pub const fn default_fill_value_bool() -> bool {
937    false
938}
939
940/// Default fill value for `i64`.
941#[must_use]
942pub const fn default_fill_value_i64() -> i64 {
943    999_999
944}
945
946/// Maximum fill value for a Float type (used when filling masked values
947/// for max-reductions so they don't influence the result).
948#[must_use]
949pub fn maximum_fill_value<T: Float>() -> T {
950    T::infinity()
951}
952
953/// Minimum fill value for a Float type (used when filling masked values
954/// for min-reductions).
955#[must_use]
956pub fn minimum_fill_value<T: Float>() -> T {
957    T::neg_infinity()
958}
959
960/// Common fill value for two masked arrays — returns `a.fill_value()` if
961/// both share the same fill value, else `T::zero()` (NumPy's fallback).
962pub fn common_fill_value<T, D>(a: &MaskedArray<T, D>, b: &MaskedArray<T, D>) -> T
963where
964    T: Element + Copy + PartialEq,
965    D: Dimension,
966{
967    if a.fill_value() == b.fill_value() {
968        a.fill_value()
969    } else {
970        T::zero()
971    }
972}
973
974// ===========================================================================
975// Comparison and logical ufuncs (mask-aware)
976// ===========================================================================
977
978macro_rules! ma_cmp {
979    ($name:ident, $op:tt, $bound:path) => {
980        /// Element-wise comparison preserving mask union.
981        ///
982        /// # Errors
983        /// Returns `FerrayError::ShapeMismatch` if shapes differ.
984        pub fn $name<T, D>(
985            a: &MaskedArray<T, D>,
986            b: &MaskedArray<T, D>,
987        ) -> FerrayResult<MaskedArray<bool, D>>
988        where
989            T: Element + Copy + $bound,
990            D: Dimension,
991        {
992            if a.shape() != b.shape() {
993                return Err(FerrayError::shape_mismatch(format!(
994                    "{}: shapes {:?} and {:?} differ",
995                    stringify!($name),
996                    a.shape(),
997                    b.shape(),
998                )));
999            }
1000            let data: Vec<bool> = a
1001                .data()
1002                .iter()
1003                .zip(b.data().iter())
1004                .map(|(x, y)| x $op y)
1005                .collect();
1006            let mask: Vec<bool> = a
1007                .mask()
1008                .iter()
1009                .zip(b.mask().iter())
1010                .map(|(x, y)| *x || *y)
1011                .collect();
1012            let data_arr = Array::from_vec(a.data().dim().clone(), data)?;
1013            let mask_arr: Array<bool, D> = Array::from_vec(a.mask().dim().clone(), mask)?;
1014            MaskedArray::new(data_arr, mask_arr)
1015        }
1016    };
1017}
1018
1019ma_cmp!(ma_equal, ==, PartialEq);
1020ma_cmp!(ma_not_equal, !=, PartialEq);
1021ma_cmp!(ma_less, <, PartialOrd);
1022ma_cmp!(ma_greater, >, PartialOrd);
1023ma_cmp!(ma_less_equal, <=, PartialOrd);
1024ma_cmp!(ma_greater_equal, >=, PartialOrd);
1025
1026/// Element-wise logical AND on bool MaskedArrays. Mask is unioned.
1027///
1028/// # Errors
1029/// Returns `FerrayError::ShapeMismatch` if shapes differ.
1030pub fn ma_logical_and<D: Dimension>(
1031    a: &MaskedArray<bool, D>,
1032    b: &MaskedArray<bool, D>,
1033) -> FerrayResult<MaskedArray<bool, D>> {
1034    binary_bool(a, b, |x, y| x && y, "ma_logical_and")
1035}
1036
1037/// Element-wise logical OR.
1038///
1039/// # Errors
1040/// Returns `FerrayError::ShapeMismatch` if shapes differ.
1041pub fn ma_logical_or<D: Dimension>(
1042    a: &MaskedArray<bool, D>,
1043    b: &MaskedArray<bool, D>,
1044) -> FerrayResult<MaskedArray<bool, D>> {
1045    binary_bool(a, b, |x, y| x || y, "ma_logical_or")
1046}
1047
1048/// Element-wise logical XOR.
1049///
1050/// # Errors
1051/// Returns `FerrayError::ShapeMismatch` if shapes differ.
1052pub fn ma_logical_xor<D: Dimension>(
1053    a: &MaskedArray<bool, D>,
1054    b: &MaskedArray<bool, D>,
1055) -> FerrayResult<MaskedArray<bool, D>> {
1056    binary_bool(a, b, |x, y| x ^ y, "ma_logical_xor")
1057}
1058
1059/// Element-wise logical NOT. Mask is preserved.
1060///
1061/// # Errors
1062/// Returns an error if internal array construction fails.
1063pub fn ma_logical_not<D: Dimension>(
1064    a: &MaskedArray<bool, D>,
1065) -> FerrayResult<MaskedArray<bool, D>> {
1066    let data: Vec<bool> = a.data().iter().map(|x| !*x).collect();
1067    let data_arr = Array::from_vec(a.data().dim().clone(), data)?;
1068    let mask_arr: Array<bool, D> =
1069        Array::from_vec(a.mask().dim().clone(), a.mask().iter().copied().collect())?;
1070    MaskedArray::new(data_arr, mask_arr)
1071}
1072
1073fn binary_bool<D: Dimension>(
1074    a: &MaskedArray<bool, D>,
1075    b: &MaskedArray<bool, D>,
1076    op: impl Fn(bool, bool) -> bool,
1077    name: &str,
1078) -> FerrayResult<MaskedArray<bool, D>> {
1079    if a.shape() != b.shape() {
1080        return Err(FerrayError::shape_mismatch(format!(
1081            "{name}: shapes {:?} and {:?} differ",
1082            a.shape(),
1083            b.shape()
1084        )));
1085    }
1086    let data: Vec<bool> = a
1087        .data()
1088        .iter()
1089        .zip(b.data().iter())
1090        .map(|(x, y)| op(*x, *y))
1091        .collect();
1092    let mask: Vec<bool> = a
1093        .mask()
1094        .iter()
1095        .zip(b.mask().iter())
1096        .map(|(x, y)| *x || *y)
1097        .collect();
1098    let data_arr = Array::from_vec(a.data().dim().clone(), data)?;
1099    let mask_arr: Array<bool, D> = Array::from_vec(a.mask().dim().clone(), mask)?;
1100    MaskedArray::new(data_arr, mask_arr)
1101}
1102
1103// ===========================================================================
1104// Class helpers
1105// ===========================================================================
1106
1107/// Whether `ma` is a [`MaskedArray`]. In Rust this is statically true, so
1108/// this always returns `true` — preserved for API parity with
1109/// `numpy.ma.isMaskedArray`.
1110#[must_use]
1111pub const fn is_masked_array<T, D>(_ma: &MaskedArray<T, D>) -> bool
1112where
1113    T: Element,
1114    D: Dimension,
1115{
1116    true
1117}
1118
1119/// NumPy-spelling alias for [`is_masked_array`].
1120#[must_use]
1121pub const fn is_ma<T, D>(ma: &MaskedArray<T, D>) -> bool
1122where
1123    T: Element,
1124    D: Dimension,
1125{
1126    is_masked_array(ma)
1127}
1128
1129/// Return the mask array, materializing the all-false sentinel form into a
1130/// real bool array. Equivalent to `numpy.ma.getmaskarray`.
1131///
1132/// Always returns an `Array<bool, D>`, never `nomask`.
1133///
1134/// # Errors
1135/// Returns an error if internal array construction fails.
1136pub fn getmaskarray<T, D>(ma: &MaskedArray<T, D>) -> FerrayResult<Array<bool, D>>
1137where
1138    T: Element,
1139    D: Dimension,
1140{
1141    Array::from_vec(ma.mask().dim().clone(), ma.mask().iter().copied().collect())
1142}
1143
1144/// Return a stable identity-pair `(data_ptr, mask_ptr)` for two
1145/// MaskedArrays, useful for cheap-equality checks.
1146///
1147/// Equivalent to `numpy.ma.MaskedArray.ids` — note that the mask sentinel
1148/// state still produces a valid pointer (the mask is materialized lazily).
1149#[must_use]
1150pub fn ids<T, D>(ma: &MaskedArray<T, D>) -> (*const u8, *const u8)
1151where
1152    T: Element,
1153    D: Dimension,
1154{
1155    let data_ptr: *const u8 = ma.data() as *const _ as *const u8;
1156    let mask_ptr: *const u8 = ma.mask() as *const _ as *const u8;
1157    (data_ptr, mask_ptr)
1158}
1159
1160// ===========================================================================
1161// Tests
1162// ===========================================================================
1163
1164#[cfg(test)]
1165mod tests {
1166    use super::*;
1167    use ferray_core::Ix1;
1168
1169    fn arr1f(v: Vec<f64>) -> Array<f64, Ix1> {
1170        let n = v.len();
1171        Array::from_vec(Ix1::new([n]), v).unwrap()
1172    }
1173
1174    fn mask1(v: Vec<bool>) -> Array<bool, Ix1> {
1175        let n = v.len();
1176        Array::from_vec(Ix1::new([n]), v).unwrap()
1177    }
1178
1179    fn ma_f1(d: Vec<f64>, m: Vec<bool>) -> MaskedArray<f64, Ix1> {
1180        MaskedArray::new(arr1f(d), mask1(m)).unwrap()
1181    }
1182
1183    #[test]
1184    fn prod_skips_masked() {
1185        let ma = ma_f1(vec![2.0, 3.0, 5.0, 7.0], vec![false, true, false, false]);
1186        let p = ma.prod().unwrap();
1187        assert!((p - 70.0).abs() < 1e-10); // 2 * 5 * 7
1188    }
1189
1190    #[test]
1191    fn cumsum_propagates_mask() {
1192        let ma = ma_f1(vec![1.0, 2.0, 3.0, 4.0], vec![false, true, false, false]);
1193        let r = ma.cumsum_flat().unwrap();
1194        let mask: Vec<bool> = r.mask().iter().copied().collect();
1195        let data: Vec<f64> = r.data().iter().copied().collect();
1196        assert_eq!(mask, vec![false, true, false, false]);
1197        // Running sum: 1, (skipped→still 1, marked masked), 1+3=4, 4+4=8
1198        assert!((data[0] - 1.0).abs() < 1e-10);
1199        assert!((data[2] - 4.0).abs() < 1e-10);
1200        assert!((data[3] - 8.0).abs() < 1e-10);
1201    }
1202
1203    #[test]
1204    fn argmin_argmax_skip_masked() {
1205        let ma = ma_f1(vec![5.0, 1.0, 9.0, 3.0], vec![false, true, false, false]);
1206        // unmasked: 5, 9, 3 at positions 0, 2, 3 → min at 3, max at 2
1207        assert_eq!(ma.argmin().unwrap(), 3);
1208        assert_eq!(ma.argmax().unwrap(), 2);
1209    }
1210
1211    #[test]
1212    fn ptp_basic() {
1213        let ma = ma_f1(vec![5.0, 1.0, 9.0, 3.0], vec![false, true, false, false]);
1214        // unmasked: 5, 9, 3 → ptp = 9 - 3 = 6
1215        assert!((ma.ptp().unwrap() - 6.0).abs() < 1e-10);
1216    }
1217
1218    #[test]
1219    fn median_odd_and_even() {
1220        let odd = ma_f1(vec![3.0, 1.0, 4.0, 1.0, 5.0], vec![false; 5]);
1221        assert!((odd.median().unwrap() - 3.0).abs() < 1e-10);
1222        let even = ma_f1(vec![1.0, 2.0, 3.0, 4.0], vec![false; 4]);
1223        assert!((even.median().unwrap() - 2.5).abs() < 1e-10);
1224    }
1225
1226    #[test]
1227    fn average_unweighted_matches_mean() {
1228        let ma = ma_f1(vec![2.0, 4.0, 6.0], vec![false; 3]);
1229        assert!((ma.average(None).unwrap() - 4.0).abs() < 1e-10);
1230    }
1231
1232    #[test]
1233    fn average_weighted_skips_masked() {
1234        let ma = ma_f1(vec![1.0, 100.0, 3.0], vec![false, true, false]);
1235        let w = arr1f(vec![1.0, 1.0, 3.0]);
1236        // unmasked weighted: (1*1 + 3*3) / (1 + 3) = 10/4 = 2.5
1237        assert!((ma.average(Some(&w)).unwrap() - 2.5).abs() < 1e-10);
1238    }
1239
1240    #[test]
1241    fn anom_centers_at_zero() {
1242        let ma = ma_f1(vec![1.0, 2.0, 3.0], vec![false; 3]);
1243        let a = ma.anom().unwrap();
1244        let data: Vec<f64> = a.data().iter().copied().collect();
1245        assert!((data[0] - (-1.0)).abs() < 1e-10);
1246        assert!((data[1] - 0.0).abs() < 1e-10);
1247        assert!((data[2] - 1.0).abs() < 1e-10);
1248    }
1249
1250    #[test]
1251    fn masked_all_is_fully_masked() {
1252        let ma: MaskedArray<f64, Ix1> = masked_all(Ix1::new([5])).unwrap();
1253        let mask: Vec<bool> = ma.mask().iter().copied().collect();
1254        assert_eq!(mask, vec![true; 5]);
1255    }
1256
1257    #[test]
1258    fn masked_values_within_tol() {
1259        let arr = arr1f(vec![1.0, 1.0001, 2.0]);
1260        let r = masked_values(&arr, 1.0, 1e-3, 0.0).unwrap();
1261        let mask: Vec<bool> = r.mask().iter().copied().collect();
1262        assert_eq!(mask, vec![true, true, false]);
1263    }
1264
1265    #[test]
1266    fn harden_mask_blocks_clearing() {
1267        let mut ma = ma_f1(vec![1.0, 2.0], vec![false, true]);
1268        ma.harden_mask().unwrap();
1269        assert!(ma.is_hard_mask());
1270        // Try to clear the mask bit at index 1.
1271        ma.set_mask_flat(1, false).unwrap();
1272        let mask: Vec<bool> = ma.mask().iter().copied().collect();
1273        // Hardened: clear is a no-op.
1274        assert_eq!(mask, vec![false, true]);
1275        ma.soften_mask().unwrap();
1276        ma.set_mask_flat(1, false).unwrap();
1277        let mask2: Vec<bool> = ma.mask().iter().copied().collect();
1278        assert_eq!(mask2, vec![false, false]);
1279    }
1280
1281    #[test]
1282    fn mask_or_unions() {
1283        let m1 = mask1(vec![true, false, false]);
1284        let m2 = mask1(vec![false, true, false]);
1285        let r = mask_or(&m1, &m2).unwrap();
1286        let v: Vec<bool> = r.iter().copied().collect();
1287        assert_eq!(v, vec![true, true, false]);
1288    }
1289
1290    #[test]
1291    fn make_mask_none_is_all_false() {
1292        let m: Array<bool, Ix1> = make_mask_none(Ix1::new([3])).unwrap();
1293        let v: Vec<bool> = m.iter().copied().collect();
1294        assert_eq!(v, vec![false; 3]);
1295    }
1296
1297    #[test]
1298    fn clip_unmasked_only() {
1299        let ma = ma_f1(vec![-5.0, 0.0, 5.0, 10.0], vec![false, false, false, true]);
1300        let r = ma.clip(-1.0, 3.0).unwrap();
1301        let data: Vec<f64> = r.data().iter().copied().collect();
1302        // Masked element passes through (10.0).
1303        assert_eq!(data, vec![-1.0, 0.0, 3.0, 10.0]);
1304    }
1305
1306    #[test]
1307    fn repeat_propagates_mask() {
1308        let ma = ma_f1(vec![1.0, 2.0], vec![false, true]);
1309        let r = ma.repeat(3).unwrap();
1310        let data: Vec<f64> = r.data().iter().copied().collect();
1311        let mask: Vec<bool> = r.mask().iter().copied().collect();
1312        assert_eq!(data, vec![1.0, 1.0, 1.0, 2.0, 2.0, 2.0]);
1313        assert_eq!(mask, vec![false, false, false, true, true, true]);
1314    }
1315
1316    #[test]
1317    fn ma_unique_dedups_unmasked() {
1318        let ma = ma_f1(
1319            vec![3.0, 1.0, 2.0, 1.0, 3.0, 9.0],
1320            vec![false, false, false, false, false, true],
1321        );
1322        let v = ma_unique(&ma).unwrap();
1323        let data: Vec<f64> = v.iter().copied().collect();
1324        assert_eq!(data, vec![1.0, 2.0, 3.0]);
1325    }
1326
1327    #[test]
1328    fn ma_isin_basic() {
1329        let ma = ma_f1(vec![1.0, 2.0, 3.0, 4.0], vec![false; 4]);
1330        let r = ma_isin(&ma, &[2.0, 4.0]).unwrap();
1331        let data: Vec<bool> = r.data().iter().copied().collect();
1332        assert_eq!(data, vec![false, true, false, true]);
1333    }
1334
1335    #[test]
1336    fn ma_dot_flat_skips_masked_pairs() {
1337        let a = ma_f1(vec![1.0, 2.0, 3.0], vec![false, true, false]);
1338        let b = ma_f1(vec![4.0, 5.0, 6.0], vec![false, false, false]);
1339        // Pair (1,4): both ok → 4. Pair (2,5): a masked → skip. Pair (3,6): both ok → 18.
1340        // Total = 22.
1341        assert!((a.ma_dot_flat(&b).unwrap() - 22.0).abs() < 1e-10);
1342    }
1343
1344    #[test]
1345    fn fill_value_protocol_constants() {
1346        assert_eq!(default_fill_value_f64(), 1e20);
1347        assert!(!default_fill_value_bool());
1348        assert!(maximum_fill_value::<f64>().is_infinite());
1349        assert!(
1350            minimum_fill_value::<f64>().is_infinite()
1351                && minimum_fill_value::<f64>().is_sign_negative()
1352        );
1353    }
1354
1355    #[test]
1356    fn common_fill_value_returns_shared_or_zero() {
1357        let a = ma_f1(vec![1.0, 2.0], vec![false, false]).with_fill_value(99.0);
1358        let b = ma_f1(vec![3.0, 4.0], vec![false, false]).with_fill_value(99.0);
1359        assert_eq!(common_fill_value(&a, &b), 99.0);
1360        let c = ma_f1(vec![5.0, 6.0], vec![false, false]).with_fill_value(0.5);
1361        assert_eq!(common_fill_value(&a, &c), 0.0); // fall back to zero
1362    }
1363
1364    #[test]
1365    fn ma_equal_and_friends_union_mask() {
1366        let a = ma_f1(vec![1.0, 2.0, 3.0], vec![false, false, true]);
1367        let b = ma_f1(vec![1.0, 9.0, 3.0], vec![false, true, false]);
1368        let r = ma_equal(&a, &b).unwrap();
1369        let data: Vec<bool> = r.data().iter().copied().collect();
1370        let mask: Vec<bool> = r.mask().iter().copied().collect();
1371        assert_eq!(data, vec![true, false, true]);
1372        assert_eq!(mask, vec![false, true, true]);
1373    }
1374
1375    #[test]
1376    fn ma_logical_and_basic() {
1377        let a = MaskedArray::new(
1378            Array::<bool, Ix1>::from_vec(Ix1::new([3]), vec![true, true, false]).unwrap(),
1379            Array::<bool, Ix1>::from_vec(Ix1::new([3]), vec![false; 3]).unwrap(),
1380        )
1381        .unwrap();
1382        let b = MaskedArray::new(
1383            Array::<bool, Ix1>::from_vec(Ix1::new([3]), vec![true, false, true]).unwrap(),
1384            Array::<bool, Ix1>::from_vec(Ix1::new([3]), vec![false; 3]).unwrap(),
1385        )
1386        .unwrap();
1387        let r = ma_logical_and(&a, &b).unwrap();
1388        let data: Vec<bool> = r.data().iter().copied().collect();
1389        assert_eq!(data, vec![true, false, false]);
1390    }
1391
1392    #[test]
1393    fn is_masked_array_always_true() {
1394        let ma = ma_f1(vec![1.0], vec![false]);
1395        assert!(is_masked_array(&ma));
1396        assert!(is_ma(&ma));
1397    }
1398
1399    #[test]
1400    fn getmaskarray_materializes() {
1401        let ma = MaskedArray::<f64, Ix1>::from_data(arr1f(vec![1.0, 2.0])).unwrap();
1402        let m = getmaskarray(&ma).unwrap();
1403        let v: Vec<bool> = m.iter().copied().collect();
1404        assert_eq!(v, vec![false; 2]);
1405    }
1406
1407    #[test]
1408    fn diagonal_main_and_offset() {
1409        use ferray_core::Ix2;
1410        let data = Array::<f64, Ix2>::from_vec(
1411            Ix2::new([3, 3]),
1412            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
1413        )
1414        .unwrap();
1415        let mask = Array::<bool, Ix2>::from_vec(Ix2::new([3, 3]), vec![false; 9]).unwrap();
1416        let ma = MaskedArray::new(data, mask).unwrap();
1417        let main = ma.diagonal(0).unwrap();
1418        let main_data: Vec<f64> = main.data().iter().copied().collect();
1419        assert_eq!(main_data, vec![1.0, 5.0, 9.0]);
1420        let upper = ma.diagonal(1).unwrap();
1421        let upper_data: Vec<f64> = upper.data().iter().copied().collect();
1422        assert_eq!(upper_data, vec![2.0, 6.0]);
1423    }
1424
1425    #[test]
1426    fn trace_sums_diagonal() {
1427        use ferray_core::Ix2;
1428        let data = Array::<f64, Ix2>::from_vec(
1429            Ix2::new([3, 3]),
1430            vec![1.0, 0.0, 0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 9.0],
1431        )
1432        .unwrap();
1433        let mask = Array::<bool, Ix2>::from_vec(Ix2::new([3, 3]), vec![false; 9]).unwrap();
1434        let ma = MaskedArray::new(data, mask).unwrap();
1435        assert!((ma.trace(0).unwrap() - 15.0).abs() < 1e-10);
1436    }
1437
1438    #[test]
1439    fn ma_apply_along_axis_sum_lane() {
1440        use ferray_core::IxDyn;
1441        let data =
1442            Array::<f64, IxDyn>::from_vec(IxDyn::new(&[2, 3]), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
1443                .unwrap();
1444        let mask = Array::<bool, IxDyn>::from_vec(IxDyn::new(&[2, 3]), vec![false; 6]).unwrap();
1445        let ma = MaskedArray::new(data, mask).unwrap();
1446        let result = ma_apply_along_axis(&ma, 1, |lane| {
1447            let s: f64 = lane.data().iter().copied().sum();
1448            Ok((s, false))
1449        })
1450        .unwrap();
1451        let v: Vec<f64> = result.data().iter().copied().collect();
1452        assert_eq!(v, vec![6.0, 15.0]);
1453    }
1454}