urng 0.4.5

Universal Random Number Generator
use crate::rng::{Rng32, Rng64};
use crate::testing::_internal::{unit_f64_from_u32, unit_f64_from_u64};
use std::{collections::HashSet, f64};
use thiserror::Error;

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct McPiConfig {
    pub pairs: usize,
    pub max_error_pct: f64,
}

impl Default for McPiConfig {
    fn default() -> Self {
        Self {
            pairs: 1_000_000,
            max_error_pct: 0.1,
        }
    }
}

impl McPiConfig {
    fn validate(&self) -> Result<(), McPiError> {
        if self.pairs == 0 {
            return Err(McPiError::InvalidPairs { pairs: self.pairs });
        }
        if !self.max_error_pct.is_finite() || self.max_error_pct <= 0.0 {
            return Err(McPiError::InvalidMaxErrorPct {
                max_error_pct: self.max_error_pct,
            });
        }
        Ok(())
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum McPiVerdict {
    Pass,
    Fail,
}

#[derive(Debug, Clone, PartialEq)]
pub struct McPiResult {
    pub name: String,
    pub pairs: usize,
    pub inside_circle: u64,
    pub pi_estimate: f64,
    pub absolute_error: f64,
    pub error_pct: f64,
    pub max_error_pct: f64,
    pub verdict: McPiVerdict,
}

#[derive(Debug, Error)]
pub enum McPiError {
    #[error("pairs must be greater than zero: pairs={pairs}")]
    InvalidPairs { pairs: usize },

    #[error("max_error_pct must be finite and > 0: max_error_pct={max_error_pct}")]
    InvalidMaxErrorPct { max_error_pct: f64 },

    #[error("test name must not be empty")]
    EmptyCaseName,

    #[error("duplicate test name in suite: {name}")]
    DuplicateCaseName { name: String },

    #[error(
        "rng produced non-finite value: case={case}, sample_index={sample_index}, axis={axis}, value={value}"
    )]
    NonFiniteSample {
        case: String,
        sample_index: usize,
        axis: &'static str,
        value: f64,
    },

    #[error(
        "rng produced out-of-range value [0,1): case={case}, sample_index={sample_index}, axis={axis}, value={value}"
    )]
    OutOfRangeSample {
        case: String,
        sample_index: usize,
        axis: &'static str,
        value: f64,
    },
}

struct McPiCase<'a> {
    name: String,
    sampler: Box<dyn FnMut() -> f64 + 'a>,
}

fn validate_case_name(name: String) -> Result<String, McPiError> {
    if name.trim().is_empty() {
        return Err(McPiError::EmptyCaseName);
    }
    Ok(name)
}

fn validate_sample(
    case: &str,
    sample_index: usize,
    axis: &'static str,
    x: f64,
) -> Result<(), McPiError> {
    if !x.is_finite() {
        return Err(McPiError::NonFiniteSample {
            case: case.to_string(),
            sample_index,
            axis,
            value: x,
        });
    }
    if !(0.0..1.0).contains(&x) {
        return Err(McPiError::OutOfRangeSample {
            case: case.to_string(),
            sample_index,
            axis,
            value: x,
        });
    }
    Ok(())
}

fn run_mcpi(
    name: String,
    sampler: &mut dyn FnMut() -> f64,
    config: McPiConfig,
) -> Result<McPiResult, McPiError> {
    config.validate()?;

    let mut inside_circle = 0u64;
    for sample_index in 0..config.pairs {
        let x = sampler();
        validate_sample(&name, sample_index, "x", x)?;
        let y = sampler();
        validate_sample(&name, sample_index, "y", y)?;
        if x * x + y * y < 1.0 {
            inside_circle += 1;
        }
    }

    let pi_estimate = 4.0 * inside_circle as f64 / config.pairs as f64;
    let absolute_error = (pi_estimate - f64::consts::PI).abs();
    let error_pct = (absolute_error / f64::consts::PI) * 100.0;
    let verdict = if error_pct <= config.max_error_pct {
        McPiVerdict::Pass
    } else {
        McPiVerdict::Fail
    };

    Ok(McPiResult {
        name,
        pairs: config.pairs,
        inside_circle,
        pi_estimate,
        absolute_error,
        error_pct,
        max_error_pct: config.max_error_pct,
        verdict,
    })
}

