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 series for sin(u) and cos(u) at work precision.
246 let sin_u = sin_taylor(&u, work_prec, mode)?;
247 let cos_u = cos_taylor(&u, work_prec, mode)?;
248
249 // Apply quadrant table.
250 let (sin_x, cos_x) = match quadrant {
251 0 => (sin_u, cos_u),
252 1 => (cos_u, sin_u.neg()),
253 2 => (sin_u.neg(), cos_u.neg()),
254 3 => (cos_u.neg(), sin_u),
255 _ => unreachable!("rem_euclid(4) always in 0..=3"),
256 };
257
258 Ok((
259 sin_x.with_precision(prec, mode),
260 cos_x.with_precision(prec, mode),
261 ))
262}
263
264// ---------------------------------------------------------------------------
265// Public BigFloat methods
266// ---------------------------------------------------------------------------
267
268impl BigFloat {
269 /// Return `sin(self)` at `prec` bits using the given rounding mode.
270 ///
271 /// Uses argument reduction mod π/2 and a Taylor series convergent for
272 /// `|u| ≤ π/4`.
273 ///
274 /// # Errors
275 ///
276 /// - [`OxiNumError::Precision`] if `|self|` is too large for the internal
277 /// argument-reduction step (approximately `|x| > 2^62 · π/2 ≈ 7.2·10^18`).
278 ///
279 /// # Examples
280 ///
281 /// ```
282 /// use oxinum_float::native::{BigFloat, RoundingMode};
283 /// let zero = BigFloat::zero(64);
284 /// let s = zero.sin(64, RoundingMode::HalfEven).expect("sin(0)");
285 /// assert!(s.is_zero());
286 /// ```
287 pub fn sin(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
288 // IEEE-754: sin(NaN) = NaN, sin(±Inf) = NaN
289 if self.is_nan() || self.is_infinite() {
290 return Ok(BigFloat::nan(prec));
291 }
292 if self.is_zero() {
293 return Ok(BigFloat::zero(prec));
294 }
295 let (s, _c) = sincos_impl(self, prec, mode)?;
296 Ok(s)
297 }
298
299 /// Return `cos(self)` at `prec` bits using the given rounding mode.
300 ///
301 /// # Errors
302 ///
303 /// Same as [`BigFloat::sin`].
304 ///
305 /// # Examples
306 ///
307 /// ```
308 /// use oxinum_float::native::{BigFloat, RoundingMode};
309 /// let zero = BigFloat::zero(64);
310 /// let c = zero.cos(64, RoundingMode::HalfEven).expect("cos(0)");
311 /// assert_eq!(c.to_f64(), 1.0);
312 /// ```
313 pub fn cos(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
314 // IEEE-754: cos(NaN) = NaN, cos(±Inf) = NaN
315 if self.is_nan() || self.is_infinite() {
316 return Ok(BigFloat::nan(prec));
317 }
318 let (_s, c) = sincos_impl(self, prec, mode)?;
319 Ok(c)
320 }
321
322 /// Return `tan(self)` at `prec` bits using the given rounding mode.
323 ///
324 /// # Errors
325 ///
326 /// - [`OxiNumError::Domain`] if `cos(self) = 0` (i.e. `self = π/2 + k·π`).
327 /// - [`OxiNumError::Precision`] for very large `|self|`.
328 ///
329 /// # Examples
330 ///
331 /// ```
332 /// use oxinum_float::native::{BigFloat, RoundingMode};
333 /// let pi_over_4 = {
334 /// use oxinum_float::native::pi;
335 /// let p = pi(64).expect("pi");
336 /// let four = BigFloat::from_i64(4, 64, RoundingMode::HalfEven);
337 /// p.div_ref(&four).expect("div")
338 /// };
339 /// let t = pi_over_4.tan(64, RoundingMode::HalfEven).expect("tan(π/4)");
340 /// assert!((t.to_f64() - 1.0).abs() < 1e-14);
341 /// ```
342 pub fn tan(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
343 // IEEE-754: tan(NaN) = NaN, tan(±Inf) = NaN
344 if self.is_nan() || self.is_infinite() {
345 return Ok(BigFloat::nan(prec));
346 }
347 let (s, c) = sincos_impl(self, prec, mode)?;
348 if c.is_zero() {
349 return Err(OxiNumError::Domain("tan undefined at π/2 + k·π".into()));
350 }
351 let result = s.div_ref_with_mode(&c, mode)?;
352 Ok(result.with_precision(prec, mode))
353 }
354}
355
356// ---------------------------------------------------------------------------
357// Internal tests
358// ---------------------------------------------------------------------------
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 fn mk(n: i64, prec: u32) -> BigFloat {
365 BigFloat::from_i64(n, prec, RoundingMode::HalfEven)
366 }
367
368 #[test]
369 fn sin_zero_is_zero() {
370 let z = mk(0, 64);
371 let s = z.sin(64, RoundingMode::HalfEven).expect("sin(0)");
372 assert!(s.is_zero());
373 }
374
375 #[test]
376 fn cos_zero_is_one() {
377 let z = mk(0, 64);
378 let c = z.cos(64, RoundingMode::HalfEven).expect("cos(0)");
379 assert!((c.to_f64() - 1.0).abs() < 1e-14, "cos(0) = {}", c.to_f64());
380 }
381
382 #[test]
383 fn sin_pi_over_2_is_one() {
384 let prec = 64u32;
385 let p = pi(prec).expect("pi");
386 let two = mk(2, prec);
387 let pi_over_2 = p
388 .div_ref_with_mode(&two, RoundingMode::HalfEven)
389 .expect("pi/2");
390 let s = pi_over_2
391 .sin(prec, RoundingMode::HalfEven)
392 .expect("sin(pi/2)");
393 assert!(
394 (s.to_f64() - 1.0).abs() < 1e-14,
395 "sin(π/2) = {}",
396 s.to_f64()
397 );
398 }
399
400 #[test]
401 fn pythagorean_identity_at_f64_angle() {
402 let prec = 100u32;
403 for x_f64 in [0.3f64, 1.0, 2.7, -1.5, 5.0, -2.9] {
404 let x = BigFloat::from_f64(x_f64, prec).expect("from_f64");
405 let s = x.sin(prec, RoundingMode::HalfEven).expect("sin");
406 let c = x.cos(prec, RoundingMode::HalfEven).expect("cos");
407 let sum = s
408 .mul_ref_with_mode(&s, RoundingMode::HalfEven)
409 .add_ref_with_mode(
410 &c.mul_ref_with_mode(&c, RoundingMode::HalfEven),
411 RoundingMode::HalfEven,
412 );
413 let err = (sum.to_f64() - 1.0).abs();
414 assert!(
415 err < 1e-25,
416 "sin²+cos² error = {:.2e} for x = {}",
417 err,
418 x_f64
419 );
420 }
421 }
422}