Skip to content

Commit b9c526e

Browse files
authored
[decimal] Use Chudnovsky with binary splitting to calculate pi (#95)
This pull request introduces significant improvements to the `BigDecimal` library by optimizing the calculation of π (pi) for high precision and documenting the performance characteristics of different algorithms. The main changes include replacing Machin's formula with the Chudnovsky algorithm for faster computation, introducing binary splitting for efficiency, and adding internal notes on time complexity and algorithm results. ### Optimizations for π Calculation: * [`src/decimojo/bigdecimal/constants.mojo`](diffhunk://#diff-41cd4654562f4a78c9b73f60941a2f1e1d8509a3dc8a93469408e4a5456619f2R154-R289): Replaced Machin's formula with the Chudnovsky algorithm for π calculation, leveraging binary splitting for higher precision and faster computation. Added caching logic to store precomputed values for precision ≤ 1024. * [`src/decimojo/bigdecimal/constants.mojo`](diffhunk://#diff-41cd4654562f4a78c9b73f60941a2f1e1d8509a3dc8a93469408e4a5456619f2R154-R289): Introduced helper functions `pi_chudnovsky_binary_split`, `chudnovsky_split`, and `compute_m_k_rational` to implement the Chudnovsky algorithm efficiently. ### Documentation Updates: * [`docs/internal_notes.md`](diffhunk://#diff-6da6b2e1a170d4269cd7863de3fd61fd4098c67fc788deb68bbe6f0f18b98d24R3-R10): Added a section describing time complexity and performance results for π calculation using Machin's formula and the Chudnovsky algorithm. ### Code Cleanup: * [`src/decimojo/bigdecimal/constants.mojo`](diffhunk://#diff-41cd4654562f4a78c9b73f60941a2f1e1d8509a3dc8a93469408e4a5456619f2L196-L197): Removed redundant code related to Machin's formula for π, including unused variables and outdated comments.
1 parent a195b9e commit b9c526e

File tree

2 files changed

+134
-15
lines changed

2 files changed

+134
-15
lines changed

docs/internal_notes.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
# Internal Notes
22

3+
## Values and results
4+
35
- For power functionality: `BigDecimal.power()`, Python's decimal, WolframAlpha give the same result, but `mpmath` gives a different result. Eample: `0.123456789 ** 1000`, `1234523894766789 ** 1098.1209848`.
6+
7+
## Time complexity
8+
9+
- #94. Implementing pi() with Machin's formula. Time taken for precision 2048: 33.580649 seconds.
10+
- #95. Implementing pi() with Chudnovsky algorithm (binary splitting). Time taken for precision 2048: 1.771954 seconds.

src/decimojo/bigdecimal/constants.mojo

Lines changed: 127 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -151,28 +151,142 @@ alias PI_1024 = BigDecimal(
151151
# we save the value of π to a certain precision in the global scope.
152152
# This will allow us to use it everywhere without recalculating it
153153
# if the required precision is the same or lower.
154+
# Everytime when user calls pi(precision),
155+
# we check whether the precision is higher than the current precision.
156+
# If yes, then we save it into the global scope as cached value.
154157
fn pi(precision: Int) raises -> BigDecimal:
155-
"""Calculates π using Machin's formula.
156-
π/4 = 4*arctan(1/5) - arctan(1/239).
158+
"""Calculates π using the fastest available algorithm.
157159
158-
Notes:
159-
Time complexity is O(n^3) ~ O(n^4) for precision n.
160-
Every time you double the precision, the time taken increases by a
161-
factor of 8 ~ 16.
160+
- precision ≤ 1024: precomputed constant
161+
- precision > 1024: Chudnovsky with binary splitting
162162
"""
163163

164164
if precision < 0:
165165
raise Error("Precision must be non-negative")
166-
# For precision up to 1024, we can use the precomputed value of π.
167-
# Since π is also input to other functions.
168-
# TODO: Everytime when user calls pi(precision),
169-
# we check whether the precision is higher than the current precision.
170-
# If yes, then we save it into the global scope.
166+
167+
# Use precomputed value for precision ≤ 1024
171168
if precision <= 1024:
172169
return PI_1024.round(precision, RoundingMode.ROUND_HALF_EVEN)
173170

174-
alias BUFFER_DIGITS = 9 # word-length, easy to append and trim
175-
var working_precision = precision + BUFFER_DIGITS
171+
# Use Chudnovsky with binary splitting for maximum speed
172+
return pi_chudnovsky_binary_split(precision)
173+
174+
175+
@value
176+
struct Rational:
177+
"""Represents a rational number p/q for exact arithmetic."""
178+
179+
var p: BigInt # numerator
180+
var q: BigInt # denominator
181+
182+
183+
fn pi_chudnovsky_binary_split(precision: Int) raises -> BigDecimal:
184+
"""Calculates π using Chudnovsky algorithm with binary splitting.
185+
186+
Notes:
187+
188+
Use the formula:
189+
π = 426880 * √10005 / Σ(k=0 to ∞) [M(k) * L(k) / X(k)],
190+
where:
191+
(1) M(k) = (6k)! / ((3k)! * (k!)³)
192+
(2) L(k) = 545140134*k + 13591409
193+
(3) X(k) = (-262537412640768000)^k
194+
"""
195+
196+
var working_precision = precision + 9 # 1 words
197+
var iterations = (
198+
precision // 14
199+
) + 9 # ~14.18 digits per iteration + safety margin
200+
201+
var bdec_10005 = BigDecimal.from_raw_components(UInt32(10005))
202+
var bdec_426880 = BigDecimal.from_raw_components(UInt32(426880))
203+
204+
# Binary splitting to compute the series sum as a single rational number
205+
var result_fraction = chudnovsky_split(0, iterations, working_precision)
206+
207+
# Convert rational result to BigDecimal: q/p
208+
var sum_series = BigDecimal(result_fraction.q).true_divide(
209+
BigDecimal(result_fraction.p), working_precision
210+
)
211+
212+
# Final formula: π = 426880 * √10005 / sum_series
213+
var result = bdec_426880 * bdec_10005.sqrt(working_precision) * sum_series
214+
215+
return result.round(precision, RoundingMode.ROUND_HALF_EVEN)
216+
217+
218+
fn chudnovsky_split(a: Int, b: Int, precision: Int) raises -> Rational:
219+
"""Conducts binary splitting for Chudnovsky series from term a to b-1."""
220+
221+
var bint_1 = BigInt(1)
222+
var bint_13591409 = BigInt(13591409)
223+
var bint_545140134 = BigInt(545140134)
224+
var bint_262537412640768000 = BigInt(262537412640768000)
225+
226+
if b - a == 1:
227+
# Base case: compute single term as exact rational
228+
if a == 0:
229+
# Special case for k=0: M(0)=1, L(0)=13591409, X(0)=1
230+
return Rational(bint_13591409, bint_1)
231+
232+
# For k > 0: compute M(k), L(k), X(k)
233+
var m_k_rational = compute_m_k_rational(a)
234+
var l_k = bint_545140134 * BigInt(a) + bint_13591409
235+
236+
# X(k) = (-262537412640768000)^k
237+
var x_k = bint_1
238+
for _ in range(a):
239+
x_k *= bint_262537412640768000
240+
241+
# Apply sign: (-1)^k
242+
if a % 2 == 1:
243+
x_k = -x_k
244+
245+
# Term = M(k) * L(k) / X(k) = (m_k_p * l_k) / (m_k_q * x_k)
246+
var term_p = m_k_rational.p * l_k
247+
var term_q = m_k_rational.q * x_k
248+
249+
return Rational(term_p, term_q)
250+
251+
# Recursive case: split range in half
252+
var mid = (a + b) // 2
253+
var left = chudnovsky_split(a, mid, precision)
254+
var right = chudnovsky_split(mid, b, precision)
255+
256+
# Combine fractions: left.p/left.q + right.p/right.q
257+
var combined_p = left.p * right.q + right.p * left.q
258+
var combined_q = left.q * right.q
259+
260+
return Rational(combined_p, combined_q)
261+
262+
263+
fn compute_m_k_rational(k: Int) raises -> Rational:
264+
"""Computes M(k) = (6k)! / ((3k)! * (k!)³) as exact rational."""
265+
266+
var bint_1 = BigInt(1)
267+
268+
if k == 0:
269+
return Rational(bint_1, bint_1)
270+
271+
# Compute numerator: (6k)! / (3k)! = (3k+1) * (3k+2) * ... * (6k)
272+
var numerator = bint_1
273+
for i in range(3 * k + 1, 6 * k + 1):
274+
numerator *= BigInt(i)
275+
276+
# Compute denominator: (k!)³
277+
var k_factorial = bint_1
278+
for i in range(1, k + 1):
279+
k_factorial *= BigInt(i)
280+
281+
var denominator = k_factorial * k_factorial * k_factorial
282+
283+
return Rational(numerator, denominator)
284+
285+
286+
fn pi_machin(precision: Int) raises -> BigDecimal:
287+
"""Fallback π calculation using Machin's formula."""
288+
289+
var working_precision = precision + 9
176290

177291
var bdec_1 = BigDecimal.from_raw_components(UInt32(1))
178292
var bdec_4 = BigDecimal.from_raw_components(UInt32(4))
@@ -193,8 +307,6 @@ fn pi(precision: Int) raises -> BigDecimal:
193307

194308
# π/4 = 4*arctan(1/5) - arctan(1/239)
195309
var pi_over_4 = term1 - term2
196-
197-
# π = 4 * (π/4)
198310
var result = bdec_4 * pi_over_4
199311

200312
return result.round(precision, RoundingMode.ROUND_HALF_EVEN)

0 commit comments

Comments
 (0)