use core::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct Parallelism {
pub max_threads: NonZeroThreadCount,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct NonZeroThreadCount(u32);
impl NonZeroThreadCount {
#[must_use]
pub const fn new(n: u32) -> Option<Self> {
if n == 0 { None } else { Some(Self(n)) }
}
#[must_use]
pub const fn one() -> Self {
Self(1)
}
#[must_use]
pub const fn get(self) -> u32 {
self.0
}
}
impl Parallelism {
#[must_use]
pub const fn serial() -> Self {
Self { max_threads: NonZeroThreadCount::one() }
}
#[must_use]
pub const fn bounded(max_threads: NonZeroThreadCount) -> Self {
Self { max_threads }
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum Determinism {
PreferFast,
Strict,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct MemoryBudget {
pub soft_limit_bytes: Option<u64>,
pub hard_limit_bytes: Option<u64>,
}
impl MemoryBudget {
#[must_use]
pub const fn unlimited() -> Self {
Self { soft_limit_bytes: None, hard_limit_bytes: None }
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct KernelPolicy {
pub allow_portable_optimized: bool,
pub allow_arch_simd: bool,
pub force_scalar: bool,
}
impl KernelPolicy {
#[must_use]
pub const fn default_policy() -> Self {
Self { allow_portable_optimized: true, allow_arch_simd: true, force_scalar: false }
}
#[must_use]
pub const fn scalar_only() -> Self {
Self { allow_portable_optimized: false, allow_arch_simd: false, force_scalar: true }
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct CachePolicy {
pub enabled: bool,
pub max_bytes: Option<u64>,
}
impl CachePolicy {
#[must_use]
pub const fn disabled() -> Self {
Self { enabled: false, max_bytes: None }
}
#[must_use]
pub const fn enabled(max_bytes: Option<u64>) -> Self {
Self { enabled: true, max_bytes }
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct CacheBudget {
pub max_bytes: u64,
pub used_bytes: u64,
}
impl CacheBudget {
#[must_use]
pub const fn new(max_bytes: u64) -> Self {
Self { max_bytes, used_bytes: 0 }
}
#[must_use]
pub const fn unlimited() -> Self {
Self { max_bytes: u64::MAX, used_bytes: 0 }
}
#[must_use]
pub const fn remaining(self) -> u64 {
self.max_bytes.saturating_sub(self.used_bytes)
}
#[must_use]
pub const fn can_admit(self, additional: u64) -> bool {
self.used_bytes.saturating_add(additional) <= self.max_bytes
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct MonteCarloBudget {
pub evaluations: u64,
pub samples: u64,
pub exact_enumerations: u64,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct MonteCarloError {
pub stderr: f64,
pub samples: u64,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AdaptiveBootstrapBudget {
pub enabled: bool,
pub min_replicates: u32,
pub se_rel_epsilon: f64,
}
impl AdaptiveBootstrapBudget {
#[must_use]
pub const fn enabled_default() -> Self {
Self { enabled: true, min_replicates: 10, se_rel_epsilon: 0.01 }
}
#[must_use]
pub const fn disabled() -> Self {
Self { enabled: false, min_replicates: 0, se_rel_epsilon: 0.0 }
}
}
impl Default for AdaptiveBootstrapBudget {
fn default() -> Self {
Self::enabled_default()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AdaptiveDrawBudget {
pub enabled: bool,
pub min_draws: usize,
pub quantile_width_rel_epsilon: f64,
pub ess_target: f64,
}
impl AdaptiveDrawBudget {
#[must_use]
pub const fn enabled_default() -> Self {
Self {
enabled: true,
min_draws: 32,
quantile_width_rel_epsilon: 0.01,
ess_target: 10_000.0,
}
}
#[must_use]
pub const fn disabled() -> Self {
Self { enabled: false, min_draws: 0, quantile_width_rel_epsilon: 0.0, ess_target: 0.0 }
}
}
impl Default for AdaptiveDrawBudget {
fn default() -> Self {
Self::enabled_default()
}
}
#[derive(Clone, Debug, Default)]
pub struct CancellationToken {
cancelled: Arc<AtomicBool>,
}
impl CancellationToken {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::SeqCst);
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::SeqCst)
}
}
pub trait ProgressSink: Send + Sync {
fn report(&self, fraction: f64, stage: &str);
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RngFactory {
master_seed: u64,
}
impl RngFactory {
#[must_use]
pub const fn from_seed(master_seed: u64) -> Self {
Self { master_seed }
}
#[must_use]
pub const fn master_seed(&self) -> u64 {
self.master_seed
}
#[must_use]
pub fn stream(&self, stream_id: u64) -> CausalRng {
let seed = mix_seed(self.master_seed, stream_id);
CausalRng::from_seed(seed)
}
}
#[derive(Clone, Debug)]
pub struct CausalRng {
state: u64,
}
impl CausalRng {
#[must_use]
pub const fn from_seed(seed: u64) -> Self {
Self { state: seed ^ 0x9E37_79B9_7F4A_7C15 }
}
#[must_use]
pub const fn from_state(state: u64) -> Self {
Self { state }
}
#[must_use]
pub const fn state(&self) -> u64 {
self.state
}
#[must_use]
pub fn next_u64(&mut self) -> u64 {
self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
#[must_use]
pub fn next_f64(&mut self) -> f64 {
let bits = self.next_u64() >> 11;
#[allow(clippy::cast_precision_loss)]
{
bits as f64 * (1.0 / ((1u64 << 53) as f64))
}
}
}
fn mix_seed(master: u64, stream_id: u64) -> u64 {
let mut z = master.wrapping_add(stream_id).wrapping_mul(0xD6E8_FEB8_6659_FD93);
z = (z ^ (z >> 32)).wrapping_mul(0xD6E8_FEB8_6659_FD93);
z ^ (z >> 32)
}
pub struct ExecutionContext {
pub parallelism: Parallelism,
pub determinism: Determinism,
pub rng: RngFactory,
pub memory: MemoryBudget,
pub cancellation: CancellationToken,
pub progress: Option<Arc<dyn ProgressSink>>,
pub kernel_policy: KernelPolicy,
pub cache_policy: CachePolicy,
pub adaptive_bootstrap: AdaptiveBootstrapBudget,
pub adaptive_draws: AdaptiveDrawBudget,
}
impl ExecutionContext {
#[must_use]
pub fn for_tests(seed: u64) -> Self {
Self {
parallelism: Parallelism::serial(),
determinism: Determinism::Strict,
rng: RngFactory::from_seed(seed),
memory: MemoryBudget::unlimited(),
cancellation: CancellationToken::new(),
progress: None,
kernel_policy: KernelPolicy::scalar_only(),
cache_policy: CachePolicy::disabled(),
adaptive_bootstrap: AdaptiveBootstrapBudget::disabled(),
adaptive_draws: AdaptiveDrawBudget::disabled(),
}
}
#[must_use]
pub fn production(seed: u64, max_threads: u32) -> Self {
let threads =
NonZeroThreadCount::new(max_threads.max(1)).unwrap_or_else(NonZeroThreadCount::one);
Self {
parallelism: Parallelism::bounded(threads),
determinism: Determinism::Strict,
rng: RngFactory::from_seed(seed),
memory: MemoryBudget::unlimited(),
cancellation: CancellationToken::new(),
progress: None,
kernel_policy: KernelPolicy::default_policy(),
cache_policy: CachePolicy::enabled(None),
adaptive_bootstrap: AdaptiveBootstrapBudget::enabled_default(),
adaptive_draws: AdaptiveDrawBudget::enabled_default(),
}
}
}
impl core::fmt::Debug for ExecutionContext {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ExecutionContext")
.field("parallelism", &self.parallelism)
.field("determinism", &self.determinism)
.field("rng", &self.rng)
.field("memory", &self.memory)
.field("cancellation_cancelled", &self.cancellation.is_cancelled())
.field("progress_is_some", &self.progress.is_some())
.field("kernel_policy", &self.kernel_policy)
.field("cache_policy", &self.cache_policy)
.field("adaptive_bootstrap", &self.adaptive_bootstrap)
.field("adaptive_draws", &self.adaptive_draws)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rng_streams_are_deterministic() {
let factory = RngFactory::from_seed(42);
let mut a1 = factory.stream(0);
let mut a2 = factory.stream(0);
let mut b = factory.stream(1);
let seq_a1: Vec<u64> = (0..8).map(|_| a1.next_u64()).collect();
let seq_a2: Vec<u64> = (0..8).map(|_| a2.next_u64()).collect();
let seq_b: Vec<u64> = (0..8).map(|_| b.next_u64()).collect();
assert_eq!(seq_a1, seq_a2);
assert_ne!(seq_a1, seq_b);
}
#[test]
fn independent_factories_same_seed_match() {
let f1 = RngFactory::from_seed(7);
let f2 = RngFactory::from_seed(7);
let mut s1 = f1.stream(99);
let mut s2 = f2.stream(99);
for _ in 0..32 {
assert_eq!(s1.next_u64(), s2.next_u64());
}
}
#[test]
fn cancellation_is_shared_across_clones() {
let token = CancellationToken::new();
let clone = token.clone();
assert!(!token.is_cancelled());
clone.cancel();
assert!(token.is_cancelled());
}
#[test]
fn f64_draws_are_in_unit_interval() {
let mut rng = CausalRng::from_seed(123);
for _ in 0..1000 {
let x = rng.next_f64();
assert!((0.0..1.0).contains(&x));
}
}
}