Skip to main content

ferray_ufunc/ops/
floatintrinsic.rs

1// ferray-ufunc: Floating-point intrinsic functions
2//
3// isnan, isinf, isfinite, isneginf, isposinf, nan_to_num, nextafter,
4// spacing, ldexp, frexp, signbit, copysign, float_power, fmax, fmin,
5// maximum, minimum, clip
6//
7// ## REQ status — REQ-10 (float intrinsics) + binary-promote tie-ins
8//
9// SHIPPED:
10//   - REQ-10 (`isnan`/`isinf`/`isfinite`/`isneginf`/`isposinf`/`nan_to_num`/
11//     `nextafter`/`spacing`/`ldexp`/`frexp`/`signbit`/`copysign`/`float_power`/
12//     `fmax`/`fmin`/`maximum`/`minimum`/`clip`): the NumPy float-intrinsic
13//     ufunc family as generic free functions (REQ-1). Anchors — classification
14//     predicates `pub fn isnan`/`pub fn isinf`/`pub fn isfinite`/
15//     `pub fn isneginf`/`pub fn isposinf`/`pub fn signbit` (all return
16//     `Array<bool, D>`); bit-level intrinsics `pub fn nextafter`/
17//     `pub fn spacing`/`pub fn ldexp`/`pub fn frexp`/`pub fn modf`/
18//     `pub fn copysign`; cleanup/clamp `pub fn nan_to_num`/`pub fn clip`/
19//     `pub fn fmax`/`pub fn fmin`/`pub fn maximum`/`pub fn minimum`/
20//     `pub fn float_power`. The `spacing` sign bug was fixed in #1076
21//     (`spacing(-x)` now matches numpy's signed result); `maximum`/`minimum`
22//     are NaN-propagating while `fmax`/`fmin` are NaN-suppressing per numpy.
23//     `*_ord`/`*_f16` companions handle non-`Float` Ord types and the f16
24//     feature. Non-test production consumer: re-exported verbatim from the
25//     crate root (`lib.rs` `pub use ops::floatintrinsic::{clip, clip_ord,
26//     copysign, float_power, fmax, fmin, frexp, isfinite, isinf, isnan,
27//     isneginf, isposinf, ldexp, maximum, maximum_ord, minimum, minimum_ord,
28//     modf, nan_to_num, nextafter, signbit, spacing}`), the public ufunc
29//     surface and the ferray-python intrinsics binding target.
30//   - REQ-25 tie-in (binary int/bool promotion): `copysign_promote`/
31//     `nextafter_promote` (in `promoted.rs`) wrap `pub fn copysign`/
32//     `pub fn nextafter` here for integer/bool operand pairs; f32/f64 callers
33//     stay byte-identical.
34//
35// NOT-STARTED: none — REQ-10 is fully shipped for this module.
36
37use ferray_core::Array;
38use ferray_core::dimension::Dimension;
39use ferray_core::dtype::Element;
40use ferray_core::error::{FerrayError, FerrayResult};
41use num_traits::Float;
42
43use crate::helpers::{binary_elementwise_op, binary_mixed_op, unary_float_op, unary_map_op};
44
45// ---------------------------------------------------------------------------
46// Classification
47// ---------------------------------------------------------------------------
48
49/// Elementwise test for NaN.
50pub fn isnan<T, D>(input: &Array<T, D>) -> FerrayResult<Array<bool, D>>
51where
52    T: Element + Float,
53    D: Dimension,
54{
55    unary_map_op(input, num_traits::Float::is_nan)
56}
57
58/// Elementwise test for infinity (positive or negative).
59pub fn isinf<T, D>(input: &Array<T, D>) -> FerrayResult<Array<bool, D>>
60where
61    T: Element + Float,
62    D: Dimension,
63{
64    unary_map_op(input, num_traits::Float::is_infinite)
65}
66
67/// Elementwise test for finiteness.
68pub fn isfinite<T, D>(input: &Array<T, D>) -> FerrayResult<Array<bool, D>>
69where
70    T: Element + Float,
71    D: Dimension,
72{
73    unary_map_op(input, num_traits::Float::is_finite)
74}
75
76/// Elementwise test for negative infinity.
77pub fn isneginf<T, D>(input: &Array<T, D>) -> FerrayResult<Array<bool, D>>
78where
79    T: Element + Float,
80    D: Dimension,
81{
82    unary_map_op(input, |x| x.is_infinite() && x < <T as Element>::zero())
83}
84
85/// Elementwise test for positive infinity.
86pub fn isposinf<T, D>(input: &Array<T, D>) -> FerrayResult<Array<bool, D>>
87where
88    T: Element + Float,
89    D: Dimension,
90{
91    unary_map_op(input, |x| x.is_infinite() && x > <T as Element>::zero())
92}
93
94/// Elementwise sign bit test.
95pub fn signbit<T, D>(input: &Array<T, D>) -> FerrayResult<Array<bool, D>>
96where
97    T: Element + Float,
98    D: Dimension,
99{
100    unary_map_op(input, num_traits::Float::is_sign_negative)
101}
102
103// ---------------------------------------------------------------------------
104// Replacement
105// ---------------------------------------------------------------------------
106
107/// Replace NaN with zero, and infinity with large finite numbers.
108///
109/// `nan` replaces NaN (default 0), `posinf` replaces +inf, `neginf` replaces -inf.
110pub fn nan_to_num<T, D>(
111    input: &Array<T, D>,
112    nan: Option<T>,
113    posinf: Option<T>,
114    neginf: Option<T>,
115) -> FerrayResult<Array<T, D>>
116where
117    T: Element + Float,
118    D: Dimension,
119{
120    let nan_val = nan.unwrap_or_else(|| <T as Element>::zero());
121    let posinf_val = posinf.unwrap_or_else(T::max_value);
122    let neginf_val = neginf.unwrap_or_else(T::min_value);
123    unary_float_op(input, |x| {
124        if x.is_nan() {
125            nan_val
126        } else if x.is_infinite() {
127            if x > <T as Element>::zero() {
128                posinf_val
129            } else {
130                neginf_val
131            }
132        } else {
133            x
134        }
135    })
136}
137
138/// Clip (limit) values to [`a_min`, `a_max`] for float types.
139pub fn clip<T, D>(input: &Array<T, D>, a_min: T, a_max: T) -> FerrayResult<Array<T, D>>
140where
141    T: Element + Float,
142    D: Dimension,
143{
144    if a_min > a_max {
145        return Err(FerrayError::invalid_value("clip: a_min must be <= a_max"));
146    }
147    unary_float_op(input, |x| {
148        if x < a_min {
149            a_min
150        } else if x > a_max {
151            a_max
152        } else {
153            x
154        }
155    })
156}
157
158/// Clip (limit) values to [`a_min`, `a_max`] for any ordered type, including integers.
159///
160/// This is a more general version of [`clip`] that works on integer arrays
161/// (i8, i16, i32, i64, u8, u16, u32, u64) as well as floats.
162///
163/// Equivalent to `numpy.clip`.
164pub fn clip_ord<T, D>(input: &Array<T, D>, a_min: T, a_max: T) -> FerrayResult<Array<T, D>>
165where
166    T: Element + PartialOrd + Copy,
167    D: Dimension,
168{
169    if a_min > a_max {
170        return Err(FerrayError::invalid_value(
171            "clip_ord: a_min must be <= a_max",
172        ));
173    }
174    let data: Vec<T> = input
175        .iter()
176        .map(|&x| {
177            if x < a_min {
178                a_min
179            } else if x > a_max {
180                a_max
181            } else {
182                x
183            }
184        })
185        .collect();
186    Array::from_vec(input.dim().clone(), data)
187}
188
189// ---------------------------------------------------------------------------
190// Float manipulation
191// ---------------------------------------------------------------------------
192
193/// IEEE 754 nextafter for f64: return the next representable value after `a` towards `b`.
194#[inline]
195fn nextafter_f64(a: f64, b: f64) -> f64 {
196    if a.is_nan() || b.is_nan() {
197        return f64::NAN;
198    }
199    if a == b {
200        return b;
201    }
202    if a == 0.0 {
203        // Return smallest subnormal with the sign of (b - a)
204        return if b > 0.0 {
205            f64::from_bits(1)
206        } else {
207            f64::from_bits(1u64 | (1u64 << 63))
208        };
209    }
210    let bits = a.to_bits();
211    let new_bits = if (a < b) == (a > 0.0) {
212        // Moving away from zero: increment magnitude
213        bits + 1
214    } else {
215        // Moving toward zero: decrement magnitude
216        bits - 1
217    };
218    f64::from_bits(new_bits)
219}
220
221/// IEEE 754 nextafter for f32: return the next representable value after `a` towards `b`.
222#[inline]
223fn nextafter_f32(a: f32, b: f32) -> f32 {
224    if a.is_nan() || b.is_nan() {
225        return f32::NAN;
226    }
227    if a == b {
228        return b;
229    }
230    if a == 0.0 {
231        return if b > 0.0 {
232            f32::from_bits(1)
233        } else {
234            f32::from_bits(1u32 | (1u32 << 31))
235        };
236    }
237    let bits = a.to_bits();
238    let new_bits = if (a < b) == (a > 0.0) {
239        bits + 1
240    } else {
241        bits - 1
242    };
243    f32::from_bits(new_bits)
244}
245
246/// Return the next floating-point value after x1 towards x2, using IEEE 754 bit manipulation.
247pub fn nextafter<T, D>(x1: &Array<T, D>, x2: &Array<T, D>) -> FerrayResult<Array<T, D>>
248where
249    T: Element + Float,
250    D: Dimension,
251{
252    use std::any::TypeId;
253
254    if TypeId::of::<T>() == TypeId::of::<f64>() {
255        // SAFETY: T is f64, verified by TypeId check above.
256        let a_f64 = unsafe { &*std::ptr::from_ref::<Array<T, D>>(x1).cast::<Array<f64, D>>() };
257        let b_f64 = unsafe { &*std::ptr::from_ref::<Array<T, D>>(x2).cast::<Array<f64, D>>() };
258        let result = binary_elementwise_op(a_f64, b_f64, nextafter_f64)?;
259        Ok(unsafe { crate::helpers::reinterpret_array::<f64, T, D>(result) })
260    } else if TypeId::of::<T>() == TypeId::of::<f32>() {
261        // SAFETY: T is f32, verified by TypeId check above.
262        let a_f32 = unsafe { &*std::ptr::from_ref::<Array<T, D>>(x1).cast::<Array<f32, D>>() };
263        let b_f32 = unsafe { &*std::ptr::from_ref::<Array<T, D>>(x2).cast::<Array<f32, D>>() };
264        let result = binary_elementwise_op(a_f32, b_f32, nextafter_f32)?;
265        Ok(unsafe { crate::helpers::reinterpret_array::<f32, T, D>(result) })
266    } else {
267        // Fallback for other float types: use f64 round-trip
268        binary_elementwise_op(x1, x2, |a, b| {
269            let a64 = a.to_f64().unwrap();
270            let b64 = b.to_f64().unwrap();
271            T::from(nextafter_f64(a64, b64)).unwrap()
272        })
273    }
274}
275
276/// IEEE 754 spacing (ULP) for f64.
277#[inline]
278fn spacing_f64(x: f64) -> f64 {
279    if x.is_nan() || x.is_infinite() {
280        return f64::NAN;
281    }
282    if x == 0.0 {
283        return f64::from_bits(1); // smallest positive subnormal (numpy: spacing(±0)=+5e-324)
284    }
285    // Carry the sign of x: step toward increasing magnitude in x's direction
286    // (numpy/libm npy_spacing = nextafter(x, copysign(inf, x)) - x).
287    nextafter_f64(x, f64::INFINITY.copysign(x)) - x
288}
289
290/// IEEE 754 spacing (ULP) for f32.
291#[inline]
292fn spacing_f32(x: f32) -> f32 {
293    if x.is_nan() || x.is_infinite() {
294        return f32::NAN;
295    }
296    if x == 0.0 {
297        return f32::from_bits(1); // smallest positive subnormal (numpy: spacing(±0)=+1e-45)
298    }
299    // Carry the sign of x: step toward increasing magnitude in x's direction
300    // (numpy/libm npy_spacing = nextafter(x, copysign(inf, x)) - x).
301    nextafter_f32(x, f32::INFINITY.copysign(x)) - x
302}
303
304/// Return the spacing of values: the ULP (unit in the last place),
305/// computed via IEEE 754 bit manipulation.
306pub fn spacing<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
307where
308    T: Element + Float,
309    D: Dimension,
310{
311    use std::any::TypeId;
312
313    if TypeId::of::<T>() == TypeId::of::<f64>() {
314        let f64_input =
315            unsafe { &*std::ptr::from_ref::<Array<T, D>>(input).cast::<Array<f64, D>>() };
316        let result = unary_float_op(f64_input, spacing_f64)?;
317        Ok(unsafe { crate::helpers::reinterpret_array::<f64, T, D>(result) })
318    } else if TypeId::of::<T>() == TypeId::of::<f32>() {
319        let f32_input =
320            unsafe { &*std::ptr::from_ref::<Array<T, D>>(input).cast::<Array<f32, D>>() };
321        let result = unary_float_op(f32_input, spacing_f32)?;
322        Ok(unsafe { crate::helpers::reinterpret_array::<f32, T, D>(result) })
323    } else {
324        // Fallback for other float types: use f64 round-trip
325        unary_float_op(input, |x| {
326            let x64 = x.to_f64().unwrap();
327            T::from(spacing_f64(x64)).unwrap()
328        })
329    }
330}
331
332/// Multiply `x` by `2^n` (ldexp), with `NumPy` broadcasting.
333///
334/// `n` is provided as an integer array; broadcast-compatible with `x`.
335pub fn ldexp<T, D>(x: &Array<T, D>, n: &Array<i32, D>) -> FerrayResult<Array<T, D>>
336where
337    T: Element + Float,
338    D: Dimension,
339{
340    let two = <T as Element>::one() + <T as Element>::one();
341    binary_mixed_op(x, n, move |xi, ni| xi * two.powi(ni))
342}
343
344/// Decompose f64 into mantissa and exponent via IEEE 754 bit extraction.
345///
346/// Returns (mantissa, exponent) where mantissa is in [0.5, 1.0) and x = mantissa * 2^exponent.
347#[inline]
348fn frexp_f64(x: f64) -> (f64, i32) {
349    if x == 0.0 || x.is_nan() || x.is_infinite() {
350        return (x, 0);
351    }
352    let bits = x.to_bits();
353    let sign = bits & (1u64 << 63);
354    let mut exp = ((bits >> 52) & 0x7FF) as i32;
355    if exp == 0 {
356        // Subnormal: multiply by 2^64 to normalize, then adjust exponent
357        let nx = x * (1u64 << 63) as f64 * 2.0;
358        let nbits = nx.to_bits();
359        exp = ((nbits >> 52) & 0x7FF) as i32 - 64;
360        let frac = f64::from_bits((nbits & !(0x7FFu64 << 52)) | (0x3FEu64 << 52));
361        return (if sign != 0 { -frac.abs() } else { frac }, exp - 0x3FE);
362    }
363    // Set exponent to -1 (biased: 0x3FE) to place mantissa in [0.5, 1.0)
364    let frac = f64::from_bits((bits & !(0x7FFu64 << 52)) | (0x3FEu64 << 52));
365    // exponent = (biased_exp - 1023) + 1 = biased_exp - 0x3FE
366    (frac, exp - 0x3FE)
367}
368
369/// Decompose f32 into mantissa and exponent via IEEE 754 bit extraction.
370///
371/// Returns (mantissa, exponent) where mantissa is in [0.5, 1.0) and x = mantissa * 2^exponent.
372#[inline]
373fn frexp_f32(x: f32) -> (f32, i32) {
374    if x == 0.0 || x.is_nan() || x.is_infinite() {
375        return (x, 0);
376    }
377    let bits = x.to_bits();
378    let sign = bits & (1u32 << 31);
379    let mut exp = ((bits >> 23) & 0xFF) as i32;
380    if exp == 0 {
381        // Subnormal: multiply by 2^32 to normalize, then adjust exponent
382        let nx = x * (1u32 << 31) as f32 * 2.0;
383        let nbits = nx.to_bits();
384        exp = ((nbits >> 23) & 0xFF) as i32 - 32;
385        let frac = f32::from_bits((nbits & !(0xFFu32 << 23)) | (0x7Eu32 << 23));
386        return (if sign != 0 { -frac.abs() } else { frac }, exp - 0x7E);
387    }
388    let frac = f32::from_bits((bits & !(0xFFu32 << 23)) | (0x7Eu32 << 23));
389    (frac, exp - 0x7E)
390}
391
392/// Decompose into mantissa and exponent: x = m * 2^e.
393///
394/// Returns (`mantissa_array`, `exponent_array`) where mantissa is in [0.5, 1.0).
395/// This matches C's `frexp`: for x=4.0, returns (0.5, 3) since 0.5 * 2^3 = 4.
396///
397/// Uses IEEE 754 bit extraction for f64 and f32 (O(1) per element), with
398/// f64 round-trip fallback for other float types.
399pub fn frexp<T, D>(input: &Array<T, D>) -> FerrayResult<(Array<T, D>, Array<i32, D>)>
400where
401    T: Element + Float,
402    D: Dimension,
403{
404    use std::any::TypeId;
405
406    if TypeId::of::<T>() == TypeId::of::<f64>() {
407        // SAFETY: T is f64, verified by TypeId check above.
408        let f64_input =
409            unsafe { &*std::ptr::from_ref::<Array<T, D>>(input).cast::<Array<f64, D>>() };
410        let mut mantissas = Vec::with_capacity(f64_input.size());
411        let mut exponents = Vec::with_capacity(f64_input.size());
412        for &x in f64_input.iter() {
413            let (m, e) = frexp_f64(x);
414            mantissas.push(m);
415            exponents.push(e);
416        }
417        let m_arr: Array<f64, D> = Array::from_vec(f64_input.dim().clone(), mantissas)?;
418        let e_arr = Array::from_vec(f64_input.dim().clone(), exponents)?;
419        let m_result = unsafe { crate::helpers::reinterpret_array::<f64, T, D>(m_arr) };
420        Ok((m_result, e_arr))
421    } else if TypeId::of::<T>() == TypeId::of::<f32>() {
422        // SAFETY: T is f32, verified by TypeId check above.
423        let f32_input =
424            unsafe { &*std::ptr::from_ref::<Array<T, D>>(input).cast::<Array<f32, D>>() };
425        let mut mantissas = Vec::with_capacity(f32_input.size());
426        let mut exponents = Vec::with_capacity(f32_input.size());
427        for &x in f32_input.iter() {
428            let (m, e) = frexp_f32(x);
429            mantissas.push(m);
430            exponents.push(e);
431        }
432        let m_arr: Array<f32, D> = Array::from_vec(f32_input.dim().clone(), mantissas)?;
433        let e_arr = Array::from_vec(f32_input.dim().clone(), exponents)?;
434        let m_result = unsafe { crate::helpers::reinterpret_array::<f32, T, D>(m_arr) };
435        Ok((m_result, e_arr))
436    } else {
437        // Fallback for other float types: use f64 round-trip
438        let mut mantissas = Vec::with_capacity(input.size());
439        let mut exponents = Vec::with_capacity(input.size());
440        for &x in input.iter() {
441            let x64 = x.to_f64().unwrap();
442            let (m64, e) = frexp_f64(x64);
443            mantissas.push(T::from(m64).unwrap());
444            exponents.push(e);
445        }
446        Ok((
447            Array::from_vec(input.dim().clone(), mantissas)?,
448            Array::from_vec(input.dim().clone(), exponents)?,
449        ))
450    }
451}
452
453/// Decompose into fractional and integer parts.
454///
455/// Returns `(fractional, integer)` where each output has the same dtype and
456/// shape as the input. The fractional part has the same sign as the input
457/// and absolute value < 1; the integer part is `trunc(x)` (round toward zero).
458///
459/// Analogous to `numpy.modf` and C's `modf`. For NaN inputs both outputs are
460/// NaN; for infinite inputs the fractional part is `±0` and the integer part
461/// is the original infinity.
462///
463/// # Errors
464/// Returns `FerrayError::ShapeMismatch` only via the underlying allocation
465/// path (cannot fail for matched shapes — the inputs share `input.dim()`).
466pub fn modf<T, D>(input: &Array<T, D>) -> FerrayResult<(Array<T, D>, Array<T, D>)>
467where
468    T: Element + Float,
469    D: Dimension,
470{
471    let mut fracs = Vec::with_capacity(input.size());
472    let mut ints = Vec::with_capacity(input.size());
473    for &x in input.iter() {
474        if x.is_nan() {
475            fracs.push(x);
476            ints.push(x);
477        } else if x.is_infinite() {
478            // NumPy: modf(±inf) → (±0, ±inf)
479            let zero = <T as num_traits::Zero>::zero();
480            fracs.push(if x.is_sign_positive() { zero } else { -zero });
481            ints.push(x);
482        } else {
483            let i = x.trunc();
484            ints.push(i);
485            fracs.push(x - i);
486        }
487    }
488    Ok((
489        Array::from_vec(input.dim().clone(), fracs)?,
490        Array::from_vec(input.dim().clone(), ints)?,
491    ))
492}
493
494/// Elementwise copysign: magnitude of x1, sign of x2.
495pub fn copysign<T, D>(x1: &Array<T, D>, x2: &Array<T, D>) -> FerrayResult<Array<T, D>>
496where
497    T: Element + Float,
498    D: Dimension,
499{
500    binary_elementwise_op(x1, x2, |a, b| {
501        let mag = a.abs();
502        if b.is_sign_negative() { -mag } else { mag }
503    })
504}
505
506/// Float power: x1^x2, always returning float.
507pub fn float_power<T, D>(x1: &Array<T, D>, x2: &Array<T, D>) -> FerrayResult<Array<T, D>>
508where
509    T: Element + Float,
510    D: Dimension,
511{
512    binary_elementwise_op(x1, x2, T::powf)
513}
514
515/// Elementwise maximum, propagating NaN.
516pub fn maximum<T, D>(a: &Array<T, D>, b: &Array<T, D>) -> FerrayResult<Array<T, D>>
517where
518    T: Element + Float,
519    D: Dimension,
520{
521    binary_elementwise_op(a, b, |x, y| {
522        if x.is_nan() || y.is_nan() {
523            <T as Float>::nan()
524        } else if x >= y {
525            x
526        } else {
527            y
528        }
529    })
530}
531
532/// Elementwise minimum, propagating NaN.
533pub fn minimum<T, D>(a: &Array<T, D>, b: &Array<T, D>) -> FerrayResult<Array<T, D>>
534where
535    T: Element + Float,
536    D: Dimension,
537{
538    binary_elementwise_op(a, b, |x, y| {
539        if x.is_nan() || y.is_nan() {
540            <T as Float>::nan()
541        } else if x <= y {
542            x
543        } else {
544            y
545        }
546    })
547}
548
549/// Elementwise maximum preserving the integer dtype.
550///
551/// `np.maximum` / `np.minimum` on integer arrays keep the integer dtype
552/// (generate_umath.py:661,671 `TD(no_obj_bool, dispatch=loops_minmax)`).
553/// No NaN handling is needed for integers — `PartialOrd` is total.
554pub fn maximum_ord<T, D>(a: &Array<T, D>, b: &Array<T, D>) -> FerrayResult<Array<T, D>>
555where
556    T: Element + Copy + PartialOrd,
557    D: Dimension,
558{
559    binary_elementwise_op(a, b, |x, y| if x >= y { x } else { y })
560}
561
562/// Elementwise minimum preserving the integer dtype. See [`maximum_ord`].
563pub fn minimum_ord<T, D>(a: &Array<T, D>, b: &Array<T, D>) -> FerrayResult<Array<T, D>>
564where
565    T: Element + Copy + PartialOrd,
566    D: Dimension,
567{
568    binary_elementwise_op(a, b, |x, y| if x <= y { x } else { y })
569}
570
571/// Elementwise maximum, ignoring NaN.
572pub fn fmax<T, D>(a: &Array<T, D>, b: &Array<T, D>) -> FerrayResult<Array<T, D>>
573where
574    T: Element + Float,
575    D: Dimension,
576{
577    binary_elementwise_op(a, b, |x, y| {
578        if x.is_nan() {
579            y
580        } else if y.is_nan() || x >= y {
581            x
582        } else {
583            y
584        }
585    })
586}
587
588/// Elementwise minimum, ignoring NaN.
589pub fn fmin<T, D>(a: &Array<T, D>, b: &Array<T, D>) -> FerrayResult<Array<T, D>>
590where
591    T: Element + Float,
592    D: Dimension,
593{
594    binary_elementwise_op(a, b, |x, y| {
595        if x.is_nan() {
596            y
597        } else if y.is_nan() || x <= y {
598            x
599        } else {
600            y
601        }
602    })
603}
604
605// ---------------------------------------------------------------------------
606// f16 variants (f32-promoted)
607// ---------------------------------------------------------------------------
608
609/// Elementwise test for NaN for f16 arrays.
610#[cfg(feature = "f16")]
611pub fn isnan_f16<D>(input: &Array<half::f16, D>) -> FerrayResult<Array<bool, D>>
612where
613    D: Dimension,
614{
615    crate::helpers::unary_f16_to_bool_op(input, f32::is_nan)
616}
617
618/// Elementwise test for infinity for f16 arrays.
619#[cfg(feature = "f16")]
620pub fn isinf_f16<D>(input: &Array<half::f16, D>) -> FerrayResult<Array<bool, D>>
621where
622    D: Dimension,
623{
624    crate::helpers::unary_f16_to_bool_op(input, f32::is_infinite)
625}
626
627/// Elementwise test for finiteness for f16 arrays.
628#[cfg(feature = "f16")]
629pub fn isfinite_f16<D>(input: &Array<half::f16, D>) -> FerrayResult<Array<bool, D>>
630where
631    D: Dimension,
632{
633    crate::helpers::unary_f16_to_bool_op(input, f32::is_finite)
634}
635
636/// Clip (limit) values to [`a_min`, `a_max`] for f16 arrays via f32 promotion.
637#[cfg(feature = "f16")]
638pub fn clip_f16<D>(
639    input: &Array<half::f16, D>,
640    a_min: half::f16,
641    a_max: half::f16,
642) -> FerrayResult<Array<half::f16, D>>
643where
644    D: Dimension,
645{
646    let min_f32 = a_min.to_f32();
647    let max_f32 = a_max.to_f32();
648    if min_f32 > max_f32 {
649        return Err(FerrayError::invalid_value("clip: a_min must be <= a_max"));
650    }
651    crate::helpers::unary_f16_op(input, |x| {
652        if x < min_f32 {
653            min_f32
654        } else if x > max_f32 {
655            max_f32
656        } else {
657            x
658        }
659    })
660}
661
662/// Replace NaN with zero, and infinity with large finite numbers, for f16 arrays.
663#[cfg(feature = "f16")]
664pub fn nan_to_num_f16<D>(
665    input: &Array<half::f16, D>,
666    nan: Option<half::f16>,
667    posinf: Option<half::f16>,
668    neginf: Option<half::f16>,
669) -> FerrayResult<Array<half::f16, D>>
670where
671    D: Dimension,
672{
673    let nan_val = nan.unwrap_or(half::f16::ZERO).to_f32();
674    let posinf_val = posinf.unwrap_or(half::f16::MAX).to_f32();
675    let neginf_val = neginf.unwrap_or(half::f16::MIN).to_f32();
676    crate::helpers::unary_f16_op(input, |x| {
677        if x.is_nan() {
678            nan_val
679        } else if x.is_infinite() {
680            if x > 0.0 { posinf_val } else { neginf_val }
681        } else {
682            x
683        }
684    })
685}
686
687/// Elementwise maximum for f16 arrays via f32 promotion, propagating NaN.
688#[cfg(feature = "f16")]
689pub fn maximum_f16<D>(
690    a: &Array<half::f16, D>,
691    b: &Array<half::f16, D>,
692) -> FerrayResult<Array<half::f16, D>>
693where
694    D: Dimension,
695{
696    crate::helpers::binary_f16_op(a, b, |x, y| {
697        if x.is_nan() || y.is_nan() {
698            f32::NAN
699        } else if x >= y {
700            x
701        } else {
702            y
703        }
704    })
705}
706
707/// Elementwise minimum for f16 arrays via f32 promotion, propagating NaN.
708#[cfg(feature = "f16")]
709pub fn minimum_f16<D>(
710    a: &Array<half::f16, D>,
711    b: &Array<half::f16, D>,
712) -> FerrayResult<Array<half::f16, D>>
713where
714    D: Dimension,
715{
716    crate::helpers::binary_f16_op(a, b, |x, y| {
717        if x.is_nan() || y.is_nan() {
718            f32::NAN
719        } else if x <= y {
720            x
721        } else {
722            y
723        }
724    })
725}
726
727#[cfg(test)]
728mod tests {
729    use super::*;
730    use ferray_core::dimension::Ix1;
731
732    use crate::test_util::arr1;
733
734    fn arr1_i32(data: Vec<i32>) -> Array<i32, Ix1> {
735        let n = data.len();
736        Array::from_vec(Ix1::new([n]), data).unwrap()
737    }
738
739    #[test]
740    fn test_isnan() {
741        let a = arr1(vec![1.0, f64::NAN, 3.0]);
742        let r = isnan(&a).unwrap();
743        assert_eq!(r.as_slice().unwrap(), &[false, true, false]);
744    }
745
746    #[test]
747    fn test_isinf() {
748        let a = arr1(vec![1.0, f64::INFINITY, f64::NEG_INFINITY]);
749        let r = isinf(&a).unwrap();
750        assert_eq!(r.as_slice().unwrap(), &[false, true, true]);
751    }
752
753    #[test]
754    fn test_isfinite() {
755        let a = arr1(vec![1.0, f64::INFINITY, f64::NAN]);
756        let r = isfinite(&a).unwrap();
757        assert_eq!(r.as_slice().unwrap(), &[true, false, false]);
758    }
759
760    #[test]
761    fn test_isneginf() {
762        let a = arr1(vec![f64::NEG_INFINITY, f64::INFINITY, 0.0]);
763        let r = isneginf(&a).unwrap();
764        assert_eq!(r.as_slice().unwrap(), &[true, false, false]);
765    }
766
767    #[test]
768    fn test_isposinf() {
769        let a = arr1(vec![f64::NEG_INFINITY, f64::INFINITY, 0.0]);
770        let r = isposinf(&a).unwrap();
771        assert_eq!(r.as_slice().unwrap(), &[false, true, false]);
772    }
773
774    #[test]
775    fn test_nan_to_num() {
776        let a = arr1(vec![f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 1.0]);
777        let r = nan_to_num(&a, None, None, None).unwrap();
778        let s = r.as_slice().unwrap();
779        assert_eq!(s[0], 0.0);
780        assert!(s[1].is_finite() && s[1] > 0.0);
781        assert!(s[2].is_finite() && s[2] < 0.0);
782        assert_eq!(s[3], 1.0);
783    }
784
785    #[test]
786    fn test_clip() {
787        let a = arr1(vec![-1.0, 0.5, 1.5, 3.0]);
788        let r = clip(&a, 0.0, 2.0).unwrap();
789        assert_eq!(r.as_slice().unwrap(), &[0.0, 0.5, 1.5, 2.0]);
790    }
791
792    #[test]
793    fn test_clip_ord_integer() {
794        let a = Array::<i32, Ix1>::from_vec(Ix1::new([5]), vec![-10, 0, 5, 100, 255]).unwrap();
795        let r = clip_ord(&a, 0, 200).unwrap();
796        assert_eq!(r.as_slice().unwrap(), &[0, 0, 5, 100, 200]);
797    }
798
799    #[test]
800    fn test_clip_ord_u8() {
801        let a = Array::<u8, Ix1>::from_vec(Ix1::new([4]), vec![0, 50, 200, 255]).unwrap();
802        let r = clip_ord(&a, 10, 128).unwrap();
803        assert_eq!(r.as_slice().unwrap(), &[10, 50, 128, 128]);
804    }
805
806    #[test]
807    fn test_signbit() {
808        let a = arr1(vec![-1.0, 0.0, 1.0]);
809        let r = signbit(&a).unwrap();
810        assert_eq!(r.as_slice().unwrap(), &[true, false, false]);
811    }
812
813    #[test]
814    fn test_copysign() {
815        let a = arr1(vec![1.0, -2.0, 3.0]);
816        let b = arr1(vec![-1.0, 1.0, -1.0]);
817        let r = copysign(&a, &b).unwrap();
818        assert_eq!(r.as_slice().unwrap(), &[-1.0, 2.0, -3.0]);
819    }
820
821    #[test]
822    fn test_maximum() {
823        let a = arr1(vec![1.0, 5.0, 3.0]);
824        let b = arr1(vec![4.0, 2.0, 3.0]);
825        let r = maximum(&a, &b).unwrap();
826        assert_eq!(r.as_slice().unwrap(), &[4.0, 5.0, 3.0]);
827    }
828
829    #[test]
830    fn test_minimum() {
831        let a = arr1(vec![1.0, 5.0, 3.0]);
832        let b = arr1(vec![4.0, 2.0, 3.0]);
833        let r = minimum(&a, &b).unwrap();
834        assert_eq!(r.as_slice().unwrap(), &[1.0, 2.0, 3.0]);
835    }
836
837    #[test]
838    fn test_maximum_nan_propagation() {
839        let a = arr1(vec![1.0, f64::NAN]);
840        let b = arr1(vec![2.0, 3.0]);
841        let r = maximum(&a, &b).unwrap();
842        let s = r.as_slice().unwrap();
843        assert_eq!(s[0], 2.0);
844        assert!(s[1].is_nan());
845    }
846
847    #[test]
848    fn test_fmax_ignores_nan() {
849        let a = arr1(vec![1.0, f64::NAN]);
850        let b = arr1(vec![2.0, 3.0]);
851        let r = fmax(&a, &b).unwrap();
852        let s = r.as_slice().unwrap();
853        assert_eq!(s[0], 2.0);
854        assert_eq!(s[1], 3.0);
855    }
856
857    #[test]
858    fn test_fmin_ignores_nan() {
859        let a = arr1(vec![1.0, f64::NAN]);
860        let b = arr1(vec![2.0, 3.0]);
861        let r = fmin(&a, &b).unwrap();
862        let s = r.as_slice().unwrap();
863        assert_eq!(s[0], 1.0);
864        assert_eq!(s[1], 3.0);
865    }
866
867    #[test]
868    fn test_float_power() {
869        let a = arr1(vec![2.0, 3.0]);
870        let b = arr1(vec![3.0, 2.0]);
871        let r = float_power(&a, &b).unwrap();
872        assert_eq!(r.as_slice().unwrap(), &[8.0, 9.0]);
873    }
874
875    #[test]
876    fn test_spacing() {
877        let a = arr1(vec![1.0]);
878        let r = spacing(&a).unwrap();
879        let s = r.as_slice().unwrap();
880        // spacing(1.0) == f64::EPSILON (2^-52)
881        assert_eq!(s[0], f64::EPSILON);
882    }
883
884    #[test]
885    fn test_spacing_zero() {
886        let a = arr1(vec![0.0]);
887        let r = spacing(&a).unwrap();
888        let s = r.as_slice().unwrap();
889        // spacing(0.0) == smallest positive subnormal = 5e-324
890        assert_eq!(s[0], f64::from_bits(1));
891    }
892
893    #[test]
894    fn test_spacing_nan_inf() {
895        let a = arr1(vec![f64::NAN, f64::INFINITY, f64::NEG_INFINITY]);
896        let r = spacing(&a).unwrap();
897        let s = r.as_slice().unwrap();
898        assert!(s[0].is_nan());
899        assert!(s[1].is_nan());
900        assert!(s[2].is_nan());
901    }
902
903    #[test]
904    fn test_ldexp() {
905        let x = arr1(vec![1.0, 2.0]);
906        let n = arr1_i32(vec![2, 3]);
907        let r = ldexp(&x, &n).unwrap();
908        let s = r.as_slice().unwrap();
909        assert!((s[0] - 4.0).abs() < 1e-12);
910        assert!((s[1] - 16.0).abs() < 1e-12);
911    }
912
913    #[test]
914    fn test_ldexp_broadcasts() {
915        use ferray_core::dimension::Ix2;
916        let x = Array::<f64, Ix2>::from_vec(Ix2::new([2, 1]), vec![1.0, 3.0]).unwrap();
917        let n = Array::<i32, Ix2>::from_vec(Ix2::new([1, 3]), vec![1, 2, 3]).unwrap();
918        let r = ldexp(&x, &n).unwrap();
919        assert_eq!(r.shape(), &[2, 3]);
920        // 1*2^{1,2,3} = {2,4,8}, 3*2^{1,2,3} = {6,12,24}
921        let v: Vec<f64> = r.iter().copied().collect();
922        assert_eq!(v, vec![2.0, 4.0, 8.0, 6.0, 12.0, 24.0]);
923    }
924
925    #[test]
926    fn test_frexp() {
927        let a = arr1(vec![4.0]);
928        let (m, e) = frexp(&a).unwrap();
929        let ms = m.as_slice().unwrap();
930        let es = e.as_slice().unwrap();
931        // 4 = 0.5 * 2^3
932        assert!((ms[0] - 0.5).abs() < 1e-12);
933        assert_eq!(es[0], 3);
934    }
935
936    #[test]
937    fn test_nextafter_zero_toward_positive() {
938        let a = arr1(vec![0.0]);
939        let b = arr1(vec![1.0]);
940        let r = nextafter(&a, &b).unwrap();
941        let s = r.as_slice().unwrap();
942        // nextafter(0, 1) == smallest positive subnormal = 5e-324
943        assert_eq!(s[0], f64::from_bits(1));
944    }
945
946    #[test]
947    fn test_nextafter_zero_toward_negative() {
948        let a = arr1(vec![0.0]);
949        let b = arr1(vec![-1.0]);
950        let r = nextafter(&a, &b).unwrap();
951        let s = r.as_slice().unwrap();
952        // nextafter(0, -1) == smallest negative subnormal = -5e-324
953        assert_eq!(s[0], f64::from_bits(1u64 | (1u64 << 63)));
954    }
955
956    #[test]
957    fn test_nextafter_equal() {
958        let a = arr1(vec![1.0]);
959        let b = arr1(vec![1.0]);
960        let r = nextafter(&a, &b).unwrap();
961        let s = r.as_slice().unwrap();
962        assert_eq!(s[0], 1.0);
963    }
964
965    #[test]
966    fn test_nextafter_nan() {
967        let a = arr1(vec![f64::NAN, 1.0]);
968        let b = arr1(vec![1.0, f64::NAN]);
969        let r = nextafter(&a, &b).unwrap();
970        let s = r.as_slice().unwrap();
971        assert!(s[0].is_nan());
972        assert!(s[1].is_nan());
973    }
974
975    #[test]
976    fn test_nextafter_one_toward_two() {
977        let a = arr1(vec![1.0]);
978        let b = arr1(vec![2.0]);
979        let r = nextafter(&a, &b).unwrap();
980        let s = r.as_slice().unwrap();
981        // nextafter(1.0, 2.0) == 1.0 + epsilon
982        assert_eq!(s[0], 1.0 + f64::EPSILON);
983    }
984
985    #[test]
986    fn test_nextafter_negative() {
987        let a = arr1(vec![-1.0]);
988        let b = arr1(vec![-2.0]);
989        let r = nextafter(&a, &b).unwrap();
990        let s = r.as_slice().unwrap();
991        // nextafter(-1.0, -2.0) == -1.0 - epsilon
992        assert_eq!(s[0], -1.0 - f64::EPSILON);
993    }
994
995    #[cfg(feature = "f16")]
996    mod f16_tests {
997        use super::*;
998
999        fn arr1_f16(data: &[f32]) -> Array<half::f16, Ix1> {
1000            let n = data.len();
1001            let vals: Vec<half::f16> = data.iter().map(|&x| half::f16::from_f32(x)).collect();
1002            Array::from_vec(Ix1::new([n]), vals).unwrap()
1003        }
1004
1005        #[test]
1006        fn test_isnan_f16() {
1007            let a = arr1_f16(&[1.0, f32::NAN, 3.0]);
1008            let r = isnan_f16(&a).unwrap();
1009            assert_eq!(r.as_slice().unwrap(), &[false, true, false]);
1010        }
1011
1012        #[test]
1013        fn test_isinf_f16() {
1014            let a = arr1_f16(&[1.0, f32::INFINITY, f32::NEG_INFINITY]);
1015            let r = isinf_f16(&a).unwrap();
1016            assert_eq!(r.as_slice().unwrap(), &[false, true, true]);
1017        }
1018
1019        #[test]
1020        fn test_isfinite_f16() {
1021            let a = arr1_f16(&[1.0, f32::INFINITY, f32::NAN]);
1022            let r = isfinite_f16(&a).unwrap();
1023            assert_eq!(r.as_slice().unwrap(), &[true, false, false]);
1024        }
1025
1026        #[test]
1027        fn test_clip_f16() {
1028            let a = arr1_f16(&[-1.0, 0.5, 1.5, 3.0]);
1029            let r = clip_f16(&a, half::f16::from_f32(0.0), half::f16::from_f32(2.0)).unwrap();
1030            let s = r.as_slice().unwrap();
1031            assert!((s[0].to_f32() - 0.0).abs() < 0.01);
1032            assert!((s[1].to_f32() - 0.5).abs() < 0.01);
1033            assert!((s[2].to_f32() - 1.5).abs() < 0.01);
1034            assert!((s[3].to_f32() - 2.0).abs() < 0.01);
1035        }
1036
1037        #[test]
1038        fn test_nan_to_num_f16() {
1039            let a = arr1_f16(&[f32::NAN, 1.0]);
1040            let r = nan_to_num_f16(&a, None, None, None).unwrap();
1041            let s = r.as_slice().unwrap();
1042            assert_eq!(s[0].to_f32(), 0.0);
1043            assert!((s[1].to_f32() - 1.0).abs() < 0.01);
1044        }
1045
1046        #[test]
1047        fn test_maximum_f16() {
1048            let a = arr1_f16(&[1.0, 5.0, 3.0]);
1049            let b = arr1_f16(&[4.0, 2.0, 3.0]);
1050            let r = maximum_f16(&a, &b).unwrap();
1051            let s = r.as_slice().unwrap();
1052            assert!((s[0].to_f32() - 4.0).abs() < 0.01);
1053            assert!((s[1].to_f32() - 5.0).abs() < 0.01);
1054            assert!((s[2].to_f32() - 3.0).abs() < 0.01);
1055        }
1056
1057        #[test]
1058        fn test_minimum_f16() {
1059            let a = arr1_f16(&[1.0, 5.0, 3.0]);
1060            let b = arr1_f16(&[4.0, 2.0, 3.0]);
1061            let r = minimum_f16(&a, &b).unwrap();
1062            let s = r.as_slice().unwrap();
1063            assert!((s[0].to_f32() - 1.0).abs() < 0.01);
1064            assert!((s[1].to_f32() - 2.0).abs() < 0.01);
1065            assert!((s[2].to_f32() - 3.0).abs() < 0.01);
1066        }
1067    }
1068
1069    // -- modf --
1070
1071    #[test]
1072    fn test_modf_positive() {
1073        let a = arr1(vec![3.5, 1.25, 0.0]);
1074        let (frac, int) = modf(&a).unwrap();
1075        let f = frac.as_slice().unwrap();
1076        let i = int.as_slice().unwrap();
1077        assert!((f[0] - 0.5).abs() < 1e-12);
1078        assert!((f[1] - 0.25).abs() < 1e-12);
1079        assert_eq!(f[2], 0.0);
1080        assert_eq!(i, &[3.0, 1.0, 0.0]);
1081    }
1082
1083    #[test]
1084    fn test_modf_negative() {
1085        let a = arr1(vec![-3.5, -1.25]);
1086        let (frac, int) = modf(&a).unwrap();
1087        let f = frac.as_slice().unwrap();
1088        let i = int.as_slice().unwrap();
1089        assert!((f[0] - (-0.5)).abs() < 1e-12);
1090        assert!((f[1] - (-0.25)).abs() < 1e-12);
1091        assert_eq!(i, &[-3.0, -1.0]);
1092    }
1093
1094    #[test]
1095    fn test_modf_nan() {
1096        let a = arr1(vec![f64::NAN]);
1097        let (frac, int) = modf(&a).unwrap();
1098        assert!(frac.as_slice().unwrap()[0].is_nan());
1099        assert!(int.as_slice().unwrap()[0].is_nan());
1100    }
1101
1102    #[test]
1103    fn test_modf_infinity() {
1104        let a = arr1(vec![f64::INFINITY, f64::NEG_INFINITY]);
1105        let (frac, int) = modf(&a).unwrap();
1106        let f = frac.as_slice().unwrap();
1107        let i = int.as_slice().unwrap();
1108        assert_eq!(f[0], 0.0);
1109        assert!(f[0].is_sign_positive());
1110        assert_eq!(f[1], 0.0);
1111        assert!(f[1].is_sign_negative());
1112        assert!(i[0].is_infinite() && i[0].is_sign_positive());
1113        assert!(i[1].is_infinite() && i[1].is_sign_negative());
1114    }
1115
1116    #[test]
1117    fn test_modf_f32() {
1118        let a: Array<f32, Ix1> = Array::from_vec(Ix1::new([3]), vec![2.75_f32, -0.5, 1.0]).unwrap();
1119        let (frac, int) = modf(&a).unwrap();
1120        let f = frac.as_slice().unwrap();
1121        let i = int.as_slice().unwrap();
1122        assert!((f[0] - 0.75).abs() < 1e-6);
1123        assert!((f[1] - (-0.5)).abs() < 1e-6);
1124        assert_eq!(f[2], 0.0);
1125        assert_eq!(i, &[2.0_f32, 0.0, 1.0]);
1126    }
1127}