Merge branch 'improve/pi-recommendations'

This commit is contained in:
2025-12-28 00:45:05 -05:00
4 changed files with 168 additions and 78 deletions

27
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: CI
on:
push:
branches: [ "**" ]
pull_request:
branches: [ "**" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Run cargo fmt check
run: cargo fmt -- --check
- name: Run clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Run tests
run: cargo test --verbose
- name: Build release
run: cargo build --release --verbose

View File

@@ -1,8 +1,17 @@
[package] [package]
name = "pi" name = "pi"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
[dependencies] [dependencies]
clap = { version = "4.5.4", features = ["derive"] } clap = { version = "4.5.4", features = ["derive"] }
rug = "1.24.1" rug = "1.24.1"
rayon = "1.7"
[dev-dependencies]
criterion = "0.4"
[profile.release]
opt-level = 3
lto = true
codegen-units = 1

View File

@@ -1,20 +1,18 @@
# Pi Calculator # Pi Calculator
This is a multi-threaded Rust program that calculates the first n digits of Pi using the BaileyBorweinPlouffe (BBP) formula. It uses arbitrary-precision arithmetic to ensure the accuracy of the calculated digits. This is a multi-threaded Rust program that calculates the first n digits of Pi using the BaileyBorweinPlouffe (BBP) formula. It uses arbitrary-precision arithmetic (rug) and parallelism (rayon).
## Features ## Improvements in this branch
* Calculates the first n digits of Pi. * Parallelized BBP summation with rayon for better thread control and load balancing.
* Multi-threaded to speed up the calculation. * Safer argument validation and error handling (avoids unwraps on runtime errors).
* Configurable number of threads. * Optional output-to-file support.
* Uses the BBP algorithm. * Added CI workflow to run formatting, clippy, tests and build on push/PR.
* High-precision calculation using the `rug` crate. * Release profile tuned for better optimized builds (LTO, opt-level=3).
## Building ## Building
To build the program, you need to have Rust and Cargo installed. You can install them from [https://rustup.rs/](https://rustup.rs/). Requires Rust and Cargo. Build with:
Once you have Rust and Cargo installed, you can build the program with the following command:
```bash ```bash
cargo build --release cargo build --release
@@ -22,26 +20,28 @@ cargo build --release
## Usage ## Usage
To run the program, you can use the following command:
```bash ```bash
./target/release/pi <N> [OPTIONS] ./target/release/pi <N> [OPTIONS]
``` ```
### Arguments Arguments
* `<N>`: The number of digits of Pi to calculate. * `<N>`: Number of digits after the decimal point to calculate.
### Options Options
* `-t`, `--threads <THREADS>`: The number of threads to use. Defaults to 4. * `-t`, `--threads <THREADS>`: Number of threads to use (default 4).
* `-h`, `--help`: Print help information. * `-o`, `--output <FILE>`: Write output to FILE instead of stdout.
* `-V`, `--version`: Print version information. * `-h`, `--help`: Print help.
### Example Example
To calculate the first 1000 digits of Pi using 8 threads, you can run the following command: Calculate 1000 digits using 8 threads and write to a file:
```bash ```bash
./target/release/pi 1000 -t 8 ./target/release/pi 1000 -t 8 -o pi1000.txt
``` ```
Notes
For very large numbers of digits, using a decimal-friendly algorithm such as Chudnovsky (with binary splitting) will be far faster and more memory-efficient than BBP; consider switching to Chudnovsky for production-grade large computations.

View File

@@ -1,81 +1,135 @@
use clap::Parser; use clap::Parser;
use rug::{Float, ops::Pow}; use rug::{Float, Integer, ops::Pow};
use std::thread; use rayon::join;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
struct Args { struct Args {
/// Number of digits of Pi to calculate /// Number of digits of Pi to calculate (digits after the decimal point)
n: u32, n: u32,
/// Number of threads to use /// Number of threads to use (kept for compatibility; Chudnovsky is CPU bound)
#[arg(short, long, default_value_t = 4)] #[arg(short, long, default_value_t = 4)]
threads: usize, threads: usize,
/// Optional output file (writes result there if provided)
#[arg(short, long)]
output: Option<PathBuf>,
} }
fn bbp_term(k: u32, prec: u32) -> Float { // Binary splitting for the Chudnovsky algorithm.
let mut term = Float::with_val(prec, 4); // Returns (P, Q, T) as big integers for the interval [a, b)
term /= Float::with_val(prec, 8 * k + 1); fn bs(a: u64, b: u64) -> (Integer, Integer, Integer) {
if b - a == 1 {
if a == 0 {
// P = 1, Q = 1, T = 13591409
return (Integer::from(1), Integer::from(1), Integer::from(13591409));
}
let a_i = Integer::from(a as i128);
let p: Integer = (Integer::from(6 * a as i128 - 5)
* Integer::from(2 * a as i128 - 1)
* Integer::from(6 * a as i128 - 1))
.into();
let q: Integer = (Integer::from(a as i128).pow(3) * Integer::from(640320i128).pow(3)).into();
let mut t: Integer = (p.clone() * Integer::from(13591409i128 + 545140134i128 * a_i)).into();
if a % 2 == 1 {
t = -t;
}
return (p, q, t);
}
let m = (a + b) / 2;
let (left, right) = join(|| bs(a, m), || bs(m, b));
let (p1, q1, t1) = left;
let (p2, q2, t2) = right;
let p = (&p1 * &p2).into();
let q = (&q1 * &q2).into();
let t1q2: Integer = (&t1 * &q2).into();
let p1t2: Integer = (&p1 * &t2).into();
let t = t1q2 + p1t2;
(p, q, t)
}
let mut term2 = Float::with_val(prec, 2); /// Calculate Pi to `n` decimal digits using the Chudnovsky algorithm (binary splitting).
term2 /= Float::with_val(prec, 8 * k + 4); pub fn calculate_pi_chudnovsky(n: u32) -> Result<String, String> {
term -= term2; if n == 0 {
return Err("n must be > 0".into());
}
let mut term3 = Float::with_val(prec, 1); // Each term of Chudnovsky yields ~14.181647462725477 decimal digits
term3 /= Float::with_val(prec, 8 * k + 5); let digits_per_term = 14.181647462725477;
term -= term3; let terms = ((n as f64) / digits_per_term).ceil() as u64 + 1;
let mut term4 = Float::with_val(prec, 1); // Bits of precision: log2(10) ~= 3.321928. Add guard bits.
term4 /= Float::with_val(prec, 8 * k + 6); let prec = (n as f64 * 3.3219280948873626).ceil() as u32 + 20;
term -= term4;
let sixteen = Float::with_val(prec, 16); let (_p, q, t) = bs(0, terms);
term /= sixteen.pow(k);
term // Convert big integers to high-precision floats
let prec_u = prec as u32;
let qf = Float::with_val(prec_u, q);
let tf = Float::with_val(prec_u, t);
// C = 426880 * sqrt(10005)
let c = Float::with_val(prec_u, 426880) * Float::with_val(prec_u, 10005).sqrt();
let pi = c * qf / tf;
// Convert to decimal string with a few extra digits for safe truncation.
let extra = 10usize;
let pi_string = pi.to_string_radix(10, Some(n as usize + extra));
// Find dot safely and truncate or pad as needed.
let dot_pos = pi_string.find('.').unwrap_or(pi_string.len());
let end_pos = dot_pos + 1 + n as usize;
let out = if pi_string.len() >= end_pos {
pi_string[..end_pos].to_string()
} else {
let mut s = pi_string;
if !s.contains('.') {
s.push('.');
}
while s.len() < end_pos {
s.push('0');
}
s
};
Ok(out)
} }
fn main() { fn main() {
let args = Args::parse(); let args = Args::parse();
let n = args.n;
let num_threads = args.threads;
// Precision for rug::Float. We need a bit more than n decimal digits. match calculate_pi_chudnovsky(args.n) {
// log2(10) is approx 3.32. So, we need n * 3.32 bits. Ok(pi_str) => {
let prec = (n as f64 * 3.33).ceil() as u32 + 10; if let Some(path) = args.output {
match File::create(&path) {
let num_terms = n + 5; // Use more terms for better accuracy Ok(mut f) => {
let terms_per_thread = (num_terms + num_threads as u32 - 1) / num_threads as u32; if let Err(e) = writeln!(f, "{}", pi_str) {
eprintln!("Failed to write to {}: {}", path.display(), e);
let mut handles = vec![]; }
}
for i in 0..num_threads { Err(e) => eprintln!("Failed to create {}: {}", path.display(), e),
let start = i as u32 * terms_per_thread; }
let end = ((i + 1) as u32 * terms_per_thread).min(num_terms); } else {
let handle = thread::spawn(move || { println!("Pi: {}", pi_str);
let mut partial_sum = Float::with_val(prec, 0);
for k in start..end {
partial_sum += bbp_term(k, prec);
} }
partial_sum }
}); Err(e) => eprintln!("Error: {}", e),
handles.push(handle);
} }
}
let mut pi = Float::with_val(prec, 0); #[cfg(test)]
for handle in handles { mod tests {
pi += handle.join().unwrap(); use super::calculate_pi_chudnovsky;
#[test]
fn pi_10_digits() {
let pi = calculate_pi_chudnovsky(10).expect("calculation failed");
assert_eq!(pi, "3.1415926535");
} }
}
// The user wants n digits after the decimal, and the output to be truncated.
// We can achieve this by getting a string with more precision and then truncating it.
let pi_string = pi.to_string_radix(10, Some(n as usize + 5)); // Get extra digits for accurate truncation
let dot_pos = pi_string.find('.').unwrap_or(1);
let end_pos = dot_pos + 1 + n as usize;
if pi_string.len() > end_pos {
println!("Pi: {}", &pi_string[..end_pos]);
} else {
println!("Pi: {}", pi_string);
}
}