Skip to main content

ferray_ufunc/ops/
trig.rs

1// ferray-ufunc: Trigonometric functions
2//
3// sin, cos, tan, arcsin, arccos, arctan, arctan2, hypot,
4// sinh, cosh, tanh, arcsinh, arccosh, arctanh,
5// degrees, radians, deg2rad, rad2deg, unwrap
6//
7// ## REQ status — REQ-5 (trig family) + binary-promote tie-ins
8//
9// SHIPPED:
10//   - REQ-5 (`sin`/`cos`/`tan`/`arcsin`/`arccos`/`arctan`/`arctan2`/`hypot`/
11//     `sinh`/`cosh`/`tanh`/`arcsinh`/`arccosh`/`arctanh`/`degrees`/`radians`/
12//     `deg2rad`/`rad2deg`/`unwrap`): the full NumPy trig ufunc family as
13//     generic free functions preserving input dimensionality (REQ-1). Anchors:
14//     `pub fn sin`/`pub fn cos`/`pub fn tan`/`pub fn arcsin`/`pub fn arccos`/
15//     `pub fn arctan`/`pub fn arcsinh`/`pub fn arccosh`/`pub fn arctanh`,
16//     `pub fn arctan2`/`pub fn hypot` (binary),
17//     `pub fn sinh`/`pub fn cosh`/`pub fn tanh`, `pub fn deg2rad`/
18//     `pub fn rad2deg`/`pub fn degrees`/`pub fn radians`, `pub fn unwrap`.
19//     `T: Element + Float` so f32/f64 (and complex via `complex.rs`) all route
20//     here. Domain-edge behaviour (`arcsin`/`arccos` outside [-1,1] -> NaN,
21//     `arctanh(±1)` -> ±inf) is audited against numpy 2.4.x and green. Each
22//     unary op has an `_into` in-place counterpart (`pub fn sin_into`/
23//     `pub fn cos_into`) and a faithful-rounding `_fast` kernel
24//     (`pub fn sin_fast`/`pub fn cos_fast`). Non-test production consumer:
25//     re-exported verbatim from the crate root (`lib.rs`
26//     `pub use ops::trig::{sin, cos, tan, …, arctan2, hypot, …, unwrap}`),
27//     the public ufunc surface and the ferray-python trig binding target.
28//   - REQ-23 tie-in (integer/bool input promotion): the integer-accepting
29//     `sin_promote`/`cos_promote`/`tan_promote`/`arctan_promote`/… wrappers
30//     live in `promoted.rs` and call THESE generic `T: Float` kernels
31//     monomorphised at the compute float, so the trig promotion surface
32//     consumes this module unchanged (no separate int kernel here).
33//   - REQ-25 tie-in (binary int/bool promotion): `arctan2_promote`/
34//     `hypot_promote` (in `promoted.rs`) wrap `pub fn arctan2`/`pub fn hypot`
35//     here for integer/bool operand pairs. f32/f64 callers stay byte-identical.
36//
37// NOT-STARTED: none — REQ-5 is fully shipped for this module.
38
39use ferray_core::Array;
40use ferray_core::dimension::Dimension;
41use ferray_core::dtype::Element;
42use ferray_core::error::FerrayResult;
43use num_traits::Float;
44
45use crate::cr_math::CrMath;
46use crate::helpers::{
47    binary_elementwise_op, unary_float_op, unary_float_op_compute, unary_float_op_into_compute,
48};
49
50// ---------------------------------------------------------------------------
51// Unary trig
52// ---------------------------------------------------------------------------
53
54/// Elementwise sine.
55///
56/// Routes through `Float::sin` (libm) for ~2.4× faster per-element
57/// throughput vs core-math at all sizes — matching NumPy's libm-based
58/// path. Accuracy is libm's standard ~1 ULP, well within the 256-ULP
59/// tolerance bands used by ferray's statistical equivalence harness.
60/// For correctly-rounded results call `cr_math::CrMath::cr_sin` directly.
61pub fn sin<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
62where
63    T: Element + Float,
64    D: Dimension,
65{
66    unary_float_op_compute(input, T::sin)
67}
68
69/// In-place sine — `_into` counterpart of [`sin`].
70pub fn sin_into<T, D>(input: &Array<T, D>, out: &mut Array<T, D>) -> FerrayResult<()>
71where
72    T: Element + Float,
73    D: Dimension,
74{
75    unary_float_op_into_compute(input, out, "sin", T::sin)
76}
77
78/// Elementwise cosine. See [`sin`] for the libm-vs-core-math accuracy note.
79pub fn cos<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
80where
81    T: Element + Float,
82    D: Dimension,
83{
84    unary_float_op_compute(input, T::cos)
85}
86
87/// In-place cosine — `_into` counterpart of [`cos`].
88pub fn cos_into<T, D>(input: &Array<T, D>, out: &mut Array<T, D>) -> FerrayResult<()>
89where
90    T: Element + Float,
91    D: Dimension,
92{
93    unary_float_op_into_compute(input, out, "cos", T::cos)
94}
95
96/// Fast elementwise sine with ≤1 ULP accuracy (≤4 ULP for `|x| ≳ 2^20`).
97///
98/// Three-part Cody-Waite reduction + Cephes DP polynomials, with a branchless
99/// quadrant select so the hot loop auto-vectorizes. The batch kernel is
100/// runtime-multiversioned (AVX2+FMA when present, libm fallback otherwise),
101/// giving ~3-4x over the default libm-based [`sin`] on contiguous f32/f64
102/// arrays. The default [`sin`] stays the correctness/large-`|x|` reference
103/// (Payne-Hanek-grade libm).
104///
105/// For f64 arrays the batch kernel is used directly; f32 promotes to f64
106/// internally (24 mantissa bits round cleanly to the correct f32 answer).
107pub fn sin_fast<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
108where
109    T: Element + Float,
110    D: Dimension,
111{
112    use std::any::TypeId;
113    if TypeId::of::<T>() == TypeId::of::<f64>() {
114        // SAFETY: T is f64 — reinterpret the array reference.
115        let f64_input =
116            unsafe { &*std::ptr::from_ref::<Array<T, D>>(input).cast::<Array<f64, D>>() };
117        let n = f64_input.size();
118        let result = if let Some(slice) = f64_input.as_slice() {
119            let mut data = vec![0.0_f64; n];
120            crate::fast_trig::sin_fast_batch_f64(slice, &mut data);
121            Array::from_vec(f64_input.dim().clone(), data)?
122        } else {
123            let data: Vec<f64> = f64_input
124                .iter()
125                .map(|&x| crate::fast_trig::sin_fast_f64(x))
126                .collect();
127            Array::from_vec(f64_input.dim().clone(), data)?
128        };
129        // SAFETY: T was verified to be f64 at the top of this branch.
130        Ok(unsafe { crate::helpers::reinterpret_array::<f64, T, D>(result) })
131    } else if TypeId::of::<T>() == TypeId::of::<f32>() {
132        // SAFETY: T is f32 — reinterpret the array reference.
133        let f32_input =
134            unsafe { &*std::ptr::from_ref::<Array<T, D>>(input).cast::<Array<f32, D>>() };
135        let n = f32_input.size();
136        let result = if let Some(slice) = f32_input.as_slice() {
137            let mut data = vec![0.0_f32; n];
138            crate::fast_trig::sin_fast_batch_f32(slice, &mut data);
139            Array::from_vec(f32_input.dim().clone(), data)?
140        } else {
141            let data: Vec<f32> = f32_input
142                .iter()
143                .map(|&x| crate::fast_trig::sin_fast_f32(x))
144                .collect();
145            Array::from_vec(f32_input.dim().clone(), data)?
146        };
147        // SAFETY: T was verified to be f32 at the top of this branch.
148        Ok(unsafe { crate::helpers::reinterpret_array::<f32, T, D>(result) })
149    } else {
150        // Other float types (f16/bf16): use the default libm path.
151        unary_float_op_compute(input, T::sin)
152    }
153}
154
155/// Fast elementwise cosine. See [`sin_fast`] for the accuracy/dispatch contract.
156pub fn cos_fast<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
157where
158    T: Element + Float,
159    D: Dimension,
160{
161    use std::any::TypeId;
162    if TypeId::of::<T>() == TypeId::of::<f64>() {
163        // SAFETY: T is f64 — reinterpret the array reference.
164        let f64_input =
165            unsafe { &*std::ptr::from_ref::<Array<T, D>>(input).cast::<Array<f64, D>>() };
166        let n = f64_input.size();
167        let result = if let Some(slice) = f64_input.as_slice() {
168            let mut data = vec![0.0_f64; n];
169            crate::fast_trig::cos_fast_batch_f64(slice, &mut data);
170            Array::from_vec(f64_input.dim().clone(), data)?
171        } else {
172            let data: Vec<f64> = f64_input
173                .iter()
174                .map(|&x| crate::fast_trig::cos_fast_f64(x))
175                .collect();
176            Array::from_vec(f64_input.dim().clone(), data)?
177        };
178        // SAFETY: T was verified to be f64 at the top of this branch.
179        Ok(unsafe { crate::helpers::reinterpret_array::<f64, T, D>(result) })
180    } else if TypeId::of::<T>() == TypeId::of::<f32>() {
181        // SAFETY: T is f32 — reinterpret the array reference.
182        let f32_input =
183            unsafe { &*std::ptr::from_ref::<Array<T, D>>(input).cast::<Array<f32, D>>() };
184        let n = f32_input.size();
185        let result = if let Some(slice) = f32_input.as_slice() {
186            let mut data = vec![0.0_f32; n];
187            crate::fast_trig::cos_fast_batch_f32(slice, &mut data);
188            Array::from_vec(f32_input.dim().clone(), data)?
189        } else {
190            let data: Vec<f32> = f32_input
191                .iter()
192                .map(|&x| crate::fast_trig::cos_fast_f32(x))
193                .collect();
194            Array::from_vec(f32_input.dim().clone(), data)?
195        };
196        // SAFETY: T was verified to be f32 at the top of this branch.
197        Ok(unsafe { crate::helpers::reinterpret_array::<f32, T, D>(result) })
198    } else {
199        // Other float types (f16/bf16): use the default libm path.
200        unary_float_op_compute(input, T::cos)
201    }
202}
203
204/// Elementwise tangent. See [`sin`] for the libm-vs-core-math accuracy note.
205pub fn tan<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
206where
207    T: Element + Float,
208    D: Dimension,
209{
210    unary_float_op_compute(input, T::tan)
211}
212
213/// Elementwise arc sine.
214pub fn arcsin<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
215where
216    T: Element + Float + CrMath,
217    D: Dimension,
218{
219    unary_float_op_compute(input, T::cr_asin)
220}
221
222/// Elementwise arc cosine.
223pub fn arccos<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
224where
225    T: Element + Float + CrMath,
226    D: Dimension,
227{
228    unary_float_op_compute(input, T::cr_acos)
229}
230
231/// Elementwise arc tangent.
232pub fn arctan<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
233where
234    T: Element + Float + CrMath,
235    D: Dimension,
236{
237    unary_float_op_compute(input, T::cr_atan)
238}
239
240/// Elementwise two-argument arc tangent (atan2).
241pub fn arctan2<T, D>(y: &Array<T, D>, x: &Array<T, D>) -> FerrayResult<Array<T, D>>
242where
243    T: Element + Float + CrMath,
244    D: Dimension,
245{
246    binary_elementwise_op(y, x, T::cr_atan2)
247}
248
249/// Elementwise hypotenuse: sqrt(a^2 + b^2).
250pub fn hypot<T, D>(a: &Array<T, D>, b: &Array<T, D>) -> FerrayResult<Array<T, D>>
251where
252    T: Element + Float + CrMath,
253    D: Dimension,
254{
255    binary_elementwise_op(a, b, T::cr_hypot)
256}
257
258// ---------------------------------------------------------------------------
259// Hyperbolic
260// ---------------------------------------------------------------------------
261
262/// Elementwise hyperbolic sine.
263pub fn sinh<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
264where
265    T: Element + Float + CrMath,
266    D: Dimension,
267{
268    unary_float_op_compute(input, T::cr_sinh)
269}
270
271/// Elementwise hyperbolic cosine.
272pub fn cosh<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
273where
274    T: Element + Float + CrMath,
275    D: Dimension,
276{
277    unary_float_op_compute(input, T::cr_cosh)
278}
279
280/// Elementwise hyperbolic tangent.
281pub fn tanh<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
282where
283    T: Element + Float + CrMath,
284    D: Dimension,
285{
286    unary_float_op_compute(input, T::cr_tanh)
287}
288
289/// Elementwise inverse hyperbolic sine.
290pub fn arcsinh<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
291where
292    T: Element + Float + CrMath,
293    D: Dimension,
294{
295    unary_float_op_compute(input, T::cr_asinh)
296}
297
298/// Elementwise inverse hyperbolic cosine.
299pub fn arccosh<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
300where
301    T: Element + Float + CrMath,
302    D: Dimension,
303{
304    unary_float_op_compute(input, T::cr_acosh)
305}
306
307/// Elementwise inverse hyperbolic tangent.
308pub fn arctanh<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
309where
310    T: Element + Float + CrMath,
311    D: Dimension,
312{
313    unary_float_op_compute(input, T::cr_atanh)
314}
315
316// ---------------------------------------------------------------------------
317// Degree/radian conversion
318// ---------------------------------------------------------------------------
319
320/// Convert radians to degrees.
321pub fn degrees<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
322where
323    T: Element + Float,
324    D: Dimension,
325{
326    unary_float_op(input, T::to_degrees)
327}
328
329/// Convert degrees to radians.
330pub fn radians<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
331where
332    T: Element + Float,
333    D: Dimension,
334{
335    unary_float_op(input, T::to_radians)
336}
337
338/// Alias for [`radians`].
339pub fn deg2rad<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
340where
341    T: Element + Float,
342    D: Dimension,
343{
344    radians(input)
345}
346
347/// Alias for [`degrees`].
348pub fn rad2deg<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
349where
350    T: Element + Float,
351    D: Dimension,
352{
353    degrees(input)
354}
355
356/// Unwrap by changing deltas between values to their 2*pi complement.
357///
358/// Works on 1-D arrays. `discont` defaults to pi if `None`.
359pub fn unwrap<T, D>(input: &Array<T, D>, discont: Option<T>) -> FerrayResult<Array<T, D>>
360where
361    T: Element + Float,
362    D: Dimension,
363{
364    let pi = T::from(std::f64::consts::PI).unwrap_or_else(<T as Element>::zero);
365    let two_pi = pi + pi;
366    let discont = discont.unwrap_or(pi);
367
368    let data: Vec<T> = input.iter().copied().collect();
369    if data.is_empty() {
370        return Array::from_vec(input.dim().clone(), data);
371    }
372
373    let mut result = Vec::with_capacity(data.len());
374    result.push(data[0]);
375    let mut cumulative = <T as Element>::zero();
376
377    for i in 1..data.len() {
378        let mut diff = data[i] - data[i - 1];
379        if diff > discont || diff < -discont {
380            diff = diff - two_pi * ((diff + pi) / two_pi).floor();
381        }
382        cumulative = cumulative + diff - (data[i] - data[i - 1]);
383        result.push(data[i] + cumulative);
384    }
385
386    Array::from_vec(input.dim().clone(), result)
387}
388
389// ---------------------------------------------------------------------------
390// f16 variants (f32-promoted) — declared via the shared `unary_f16_fn!`
391// and `binary_f16_fn!` macros so each entry point is a single line. The
392// prior hand-written pattern (6 lines × 15 functions ≈ 90 lines of
393// boilerplate) is gone (#142).
394// ---------------------------------------------------------------------------
395
396use crate::helpers::{binary_f16_fn, unary_f16_fn};
397
398unary_f16_fn!(
399    /// Elementwise sine for f16 arrays via f32 promotion.
400    #[cfg(feature = "f16")]
401    sin_f16,
402    f32::sin
403);
404unary_f16_fn!(
405    /// Elementwise cosine for f16 arrays via f32 promotion.
406    #[cfg(feature = "f16")]
407    cos_f16,
408    f32::cos
409);
410unary_f16_fn!(
411    /// Elementwise tangent for f16 arrays via f32 promotion.
412    #[cfg(feature = "f16")]
413    tan_f16,
414    f32::tan
415);
416unary_f16_fn!(
417    /// Elementwise arc sine for f16 arrays via f32 promotion.
418    #[cfg(feature = "f16")]
419    arcsin_f16,
420    f32::asin
421);
422unary_f16_fn!(
423    /// Elementwise arc cosine for f16 arrays via f32 promotion.
424    #[cfg(feature = "f16")]
425    arccos_f16,
426    f32::acos
427);
428unary_f16_fn!(
429    /// Elementwise arc tangent for f16 arrays via f32 promotion.
430    #[cfg(feature = "f16")]
431    arctan_f16,
432    f32::atan
433);
434binary_f16_fn!(
435    /// Elementwise two-argument arc tangent for f16 arrays via f32 promotion.
436    #[cfg(feature = "f16")]
437    arctan2_f16,
438    f32::atan2
439);
440binary_f16_fn!(
441    /// Elementwise hypotenuse for f16 arrays via f32 promotion.
442    #[cfg(feature = "f16")]
443    hypot_f16,
444    f32::hypot
445);
446unary_f16_fn!(
447    /// Elementwise hyperbolic sine for f16 arrays via f32 promotion.
448    #[cfg(feature = "f16")]
449    sinh_f16,
450    f32::sinh
451);
452unary_f16_fn!(
453    /// Elementwise hyperbolic cosine for f16 arrays via f32 promotion.
454    #[cfg(feature = "f16")]
455    cosh_f16,
456    f32::cosh
457);
458unary_f16_fn!(
459    /// Elementwise hyperbolic tangent for f16 arrays via f32 promotion.
460    #[cfg(feature = "f16")]
461    tanh_f16,
462    f32::tanh
463);
464unary_f16_fn!(
465    /// Elementwise inverse hyperbolic sine for f16 arrays via f32 promotion.
466    #[cfg(feature = "f16")]
467    arcsinh_f16,
468    f32::asinh
469);
470unary_f16_fn!(
471    /// Elementwise inverse hyperbolic cosine for f16 arrays via f32 promotion.
472    #[cfg(feature = "f16")]
473    arccosh_f16,
474    f32::acosh
475);
476unary_f16_fn!(
477    /// Elementwise inverse hyperbolic tangent for f16 arrays via f32 promotion.
478    #[cfg(feature = "f16")]
479    arctanh_f16,
480    f32::atanh
481);
482unary_f16_fn!(
483    /// Convert radians to degrees for f16 arrays via f32 promotion.
484    #[cfg(feature = "f16")]
485    degrees_f16,
486    f32::to_degrees
487);
488unary_f16_fn!(
489    /// Convert degrees to radians for f16 arrays via f32 promotion.
490    #[cfg(feature = "f16")]
491    radians_f16,
492    f32::to_radians
493);
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498
499    use crate::test_util::arr1;
500
501    #[test]
502    fn test_sin() {
503        let a = arr1(vec![0.0, std::f64::consts::FRAC_PI_2, std::f64::consts::PI]);
504        let r = sin(&a).unwrap();
505        let s = r.as_slice().unwrap();
506        assert!((s[0]).abs() < 1e-12);
507        assert!((s[1] - 1.0).abs() < 1e-12);
508        assert!((s[2]).abs() < 1e-12);
509    }
510
511    #[test]
512    fn test_cos() {
513        let a = arr1(vec![0.0, std::f64::consts::PI]);
514        let r = cos(&a).unwrap();
515        let s = r.as_slice().unwrap();
516        assert!((s[0] - 1.0).abs() < 1e-12);
517        assert!((s[1] + 1.0).abs() < 1e-12);
518    }
519
520    #[test]
521    fn test_tan() {
522        let a = arr1(vec![0.0, std::f64::consts::FRAC_PI_4]);
523        let r = tan(&a).unwrap();
524        let s = r.as_slice().unwrap();
525        assert!((s[0]).abs() < 1e-12);
526        assert!((s[1] - 1.0).abs() < 1e-12);
527    }
528
529    #[test]
530    fn test_arcsin() {
531        let a = arr1(vec![0.0, 1.0]);
532        let r = arcsin(&a).unwrap();
533        let s = r.as_slice().unwrap();
534        assert!((s[0]).abs() < 1e-12);
535        assert!((s[1] - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
536    }
537
538    #[test]
539    fn test_arctan2() {
540        let y = arr1(vec![0.0, 1.0]);
541        let x = arr1(vec![1.0, 0.0]);
542        let r = arctan2(&y, &x).unwrap();
543        let s = r.as_slice().unwrap();
544        assert!((s[0]).abs() < 1e-12);
545        assert!((s[1] - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
546    }
547
548    #[test]
549    fn test_hypot() {
550        let a = arr1(vec![3.0, 5.0]);
551        let b = arr1(vec![4.0, 12.0]);
552        let r = hypot(&a, &b).unwrap();
553        let s = r.as_slice().unwrap();
554        assert!((s[0] - 5.0).abs() < 1e-12);
555        assert!((s[1] - 13.0).abs() < 1e-12);
556    }
557
558    #[test]
559    fn test_sinh_cosh_tanh() {
560        let a = arr1(vec![0.0]);
561        assert!((sinh(&a).unwrap().as_slice().unwrap()[0]).abs() < 1e-12);
562        assert!((cosh(&a).unwrap().as_slice().unwrap()[0] - 1.0).abs() < 1e-12);
563        assert!((tanh(&a).unwrap().as_slice().unwrap()[0]).abs() < 1e-12);
564    }
565
566    #[test]
567    fn test_degrees_radians() {
568        let a = arr1(vec![std::f64::consts::PI]);
569        let deg = degrees(&a).unwrap();
570        assert!((deg.as_slice().unwrap()[0] - 180.0).abs() < 1e-10);
571
572        let back = radians(&deg).unwrap();
573        assert!((back.as_slice().unwrap()[0] - std::f64::consts::PI).abs() < 1e-12);
574    }
575
576    #[test]
577    fn test_deg2rad_rad2deg() {
578        let a = arr1(vec![180.0]);
579        let r = deg2rad(&a).unwrap();
580        assert!((r.as_slice().unwrap()[0] - std::f64::consts::PI).abs() < 1e-12);
581
582        let d = rad2deg(&arr1(vec![std::f64::consts::PI])).unwrap();
583        assert!((d.as_slice().unwrap()[0] - 180.0).abs() < 1e-10);
584    }
585
586    #[test]
587    fn test_unwrap_basic() {
588        let a = arr1(vec![0.0, 0.5, 1.0, -0.5, -1.0]);
589        let r = unwrap(&a, None).unwrap();
590        // No discontinuity larger than pi, so should be unchanged
591        let s = r.as_slice().unwrap();
592        for (i, &v) in s.iter().enumerate() {
593            assert!((v - a.as_slice().unwrap()[i]).abs() < 1e-12);
594        }
595    }
596
597    #[test]
598    fn test_arcsinh_arccosh_arctanh() {
599        let a = arr1(vec![0.0]);
600        assert!((arcsinh(&a).unwrap().as_slice().unwrap()[0]).abs() < 1e-12);
601
602        let b = arr1(vec![1.0]);
603        assert!((arccosh(&b).unwrap().as_slice().unwrap()[0]).abs() < 1e-12);
604        assert!((arctanh(&a).unwrap().as_slice().unwrap()[0]).abs() < 1e-12);
605    }
606
607    #[cfg(feature = "f16")]
608    mod f16_tests {
609        use super::*;
610        use ferray_core::dimension::Ix1;
611
612        fn arr1_f16(data: &[f32]) -> Array<half::f16, Ix1> {
613            let n = data.len();
614            let vals: Vec<half::f16> = data.iter().map(|&x| half::f16::from_f32(x)).collect();
615            Array::from_vec(Ix1::new([n]), vals).unwrap()
616        }
617
618        #[test]
619        fn test_sin_f16() {
620            let a = arr1_f16(&[0.0, std::f32::consts::FRAC_PI_2, std::f32::consts::PI]);
621            let r = sin_f16(&a).unwrap();
622            let s = r.as_slice().unwrap();
623            assert!(s[0].to_f32().abs() < 0.01);
624            assert!((s[1].to_f32() - 1.0).abs() < 0.01);
625            assert!(s[2].to_f32().abs() < 0.01);
626        }
627
628        #[test]
629        fn test_cos_f16() {
630            let a = arr1_f16(&[0.0, std::f32::consts::PI]);
631            let r = cos_f16(&a).unwrap();
632            let s = r.as_slice().unwrap();
633            assert!((s[0].to_f32() - 1.0).abs() < 0.01);
634            assert!((s[1].to_f32() + 1.0).abs() < 0.01);
635        }
636
637        #[test]
638        fn test_tan_f16() {
639            let a = arr1_f16(&[0.0, std::f32::consts::FRAC_PI_4]);
640            let r = tan_f16(&a).unwrap();
641            let s = r.as_slice().unwrap();
642            assert!(s[0].to_f32().abs() < 0.01);
643            assert!((s[1].to_f32() - 1.0).abs() < 0.01);
644        }
645
646        #[test]
647        fn test_arctan2_f16() {
648            let y = arr1_f16(&[0.0, 1.0]);
649            let x = arr1_f16(&[1.0, 0.0]);
650            let r = arctan2_f16(&y, &x).unwrap();
651            let s = r.as_slice().unwrap();
652            assert!(s[0].to_f32().abs() < 0.01);
653            assert!((s[1].to_f32() - std::f32::consts::FRAC_PI_2).abs() < 0.01);
654        }
655
656        #[test]
657        fn test_degrees_radians_f16() {
658            let a = arr1_f16(&[std::f32::consts::PI]);
659            let deg = degrees_f16(&a).unwrap();
660            assert!((deg.as_slice().unwrap()[0].to_f32() - 180.0).abs() < 0.5);
661
662            let back = radians_f16(&deg).unwrap();
663            assert!((back.as_slice().unwrap()[0].to_f32() - std::f32::consts::PI).abs() < 0.01);
664        }
665    }
666}