prism_q/threading.rs
1//! Caller-supplied Rayon pool, for embedding PRISM-Q in an application that
2//! owns the process-wide pool.
3
4use crate::error::{PrismError, Result};
5
6/// A bounded Rayon pool that PRISM-Q runs inside, leaving the process-wide pool
7/// untouched.
8///
9/// Simulation entry points size the global Rayon pool on first use, which an
10/// embedding application may not want. Work run through [`ThreadPool::install`]
11/// uses this pool instead, and the global pool is neither built nor resized, so
12/// an application that installed its own keeps it.
13///
14/// Pool width is part of what a result depends on. Dense unitary evolution,
15/// seeded terminal sampling, and the stabilizer tableau are bitwise at any
16/// width, parallel reductions move by about 1e-12, and compiled (BTS) shot
17/// payloads differ between widths because the batched sampler splits shots by
18/// pool width. A narrower pool is therefore not a slower route to the same
19/// bytes for every result; the per-path contract is the determinism section of
20/// the threading architecture page.
21///
22/// # Examples
23///
24/// ```
25/// use prism_q::{ThreadPool, run_qasm};
26///
27/// let qasm = r#"
28/// OPENQASM 3.0;
29/// include "stdgates.inc";
30/// qubit[2] q;
31/// h q[0];
32/// cx q[0], q[1];
33/// "#;
34///
35/// let pool = ThreadPool::with_threads(2).expect("build pool");
36/// let result = pool.install(|| run_qasm(qasm, 42)).expect("simulation");
37/// let probs = result.probabilities.expect("no probabilities").to_vec();
38/// assert!((probs[0] - 0.5).abs() < 1e-10);
39/// ```
40pub struct ThreadPool {
41 inner: rayon::ThreadPool,
42}
43
44impl ThreadPool {
45 /// Build a pool of `threads` workers, or of the Rayon default width when
46 /// `threads` is 0.
47 ///
48 /// # Errors
49 ///
50 /// Returns [`PrismError::InvalidParameter`] when the operating system
51 /// refuses to spawn the workers.
52 pub fn with_threads(threads: usize) -> Result<Self> {
53 rayon::ThreadPoolBuilder::new()
54 .num_threads(threads)
55 .build()
56 .map(|inner| Self { inner })
57 .map_err(|e| PrismError::InvalidParameter {
58 message: format!("could not build a Rayon pool of {threads} threads: {e}"),
59 })
60 }
61
62 /// Run `op` on this pool, blocking the calling thread until it returns.
63 ///
64 /// Every Rayon kernel PRISM-Q reaches from `op` runs on these workers. Work
65 /// that escapes `op`, such as a simulation handed to another thread, falls
66 /// back to the global pool.
67 pub fn install<T: Send>(&self, op: impl FnOnce() -> T + Send) -> T {
68 self.inner.install(op)
69 }
70
71 /// Worker count, which is what `rayon::current_num_threads` reports inside
72 /// [`install`](Self::install). Resolves the default width taken by
73 /// `with_threads(0)`.
74 pub fn num_threads(&self) -> usize {
75 self.inner.current_num_threads()
76 }
77}