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