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
7use ferray_core::Array;
8use ferray_core::dimension::Dimension;
9use ferray_core::dtype::Element;
10use ferray_core::error::FerrayResult;
11use num_traits::Float;
12
13use crate::cr_math::CrMath;
14use crate::helpers::{
15    binary_elementwise_op, unary_float_op, unary_float_op_compute, unary_float_op_into_compute,
16};
17
18// ---------------------------------------------------------------------------
19// Unary trig
20// ---------------------------------------------------------------------------
21
22/// Elementwise sine.
23///
24/// Routes through `Float::sin` (libm) for ~2.4× faster per-element
25/// throughput vs core-math at all sizes — matching NumPy's libm-based
26/// path. Accuracy is libm's standard ~1 ULP, well within the 256-ULP
27/// tolerance bands used by ferray's statistical equivalence harness.
28/// For correctly-rounded results call `cr_math::CrMath::cr_sin` directly.
29pub fn sin<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
30where
31    T: Element + Float,
32    D: Dimension,
33{
34    unary_float_op_compute(input, T::sin)
35}
36
37/// In-place sine — `_into` counterpart of [`sin`].
38pub fn sin_into<T, D>(input: &Array<T, D>, out: &mut Array<T, D>) -> FerrayResult<()>
39where
40    T: Element + Float,
41    D: Dimension,
42{
43    unary_float_op_into_compute(input, out, "sin", T::sin)
44}
45
46/// Elementwise cosine. See [`sin`] for the libm-vs-core-math accuracy note.
47pub fn cos<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
48where
49    T: Element + Float,
50    D: Dimension,
51{
52    unary_float_op_compute(input, T::cos)
53}
54
55/// In-place cosine — `_into` counterpart of [`cos`].
56pub fn cos_into<T, D>(input: &Array<T, D>, out: &mut Array<T, D>) -> FerrayResult<()>
57where
58    T: Element + Float,
59    D: Dimension,
60{
61    unary_float_op_into_compute(input, out, "cos", T::cos)
62}
63
64/// Elementwise tangent. See [`sin`] for the libm-vs-core-math accuracy note.
65pub fn tan<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
66where
67    T: Element + Float,
68    D: Dimension,
69{
70    unary_float_op_compute(input, T::tan)
71}
72
73/// Elementwise arc sine.
74pub fn arcsin<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
75where
76    T: Element + Float + CrMath,
77    D: Dimension,
78{
79    unary_float_op_compute(input, T::cr_asin)
80}
81
82/// Elementwise arc cosine.
83pub fn arccos<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
84where
85    T: Element + Float + CrMath,
86    D: Dimension,
87{
88    unary_float_op_compute(input, T::cr_acos)
89}
90
91/// Elementwise arc tangent.
92pub fn arctan<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
93where
94    T: Element + Float + CrMath,
95    D: Dimension,
96{
97    unary_float_op_compute(input, T::cr_atan)
98}
99
100/// Elementwise two-argument arc tangent (atan2).
101pub fn arctan2<T, D>(y: &Array<T, D>, x: &Array<T, D>) -> FerrayResult<Array<T, D>>
102where
103    T: Element + Float + CrMath,
104    D: Dimension,
105{
106    binary_elementwise_op(y, x, T::cr_atan2)
107}
108
109/// Elementwise hypotenuse: sqrt(a^2 + b^2).
110pub fn hypot<T, D>(a: &Array<T, D>, b: &Array<T, D>) -> FerrayResult<Array<T, D>>
111where
112    T: Element + Float + CrMath,
113    D: Dimension,
114{
115    binary_elementwise_op(a, b, T::cr_hypot)
116}
117
118// ---------------------------------------------------------------------------
119// Hyperbolic
120// ---------------------------------------------------------------------------
121
122/// Elementwise hyperbolic sine.
123pub fn sinh<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
124where
125    T: Element + Float + CrMath,
126    D: Dimension,
127{
128    unary_float_op_compute(input, T::cr_sinh)
129}
130
131/// Elementwise hyperbolic cosine.
132pub fn cosh<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
133where
134    T: Element + Float + CrMath,
135    D: Dimension,
136{
137    unary_float_op_compute(input, T::cr_cosh)
138}
139
140/// Elementwise hyperbolic tangent.
141pub fn tanh<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
142where
143    T: Element + Float + CrMath,
144    D: Dimension,
145{
146    unary_float_op_compute(input, T::cr_tanh)
147}
148
149/// Elementwise inverse hyperbolic sine.
150pub fn arcsinh<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
151where
152    T: Element + Float + CrMath,
153    D: Dimension,
154{
155    unary_float_op_compute(input, T::cr_asinh)
156}
157
158/// Elementwise inverse hyperbolic cosine.
159pub fn arccosh<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
160where
161    T: Element + Float + CrMath,
162    D: Dimension,
163{
164    unary_float_op_compute(input, T::cr_acosh)
165}
166
167/// Elementwise inverse hyperbolic tangent.
168pub fn arctanh<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
169where
170    T: Element + Float + CrMath,
171    D: Dimension,
172{
173    unary_float_op_compute(input, T::cr_atanh)
174}
175
176// ---------------------------------------------------------------------------
177// Degree/radian conversion
178// ---------------------------------------------------------------------------
179
180/// Convert radians to degrees.
181pub fn degrees<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
182where
183    T: Element + Float,
184    D: Dimension,
185{
186    unary_float_op(input, T::to_degrees)
187}
188
189/// Convert degrees to radians.
190pub fn radians<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
191where
192    T: Element + Float,
193    D: Dimension,
194{
195    unary_float_op(input, T::to_radians)
196}
197
198/// Alias for [`radians`].
199pub fn deg2rad<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
200where
201    T: Element + Float,
202    D: Dimension,
203{
204    radians(input)
205}
206
207/// Alias for [`degrees`].
208pub fn rad2deg<T, D>(input: &Array<T, D>) -> FerrayResult<Array<T, D>>
209where
210    T: Element + Float,
211    D: Dimension,
212{
213    degrees(input)
214}
215
216/// Unwrap by changing deltas between values to their 2*pi complement.
217///
218/// Works on 1-D arrays. `discont` defaults to pi if `None`.
219pub fn unwrap<T, D>(input: &Array<T, D>, discont: Option<T>) -> FerrayResult<Array<T, D>>
220where
221    T: Element + Float,
222    D: Dimension,
223{
224    let pi = T::from(std::f64::consts::PI).unwrap_or_else(<T as Element>::zero);
225    let two_pi = pi + pi;
226    let discont = discont.unwrap_or(pi);
227
228    let data: Vec<T> = input.iter().copied().collect();
229    if data.is_empty() {
230        return Array::from_vec(input.dim().clone(), data);
231    }
232
233    let mut result = Vec::with_capacity(data.len());
234    result.push(data[0]);
235    let mut cumulative = <T as Element>::zero();
236
237    for i in 1..data.len() {
238        let mut diff = data[i] - data[i - 1];
239        if diff > discont || diff < -discont {
240            diff = diff - two_pi * ((diff + pi) / two_pi).floor();
241        }
242        cumulative = cumulative + diff - (data[i] - data[i - 1]);
243        result.push(data[i] + cumulative);
244    }
245
246    Array::from_vec(input.dim().clone(), result)
247}
248
249// ---------------------------------------------------------------------------
250// f16 variants (f32-promoted) — declared via the shared `unary_f16_fn!`
251// and `binary_f16_fn!` macros so each entry point is a single line. The
252// prior hand-written pattern (6 lines × 15 functions ≈ 90 lines of
253// boilerplate) is gone (#142).
254// ---------------------------------------------------------------------------
255
256use crate::helpers::{binary_f16_fn, unary_f16_fn};
257
258unary_f16_fn!(
259    /// Elementwise sine for f16 arrays via f32 promotion.
260    #[cfg(feature = "f16")]
261    sin_f16,
262    f32::sin
263);
264unary_f16_fn!(
265    /// Elementwise cosine for f16 arrays via f32 promotion.
266    #[cfg(feature = "f16")]
267    cos_f16,
268    f32::cos
269);
270unary_f16_fn!(
271    /// Elementwise tangent for f16 arrays via f32 promotion.
272    #[cfg(feature = "f16")]
273    tan_f16,
274    f32::tan
275);
276unary_f16_fn!(
277    /// Elementwise arc sine for f16 arrays via f32 promotion.
278    #[cfg(feature = "f16")]
279    arcsin_f16,
280    f32::asin
281);
282unary_f16_fn!(
283    /// Elementwise arc cosine for f16 arrays via f32 promotion.
284    #[cfg(feature = "f16")]
285    arccos_f16,
286    f32::acos
287);
288unary_f16_fn!(
289    /// Elementwise arc tangent for f16 arrays via f32 promotion.
290    #[cfg(feature = "f16")]
291    arctan_f16,
292    f32::atan
293);
294binary_f16_fn!(
295    /// Elementwise two-argument arc tangent for f16 arrays via f32 promotion.
296    #[cfg(feature = "f16")]
297    arctan2_f16,
298    f32::atan2
299);
300binary_f16_fn!(
301    /// Elementwise hypotenuse for f16 arrays via f32 promotion.
302    #[cfg(feature = "f16")]
303    hypot_f16,
304    f32::hypot
305);
306unary_f16_fn!(
307    /// Elementwise hyperbolic sine for f16 arrays via f32 promotion.
308    #[cfg(feature = "f16")]
309    sinh_f16,
310    f32::sinh
311);
312unary_f16_fn!(
313    /// Elementwise hyperbolic cosine for f16 arrays via f32 promotion.
314    #[cfg(feature = "f16")]
315    cosh_f16,
316    f32::cosh
317);
318unary_f16_fn!(
319    /// Elementwise hyperbolic tangent for f16 arrays via f32 promotion.
320    #[cfg(feature = "f16")]
321    tanh_f16,
322    f32::tanh
323);
324unary_f16_fn!(
325    /// Elementwise inverse hyperbolic sine for f16 arrays via f32 promotion.
326    #[cfg(feature = "f16")]
327    arcsinh_f16,
328    f32::asinh
329);
330unary_f16_fn!(
331    /// Elementwise inverse hyperbolic cosine for f16 arrays via f32 promotion.
332    #[cfg(feature = "f16")]
333    arccosh_f16,
334    f32::acosh
335);
336unary_f16_fn!(
337    /// Elementwise inverse hyperbolic tangent for f16 arrays via f32 promotion.
338    #[cfg(feature = "f16")]
339    arctanh_f16,
340    f32::atanh
341);
342unary_f16_fn!(
343    /// Convert radians to degrees for f16 arrays via f32 promotion.
344    #[cfg(feature = "f16")]
345    degrees_f16,
346    f32::to_degrees
347);
348unary_f16_fn!(
349    /// Convert degrees to radians for f16 arrays via f32 promotion.
350    #[cfg(feature = "f16")]
351    radians_f16,
352    f32::to_radians
353);
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    use crate::test_util::arr1;
360
361    #[test]
362    fn test_sin() {
363        let a = arr1(vec![0.0, std::f64::consts::FRAC_PI_2, std::f64::consts::PI]);
364        let r = sin(&a).unwrap();
365        let s = r.as_slice().unwrap();
366        assert!((s[0]).abs() < 1e-12);
367        assert!((s[1] - 1.0).abs() < 1e-12);
368        assert!((s[2]).abs() < 1e-12);
369    }
370
371    #[test]
372    fn test_cos() {
373        let a = arr1(vec![0.0, std::f64::consts::PI]);
374        let r = cos(&a).unwrap();
375        let s = r.as_slice().unwrap();
376        assert!((s[0] - 1.0).abs() < 1e-12);
377        assert!((s[1] + 1.0).abs() < 1e-12);
378    }
379
380    #[test]
381    fn test_tan() {
382        let a = arr1(vec![0.0, std::f64::consts::FRAC_PI_4]);
383        let r = tan(&a).unwrap();
384        let s = r.as_slice().unwrap();
385        assert!((s[0]).abs() < 1e-12);
386        assert!((s[1] - 1.0).abs() < 1e-12);
387    }
388
389    #[test]
390    fn test_arcsin() {
391        let a = arr1(vec![0.0, 1.0]);
392        let r = arcsin(&a).unwrap();
393        let s = r.as_slice().unwrap();
394        assert!((s[0]).abs() < 1e-12);
395        assert!((s[1] - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
396    }
397
398    #[test]
399    fn test_arctan2() {
400        let y = arr1(vec![0.0, 1.0]);
401        let x = arr1(vec![1.0, 0.0]);
402        let r = arctan2(&y, &x).unwrap();
403        let s = r.as_slice().unwrap();
404        assert!((s[0]).abs() < 1e-12);
405        assert!((s[1] - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
406    }
407
408    #[test]
409    fn test_hypot() {
410        let a = arr1(vec![3.0, 5.0]);
411        let b = arr1(vec![4.0, 12.0]);
412        let r = hypot(&a, &b).unwrap();
413        let s = r.as_slice().unwrap();
414        assert!((s[0] - 5.0).abs() < 1e-12);
415        assert!((s[1] - 13.0).abs() < 1e-12);
416    }
417
418    #[test]
419    fn test_sinh_cosh_tanh() {
420        let a = arr1(vec![0.0]);
421        assert!((sinh(&a).unwrap().as_slice().unwrap()[0]).abs() < 1e-12);
422        assert!((cosh(&a).unwrap().as_slice().unwrap()[0] - 1.0).abs() < 1e-12);
423        assert!((tanh(&a).unwrap().as_slice().unwrap()[0]).abs() < 1e-12);
424    }
425
426    #[test]
427    fn test_degrees_radians() {
428        let a = arr1(vec![std::f64::consts::PI]);
429        let deg = degrees(&a).unwrap();
430        assert!((deg.as_slice().unwrap()[0] - 180.0).abs() < 1e-10);
431
432        let back = radians(&deg).unwrap();
433        assert!((back.as_slice().unwrap()[0] - std::f64::consts::PI).abs() < 1e-12);
434    }
435
436    #[test]
437    fn test_deg2rad_rad2deg() {
438        let a = arr1(vec![180.0]);
439        let r = deg2rad(&a).unwrap();
440        assert!((r.as_slice().unwrap()[0] - std::f64::consts::PI).abs() < 1e-12);
441
442        let d = rad2deg(&arr1(vec![std::f64::consts::PI])).unwrap();
443        assert!((d.as_slice().unwrap()[0] - 180.0).abs() < 1e-10);
444    }
445
446    #[test]
447    fn test_unwrap_basic() {
448        let a = arr1(vec![0.0, 0.5, 1.0, -0.5, -1.0]);
449        let r = unwrap(&a, None).unwrap();
450        // No discontinuity larger than pi, so should be unchanged
451        let s = r.as_slice().unwrap();
452        for (i, &v) in s.iter().enumerate() {
453            assert!((v - a.as_slice().unwrap()[i]).abs() < 1e-12);
454        }
455    }
456
457    #[test]
458    fn test_arcsinh_arccosh_arctanh() {
459        let a = arr1(vec![0.0]);
460        assert!((arcsinh(&a).unwrap().as_slice().unwrap()[0]).abs() < 1e-12);
461
462        let b = arr1(vec![1.0]);
463        assert!((arccosh(&b).unwrap().as_slice().unwrap()[0]).abs() < 1e-12);
464        assert!((arctanh(&a).unwrap().as_slice().unwrap()[0]).abs() < 1e-12);
465    }
466
467    #[cfg(feature = "f16")]
468    mod f16_tests {
469        use super::*;
470        use ferray_core::dimension::Ix1;
471
472        fn arr1_f16(data: &[f32]) -> Array<half::f16, Ix1> {
473            let n = data.len();
474            let vals: Vec<half::f16> = data.iter().map(|&x| half::f16::from_f32(x)).collect();
475            Array::from_vec(Ix1::new([n]), vals).unwrap()
476        }
477
478        #[test]
479        fn test_sin_f16() {
480            let a = arr1_f16(&[0.0, std::f32::consts::FRAC_PI_2, std::f32::consts::PI]);
481            let r = sin_f16(&a).unwrap();
482            let s = r.as_slice().unwrap();
483            assert!(s[0].to_f32().abs() < 0.01);
484            assert!((s[1].to_f32() - 1.0).abs() < 0.01);
485            assert!(s[2].to_f32().abs() < 0.01);
486        }
487
488        #[test]
489        fn test_cos_f16() {
490            let a = arr1_f16(&[0.0, std::f32::consts::PI]);
491            let r = cos_f16(&a).unwrap();
492            let s = r.as_slice().unwrap();
493            assert!((s[0].to_f32() - 1.0).abs() < 0.01);
494            assert!((s[1].to_f32() + 1.0).abs() < 0.01);
495        }
496
497        #[test]
498        fn test_tan_f16() {
499            let a = arr1_f16(&[0.0, std::f32::consts::FRAC_PI_4]);
500            let r = tan_f16(&a).unwrap();
501            let s = r.as_slice().unwrap();
502            assert!(s[0].to_f32().abs() < 0.01);
503            assert!((s[1].to_f32() - 1.0).abs() < 0.01);
504        }
505
506        #[test]
507        fn test_arctan2_f16() {
508            let y = arr1_f16(&[0.0, 1.0]);
509            let x = arr1_f16(&[1.0, 0.0]);
510            let r = arctan2_f16(&y, &x).unwrap();
511            let s = r.as_slice().unwrap();
512            assert!(s[0].to_f32().abs() < 0.01);
513            assert!((s[1].to_f32() - std::f32::consts::FRAC_PI_2).abs() < 0.01);
514        }
515
516        #[test]
517        fn test_degrees_radians_f16() {
518            let a = arr1_f16(&[std::f32::consts::PI]);
519            let deg = degrees_f16(&a).unwrap();
520            assert!((deg.as_slice().unwrap()[0].to_f32() - 180.0).abs() < 0.5);
521
522            let back = radians_f16(&deg).unwrap();
523            assert!((back.as_slice().unwrap()[0].to_f32() - std::f32::consts::PI).abs() < 0.01);
524        }
525    }
526}