oxinum_float/native/trig.rs
1//! Trigonometric functions for native `BigFloat`: sin, cos, tan.
2//!
3//! # Algorithms
4//!
5//! ## Argument reduction (quadrant)
6//!
7//! Given `x`, compute `q = round(x / (π/2))` and `u = x - q * (π/2)`.
8//! The remainder `u ∈ [−π/4, π/4]` satisfies `|u| ≤ π/4 ≈ 0.785`.
9//!
10//! For large `|x|` where `|q|` would overflow `i64`, the quotient is computed
11//! via `BigFloat` arithmetic: we extract the integer `q` from the BigFloat
12//! representation by shifting the mantissa using the stored exponent.
13//!
14//! The quadrant `q mod 4` maps to the final signs and sin/cos exchange:
15//!
16//! | quadrant | sin(x) | cos(x) |
17//! |----------|----------|----------|
18//! | 0 | +sin(u) | +cos(u) |
19//! | 1 | +cos(u) | −sin(u) |
20//! | 2 | −sin(u) | −cos(u) |
21//! | 3 | −cos(u) | +sin(u) |
22//!
23//! ## Taylor series for `|u| ≤ π/4`
24//!
25//! `sin(u) = u − u³/3! + u⁵/5! − …` with `n_terms = prec/2 + 10`.
26//! `cos(u) = 1 − u²/2! + u⁴/4! − …` with `n_terms = prec/2 + 10`.
27//!
28//! These term counts ensure convergence because for `|u| ≤ 1` the ratio
29//! `|u^(2k+1)| / (2k+1)!` decays faster than `(π/4)^(2k+1) / (2k+1)!`.
30//! At 2k+1 ≈ prec/2 terms, this is well below `2^{-prec}`.
31
32use oxinum_core::{OxiNumError, OxiNumResult};
33
34use super::constants::pi;
35use super::float::{BigFloat, RoundingMode};
36
37// ---------------------------------------------------------------------------
38// Taylor series helpers
39// ---------------------------------------------------------------------------
40
41/// Compute `sin(u)` via Taylor series for `|u| ≤ π/4`.
42///
43/// `sin(u) = u − u³/3! + u⁵/5! − …`
44///
45/// Iterative form: `term_0 = u`; `term_k = −term_{k−1} * u² / ((2k)(2k+1))`.
46fn sin_taylor(u: &BigFloat, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
47 if u.is_zero() {
48 return Ok(BigFloat::zero(prec));
49 }
50
51 // u² at work precision.
52 let u_sq = u.mul_ref_with_mode(u, mode).with_precision(prec, mode);
53
54 // term = u; result = u.
55 let mut term = u.clone().with_precision(prec, mode);
56 let mut result = term.clone();
57
58 // n_terms is a safe upper bound for convergence at |u| ≤ π/4.
59 let n_terms: u64 = (prec as u64) / 2 + 10;
60
61 for k in 1..=n_terms {
62 // term *= u² / ((2k)(2k+1))
63 // Denominator: (2k)(2k+1) fits in i64 for all practical k.
64 let denom_val = (2 * k) * (2 * k + 1);
65 // Guard against enormous k turning denom > i64::MAX (unreachable in
66 // practice: prec ≤ 2^32 so n_terms ≤ 2^31+10, denom ≤ 4*(2^31)^2 > i64,
67 // but also at that scale |u|^(2k+1)/(2k+1)! is exactly 0 in float).
68 // Use saturating to avoid panic; if it saturates the term is negligible.
69 let denom_i64 = denom_val.min(i64::MAX as u64) as i64;
70 let denom_f = BigFloat::from_i64(denom_i64, prec, mode);
71 // term = -(term * u_sq) / denom
72 let numer = term
73 .mul_ref_with_mode(&u_sq, mode)
74 .with_precision(prec, mode);
75 term = numer
76 .div_ref_with_mode(&denom_f, mode)
77 .map_err(|e| OxiNumError::Precision(format!("sin_taylor denom zero: {e}").into()))?;
78 term = term.neg().with_precision(prec, mode);
79 result = result
80 .add_ref_with_mode(&term, mode)
81 .with_precision(prec, mode);
82 }
83
84 Ok(result.with_precision(prec, mode))
85}
86
87/// Compute `cos(u)` via Taylor series for `|u| ≤ π/4`.
88///
89/// `cos(u) = 1 − u²/2! + u⁴/4! − …`
90///
91/// Iterative form: `term_0 = 1`; `term_k = −term_{k−1} * u² / ((2k−1)(2k))`.
92fn cos_taylor(u: &BigFloat, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
93 // u² at work precision.
94 let u_sq = u.mul_ref_with_mode(u, mode).with_precision(prec, mode);
95
96 // term = 1; result = 1.
97 let one = BigFloat::from_i64(1, prec, mode);
98 let mut term = one.clone();
99 let mut result = one;
100
101 let n_terms: u64 = (prec as u64) / 2 + 10;
102
103 for k in 1..=n_terms {
104 let denom_val = (2 * k - 1) * (2 * k);
105 let denom_i64 = denom_val.min(i64::MAX as u64) as i64;
106 let denom_f = BigFloat::from_i64(denom_i64, prec, mode);
107 let numer = term
108 .mul_ref_with_mode(&u_sq, mode)
109 .with_precision(prec, mode);
110 term = numer
111 .div_ref_with_mode(&denom_f, mode)
112 .map_err(|e| OxiNumError::Precision(format!("cos_taylor denom zero: {e}").into()))?;
113 term = term.neg().with_precision(prec, mode);
114 result = result
115 .add_ref_with_mode(&term, mode)
116 .with_precision(prec, mode);
117 }
118
119 Ok(result.with_precision(prec, mode))
120}
121
122// ---------------------------------------------------------------------------
123// Argument reduction: extract integer quotient from BigFloat
124// ---------------------------------------------------------------------------
125
126/// Extract the integer part (floor toward zero) of a `BigFloat`.
127///
128/// Returns `None` if the value is too large to represent in `i64` (exponent
129/// plus bit-length would overflow). Returns `0` for sub-unit values.
130fn bigfloat_to_i64_round(x: &BigFloat) -> Option<i64> {
131 // The value is (-1)^s * mantissa * 2^e.
132 // The integer part is non-zero only when e + bit_length > 0 (i.e. the
133 // value has magnitude >= 1).
134 if x.is_zero() {
135 return Some(0);
136 }
137 let e = x.exponent();
138 let bits = x.mantissa().bit_length() as i64;
139 // Effective position of the top bit in the integer: e + bits - 1.
140 let top_pos = e.saturating_add(bits - 1);
141 if top_pos < 0 {
142 // |value| < 1, integer part is 0.
143 return Some(0);
144 }
145 if top_pos >= 63 {
146 // Too large to represent in i64.
147 return None;
148 }
149 // The integer part has at most `top_pos + 1` bits which is < 63.
150 // integer_part_biguint = mantissa >> (-e) when e < 0, or mantissa << e when e >= 0.
151 // We operate in BigUint space so the shift is exact regardless of mantissa size.
152 let int_biguint = if e >= 0 {
153 // Left-shift: value = mantissa * 2^e. The integer part is the full value.
154 // top_pos < 63 guarantees this fits in i64.
155 x.mantissa().shl_bits(e as u64)
156 } else {
157 // Right-shift: integer part = mantissa >> (-e).
158 let shift = (-e) as u64;
159 x.mantissa().shr_bits(shift)
160 };
161 let int_mag = int_biguint.to_u64()?;
162 // Apply sign.
163 if x.signum() < 0 {
164 // Negative: int_mag fits in i64 because top_pos < 63.
165 Some(-(int_mag as i64))
166 } else {
167 Some(int_mag as i64)
168 }
169}
170
171/// Compute `q = round(x / (π/2))` as an `i64` at work precision.
172///
173/// Returns `None` if `|q|` is too large to represent in `i64` (i.e. `|x| ≥
174/// 2^62 * π/2 ≈ 7e18`). Callers should propagate a `Precision` error in that
175/// case.
176fn round_div_pi_over_2(
177 x: &BigFloat,
178 pi_over_2: &BigFloat,
179 work_prec: u32,
180 mode: RoundingMode,
181) -> Option<i64> {
182 let ratio = x
183 .clone()
184 .with_precision(work_prec, mode)
185 .div_ref_with_mode(pi_over_2, mode)
186 .ok()?;
187
188 // Add ±0.5 and take the floor to implement rounding.
189 let half = BigFloat::from_f64(0.5, work_prec).ok()?;
190 let shifted = if ratio.signum() >= 0 {
191 ratio.add_ref_with_mode(&half, mode)
192 } else {
193 ratio.sub_ref_with_mode(&half, mode)
194 };
195 // Extract integer part.
196 bigfloat_to_i64_round(&shifted)
197}
198
199// ---------------------------------------------------------------------------
200// Core sin/cos implementation
201// ---------------------------------------------------------------------------
202
203/// Compute `(sin(x), cos(x))` at `prec` bits via argument reduction + Taylor.
204pub(crate) fn sincos_impl(
205 x: &BigFloat,
206 prec: u32,
207 mode: RoundingMode,
208) -> OxiNumResult<(BigFloat, BigFloat)> {
209 // Guard bits: extra bits to absorb cancellation during argument reduction.
210 // The reduction computes u = x − q*(π/2). For large |x| we need extra
211 // precision to get the low bits of u right. We use |exponent| + 32 as
212 // a conservative guard.
213 let exp_guard = if x.exponent() > 0 {
214 (x.exponent() as u32).min(256)
215 } else {
216 0u32
217 };
218 let work_prec = prec.saturating_add(exp_guard).saturating_add(32);
219
220 // π/2 at work precision.
221 let pi_val = pi(work_prec)?;
222 let two = BigFloat::from_i64(2, work_prec, mode);
223 let pi_over_2 = pi_val.div_ref_with_mode(&two, mode)?;
224
225 // Compute q = round(x / (π/2)).
226 let q = match round_div_pi_over_2(x, &pi_over_2, work_prec, mode) {
227 Some(v) => v,
228 None => {
229 return Err(OxiNumError::Precision(
230 "sin/cos: argument magnitude too large (|x| > 2^62 * π/2); \
231 use argument reduction before calling"
232 .into(),
233 ));
234 }
235 };
236 let quadrant = q.rem_euclid(4) as u32;
237
238 // u = x − q * (π/2).
239 let q_bf = BigFloat::from_i64(q, work_prec, mode);
240 let u = x
241 .clone()
242 .with_precision(work_prec, mode)
243 .sub_ref_with_mode(&q_bf.mul_ref_with_mode(&pi_over_2, mode), mode);
244
245 // Taylor / binary-splitting series for sin(u) and cos(u) at work precision.
246 // Above BS_THRESHOLD_BITS, the binary-splitting path replaces both Taylor
247 // calls and is strictly faster at high precision.
248 let (sin_u, cos_u) = if prec >= super::bs_transcendental::BS_THRESHOLD_BITS {
249 super::bs_transcendental::sincos_bs(&u, work_prec, mode)?
250 } else {
251 let s = sin_taylor(&u, work_prec, mode)?;
252 let c = cos_taylor(&u, work_prec, mode)?;
253 (s, c)
254 };
255
256 // Apply quadrant table.
257 let (sin_x, cos_x) = match quadrant {
258 0 => (sin_u, cos_u),
259 1 => (cos_u, sin_u.neg()),
260 2 => (sin_u.neg(), cos_u.neg()),
261 3 => (cos_u.neg(), sin_u),
262 _ => unreachable!("rem_euclid(4) always in 0..=3"),
263 };
264
265 Ok((
266 sin_x.with_precision(prec, mode),
267 cos_x.with_precision(prec, mode),
268 ))
269}
270
271// ---------------------------------------------------------------------------
272// Public BigFloat methods
273// ---------------------------------------------------------------------------
274
275impl BigFloat {
276 /// Return `sin(self)` at `prec` bits using the given rounding mode.
277 ///
278 /// Uses argument reduction mod π/2 and a Taylor series convergent for
279 /// `|u| ≤ π/4`.
280 ///
281 /// # Errors
282 ///
283 /// - [`OxiNumError::Precision`] if `|self|` is too large for the internal
284 /// argument-reduction step (approximately `|x| > 2^62 · π/2 ≈ 7.2·10^18`).
285 ///
286 /// # Examples
287 ///
288 /// ```
289 /// use oxinum_float::native::{BigFloat, RoundingMode};
290 /// let zero = BigFloat::zero(64);
291 /// let s = zero.sin(64, RoundingMode::HalfEven).expect("sin(0)");
292 /// assert!(s.is_zero());
293 /// ```
294 pub fn sin(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
295 // IEEE-754: sin(NaN) = NaN, sin(±Inf) = NaN
296 if self.is_nan() || self.is_infinite() {
297 return Ok(BigFloat::nan(prec));
298 }
299 if self.is_zero() {
300 return Ok(BigFloat::zero(prec));
301 }
302 let (s, _c) = sincos_impl(self, prec, mode)?;
303 Ok(s)
304 }
305
306 /// Return `cos(self)` at `prec` bits using the given rounding mode.
307 ///
308 /// # Errors
309 ///
310 /// Same as [`BigFloat::sin`].
311 ///
312 /// # Examples
313 ///
314 /// ```
315 /// use oxinum_float::native::{BigFloat, RoundingMode};
316 /// let zero = BigFloat::zero(64);
317 /// let c = zero.cos(64, RoundingMode::HalfEven).expect("cos(0)");
318 /// assert_eq!(c.to_f64(), 1.0);
319 /// ```
320 pub fn cos(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
321 // IEEE-754: cos(NaN) = NaN, cos(±Inf) = NaN
322 if self.is_nan() || self.is_infinite() {
323 return Ok(BigFloat::nan(prec));
324 }
325 let (_s, c) = sincos_impl(self, prec, mode)?;
326 Ok(c)
327 }
328
329 /// Return `tan(self)` at `prec` bits using the given rounding mode.
330 ///
331 /// # Errors
332 ///
333 /// - [`OxiNumError::Domain`] if `cos(self) = 0` (i.e. `self = π/2 + k·π`).
334 /// - [`OxiNumError::Precision`] for very large `|self|`.
335 ///
336 /// # Examples
337 ///
338 /// ```
339 /// use oxinum_float::native::{BigFloat, RoundingMode};
340 /// let pi_over_4 = {
341 /// use oxinum_float::native::pi;
342 /// let p = pi(64).expect("pi");
343 /// let four = BigFloat::from_i64(4, 64, RoundingMode::HalfEven);
344 /// p.div_ref(&four).expect("div")
345 /// };
346 /// let t = pi_over_4.tan(64, RoundingMode::HalfEven).expect("tan(π/4)");
347 /// assert!((t.to_f64() - 1.0).abs() < 1e-14);
348 /// ```
349 pub fn tan(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
350 // IEEE-754: tan(NaN) = NaN, tan(±Inf) = NaN
351 if self.is_nan() || self.is_infinite() {
352 return Ok(BigFloat::nan(prec));
353 }
354 let (s, c) = sincos_impl(self, prec, mode)?;
355 if c.is_zero() {
356 return Err(OxiNumError::Domain("tan undefined at π/2 + k·π".into()));
357 }
358 let result = s.div_ref_with_mode(&c, mode)?;
359 Ok(result.with_precision(prec, mode))
360 }
361}
362
363// ---------------------------------------------------------------------------
364// Internal tests
365// ---------------------------------------------------------------------------
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 fn mk(n: i64, prec: u32) -> BigFloat {
372 BigFloat::from_i64(n, prec, RoundingMode::HalfEven)
373 }
374
375 #[test]
376 fn sin_zero_is_zero() {
377 let z = mk(0, 64);
378 let s = z.sin(64, RoundingMode::HalfEven).expect("sin(0)");
379 assert!(s.is_zero());
380 }
381
382 #[test]
383 fn cos_zero_is_one() {
384 let z = mk(0, 64);
385 let c = z.cos(64, RoundingMode::HalfEven).expect("cos(0)");
386 assert!((c.to_f64() - 1.0).abs() < 1e-14, "cos(0) = {}", c.to_f64());
387 }
388
389 #[test]
390 fn sin_pi_over_2_is_one() {
391 let prec = 64u32;
392 let p = pi(prec).expect("pi");
393 let two = mk(2, prec);
394 let pi_over_2 = p
395 .div_ref_with_mode(&two, RoundingMode::HalfEven)
396 .expect("pi/2");
397 let s = pi_over_2
398 .sin(prec, RoundingMode::HalfEven)
399 .expect("sin(pi/2)");
400 assert!(
401 (s.to_f64() - 1.0).abs() < 1e-14,
402 "sin(π/2) = {}",
403 s.to_f64()
404 );
405 }
406
407 #[test]
408 fn pythagorean_identity_at_f64_angle() {
409 let prec = 100u32;
410 for x_f64 in [0.3f64, 1.0, 2.7, -1.5, 5.0, -2.9] {
411 let x = BigFloat::from_f64(x_f64, prec).expect("from_f64");
412 let s = x.sin(prec, RoundingMode::HalfEven).expect("sin");
413 let c = x.cos(prec, RoundingMode::HalfEven).expect("cos");
414 let sum = s
415 .mul_ref_with_mode(&s, RoundingMode::HalfEven)
416 .add_ref_with_mode(
417 &c.mul_ref_with_mode(&c, RoundingMode::HalfEven),
418 RoundingMode::HalfEven,
419 );
420 let err = (sum.to_f64() - 1.0).abs();
421 assert!(
422 err < 1e-25,
423 "sin²+cos² error = {:.2e} for x = {}",
424 err,
425 x_f64
426 );
427 }
428 }
429}