Skip to content

Commit 2ddf0e2

Browse files
committed
feat(float): implement arbitrary-precision trigonometric functions and constants
1 parent 4c8f9ee commit 2ddf0e2

12 files changed

Lines changed: 1349 additions & 18 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ members = [
2525
"macros",
2626
"python",
2727
"rational",
28+
"fuzz",
2829
]
2930
exclude = ["benchmark"]
3031
default-members = ["base", "integer", "float", "rational", "macros"]

float/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ zeroize = { optional = true, version = "1.5.7", default-features = false }
4747
diesel_v1 = { optional = true, version = "1.4.0", package = "diesel", default-features = false, features = ["postgres"]}
4848
diesel_v2 = { optional = true, version = "2.0.0", package = "diesel", default-features = false, features = ["postgres_backend"]}
4949
_bytes = { optional = true, version = "1.0", package = "bytes", default-features = false }
50-
5150
# unstable dependencies
5251
rand_v08 = { optional = true, version = "0.8.3", package = "rand", default-features = false }
5352
num-traits_v02 = { optional = true, version = "0.2.15", package = "num-traits", default-features = false }

float/src/error.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,26 @@ pub const fn panic_power_negative_base() -> ! {
3737
}
3838

3939
/// Panics when taking an even order root of an negative number
40-
pub(crate) fn panic_root_negative() -> ! {
40+
pub fn panic_root_negative() -> ! {
4141
panic!("the root is a complex number!")
4242
}
43+
44+
/// Panics when the result of an operation is NaN
45+
pub fn panic_nan() -> ! {
46+
panic!("the result of the operation is NaN!")
47+
}
48+
49+
/// Panics when the result of an operation overflows
50+
pub fn panic_overflow() -> ! {
51+
panic!("the result of the operation overflowed!")
52+
}
53+
54+
/// Panics when the result of an operation underflows
55+
pub fn panic_underflow() -> ! {
56+
panic!("the result of the operation underflowed!")
57+
}
58+
59+
/// Panics when the result of an operation is an exact infinity
60+
pub fn panic_infinite() -> ! {
61+
panic!("the result of the operation is an exact infinity!")
62+
}

float/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ mod fmt;
7676
mod helper_macros;
7777
mod iter;
7878
mod log;
79+
pub mod math;
7980
mod mul;
8081
pub mod ops;
8182
mod parse;

float/src/math/consts.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use crate::{
2+
error::assert_limited_precision,
3+
fbig::FBig,
4+
repr::{Context, Word},
5+
round::{Round, Rounded},
6+
};
7+
use dashu_base::{BitTest, UnsignedAbs};
8+
use dashu_int::{IBig, UBig};
9+
10+
impl<R: Round> Context<R> {
11+
/// Calculate π using the Chudnovsky algorithm with binary splitting.
12+
///
13+
/// The Chudnovsky algorithm is one of the most efficient methods for
14+
/// high-precision π calculation, providing ~14.18 decimal digits per term.
15+
///
16+
/// # Methodology
17+
/// We use Binary Splitting to evaluate the series. This technique transforms
18+
/// the linear-time summation into a recursive tree evaluation. By combining
19+
/// terms into large products, it allows the library to leverage fast
20+
/// multiplication algorithms (like Toom-3 or FFT) as the numbers grow,
21+
/// leading to significant performance gains over simple iterative summation.
22+
///
23+
/// // TODO: consider adding a static cache for π at common precisions.
24+
#[must_use]
25+
pub fn pi<const B: Word>(&self) -> Rounded<FBig<R, B>> {
26+
assert_limited_precision(self.precision);
27+
28+
// Calculate required bits based on target precision in base B.
29+
// bits = ceil(precision * log2(B))
30+
let bits = if B.is_power_of_two() {
31+
self.precision.saturating_mul(B.ilog2() as usize)
32+
} else {
33+
self.precision.saturating_mul(B.ilog2() as usize + 1)
34+
};
35+
36+
let num_terms = (bits * 100 / 4708) + 1;
37+
let guard_bits = num_terms.bit_len() + 32;
38+
let work_bits = bits + guard_bits;
39+
40+
// Evaluate the series components using binary splitting
41+
let (_p, q, t) = chudnovsky_bs(0, num_terms);
42+
43+
// Final formula: pi = (426880 * sqrt(10005) * Q) / T
44+
45+
// Convert work bits back to base B precision.
46+
// precision_B = ceil(work_bits / log2(B))
47+
let work_precision = if B == 2 {
48+
work_bits
49+
} else {
50+
work_bits / B.ilog2() as usize + 1
51+
};
52+
let work_context = Self::new(work_precision);
53+
54+
let q_f = work_context.convert_int::<B>(q.into()).value();
55+
let t_f = work_context.convert_int::<B>(t).value();
56+
57+
let sqrt_10005 = work_context
58+
.sqrt(&work_context.convert_int::<B>(10005.into()).value().repr)
59+
.value();
60+
let constant = work_context.convert_int::<B>(426_880.into()).value();
61+
62+
let pi = (constant * sqrt_10005 * q_f) / t_f;
63+
pi.with_precision(self.precision)
64+
}
65+
}
66+
67+
/// Binary splitting implementation for the Chudnovsky series.
68+
/// Returns (P, Q, T) for the range [a, b).
69+
fn chudnovsky_bs(a: usize, b: usize) -> (UBig, UBig, IBig) {
70+
if b - a == 1 {
71+
// Base case: calculate single term
72+
if a == 0 {
73+
return (UBig::ONE, UBig::ONE, IBig::from(13_591_409));
74+
}
75+
76+
let k = a as u64;
77+
let p = UBig::from(6 * k - 5) * (2 * k - 1) * (6 * k - 1);
78+
let q = UBig::from(k).pow(3) * UBig::from(10_939_058_860_032_000_u64);
79+
let t_val = IBig::from(13_591_409) + IBig::from(545_140_134_u64) * k;
80+
let t_abs = &p * t_val.unsigned_abs();
81+
let t = if a % 2 == 1 {
82+
-IBig::from(t_abs)
83+
} else {
84+
IBig::from(t_abs)
85+
};
86+
return (p, q, t);
87+
}
88+
89+
// Recursive step
90+
let mid = (a + b) / 2;
91+
let (p_l, q_l, t_l) = chudnovsky_bs(a, mid);
92+
let (p_r, q_r, t_r) = chudnovsky_bs(mid, b);
93+
94+
let p = &p_l * &p_r;
95+
let q = &q_l * &q_r;
96+
// T = T_L * Q_R + T_R * P_L
97+
let t = IBig::from(q_r) * t_l + IBig::from(p_l) * t_r;
98+
(p, q, t)
99+
}
100+
101+
impl<R: Round, const B: Word> FBig<R, B> {
102+
/// Calculate π with the given precision and the default rounding mode.
103+
#[inline]
104+
#[must_use]
105+
pub fn pi(precision: usize) -> Self {
106+
Context::<R>::new(precision).pi().value()
107+
}
108+
}

float/src/math/mod.rs

Lines changed: 135 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,151 @@
1-
//! Implementations of advanced math functions
1+
//! Advanced mathematical functions
22
3-
// TODO: implement the math functions as associated methods, and add them to FBig through a trait
4-
// REF: https://pkg.go.dev/github.com/ericlagergren/decimal
3+
use crate::{
4+
error::{panic_infinite, panic_nan, panic_overflow, panic_underflow},
5+
fbig::FBig,
6+
repr::{Context, Repr, Word},
7+
round::{Round, Rounded},
8+
};
59

6-
enum FpResult {
7-
Normal(Repr),
10+
pub mod consts;
11+
pub mod trig;
12+
13+
/// The result of an advanced mathematical operation.
14+
///
15+
/// This enum is used to handle non-finite results (NaN, Infinite) and
16+
/// boundary conditions (Overflow, Underflow) without panicking,
17+
/// as the core [`FBig`] type only represents finite numbers.
18+
///
19+
/// Finite results are wrapped in a [Rounded] to preserve rounding information.
20+
#[derive(Clone, Debug, PartialEq, Eq)]
21+
pub enum FpResult<const B: Word> {
22+
Normal(Rounded<Repr<B>>),
823
Overflow,
924
Underflow,
1025
NaN,
11-
1226
/// An exact infinite result is obtained from finite inputs, such as
13-
/// divide by zero, logarithm on zero.
27+
/// divide by zero or logarithm of zero.
1428
Infinite,
1529
}
1630

17-
impl Context {
18-
fn sin(&self, repr: Repr) -> FpResult {
19-
todo!()
31+
impl<const B: Word> FpResult<B> {
32+
/// Convert the result into an [`FBig`] with the given context.
33+
///
34+
/// # Panics
35+
/// Panics if the result is not `Normal`.
36+
#[inline]
37+
#[must_use]
38+
pub fn value<R: Round>(self, context: &Context<R>) -> FBig<R, B> {
39+
match self {
40+
Self::Normal(rounded) => FBig::new(rounded.value(), *context),
41+
Self::NaN => panic_nan(),
42+
Self::Infinite => panic_infinite(),
43+
Self::Overflow => panic_overflow(),
44+
Self::Underflow => panic_underflow(),
45+
}
46+
}
47+
48+
/// Convert the result into an optional [`FBig`] with the given context.
49+
/// Returns `None` if the result is not `Normal`.
50+
#[inline]
51+
#[must_use]
52+
pub fn ok<R: Round>(self, context: &Context<R>) -> Option<Rounded<FBig<R, B>>> {
53+
match self {
54+
Self::Normal(rounded) => Some(rounded.map(|repr| FBig::new(repr, *context))),
55+
_ => None,
56+
}
57+
}
58+
59+
/// Returns `true` if the result is `NaN`.
60+
#[inline]
61+
#[must_use]
62+
pub const fn is_nan(&self) -> bool {
63+
matches!(self, Self::NaN)
64+
}
65+
66+
/// Returns `true` if the result is `Infinite`.
67+
#[inline]
68+
#[must_use]
69+
pub const fn is_infinite(&self) -> bool {
70+
matches!(self, Self::Infinite)
71+
}
72+
73+
/// Returns `true` if the result is a normal finite value.
74+
#[inline]
75+
#[must_use]
76+
pub const fn is_normal(&self) -> bool {
77+
matches!(self, Self::Normal(_))
78+
}
79+
80+
/// Returns `true` if the result is a finite value (Normal, Overflow, or Underflow).
81+
#[inline]
82+
#[must_use]
83+
pub const fn is_finite(&self) -> bool {
84+
matches!(self, Self::Normal(_) | Self::Overflow | Self::Underflow)
2085
}
2186
}
2287

23-
trait ContextOps {
24-
fn context(&self) -> &Context;
25-
fn repr(&self) -> &Repr;
88+
/// Operations that can be performed on floating point numbers via their context.
89+
pub trait ContextOps<R: Round, const B: Word> {
90+
fn context(&self) -> &Context<R>;
91+
fn repr(&self) -> &Repr<B>;
2692

93+
/// Calculate the sine of the number.
2794
#[inline]
28-
fn sin(&self) -> FpResult {
95+
fn sin(&self) -> FpResult<B> {
2996
self.context().sin(self.repr())
3097
}
31-
}
98+
99+
/// Calculate the cosine of the number.
100+
#[inline]
101+
fn cos(&self) -> FpResult<B> {
102+
self.context().cos(self.repr())
103+
}
104+
105+
/// Calculate both the sine and cosine of the number.
106+
#[inline]
107+
fn sin_cos(&self) -> (FpResult<B>, FpResult<B>) {
108+
self.context().sin_cos(self.repr())
109+
}
110+
111+
/// Calculate the tangent of the number.
112+
#[inline]
113+
fn tan(&self) -> FpResult<B> {
114+
self.context().tan(self.repr())
115+
}
116+
117+
/// Calculate the arcsine of the number.
118+
#[inline]
119+
fn asin(&self) -> FpResult<B> {
120+
self.context().asin(self.repr())
121+
}
122+
123+
/// Calculate the arccosine of the number.
124+
#[inline]
125+
fn acos(&self) -> FpResult<B> {
126+
self.context().acos(self.repr())
127+
}
128+
129+
/// Calculate the arctangent of the number.
130+
#[inline]
131+
fn atan(&self) -> FpResult<B> {
132+
self.context().atan(self.repr())
133+
}
134+
135+
/// Calculate the 2-argument arctangent of the number (`y`) and `x`.
136+
#[inline]
137+
fn atan2(&self, x: &Repr<B>) -> FpResult<B> {
138+
self.context().atan2(self.repr(), x)
139+
}
140+
}
141+
142+
impl<R: Round, const B: Word> ContextOps<R, B> for FBig<R, B> {
143+
#[inline]
144+
fn context(&self) -> &Context<R> {
145+
&self.context
146+
}
147+
#[inline]
148+
fn repr(&self) -> &Repr<B> {
149+
&self.repr
150+
}
151+
}

0 commit comments

Comments
 (0)