fixed_analytics/ops/
circular.rs

1//! Trigonometric functions via circular CORDIC.
2
3use crate::error::{Error, Result};
4use crate::kernel::{circular_rotation, circular_vectoring, cordic_scale_factor};
5use crate::traits::CordicNumber;
6
7/// Sine and cosine. More efficient than separate calls. Accepts any angle.
8#[must_use]
9pub fn sin_cos<T: CordicNumber>(angle: T) -> (T, T) {
10    let pi = T::pi();
11    let frac_pi_2 = T::frac_pi_2();
12    let zero = T::zero();
13
14    // Reduce angle to [-π, π] range first.
15    let mut reduced = angle;
16    let two_pi = pi + pi;
17    let mut i = 0;
18    while reduced > pi && i < 64 {
19        reduced -= two_pi;
20        i += 1;
21    }
22    i = 0;
23    while reduced < -pi && i < 64 {
24        reduced += two_pi;
25        i += 1;
26    }
27
28    // Further reduce to [-π/2, π/2] and track sign
29    let (reduced, negate) = if reduced > frac_pi_2 {
30        (reduced - pi, true)
31    } else if reduced < -frac_pi_2 {
32        (reduced + pi, true)
33    } else {
34        (reduced, false)
35    };
36
37    // Run CORDIC with unit vector scaled by inverse gain
38    let inv_gain = cordic_scale_factor();
39    let (cos_val, sin_val, _) = circular_rotation(inv_gain, zero, reduced);
40
41    if negate {
42        (-sin_val, -cos_val)
43    } else {
44        (sin_val, cos_val)
45    }
46}
47
48/// Sine. Accepts any angle (reduced internally).
49#[inline]
50#[must_use]
51pub fn sin<T: CordicNumber>(angle: T) -> T {
52    sin_cos(angle).0
53}
54
55/// Cosine. Accepts any angle (reduced internally).
56#[inline]
57#[must_use]
58pub fn cos<T: CordicNumber>(angle: T) -> T {
59    sin_cos(angle).1
60}
61
62/// Tangent. May overflow near ±π/2.
63#[must_use]
64pub fn tan<T: CordicNumber>(angle: T) -> T {
65    let (s, c) = sin_cos(angle);
66    s.div(c)
67}
68
69/// Arcsine. Domain: `[-1, 1]`. Returns angle in `[-π/2, π/2]`.
70///
71/// # Errors
72/// Returns `DomainError` if `|x| > 1`.
73///
74/// # Panics
75/// Panics if the internal sqrt computation fails, which should never happen
76/// for valid domain inputs as 1-x² is always non-negative when |x| ≤ 1.
77#[must_use = "returns the arcsine result which should be handled"]
78#[allow(clippy::missing_panics_doc)] // Panic only on internal invariant violation
79pub fn asin<T: CordicNumber>(x: T) -> Result<T> {
80    let one = T::one();
81    let neg_one = -one;
82
83    if x > one || x < neg_one {
84        return Err(Error::domain("asin", "value in range [-1, 1]"));
85    }
86
87    // Special cases
88    if x == one {
89        return Ok(T::frac_pi_2());
90    }
91    if x == neg_one {
92        return Ok(-T::frac_pi_2());
93    }
94    if x == T::zero() {
95        return Ok(T::zero());
96    }
97
98    // Use the identity: asin(x) = atan(x / sqrt(1 - x²))
99    // This gives better accuracy than iterative methods
100    let x_sq = x.saturating_mul(x);
101    let one_minus_x_sq = one.saturating_sub(x_sq);
102    // SAFETY: x ∈ [-1,1] is enforced above, so 1-x² ∈ [0,1], sqrt cannot fail.
103    #[allow(clippy::expect_used)] // Invariant: x in [-1,1] guarantees valid sqrt input
104    let sqrt_term =
105        crate::ops::algebraic::sqrt(one_minus_x_sq).expect("x in [-1,1] guarantees 1-x² ≥ 0");
106
107    // Handle case where sqrt_term is very small (x close to ±1)
108    if sqrt_term < T::from_i1f63(0x0001_0000_0000_0000) {
109        // Very close to ±1, return ±π/2
110        return if x.is_positive() {
111            Ok(T::frac_pi_2())
112        } else {
113            Ok(-T::frac_pi_2())
114        };
115    }
116
117    Ok(atan(x.div(sqrt_term)))
118}
119
120/// Arccosine. Domain: `[-1, 1]`. Returns angle in `[0, π]`.
121///
122/// # Errors
123/// Returns `DomainError` if `|x| > 1`.
124#[must_use = "returns the arccosine result which should be handled"]
125pub fn acos<T: CordicNumber>(x: T) -> Result<T> {
126    // acos(x) = π/2 - asin(x)
127    asin(x).map(|a| T::frac_pi_2() - a)
128}
129
130/// Arctangent. Accepts any value. Returns angle in `(-π/2, π/2)`.
131#[must_use]
132pub fn atan<T: CordicNumber>(x: T) -> T {
133    let zero = T::zero();
134    let one = T::one();
135
136    // Special cases
137    if x == zero {
138        return zero;
139    }
140
141    // For |x| > 1, use atan(x) = sign(x) * π/2 - atan(1/x)
142    // This keeps the argument in the convergent range
143    let abs_x = x.abs();
144    if abs_x > one {
145        let recip = one.div(x);
146        let atan_recip = circular_vectoring(one, recip, zero).2;
147
148        if x.is_positive() {
149            T::frac_pi_2() - atan_recip
150        } else {
151            -T::frac_pi_2() - atan_recip
152        }
153    } else {
154        // |x| <= 1, use CORDIC directly
155        circular_vectoring(one, x, zero).2
156    }
157}
158
159/// Four-quadrant arctangent. Returns angle in `[-π, π]`. Returns 0 for (0, 0).
160#[must_use]
161pub fn atan2<T: CordicNumber>(y: T, x: T) -> T {
162    let zero = T::zero();
163    let pi = T::pi();
164    let frac_pi_2 = T::frac_pi_2();
165
166    // Handle special cases
167    if x == zero {
168        return if y.is_negative() {
169            -frac_pi_2
170        } else if y == zero {
171            zero // Undefined, but return 0
172        } else {
173            frac_pi_2
174        };
175    }
176
177    if y == zero {
178        return if x.is_negative() { pi } else { zero };
179    }
180
181    // Compute atan(|y|/|x|) using CORDIC vectoring mode
182    // Using absolute values ensures the base angle is always positive
183    let (_, _, base_angle) = circular_vectoring(x.abs(), y.abs(), zero);
184
185    // Adjust for quadrant based on signs of original x and y
186    match (x.is_negative(), y.is_negative()) {
187        // Q1: x positive, y positive -> angle is base_angle
188        (false, false) => base_angle,
189        // Q4: x positive, y negative -> angle is -base_angle
190        (false, true) => -base_angle,
191        // Q2: x negative, y positive -> angle is π - base_angle
192        (true, false) => pi - base_angle,
193        // Q3: x negative, y negative -> angle is -(π - base_angle) = base_angle - π
194        (true, true) => base_angle - pi,
195    }
196}