macro_rules! impl_mcpi_for_rng {
    ($bits:expr) => {
        paste::paste!{
            #[doc = concat!("Monte Carlo estimation of π using ", $bits, "-bit RNGs.")]
            pub struct [<McPi $bits>]<'a, R: [<Rng $bits>] + 'a> {
                rng: &'a mut R,
                config: McPiConfig,
            }

            impl<'a, R: [<Rng $bits>] + 'a> [<McPi $bits>]<'a, R> {
                pub fn new(rng: &'a mut R) -> Self {
                    Self {
                        rng,
                        config: McPiConfig::default(),
                    }
                }

                pub fn with_config(rng: &'a mut R, config: McPiConfig) -> Result<Self, McPiError> {
                    config.validate()?;
                    Ok(Self { rng, config })
                }

                pub fn config(&self) -> McPiConfig {
                    self.config
                }

                pub fn set_config(&mut self, config: McPiConfig) -> Result<(), McPiError> {
                    config.validate()?;
                    self.config = config;
                    Ok(())
                }

                pub fn run(&mut self, name: impl Into<String>) -> Result<McPiResult, McPiError> {
                    let name = validate_case_name(name.into())?;
                    let mut sampler = || [<unit_f64_from_u $bits>](self.rng.nextu());
                    run_mcpi(name, &mut sampler, self.config)
                }
            }

            pub struct [<McPiSuite $bits>]<'a> {
                config: McPiConfig,
                cases: Vec<McPiCase<'a>>,
            }

            impl<'a> Default for [<McPiSuite $bits>]<'a> {
                fn default() -> Self {
                    Self {
                        config: McPiConfig::default(),
                        cases: Vec::new(),
                    }
                }
            }

            impl<'a> [<McPiSuite $bits>]<'a> {
                pub fn new() -> Self {
                    Self::default()
                }

                pub fn with_config(config: McPiConfig) -> Result<Self, McPiError> {
                    config.validate()?;
                    Ok(Self {
                        config,
                        cases: Vec::new(),
                    })
                }

                pub fn config(&self) -> McPiConfig {
                    self.config
                }

                pub fn set_config(&mut self, config: McPiConfig) -> Result<(), McPiError> {
                    config.validate()?;
                    self.config = config;
                    Ok(())
                }

                pub fn len(&self) -> usize {
                    self.cases.len()
                }

                pub fn is_empty(&self) -> bool {
                    self.cases.is_empty()
                }

                pub fn clear(&mut self) {
                    self.cases.clear();
                }

                pub fn add_rng<R: [<Rng $bits>] + 'a>(
                    &mut self,
                    name: impl Into<String>,
                    rng: &'a mut R,
                ) -> Result<&mut Self, McPiError> {
                    let name = validate_case_name(name.into())?;
                    self.cases.push(McPiCase {
                        name,
                        sampler: Box::new(move || [<unit_f64_from_u $bits>](rng.nextu())),
                    });
                    Ok(self)
                }

                pub fn add_sampler<F>(
                    &mut self,
                    name: impl Into<String>,
                    sampler: F,
                ) -> Result<&mut Self, McPiError>
                where
                    F: FnMut() -> f64 + 'a,
                {
                    let name = validate_case_name(name.into())?;
                    self.cases.push(McPiCase {
                        name,
                        sampler: Box::new(sampler),
                    });
                    Ok(self)
                }

                pub fn run(&mut self) -> Result<Vec<McPiResult>, McPiError> {
                    let mut seen = HashSet::with_capacity(self.cases.len());
                    let mut out = Vec::with_capacity(self.cases.len());
                    for case in &mut self.cases {
                        if !seen.insert(case.name.clone()) {
                            return Err(McPiError::DuplicateCaseName {
                                name: case.name.clone(),
                            });
                        }
                        let result = run_mcpi(case.name.clone(), case.sampler.as_mut(), self.config)?;
                        out.push(result);
                    }
                    Ok(out)
                }
            }
        }
    };
}

impl_mcpi_for_rng!(32);
impl_mcpi_for_rng!(64);

#[cfg(test)]
mod tests {
    use super::*;
    use crate::prelude::*;

    #[test]
    fn mcpi32_works() {
        let mut rng = Sfmt19937::new(0);
        let mut mcpi = McPi32::new(&mut rng);
        let res = unsafe { mcpi.run(stringify!(Sfmt19937)).unwrap_unchecked() };
        assert_eq!(res.verdict, McPiVerdict::Pass);
    }

    #[test]
    fn mcpi64_works() {
        let mut rng = Sfmt1993764::new(0);
        let mut mcpi = McPi64::new(&mut rng);
        let res = unsafe { mcpi.run(stringify!(Sfmt1993764)).unwrap_unchecked() };
        assert_eq!(res.verdict, McPiVerdict::Pass);
    }
}