///| Deterministic pseudo-random numbers for offline experiments. Model-serving
///| integrations should use their platform RNG, but fixtures need a small,
///|
/// portable generator whose state can be recorded in a benchmark report.
pub enum RngError {
InvalidSeed(Int)
InvalidBound(Int)
} derive(Eq, Debug)
///| Park-Miller's minimal standard generator, implemented with Schrage's
///|
/// method so intermediate multiplication stays within signed 32-bit range.
pub struct DeterministicRng {
mut state : Int
}
///|
pub fn DeterministicRng::new(seed : Int) -> Result[DeterministicRng, RngError] {
if seed <= 0 || seed >= 2147483647 {
Err(InvalidSeed(seed))
} else {
Ok({ state: seed })
}
}
///|
pub fn DeterministicRng::state(self : DeterministicRng) -> Int {
self.state
}
///|
/// Advance once and return an integer in `[1, 2147483646]`.
pub fn DeterministicRng::next(self : DeterministicRng) -> Int {
let quotient = self.state / 44488
let remainder = self.state % 44488
let candidate = 48271 * remainder - 3399 * quotient
self.state = if candidate > 0 { candidate } else { candidate + 2147483647 }
self.state
}
///|
/// Return a portable unit-interval value in `[0, 1)`.
pub fn DeterministicRng::next_unit(self : DeterministicRng) -> Double {
(self.next() - 1).to_double() / 2147483646.0
}
///| Choose an integer in `[0, bound)` without introducing a dependency on an
///| OS RNG. For synthetic workloads the tiny modulo bias is irrelevant; the
///|
/// important property is exact replay from the same seed.
pub fn DeterministicRng::next_below(
self : DeterministicRng,
bound : Int,
) -> Result[Int, RngError] {
if bound <= 0 {
return Err(InvalidBound(bound))
}
Ok(self.next() % bound)
}
///|
/// Fill caller-owned fixture arrays with deterministic random thresholds.
pub fn DeterministicRng::uniforms(
self : DeterministicRng,
count : Int,
) -> Result[Array[Double], RngError] {
if count < 0 {
return Err(InvalidBound(count))
}
let values : Array[Double] = []
for _ in 0..