Skip to main content

oxinum_float/
trig.rs

1//! Trigonometric and hyperbolic functions for arbitrary-precision floats.
2//!
3//! Implementations use Taylor series with argument reduction for
4//! trig functions, and exp-based formulas for hyperbolic functions.
5//! All functions accept `DBig` (base-10) inputs and return results
6//! at the requested precision.
7
8use crate::constants::compute_pi;
9use crate::elementary::{convert_dbig_to_fbig, exp, fbig_to_dbig, truncate_to_precision};
10use crate::{DBig, OxiNumError, OxiNumResult};
11use std::str::FromStr;
12
13// ---------------------------------------------------------------------------
14// Trigonometric functions
15// ---------------------------------------------------------------------------
16
17/// Compute `sin(x)` using Taylor series with argument reduction.
18///
19/// # Examples
20///
21/// ```
22/// use oxinum_float::sin;
23/// use std::str::FromStr;
24/// let x = dashu_float::DBig::from_str("0.0").unwrap();
25/// let result = sin(&x, 30).unwrap();
26/// let s = result.to_string();
27/// assert!(s.starts_with("0"), "sin(0) = {}", s);
28/// ```
29pub fn sin(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
30    validate_precision(precision)?;
31    let zero = make_dbig("0.0")?;
32    if *x == zero {
33        return Ok(zero);
34    }
35    // Use extra guard digits for intermediate computations
36    let guard_prec = precision + 10;
37    let reduced = reduce_argument(x, guard_prec);
38    let result = sin_taylor(&reduced, guard_prec);
39    Ok(truncate_to_precision(result, precision))
40}
41
42/// Compute `cos(x)` using Taylor series with argument reduction.
43///
44/// # Examples
45///
46/// ```
47/// use oxinum_float::cos;
48/// use std::str::FromStr;
49/// let x = dashu_float::DBig::from_str("0.0").unwrap();
50/// let result = cos(&x, 30).unwrap();
51/// let s = result.to_string();
52/// assert!(s.starts_with("1"), "cos(0) = {}", s);
53/// ```
54pub fn cos(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
55    validate_precision(precision)?;
56    let zero = make_dbig("0.0")?;
57    if *x == zero {
58        return make_dbig("1.0");
59    }
60    let guard_prec = precision + 10;
61    let reduced = reduce_argument(x, guard_prec);
62    let result = cos_taylor(&reduced, guard_prec);
63    Ok(truncate_to_precision(result, precision))
64}
65
66/// Compute `tan(x) = sin(x) / cos(x)`.
67///
68/// # Errors
69///
70/// Returns `OxiNumError::DivByZero` if `cos(x) = 0`.
71pub fn tan(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
72    validate_precision(precision)?;
73    let guard_prec = precision + 10;
74    let s = sin(x, guard_prec)?;
75    let c = cos(x, guard_prec)?;
76    let zero = make_dbig("0.0")?;
77    if c == zero {
78        return Err(OxiNumError::DivByZero);
79    }
80    let result = &s / &c;
81    Ok(truncate_to_precision(result, precision))
82}
83
84/// Compute `atan(x)` using dashu-float's binary conversion and Taylor
85/// approach internally. For |x| <= 0.5 we use the series directly;
86/// for larger |x| <= 1 we use the half-angle formula; for |x| > 1 we
87/// use the identity `atan(x) = pi/2 - atan(1/x)`.
88pub fn atan(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
89    validate_precision(precision)?;
90    let zero = make_dbig("0.0")?;
91    if *x == zero {
92        return Ok(zero);
93    }
94
95    let guard_prec = precision + 15;
96    let one = make_dbig("1.0")?;
97    let neg_one = make_dbig("-1.0")?;
98
99    let is_negative = *x < zero;
100    let abs_x = if is_negative { x * &neg_one } else { x.clone() };
101
102    let result = if abs_x > one {
103        // atan(x) = pi/2 - atan(1/x) for x > 0
104        let pi = compute_pi(guard_prec);
105        let half_pi = &pi / &make_dbig("2.0")?;
106        let recip = &one / &abs_x;
107        let atan_recip = atan_small(&recip, guard_prec);
108        &half_pi - &atan_recip
109    } else {
110        atan_small(&abs_x, guard_prec)
111    };
112
113    let final_result = if is_negative {
114        &result * &neg_one
115    } else {
116        result
117    };
118    Ok(truncate_to_precision(final_result, precision))
119}
120
121/// Compute `atan2(y, x)` with proper quadrant handling.
122///
123/// Returns the angle in radians between the positive x-axis and the
124/// point (x, y), in the range (-pi, pi].
125pub fn atan2(y: &DBig, x: &DBig, precision: usize) -> OxiNumResult<DBig> {
126    validate_precision(precision)?;
127    let zero = make_dbig("0.0")?;
128    let guard_prec = precision + 10;
129    let pi = compute_pi(guard_prec);
130
131    if *x > zero {
132        let ratio = y / x;
133        atan(&ratio, precision)
134    } else if *x < zero && *y >= zero {
135        let ratio = y / x;
136        let a = atan(&ratio, guard_prec)?;
137        Ok(truncate_to_precision(&a + &pi, precision))
138    } else if *x < zero && *y < zero {
139        let ratio = y / x;
140        let a = atan(&ratio, guard_prec)?;
141        Ok(truncate_to_precision(&a - &pi, precision))
142    } else if *x == zero && *y > zero {
143        let half_pi = &pi / &make_dbig("2.0")?;
144        Ok(truncate_to_precision(half_pi, precision))
145    } else if *x == zero && *y < zero {
146        let neg_half_pi = &(&pi / &make_dbig("2.0")?) * &make_dbig("-1.0")?;
147        Ok(truncate_to_precision(neg_half_pi, precision))
148    } else {
149        // x == 0 && y == 0 -- undefined, return 0
150        Ok(zero)
151    }
152}
153
154// ---------------------------------------------------------------------------
155// Hyperbolic functions
156// ---------------------------------------------------------------------------
157
158/// Compute `sinh(x) = (e^x - e^(-x)) / 2`.
159pub fn sinh(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
160    validate_precision(precision)?;
161    let guard_prec = precision + 10;
162    let exp_x = exp(x, guard_prec)?;
163    let neg_x = x * &make_dbig("-1.0")?;
164    let exp_neg_x = exp(&neg_x, guard_prec)?;
165    let diff = &exp_x - &exp_neg_x;
166    let result = &diff / &make_dbig("2.0")?;
167    Ok(truncate_to_precision(result, precision))
168}
169
170/// Compute `cosh(x) = (e^x + e^(-x)) / 2`.
171pub fn cosh(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
172    validate_precision(precision)?;
173    let guard_prec = precision + 10;
174    let exp_x = exp(x, guard_prec)?;
175    let neg_x = x * &make_dbig("-1.0")?;
176    let exp_neg_x = exp(&neg_x, guard_prec)?;
177    let sum = &exp_x + &exp_neg_x;
178    let result = &sum / &make_dbig("2.0")?;
179    Ok(truncate_to_precision(result, precision))
180}
181
182/// Compute `tanh(x) = sinh(x) / cosh(x)`.
183pub fn tanh(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
184    validate_precision(precision)?;
185    let guard_prec = precision + 10;
186    let s = sinh(x, guard_prec)?;
187    let c = cosh(x, guard_prec)?;
188    let result = &s / &c;
189    Ok(truncate_to_precision(result, precision))
190}
191
192// ---------------------------------------------------------------------------
193// Internal: Taylor series
194// ---------------------------------------------------------------------------
195
196/// Compute sin(x) via Taylor series: sum_{k=0}^{inf} (-1)^k * x^{2k+1} / (2k+1)!
197///
198/// Assumes |x| is small (after argument reduction). All arithmetic is
199/// performed at `precision` significant digits to avoid precision loss.
200fn sin_taylor(x: &DBig, precision: usize) -> DBig {
201    let x = extend_precision(x, precision);
202    let mut term = x.clone();
203    let mut sum = x.clone();
204    let x_sq = &x * &x;
205    let neg_one = dbig_at_precision("-1.0", precision);
206
207    for k in 1..=(precision as u32 + 20) {
208        let denom_a = dbig_u32_at_precision(2 * k, precision);
209        let denom_b = dbig_u32_at_precision(2 * k + 1, precision);
210        term = &term * &x_sq;
211        term = &term / &denom_a;
212        term = &term / &denom_b;
213        term = &term * &neg_one;
214        sum = &sum + &term;
215
216        if is_negligible(&term, precision) {
217            break;
218        }
219    }
220    sum
221}
222
223/// Compute cos(x) via Taylor series: sum_{k=0}^{inf} (-1)^k * x^{2k} / (2k)!
224///
225/// Assumes |x| is small (after argument reduction). All arithmetic is
226/// performed at `precision` significant digits to avoid precision loss.
227fn cos_taylor(x: &DBig, precision: usize) -> DBig {
228    let x = extend_precision(x, precision);
229    let one = dbig_at_precision("1.0", precision);
230    let mut term = one.clone();
231    let mut sum = one;
232    let x_sq = &x * &x;
233    let neg_one = dbig_at_precision("-1.0", precision);
234
235    for k in 1..=(precision as u32 + 20) {
236        let denom_a = dbig_u32_at_precision(2 * k - 1, precision);
237        let denom_b = dbig_u32_at_precision(2 * k, precision);
238        term = &term * &x_sq;
239        term = &term / &denom_a;
240        term = &term / &denom_b;
241        term = &term * &neg_one;
242        sum = &sum + &term;
243
244        if is_negligible(&term, precision) {
245            break;
246        }
247    }
248    sum
249}
250
251/// Compute atan(x) for |x| <= 1 using argument reduction + Taylor series.
252///
253/// For faster convergence when x is close to 1, we use the identity:
254///   atan(x) = 2 * atan(x / (1 + sqrt(1 + x^2)))
255/// which reduces the argument closer to 0.
256fn atan_small(x: &DBig, precision: usize) -> DBig {
257    // Reduce argument: apply halving a few times for faster convergence
258    let threshold = make_dbig("0.5").expect("valid literal");
259    let one = make_dbig("1.0").expect("valid literal");
260
261    let mut reduced = x.clone();
262    let mut halvings = 0u32;
263
264    while reduced > threshold {
265        // atan(x) = 2 * atan(x / (1 + sqrt(1 + x^2)))
266        let x_sq = &reduced * &reduced;
267        let inner = &one + &x_sq;
268        // Use dashu's sqrt via binary conversion
269        let guard_bits = precision * 4 + 20;
270        let fbig = convert_dbig_to_fbig(&inner, guard_bits);
271        let sqrt_val = dashu_base::SquareRoot::sqrt(&fbig);
272        let sqrt_dbig = fbig_to_dbig(&sqrt_val, precision);
273        reduced = &reduced / &(&one + &sqrt_dbig);
274        halvings += 1;
275
276        if halvings > 50 {
277            break; // safety limit
278        }
279    }
280
281    let mut result = atan_taylor_core(&reduced, precision);
282
283    // Undo halvings: multiply by 2^halvings
284    for _ in 0..halvings {
285        result = &result * &make_dbig("2.0").expect("valid literal");
286    }
287
288    result
289}
290
291/// Raw Taylor series for atan(x), valid for |x| < 1:
292///
293/// atan(x) = x - x^3/3 + x^5/5 - x^7/7 + ...
294///
295/// All arithmetic is performed at `precision` significant digits.
296fn atan_taylor_core(x: &DBig, precision: usize) -> DBig {
297    let x = extend_precision(x, precision);
298    let mut power = x.clone();
299    let mut sum = x.clone();
300    let x_sq = &x * &x;
301    let neg_one = dbig_at_precision("-1.0", precision);
302    let mut sign = neg_one.clone();
303
304    for k in 1..=(precision as u32 * 3 + 50) {
305        power = &power * &x_sq;
306        let denom = dbig_u32_at_precision(2 * k + 1, precision);
307        let contrib = &power / &denom;
308        sum = &sum + &(&contrib * &sign);
309        sign = &sign * &neg_one;
310
311        if is_negligible(&contrib, precision) {
312            break;
313        }
314    }
315    sum
316}
317
318// ---------------------------------------------------------------------------
319// Internal: argument reduction
320// ---------------------------------------------------------------------------
321
322/// Reduce `x` modulo 2*pi so the Taylor series converges faster.
323fn reduce_argument(x: &DBig, precision: usize) -> DBig {
324    let pi = compute_pi(precision.min(195) + 5);
325    let two = dbig_at_precision("2.0", precision + 5);
326    let two_pi = (&pi * &two).with_precision(precision + 5).value();
327    let zero = dbig_at_precision("0.0", precision + 5);
328
329    if two_pi == zero {
330        return x.clone();
331    }
332
333    // Compute x mod 2*pi
334    let mut reduced = extend_precision(x, precision + 5);
335    if reduced > zero {
336        while reduced > two_pi {
337            reduced = (&reduced - &two_pi).with_precision(precision + 5).value();
338        }
339    } else {
340        while reduced < zero {
341            reduced = (&reduced + &two_pi).with_precision(precision + 5).value();
342        }
343    }
344    reduced
345}
346
347/// Check if a term is negligible for the given precision.
348fn is_negligible(term: &DBig, precision: usize) -> bool {
349    let s = term.to_string();
350    let s = s.trim_start_matches('-');
351
352    if let Some(dot_pos) = s.find('.') {
353        let integer_part = &s[..dot_pos];
354        if integer_part == "0" {
355            let frac = &s[dot_pos + 1..];
356            let leading_zeros = frac.chars().take_while(|&c| c == '0').count();
357            return leading_zeros >= precision + 3;
358        }
359    }
360    s == "0" || s == "0.0"
361}
362
363// ---------------------------------------------------------------------------
364// Internal: helpers
365// ---------------------------------------------------------------------------
366
367/// Parse a `DBig` from a string literal. Returns `OxiNumResult`.
368fn make_dbig(s: &str) -> OxiNumResult<DBig> {
369    DBig::from_str(s).map_err(|e| OxiNumError::Parse(format!("{e}").into()))
370}
371
372/// Extend a `DBig` to carry at least `precision` significant digits, so
373/// subsequent arithmetic does not round to the input's narrow precision.
374fn extend_precision(value: &DBig, precision: usize) -> DBig {
375    value.clone().with_precision(precision).value()
376}
377
378/// Parse a literal and extend it to `precision` significant digits.
379fn dbig_at_precision(s: &str, precision: usize) -> DBig {
380    let v = DBig::from_str(s).expect("valid decimal literal");
381    v.with_precision(precision).value()
382}
383
384/// Create a `DBig` from a `u32` at `precision` significant digits.
385fn dbig_u32_at_precision(n: u32, precision: usize) -> DBig {
386    let v = DBig::from_str(&format!("{n}.0")).expect("integer.0 is always a valid decimal");
387    v.with_precision(precision).value()
388}
389
390fn validate_precision(precision: usize) -> OxiNumResult<()> {
391    if precision == 0 {
392        return Err(OxiNumError::Precision("precision must be > 0".into()));
393    }
394    Ok(())
395}
396
397// ---------------------------------------------------------------------------
398// Tests
399// ---------------------------------------------------------------------------
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    #[test]
406    fn sin_of_zero() {
407        let x = make_dbig("0.0").expect("ok");
408        let result = sin(&x, 30).expect("ok");
409        assert_eq!(result, make_dbig("0.0").expect("ok"));
410    }
411
412    #[test]
413    fn cos_of_zero() {
414        let result = cos(&make_dbig("0.0").expect("ok"), 30).expect("ok");
415        let s = result.to_string();
416        assert!(s.starts_with("1"), "cos(0) = {s}");
417    }
418
419    #[test]
420    fn sin_of_pi_is_near_zero() {
421        let pi = compute_pi(40);
422        let result = sin(&pi, 20).expect("ok");
423        let s = result.to_string();
424        let s_trimmed = s.trim_start_matches('-');
425        assert!(
426            s_trimmed.starts_with("0.000") || s_trimmed == "0" || s_trimmed.starts_with("0.0"),
427            "sin(pi) = {s}"
428        );
429    }
430
431    #[test]
432    fn cos_of_pi_is_near_neg_one() {
433        let pi = compute_pi(40);
434        let result = cos(&pi, 20).expect("ok");
435        let s = result.to_string();
436        assert!(
437            s.starts_with("-1") || s.starts_with("-0.9999"),
438            "cos(pi) = {s}"
439        );
440    }
441
442    #[test]
443    fn sinh_of_zero() {
444        let x = make_dbig("0.0").expect("ok");
445        let result = sinh(&x, 30).expect("ok");
446        let s = result.to_string();
447        let s_trimmed = s.trim_start_matches('-');
448        assert!(s_trimmed.starts_with("0"), "sinh(0) = {s}");
449    }
450
451    #[test]
452    fn cosh_of_zero() {
453        let x = make_dbig("0.0").expect("ok");
454        let result = cosh(&x, 30).expect("ok");
455        let s = result.to_string();
456        assert!(
457            s.starts_with("1.000") || s.starts_with("1"),
458            "cosh(0) = {s}"
459        );
460    }
461
462    #[test]
463    fn tanh_of_zero() {
464        let x = make_dbig("0.0").expect("ok");
465        let result = tanh(&x, 30).expect("ok");
466        let s = result.to_string();
467        let s_trimmed = s.trim_start_matches('-');
468        assert!(s_trimmed.starts_with("0"), "tanh(0) = {s}");
469    }
470
471    #[test]
472    fn atan_of_zero() {
473        let x = make_dbig("0.0").expect("ok");
474        let result = atan(&x, 30).expect("ok");
475        assert_eq!(result, make_dbig("0.0").expect("ok"));
476    }
477
478    #[test]
479    fn atan_of_one_is_pi_over_4() {
480        let x = make_dbig("1.0").expect("ok");
481        let result = atan(&x, 30).expect("ok");
482        let s = result.to_string();
483        assert!(
484            s.starts_with("0.7853981"),
485            "atan(1) = {s}, expected ~0.7853981..."
486        );
487    }
488
489    #[test]
490    fn atan2_positive_x() {
491        let y = make_dbig("1.0").expect("ok");
492        let x = make_dbig("1.0").expect("ok");
493        let result = atan2(&y, &x, 20).expect("ok");
494        let s = result.to_string();
495        assert!(s.starts_with("0.785"), "atan2(1,1) = {s}");
496    }
497
498    #[test]
499    fn precision_zero_errors_trig() {
500        let x = make_dbig("1.0").expect("ok");
501        assert!(sin(&x, 0).is_err());
502        assert!(cos(&x, 0).is_err());
503        assert!(tan(&x, 0).is_err());
504        assert!(atan(&x, 0).is_err());
505        assert!(sinh(&x, 0).is_err());
506        assert!(cosh(&x, 0).is_err());
507        assert!(tanh(&x, 0).is_err());
508    }
509
510    #[test]
511    fn sin_cos_identity() {
512        // sin^2(x) + cos^2(x) = 1 to high precision
513        let x = make_dbig("0.7").expect("ok");
514        let prec = 30;
515        let s = sin(&x, prec).expect("ok");
516        let c = cos(&x, prec).expect("ok");
517        let sum = &(&s * &s) + &(&c * &c);
518        let s_str = sum.to_string();
519        // Should agree with 1.0 to at least ~25 digits
520        assert!(
521            s_str.starts_with("0.9999999999999999999999999")
522                || s_str.starts_with("1.000000000000000000000000"),
523            "sin^2(0.7) + cos^2(0.7) = {s_str}"
524        );
525    }
526
527    #[test]
528    fn sin_of_known_value() {
529        // sin(0.7) = 0.6442176872376910536726143513...
530        let x = make_dbig("0.7").expect("ok");
531        let result = sin(&x, 25).expect("ok");
532        let s = result.to_string();
533        assert!(
534            s.starts_with("0.644217687237691053672614"),
535            "sin(0.7) = {s}"
536        );
537    }
538
539    #[test]
540    fn cos_of_known_value() {
541        // cos(0.7) = 0.7648421872844884262558599...
542        let x = make_dbig("0.7").expect("ok");
543        let result = cos(&x, 25).expect("ok");
544        let s = result.to_string();
545        assert!(
546            s.starts_with("0.764842187284488426255859"),
547            "cos(0.7) = {s}"
548        );
549    }
550}