Skip to main content

oxinum_float/native/
atan.rs

1//! `atan` and `atan2` for native `BigFloat`.
2//!
3//! # Algorithm for `atan(x)`
4//!
5//! 1. **Special cases:** `atan(0) = 0`.
6//!
7//! 2. **Range reduction for `|x| > 1`:** `atan(x) = sign(x) · π/2 − atan(1/|x|)`.
8//!    After this step, the input satisfies `|x| ≤ 1`.
9//!
10//! 3. **Half-angle acceleration:** apply
11//!    `atan(x) = 2 · atan(x / (1 + √(1 + x²)))` repeatedly until `|x| < 2^{−32}`.
12//!    Count the number of halvings `m`. This reduces to an exponentially small
13//!    argument for which the Taylor series converges in O(prec / 32) terms.
14//!
15//! 4. **Taylor series for small `u`:**
16//!    `atan(u) = u − u³/3 + u⁵/5 − …`  (convergent for `|u| < 1`).
17//!    Term count: with `|u| < 2^{−32}`, the `(2k+1)`-th term is bounded by
18//!    `|u|^(2k+1) / (2k+1) < 2^{−32(2k+1)} / (2k+1)`.  For `k = prec/64` this
19//!    is already below `2^{−prec}`, so `n_terms = prec / 64 + 8` suffices.
20//!
21//! 5. **Undo half-angle:** multiply result by `2^m`.
22//!
23//! 6. **Undo range reduction:** if `|x| > 1`, return `sign · (π/2 − result)`.
24//!
25//! # Algorithm for `atan2(y, x)`
26//!
27//! Standard four-quadrant table:
28//!
29//! | y | x  | result            |
30//! |---|----|-------------------|
31//! | 0 | 0  | 0 (by convention) |
32//! | * | >0 | atan(y/x)         |
33//! | ≥0| <0 | π + atan(y/x)     |
34//! | <0| <0 | −π + atan(y/x)    |
35//! | ≥0| 0  | +π/2              |
36//! | <0| 0  | −π/2              |
37
38use oxinum_core::{OxiNumError, OxiNumResult, Sign};
39
40use super::constants::pi;
41use super::float::{BigFloat, RoundingMode};
42
43// ---------------------------------------------------------------------------
44// Half-angle reduction threshold
45// ---------------------------------------------------------------------------
46
47/// Apply `atan(x) = 2·atan(x / (1 + sqrt(1 + x²)))` until `|x| < 2^{-32}`.
48/// Returns `(reduced_x, m)` where `m` is the number of halvings applied.
49fn half_angle_reduce(x: BigFloat, prec: u32, mode: RoundingMode) -> OxiNumResult<(BigFloat, u32)> {
50    let one = BigFloat::from_i64(1, prec, mode);
51    let mut u = x;
52    let mut m = 0u32;
53
54    // Threshold: |u| < 2^{-32} means exponent(mantissa) < -32 relative to 1.
55    // We check via to_f64: safe as long as |u| ≤ 1, which it is after the
56    // range-reduction step.
57    loop {
58        // f64 comparison is safe here since |u| ≤ 1.
59        let abs_f64 = u.abs().to_f64();
60        if abs_f64 < f64::from_bits(0x3e00_0000_0000_0000u64) {
61            // 2^{-31} — conservative threshold
62            break;
63        }
64        if m >= 300 {
65            // Safety cap — should never be reached for any sane precision.
66            break;
67        }
68        // u = u / (1 + sqrt(1 + u²))
69        let u_sq = u.mul_ref_with_mode(&u, mode).with_precision(prec, mode);
70        let one_plus_u_sq = one.add_ref_with_mode(&u_sq, mode);
71        let sqrt_val = one_plus_u_sq.sqrt(prec, mode)?;
72        let denom = one.add_ref_with_mode(&sqrt_val, mode);
73        u = u.div_ref_with_mode(&denom, mode)?;
74        u = u.with_precision(prec, mode);
75        m += 1;
76    }
77
78    Ok((u, m))
79}
80
81// ---------------------------------------------------------------------------
82// Taylor series for atan on small arguments
83// ---------------------------------------------------------------------------
84
85/// Compute `atan(u)` via Taylor series for `|u| < 2^{-31}`.
86///
87/// `atan(u) = u − u³/3 + u⁵/5 − …`
88///
89/// Term count: `prec / 64 + 8` suffices because each halving reduces by
90/// 32 bits, so `|u|^{2k+1}/(2k+1) < 2^{-32(2k+1)} / (2k+1) < 2^{-prec}`
91/// for `k ≥ prec/64`.
92fn atan_taylor(u: &BigFloat, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
93    if u.is_zero() {
94        return Ok(BigFloat::zero(prec));
95    }
96
97    let u_sq = u.mul_ref_with_mode(u, mode).with_precision(prec, mode);
98    // term = u; result = u
99    let mut term = u.clone().with_precision(prec, mode);
100    let mut result = term.clone();
101
102    let n_terms: u64 = (prec as u64) / 64 + 8;
103
104    for k in 1..=n_terms {
105        // term *= -u²;  then divide by (2k+1)
106        let numer = term
107            .mul_ref_with_mode(&u_sq, mode)
108            .with_precision(prec, mode);
109        term = numer.neg().with_precision(prec, mode);
110        let denom_val = 2 * k + 1;
111        let denom_i64 = denom_val.min(i64::MAX as u64) as i64;
112        let denom_f = BigFloat::from_i64(denom_i64, prec, mode);
113        let scaled = term
114            .div_ref_with_mode(&denom_f, mode)
115            .map_err(|e| OxiNumError::Precision(format!("atan_taylor denom zero: {e}").into()))?;
116        result = result
117            .add_ref_with_mode(&scaled, mode)
118            .with_precision(prec, mode);
119    }
120
121    Ok(result.with_precision(prec, mode))
122}
123
124// ---------------------------------------------------------------------------
125// Public BigFloat methods
126// ---------------------------------------------------------------------------
127
128impl BigFloat {
129    /// Return `atan(self)` at `prec` bits using the given rounding mode.
130    ///
131    /// The result lies in `(−π/2, π/2)`.
132    ///
133    /// # Errors
134    ///
135    /// Propagates sqrt or division errors (only if internal invariants break,
136    /// which should not happen for well-formed inputs).
137    ///
138    /// # Examples
139    ///
140    /// ```
141    /// use oxinum_float::native::{BigFloat, RoundingMode};
142    /// let one = BigFloat::from_i64(1, 64, RoundingMode::HalfEven);
143    /// let a = one.atan(64, RoundingMode::HalfEven).expect("atan(1)");
144    /// // atan(1) = π/4 ≈ 0.7853981633974483
145    /// assert!((a.to_f64() - std::f64::consts::FRAC_PI_4).abs() < 1e-14);
146    /// ```
147    pub fn atan(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
148        // IEEE-754 non-finite guards
149        if self.is_nan() {
150            return Ok(BigFloat::nan(prec));
151        }
152        if self.is_infinite() {
153            // atan(+Inf) = +π/2, atan(-Inf) = -π/2
154            let pi_val = pi(prec.saturating_add(16))?;
155            let two = BigFloat::from_i64(2, prec.saturating_add(16), mode);
156            let half_pi = pi_val.div_ref_with_mode(&two, mode)?;
157            return if self.sign == Sign::Negative {
158                Ok(half_pi.neg().with_precision(prec, mode))
159            } else {
160                Ok(half_pi.with_precision(prec, mode))
161            };
162        }
163
164        if self.is_zero() {
165            return Ok(BigFloat::zero(prec));
166        }
167
168        // Work at higher precision to absorb rounding errors.
169        let work_prec = prec.saturating_add(64);
170
171        // We work with |x| and track the sign separately.
172        let orig_sign = self.signum();
173        let abs_x = self.abs().with_precision(work_prec, mode);
174
175        let one = BigFloat::from_i64(1, work_prec, mode);
176
177        // Step 1: Range reduction — if |x| > 1, use atan(x) = π/2 − atan(1/x).
178        let (working_x, use_complement): (BigFloat, bool) = if abs_x > one.clone() {
179            let inv = one.div_ref_with_mode(&abs_x, mode)?;
180            (inv.with_precision(work_prec, mode), true)
181        } else {
182            (abs_x, false)
183        };
184
185        // Step 2: Half-angle acceleration.
186        let (reduced, m) = half_angle_reduce(working_x, work_prec, mode)?;
187
188        // Step 3: Taylor series for the small argument.
189        let mut result = atan_taylor(&reduced, work_prec, mode)?;
190
191        // Step 4: Undo half-angle by multiplying by 2^m.
192        let two = BigFloat::from_i64(2, work_prec, mode);
193        for _ in 0..m {
194            result = result
195                .mul_ref_with_mode(&two, mode)
196                .with_precision(work_prec, mode);
197        }
198
199        // Step 5: Undo range reduction if |x| > 1.
200        if use_complement {
201            let pi_val = pi(work_prec)?;
202            let two_wp = BigFloat::from_i64(2, work_prec, mode);
203            let pi_over_2 = pi_val.div_ref_with_mode(&two_wp, mode)?;
204            result = pi_over_2
205                .sub_ref_with_mode(&result, mode)
206                .with_precision(work_prec, mode);
207        }
208
209        // Step 6: Apply sign.
210        if orig_sign < 0 {
211            result = result.neg();
212        }
213
214        Ok(result.with_precision(prec, mode))
215    }
216
217    /// Return `atan2(self, x)` at `prec` bits using the given rounding mode.
218    ///
219    /// Conventionally `atan2(y, x)` — `self` is `y` and `x` is the argument.
220    /// The result lies in `(−π, π]`.
221    ///
222    /// # Special cases
223    ///
224    /// - `atan2(0, 0) = 0` (by convention, not mathematically defined).
225    /// - `atan2(y, 0) = ±π/2` depending on sign of `y`.
226    ///
227    /// # Errors
228    ///
229    /// Propagates errors from [`BigFloat::atan`].
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use oxinum_float::native::{BigFloat, RoundingMode};
235    /// let y = BigFloat::from_i64(1, 64, RoundingMode::HalfEven);
236    /// let x = BigFloat::from_i64(1, 64, RoundingMode::HalfEven);
237    /// let a = y.atan2(&x, 64, RoundingMode::HalfEven).expect("atan2(1,1)");
238    /// // atan2(1,1) = π/4 ≈ 0.7853981633974483
239    /// assert!((a.to_f64() - std::f64::consts::FRAC_PI_4).abs() < 1e-14);
240    /// ```
241    pub fn atan2(&self, x: &BigFloat, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
242        let y = self;
243
244        // IEEE-754 non-finite guards
245        // NaN propagates immediately.
246        if y.is_nan() || x.is_nan() {
247            return Ok(BigFloat::nan(prec));
248        }
249        // Both infinite: atan2(±Inf, ±Inf) — four-quadrant results per IEEE.
250        if y.is_infinite() && x.is_infinite() {
251            // atan2(+Inf,+Inf)=+π/4, atan2(+Inf,-Inf)=+3π/4
252            // atan2(-Inf,+Inf)=-π/4, atan2(-Inf,-Inf)=-3π/4
253            let pi_val = pi(prec.saturating_add(16))?;
254            let four = BigFloat::from_i64(4, prec.saturating_add(16), mode);
255            let pi_over_4 = pi_val
256                .div_ref_with_mode(&four, mode)?
257                .with_precision(prec, mode);
258            let three_pi_over_4 = {
259                let three = BigFloat::from_i64(3, prec, mode);
260                three
261                    .mul_ref_with_mode(&pi_over_4, mode)
262                    .with_precision(prec, mode)
263            };
264            let (mag, apply_neg) = if x.sign == Sign::Negative {
265                (three_pi_over_4, y.sign == Sign::Negative)
266            } else {
267                (pi_over_4, y.sign == Sign::Negative)
268            };
269            return Ok(if apply_neg { mag.neg() } else { mag });
270        }
271        // y is Inf, x is finite: atan2(+Inf, x) = +π/2, atan2(-Inf, x) = -π/2
272        if y.is_infinite() {
273            let pi_val = pi(prec.saturating_add(16))?;
274            let two = BigFloat::from_i64(2, prec.saturating_add(16), mode);
275            let half_pi = pi_val
276                .div_ref_with_mode(&two, mode)?
277                .with_precision(prec, mode);
278            return if y.sign == Sign::Negative {
279                Ok(half_pi.neg())
280            } else {
281                Ok(half_pi)
282            };
283        }
284        // x is Inf, y is finite: atan2(y, +Inf) = +0, atan2(y, -Inf) = ±π
285        if x.is_infinite() {
286            if x.sign == Sign::Positive {
287                return Ok(BigFloat::zero(prec));
288            } else {
289                // atan2(y, -Inf) = +π if y ≥ 0, -π if y < 0
290                let pi_val = pi(prec.saturating_add(16))?.with_precision(prec, mode);
291                return if y.sign == Sign::Negative {
292                    Ok(pi_val.neg())
293                } else {
294                    Ok(pi_val)
295                };
296            }
297        }
298
299        let y_sign = y.signum();
300        let x_sign = x.signum();
301
302        // Both zero: return 0 by convention.
303        if y.is_zero() && x.is_zero() {
304            return Ok(BigFloat::zero(prec));
305        }
306
307        // x = 0: result is ±π/2.
308        if x.is_zero() {
309            let pi_val = pi(prec.saturating_add(16))?;
310            let two = BigFloat::from_i64(2, prec.saturating_add(16), mode);
311            let pi_over_2 = pi_val.div_ref_with_mode(&two, mode)?;
312            return if y_sign >= 0 {
313                Ok(pi_over_2.with_precision(prec, mode))
314            } else {
315                Ok(pi_over_2.neg().with_precision(prec, mode))
316            };
317        }
318
319        // x > 0: atan(y/x).
320        if x_sign > 0 {
321            let work_prec = prec.saturating_add(16);
322            let ratio = y
323                .clone()
324                .with_precision(work_prec, mode)
325                .div_ref_with_mode(&x.clone().with_precision(work_prec, mode), mode)?;
326            return ratio.atan(prec, mode);
327        }
328
329        // x < 0: atan(y/x) ± π.
330        let work_prec = prec.saturating_add(32);
331        let ratio = y
332            .clone()
333            .with_precision(work_prec, mode)
334            .div_ref_with_mode(&x.clone().with_precision(work_prec, mode), mode)?;
335        let atan_val = ratio.atan(work_prec, mode)?;
336        let pi_val = pi(work_prec)?;
337
338        if y_sign >= 0 {
339            // y ≥ 0, x < 0: result in (π/2, π]
340            let result = pi_val.add_ref_with_mode(&atan_val, mode);
341            Ok(result.with_precision(prec, mode))
342        } else {
343            // y < 0, x < 0: result in (−π, −π/2)
344            let result = atan_val.sub_ref_with_mode(&pi_val, mode);
345            Ok(result.with_precision(prec, mode))
346        }
347    }
348}
349
350// ---------------------------------------------------------------------------
351// Internal tests
352// ---------------------------------------------------------------------------
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    fn mk(n: i64, prec: u32) -> BigFloat {
359        BigFloat::from_i64(n, prec, RoundingMode::HalfEven)
360    }
361
362    #[test]
363    fn atan_zero() {
364        let z = mk(0, 64);
365        let r = z.atan(64, RoundingMode::HalfEven).expect("atan(0)");
366        assert!(r.is_zero());
367    }
368
369    #[test]
370    fn atan_one_is_pi_over_4() {
371        let prec = 64u32;
372        let one = mk(1, prec);
373        let a = one.atan(prec, RoundingMode::HalfEven).expect("atan(1)");
374        assert!(
375            (a.to_f64() - std::f64::consts::FRAC_PI_4).abs() < 1e-14,
376            "got {}",
377            a.to_f64()
378        );
379    }
380
381    #[test]
382    fn atan_minus_one() {
383        let prec = 64u32;
384        let minus_one = mk(-1, prec);
385        let a = minus_one
386            .atan(prec, RoundingMode::HalfEven)
387            .expect("atan(-1)");
388        assert!((a.to_f64() + std::f64::consts::FRAC_PI_4).abs() < 1e-14);
389    }
390
391    #[test]
392    fn atan2_quadrant_i() {
393        let prec = 64u32;
394        let mode = RoundingMode::HalfEven;
395        let one = mk(1, prec);
396        let a = one.atan2(&one, prec, mode).expect("atan2(1,1)");
397        assert!((a.to_f64() - std::f64::consts::FRAC_PI_4).abs() < 1e-14);
398    }
399
400    #[test]
401    fn atan2_negative_x() {
402        let prec = 64u32;
403        let mode = RoundingMode::HalfEven;
404        let one = mk(1, prec);
405        let neg_one = mk(-1, prec);
406        // atan2(1, -1) = 3π/4
407        let a = one.atan2(&neg_one, prec, mode).expect("atan2(1,-1)");
408        let expected = 3.0 * std::f64::consts::FRAC_PI_4;
409        assert!(
410            (a.to_f64() - expected).abs() < 1e-13,
411            "got {}, expected {}",
412            a.to_f64(),
413            expected
414        );
415    }
416
417    #[test]
418    fn atan2_zero_x_positive_y() {
419        let prec = 64u32;
420        let mode = RoundingMode::HalfEven;
421        let one = mk(1, prec);
422        let zero = mk(0, prec);
423        let a = one.atan2(&zero, prec, mode).expect("atan2(1,0)");
424        assert!((a.to_f64() - std::f64::consts::FRAC_PI_2).abs() < 1e-14);
425    }
426
427    #[test]
428    fn atan2_zero_x_negative_y() {
429        let prec = 64u32;
430        let mode = RoundingMode::HalfEven;
431        let neg_one = mk(-1, prec);
432        let zero = mk(0, prec);
433        let a = neg_one.atan2(&zero, prec, mode).expect("atan2(-1,0)");
434        assert!((a.to_f64() + std::f64::consts::FRAC_PI_2).abs() < 1e-14);
435    }
436}