Skip to content

Commit cb72c86

Browse files
authored
feat: add threaded monte carlo simulation example (#1)
1 parent 1d1ae01 commit cb72c86

File tree

8 files changed

+230
-3
lines changed

8 files changed

+230
-3
lines changed

Cargo.lock

Lines changed: 123 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ pyrust-cli = { path = "crates/pyrust-cli" }
2222

2323

2424
num_cpus = "1.16.0"
25+
rand = "0.9.0"
2526
anyhow = "1.0"
2627
clap = { version = "4.5.28", features = ["derive"] }
28+
2729
# "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so)
2830
# "abi3-py39" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.9
2931
pyo3 = { version = "0.23.4", features = ["extension-module", "abi3-py39"] }

crates/pyrust-cli/src/cli.rs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,25 @@ pub struct Cli {
1010

1111
#[derive(clap::Subcommand)]
1212
pub enum Subcommand {
13-
#[clap(name = "greet")]
14-
Greet { name: Option<String> },
13+
/// Greet someone.
14+
Greet {
15+
#[clap(long, short, default_value = "world")]
16+
name: String,
17+
},
18+
19+
/// Print the number of CPUs available on the system.
1520
#[clap(name = "num-cpus")]
1621
NumCPUs,
22+
23+
/// Estimate the value of pi using a Monte Carlo method.
24+
EstimatePi {
25+
/// Number of samples to use.
26+
#[clap(long, short)]
27+
num_samples: usize,
28+
#[clap(long, short = 't')]
29+
/// Number of threads to use. If not provided, the number of CPUs available on the system will be used.
30+
num_threads: Option<usize>,
31+
},
1732
}
1833

1934
pub fn run(args: Option<&Vec<String>>) {
@@ -26,10 +41,19 @@ pub fn run(args: Option<&Vec<String>>) {
2641

2742
match cli.command {
2843
Subcommand::Greet { name } => {
29-
println!("Hello, {}!", name.unwrap_or_else(|| "world".to_string()));
44+
println!("Hello, {name}!",);
3045
}
3146
Subcommand::NumCPUs => {
3247
println!("{}", num_cpus_available());
3348
}
49+
Subcommand::EstimatePi {
50+
num_samples,
51+
num_threads,
52+
} => {
53+
let pi_estimated = pyrust_internal::pi::monte_carlo_pi(num_samples, num_threads);
54+
let pi_actual = std::f32::consts::PI;
55+
let error = (pi_estimated - pi_actual).abs();
56+
println!("🍰 Estimated Pi to be {pi_estimated}. That's an error of {error}.");
57+
}
3458
};
3559
}

crates/pyrust-internal/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ repository = { workspace = true }
1212

1313
[dependencies]
1414
num_cpus = { workspace = true }
15+
rand = { workspace = true }

crates/pyrust-internal/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
pub mod pi;
2+
13
pub fn num_cpus_available() -> usize {
24
num_cpus::get()
35
}

crates/pyrust-internal/src/pi.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use rand::distr::StandardUniform;
2+
use rand::Rng;
3+
use std::sync::{Arc, Mutex};
4+
use std::time::Instant;
5+
6+
#[derive(Debug, Default)]
7+
struct MonteCarloPiEstimate {
8+
points_in_circle: usize,
9+
points_total: usize,
10+
}
11+
12+
fn get_random_points(num_samples: usize) -> Vec<(f32, f32)> {
13+
let mut rng = rand::rng();
14+
let x: Vec<f32> = (&mut rng)
15+
.sample_iter(StandardUniform)
16+
.take(num_samples)
17+
.collect();
18+
let y: Vec<f32> = (&mut rng)
19+
.sample_iter(StandardUniform)
20+
.take(num_samples)
21+
.collect();
22+
23+
x.into_iter().zip(y).collect()
24+
}
25+
26+
fn is_in_circle(point: &(f32, f32)) -> bool {
27+
let (x, y) = point;
28+
x * x + y * y <= 1.0
29+
}
30+
31+
pub fn monte_carlo_pi(num_samples: usize, n_threads: Option<usize>) -> f32 {
32+
let n_threads = n_threads.unwrap_or_else(num_cpus::get);
33+
let samples_per_thread = num_samples.div_ceil(n_threads);
34+
35+
println!("Using {} threads", n_threads);
36+
println!(
37+
"Using {} samples per thread, {} total samples",
38+
samples_per_thread, num_samples
39+
);
40+
41+
let mut thread_handles = Vec::with_capacity(n_threads);
42+
// Sure enough, we could simply compute an estimate for each thread and average them at the end. To show that mutexes allow sharing data between threads, we'll use a single estimate struct instead
43+
let estimates = Arc::new(Mutex::new(MonteCarloPiEstimate::default()));
44+
45+
let start = Instant::now();
46+
47+
for thread_id in 0..n_threads {
48+
let estimates = estimates.clone();
49+
let thread = std::thread::spawn(move || {
50+
println!("Thread {thread_id} started");
51+
52+
let points = get_random_points(samples_per_thread);
53+
let points_in_circle = points.iter().filter(|&p| is_in_circle(p)).count();
54+
let mut estimates = estimates.lock().unwrap();
55+
estimates.points_in_circle += points_in_circle;
56+
estimates.points_total += samples_per_thread;
57+
58+
println!("Thread {thread_id} finished");
59+
});
60+
61+
thread_handles.push(thread);
62+
}
63+
64+
for thread_handle in thread_handles {
65+
thread_handle.join().unwrap();
66+
}
67+
68+
println!("Estimating pi took {:?}", start.elapsed());
69+
let estimates = estimates.lock().unwrap();
70+
println!("Simulation summary: {estimates:?}");
71+
72+
4.0 * estimates.points_in_circle as f32 / estimates.points_total as f32
73+
}

rustfmt.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
max_width = 100

tests/test_cli.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ test_command() {
2020

2121
# entry points
2222
test_command "uv run pyrust num-cpus"
23+
test_command "uv run pyrust estimate-pi -n 100 -t 2"
2324
test_command "uv run python -m pyrust greet"
2425

2526
echo -e "${GREEN}All tests passed!${NC}"

0 commit comments

Comments
 (0)