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
7use ferray_core::Array;
8use ferray_core::dimension::Dimension;
9use ferray_core::dtype::Element;
10use ferray_core::error::{FerrayError, FerrayResult};
11use num_traits::Float;
12
13use crate::helpers::{binary_float_op, unary_float_op, unary_map_op};
14
15// ---------------------------------------------------------------------------
16// Classification
17// ---------------------------------------------------------------------------
18
19/// Elementwise test for NaN.
20pub fn isnan<T, D>(input: &Array<T, D>) -> FerrayResult<Array<bool, D>>
21where
22    T: Element + Float,
23    D: Dimension,
24{
25    unary_map_op(input, |x| x.is_nan())
26}
27
28/// Elementwise test for infinity (positive or negative).
29pub fn isinf<T, D>(input: &Array<T, D>) -> FerrayResult<Array<bool, D>>
30where
31    T: Element + Float,
32    D: Dimension,
33{
34    unary_map_op(input, |x| x.is_infinite())
35}
36
37/// Elementwise test for finiteness.
38pub fn isfinite<T, D>(input: &Array<T, D>) -> FerrayResult<Array<bool, D>>
39where
40    T: Element + Float,
41    D: Dimension,
42{
43    unary_map_op(input, |x| x.is_finite())
44}
45
46/// Elementwise test for negative infinity.
47pub fn isneginf<T, D>(input: &Array<T, D>) -> FerrayResult<Array<bool, D>>
48where
49    T: Element + Float,
50    D: Dimension,
51{
52    unary_map_op(input, |x| x.is_infinite() && x < <T as Element>::zero())
53}
54
55/// Elementwise test for positive infinity.
56pub fn isposinf<T, D>(input: &Array<T, D>) -> FerrayResult<Array<bool, D>>
57where
58    T: Element + Float,
59    D: Dimension,
60{
61    unary_map_op(input, |x| x.is_infinite() && x > <T as Element>::zero())
62}
63
64/// Elementwise sign bit test.
65pub fn signbit<T, D>(input: &Array<T, D>) -> FerrayResult<Array<bool, D>>
66where
67    T: Element + Float,
68    D: Dimension,
69{
70    unary_map_op(input, |x| x.is_sign_negative())
71}
72
73// ---------------------------------------------------------------------------
74// Replacement
75// ---------------------------------------------------------------------------
76
77/// Replace NaN with zero, and infinity with large finite numbers.
78///
79/// `nan` replaces NaN (default 0), `posinf` replaces +inf, `neginf` replaces -inf.
80pub fn nan_to_num<T, D>(
81    input: &Array<T, D>,
82    nan: Option<T>,
83    posinf: Option<T>,
84    neginf: Option<T>,
85) -> FerrayResult<Array<T, D>>
86where
87    T: Element + Float,
88    D: Dimension,
89{
90    let nan_val = nan.unwrap_or_else(|| <T as Element>::zero());
91    let posinf_val = posinf.unwrap_or_else(T::max_value);
92    let neginf_val = neginf.unwrap_or_else(T::min_value);
93    unary_float_op(input, |x| {
94        if x.is_nan() {
95            nan_val
96        } else if x.is_infinite() {
97            if x > <T as Element>::zero() {
98                posinf_val
99            } else {
100                neginf_val
101            }
102        } else {
103            x
104        }
105    })
106}
107
108/// Clip (limit) values to [a_min, a_max].
109pub fn clip<T, D>(input: &Array<T, D>, a_min: T, a_max: T) -> FerrayResult<Array<T, D>>
110where
111    T: Element + Float,
112    D: Dimension,
113{
114    if a_min > a_max {
115        return Err(FerrayError::invalid_value("clip: a_min must be <= a_max"));
116    }
117    unary_float_op(input, |x| {
118        if x < a_min {
119            a_min
120        } else if x > a_max {
121            a_max
122        } else {
123            x
124        }
125    })
126}
127
128// ---------------------------------------------------------------------------
129// Float manipulation
130// ---------------------------------------------------------------------------
131
132/// Return the next floating-point value after x1 towards x2.
133pub fn nextafter<T, D>(x1: &Array<T, D>, x2: &Array<T, D>) -> FerrayResult<Array<T, D>>
134where
135    T: Element + Float,
136    D: Dimension,
137{
138    binary_float_op(x1, x2, |a, b| {
139        if a == b {
140            b
141        } else if a < b {
142            // Move toward positive infinity
143            let tiny = T::min_positive_value();
144            let eps = a.abs() * T::epsilon();
145            a + if eps > tiny { eps } else { tiny }
146        } else {
147            let tiny = T::min_positive_value();
148            let eps = a.abs() * T::epsilon();
149            a - if eps > tiny { eps } else { tiny }
150        }
151    })
152}
153
154/// Return the spacing of values: the ULP (unit in the last place).
155pub fn spacing<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
156where
157    T: Element + Float,
158    D: Dimension,
159{
160    unary_float_op(input, |x| {
161        if x.is_nan() || x.is_infinite() {
162            <T as Float>::nan()
163        } else {
164            let tiny = T::min_positive_value();
165            let eps = x.abs() * T::epsilon();
166            if eps > tiny { eps } else { tiny }
167        }
168    })
169}
170
171/// Multiply x by 2^n (ldexp).
172///
173/// `n` is provided as an integer array of same shape.
174pub fn ldexp<T, D>(x: &Array<T, D>, n: &Array<i32, D>) -> FerrayResult<Array<T, D>>
175where
176    T: Element + Float,
177    D: Dimension,
178{
179    if x.shape() != n.shape() {
180        return Err(FerrayError::shape_mismatch(format!(
181            "ldexp: shapes {:?} and {:?} do not match",
182            x.shape(),
183            n.shape()
184        )));
185    }
186    let two = <T as Element>::one() + <T as Element>::one();
187    let data: Vec<T> = x
188        .iter()
189        .zip(n.iter())
190        .map(|(&xi, &ni)| xi * two.powi(ni))
191        .collect();
192    Array::from_vec(x.dim().clone(), data)
193}
194
195/// Decompose into mantissa and exponent: x = m * 2^e.
196///
197/// Returns (mantissa_array, exponent_array) where mantissa is in [0.5, 1.0).
198/// This matches C's `frexp`: for x=4.0, returns (0.5, 3) since 0.5 * 2^3 = 4.
199pub fn frexp<T, D>(input: &Array<T, D>) -> FerrayResult<(Array<T, D>, Array<i32, D>)>
200where
201    T: Element + Float,
202    D: Dimension,
203{
204    let mut mantissas = Vec::with_capacity(input.size());
205    let mut exponents = Vec::with_capacity(input.size());
206
207    let zero = <T as Element>::zero();
208    let half = T::from(0.5).unwrap();
209    let two = T::from(2.0).unwrap();
210
211    for &x in input.iter() {
212        if x == zero || x.is_nan() || x.is_infinite() {
213            mantissas.push(x);
214            exponents.push(0);
215            continue;
216        }
217        let mut abs_x = x.abs();
218        let mut exp: i32 = 0;
219
220        // Normalize to [0.5, 1.0)
221        while abs_x >= T::from(1.0).unwrap() {
222            abs_x = abs_x / two;
223            exp += 1;
224        }
225        while abs_x < half {
226            abs_x = abs_x * two;
227            exp -= 1;
228        }
229
230        if x.is_sign_negative() {
231            mantissas.push(-abs_x);
232        } else {
233            mantissas.push(abs_x);
234        }
235        exponents.push(exp);
236    }
237
238    Ok((
239        Array::from_vec(input.dim().clone(), mantissas)?,
240        Array::from_vec(input.dim().clone(), exponents)?,
241    ))
242}
243
244/// Elementwise copysign: magnitude of x1, sign of x2.
245pub fn copysign<T, D>(x1: &Array<T, D>, x2: &Array<T, D>) -> FerrayResult<Array<T, D>>
246where
247    T: Element + Float,
248    D: Dimension,
249{
250    binary_float_op(x1, x2, |a, b| {
251        let mag = a.abs();
252        if b.is_sign_negative() { -mag } else { mag }
253    })
254}
255
256/// Float power: x1^x2, always returning float.
257pub fn float_power<T, D>(x1: &Array<T, D>, x2: &Array<T, D>) -> FerrayResult<Array<T, D>>
258where
259    T: Element + Float,
260    D: Dimension,
261{
262    binary_float_op(x1, x2, T::powf)
263}
264
265/// Elementwise maximum, propagating NaN.
266pub fn maximum<T, D>(a: &Array<T, D>, b: &Array<T, D>) -> FerrayResult<Array<T, D>>
267where
268    T: Element + Float,
269    D: Dimension,
270{
271    binary_float_op(a, b, |x, y| {
272        if x.is_nan() || y.is_nan() {
273            <T as Float>::nan()
274        } else if x >= y {
275            x
276        } else {
277            y
278        }
279    })
280}
281
282/// Elementwise minimum, propagating NaN.
283pub fn minimum<T, D>(a: &Array<T, D>, b: &Array<T, D>) -> FerrayResult<Array<T, D>>
284where
285    T: Element + Float,
286    D: Dimension,
287{
288    binary_float_op(a, b, |x, y| {
289        if x.is_nan() || y.is_nan() {
290            <T as Float>::nan()
291        } else if x <= y {
292            x
293        } else {
294            y
295        }
296    })
297}
298
299/// Elementwise maximum, ignoring NaN.
300pub fn fmax<T, D>(a: &Array<T, D>, b: &Array<T, D>) -> FerrayResult<Array<T, D>>
301where
302    T: Element + Float,
303    D: Dimension,
304{
305    binary_float_op(a, b, |x, y| {
306        if x.is_nan() {
307            y
308        } else if y.is_nan() || x >= y {
309            x
310        } else {
311            y
312        }
313    })
314}
315
316/// Elementwise minimum, ignoring NaN.
317pub fn fmin<T, D>(a: &Array<T, D>, b: &Array<T, D>) -> FerrayResult<Array<T, D>>
318where
319    T: Element + Float,
320    D: Dimension,
321{
322    binary_float_op(a, b, |x, y| {
323        if x.is_nan() {
324            y
325        } else if y.is_nan() || x <= y {
326            x
327        } else {
328            y
329        }
330    })
331}
332
333// ---------------------------------------------------------------------------
334// f16 variants (f32-promoted)
335// ---------------------------------------------------------------------------
336
337/// Elementwise test for NaN for f16 arrays.
338#[cfg(feature = "f16")]
339pub fn isnan_f16<D>(input: &Array<half::f16, D>) -> FerrayResult<Array<bool, D>>
340where
341    D: Dimension,
342{
343    crate::helpers::unary_f16_to_bool_op(input, f32::is_nan)
344}
345
346/// Elementwise test for infinity for f16 arrays.
347#[cfg(feature = "f16")]
348pub fn isinf_f16<D>(input: &Array<half::f16, D>) -> FerrayResult<Array<bool, D>>
349where
350    D: Dimension,
351{
352    crate::helpers::unary_f16_to_bool_op(input, f32::is_infinite)
353}
354
355/// Elementwise test for finiteness for f16 arrays.
356#[cfg(feature = "f16")]
357pub fn isfinite_f16<D>(input: &Array<half::f16, D>) -> FerrayResult<Array<bool, D>>
358where
359    D: Dimension,
360{
361    crate::helpers::unary_f16_to_bool_op(input, f32::is_finite)
362}
363
364/// Clip (limit) values to [a_min, a_max] for f16 arrays via f32 promotion.
365#[cfg(feature = "f16")]
366pub fn clip_f16<D>(
367    input: &Array<half::f16, D>,
368    a_min: half::f16,
369    a_max: half::f16,
370) -> FerrayResult<Array<half::f16, D>>
371where
372    D: Dimension,
373{
374    let min_f32 = a_min.to_f32();
375    let max_f32 = a_max.to_f32();
376    if min_f32 > max_f32 {
377        return Err(FerrayError::invalid_value("clip: a_min must be <= a_max"));
378    }
379    crate::helpers::unary_f16_op(input, |x| {
380        if x < min_f32 {
381            min_f32
382        } else if x > max_f32 {
383            max_f32
384        } else {
385            x
386        }
387    })
388}
389
390/// Replace NaN with zero, and infinity with large finite numbers, for f16 arrays.
391#[cfg(feature = "f16")]
392pub fn nan_to_num_f16<D>(
393    input: &Array<half::f16, D>,
394    nan: Option<half::f16>,
395    posinf: Option<half::f16>,
396    neginf: Option<half::f16>,
397) -> FerrayResult<Array<half::f16, D>>
398where
399    D: Dimension,
400{
401    let nan_val = nan.unwrap_or(half::f16::ZERO).to_f32();
402    let posinf_val = posinf.unwrap_or(half::f16::MAX).to_f32();
403    let neginf_val = neginf.unwrap_or(half::f16::MIN).to_f32();
404    crate::helpers::unary_f16_op(input, |x| {
405        if x.is_nan() {
406            nan_val
407        } else if x.is_infinite() {
408            if x > 0.0 { posinf_val } else { neginf_val }
409        } else {
410            x
411        }
412    })
413}
414
415/// Elementwise maximum for f16 arrays via f32 promotion, propagating NaN.
416#[cfg(feature = "f16")]
417pub fn maximum_f16<D>(
418    a: &Array<half::f16, D>,
419    b: &Array<half::f16, D>,
420) -> FerrayResult<Array<half::f16, D>>
421where
422    D: Dimension,
423{
424    crate::helpers::binary_f16_op(a, b, |x, y| {
425        if x.is_nan() || y.is_nan() {
426            f32::NAN
427        } else if x >= y {
428            x
429        } else {
430            y
431        }
432    })
433}
434
435/// Elementwise minimum for f16 arrays via f32 promotion, propagating NaN.
436#[cfg(feature = "f16")]
437pub fn minimum_f16<D>(
438    a: &Array<half::f16, D>,
439    b: &Array<half::f16, D>,
440) -> FerrayResult<Array<half::f16, D>>
441where
442    D: Dimension,
443{
444    crate::helpers::binary_f16_op(a, b, |x, y| {
445        if x.is_nan() || y.is_nan() {
446            f32::NAN
447        } else if x <= y {
448            x
449        } else {
450            y
451        }
452    })
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use ferray_core::dimension::Ix1;
459
460    fn arr1(data: Vec<f64>) -> Array<f64, Ix1> {
461        let n = data.len();
462        Array::from_vec(Ix1::new([n]), data).unwrap()
463    }
464
465    fn arr1_i32(data: Vec<i32>) -> Array<i32, Ix1> {
466        let n = data.len();
467        Array::from_vec(Ix1::new([n]), data).unwrap()
468    }
469
470    #[test]
471    fn test_isnan() {
472        let a = arr1(vec![1.0, f64::NAN, 3.0]);
473        let r = isnan(&a).unwrap();
474        assert_eq!(r.as_slice().unwrap(), &[false, true, false]);
475    }
476
477    #[test]
478    fn test_isinf() {
479        let a = arr1(vec![1.0, f64::INFINITY, f64::NEG_INFINITY]);
480        let r = isinf(&a).unwrap();
481        assert_eq!(r.as_slice().unwrap(), &[false, true, true]);
482    }
483
484    #[test]
485    fn test_isfinite() {
486        let a = arr1(vec![1.0, f64::INFINITY, f64::NAN]);
487        let r = isfinite(&a).unwrap();
488        assert_eq!(r.as_slice().unwrap(), &[true, false, false]);
489    }
490
491    #[test]
492    fn test_isneginf() {
493        let a = arr1(vec![f64::NEG_INFINITY, f64::INFINITY, 0.0]);
494        let r = isneginf(&a).unwrap();
495        assert_eq!(r.as_slice().unwrap(), &[true, false, false]);
496    }
497
498    #[test]
499    fn test_isposinf() {
500        let a = arr1(vec![f64::NEG_INFINITY, f64::INFINITY, 0.0]);
501        let r = isposinf(&a).unwrap();
502        assert_eq!(r.as_slice().unwrap(), &[false, true, false]);
503    }
504
505    #[test]
506    fn test_nan_to_num() {
507        let a = arr1(vec![f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 1.0]);
508        let r = nan_to_num(&a, None, None, None).unwrap();
509        let s = r.as_slice().unwrap();
510        assert_eq!(s[0], 0.0);
511        assert!(s[1].is_finite() && s[1] > 0.0);
512        assert!(s[2].is_finite() && s[2] < 0.0);
513        assert_eq!(s[3], 1.0);
514    }
515
516    #[test]
517    fn test_clip() {
518        let a = arr1(vec![-1.0, 0.5, 1.5, 3.0]);
519        let r = clip(&a, 0.0, 2.0).unwrap();
520        assert_eq!(r.as_slice().unwrap(), &[0.0, 0.5, 1.5, 2.0]);
521    }
522
523    #[test]
524    fn test_signbit() {
525        let a = arr1(vec![-1.0, 0.0, 1.0]);
526        let r = signbit(&a).unwrap();
527        assert_eq!(r.as_slice().unwrap(), &[true, false, false]);
528    }
529
530    #[test]
531    fn test_copysign() {
532        let a = arr1(vec![1.0, -2.0, 3.0]);
533        let b = arr1(vec![-1.0, 1.0, -1.0]);
534        let r = copysign(&a, &b).unwrap();
535        assert_eq!(r.as_slice().unwrap(), &[-1.0, 2.0, -3.0]);
536    }
537
538    #[test]
539    fn test_maximum() {
540        let a = arr1(vec![1.0, 5.0, 3.0]);
541        let b = arr1(vec![4.0, 2.0, 3.0]);
542        let r = maximum(&a, &b).unwrap();
543        assert_eq!(r.as_slice().unwrap(), &[4.0, 5.0, 3.0]);
544    }
545
546    #[test]
547    fn test_minimum() {
548        let a = arr1(vec![1.0, 5.0, 3.0]);
549        let b = arr1(vec![4.0, 2.0, 3.0]);
550        let r = minimum(&a, &b).unwrap();
551        assert_eq!(r.as_slice().unwrap(), &[1.0, 2.0, 3.0]);
552    }
553
554    #[test]
555    fn test_maximum_nan_propagation() {
556        let a = arr1(vec![1.0, f64::NAN]);
557        let b = arr1(vec![2.0, 3.0]);
558        let r = maximum(&a, &b).unwrap();
559        let s = r.as_slice().unwrap();
560        assert_eq!(s[0], 2.0);
561        assert!(s[1].is_nan());
562    }
563
564    #[test]
565    fn test_fmax_ignores_nan() {
566        let a = arr1(vec![1.0, f64::NAN]);
567        let b = arr1(vec![2.0, 3.0]);
568        let r = fmax(&a, &b).unwrap();
569        let s = r.as_slice().unwrap();
570        assert_eq!(s[0], 2.0);
571        assert_eq!(s[1], 3.0);
572    }
573
574    #[test]
575    fn test_fmin_ignores_nan() {
576        let a = arr1(vec![1.0, f64::NAN]);
577        let b = arr1(vec![2.0, 3.0]);
578        let r = fmin(&a, &b).unwrap();
579        let s = r.as_slice().unwrap();
580        assert_eq!(s[0], 1.0);
581        assert_eq!(s[1], 3.0);
582    }
583
584    #[test]
585    fn test_float_power() {
586        let a = arr1(vec![2.0, 3.0]);
587        let b = arr1(vec![3.0, 2.0]);
588        let r = float_power(&a, &b).unwrap();
589        assert_eq!(r.as_slice().unwrap(), &[8.0, 9.0]);
590    }
591
592    #[test]
593    fn test_spacing() {
594        let a = arr1(vec![1.0]);
595        let r = spacing(&a).unwrap();
596        let s = r.as_slice().unwrap();
597        assert!(s[0] > 0.0 && s[0] < 1e-10);
598    }
599
600    #[test]
601    fn test_ldexp() {
602        let x = arr1(vec![1.0, 2.0]);
603        let n = arr1_i32(vec![2, 3]);
604        let r = ldexp(&x, &n).unwrap();
605        let s = r.as_slice().unwrap();
606        assert!((s[0] - 4.0).abs() < 1e-12);
607        assert!((s[1] - 16.0).abs() < 1e-12);
608    }
609
610    #[test]
611    fn test_frexp() {
612        let a = arr1(vec![4.0]);
613        let (m, e) = frexp(&a).unwrap();
614        let ms = m.as_slice().unwrap();
615        let es = e.as_slice().unwrap();
616        // 4 = 0.5 * 2^3
617        assert!((ms[0] - 0.5).abs() < 1e-12);
618        assert_eq!(es[0], 3);
619    }
620
621    #[test]
622    fn test_nextafter() {
623        let a = arr1(vec![0.0]);
624        let b = arr1(vec![1.0]);
625        let r = nextafter(&a, &b).unwrap();
626        let s = r.as_slice().unwrap();
627        assert!(s[0] > 0.0);
628    }
629
630    #[cfg(feature = "f16")]
631    mod f16_tests {
632        use super::*;
633
634        fn arr1_f16(data: &[f32]) -> Array<half::f16, Ix1> {
635            let n = data.len();
636            let vals: Vec<half::f16> = data.iter().map(|&x| half::f16::from_f32(x)).collect();
637            Array::from_vec(Ix1::new([n]), vals).unwrap()
638        }
639
640        #[test]
641        fn test_isnan_f16() {
642            let a = arr1_f16(&[1.0, f32::NAN, 3.0]);
643            let r = isnan_f16(&a).unwrap();
644            assert_eq!(r.as_slice().unwrap(), &[false, true, false]);
645        }
646
647        #[test]
648        fn test_isinf_f16() {
649            let a = arr1_f16(&[1.0, f32::INFINITY, f32::NEG_INFINITY]);
650            let r = isinf_f16(&a).unwrap();
651            assert_eq!(r.as_slice().unwrap(), &[false, true, true]);
652        }
653
654        #[test]
655        fn test_isfinite_f16() {
656            let a = arr1_f16(&[1.0, f32::INFINITY, f32::NAN]);
657            let r = isfinite_f16(&a).unwrap();
658            assert_eq!(r.as_slice().unwrap(), &[true, false, false]);
659        }
660
661        #[test]
662        fn test_clip_f16() {
663            let a = arr1_f16(&[-1.0, 0.5, 1.5, 3.0]);
664            let r = clip_f16(&a, half::f16::from_f32(0.0), half::f16::from_f32(2.0)).unwrap();
665            let s = r.as_slice().unwrap();
666            assert!((s[0].to_f32() - 0.0).abs() < 0.01);
667            assert!((s[1].to_f32() - 0.5).abs() < 0.01);
668            assert!((s[2].to_f32() - 1.5).abs() < 0.01);
669            assert!((s[3].to_f32() - 2.0).abs() < 0.01);
670        }
671
672        #[test]
673        fn test_nan_to_num_f16() {
674            let a = arr1_f16(&[f32::NAN, 1.0]);
675            let r = nan_to_num_f16(&a, None, None, None).unwrap();
676            let s = r.as_slice().unwrap();
677            assert_eq!(s[0].to_f32(), 0.0);
678            assert!((s[1].to_f32() - 1.0).abs() < 0.01);
679        }
680
681        #[test]
682        fn test_maximum_f16() {
683            let a = arr1_f16(&[1.0, 5.0, 3.0]);
684            let b = arr1_f16(&[4.0, 2.0, 3.0]);
685            let r = maximum_f16(&a, &b).unwrap();
686            let s = r.as_slice().unwrap();
687            assert!((s[0].to_f32() - 4.0).abs() < 0.01);
688            assert!((s[1].to_f32() - 5.0).abs() < 0.01);
689            assert!((s[2].to_f32() - 3.0).abs() < 0.01);
690        }
691
692        #[test]
693        fn test_minimum_f16() {
694            let a = arr1_f16(&[1.0, 5.0, 3.0]);
695            let b = arr1_f16(&[4.0, 2.0, 3.0]);
696            let r = minimum_f16(&a, &b).unwrap();
697            let s = r.as_slice().unwrap();
698            assert!((s[0].to_f32() - 1.0).abs() < 0.01);
699            assert!((s[1].to_f32() - 2.0).abs() < 0.01);
700            assert!((s[2].to_f32() - 3.0).abs() < 0.01);
701        }
702    }
703}