From d0b0f295b10e15fd172577b6f47d89d0de4396c5 Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Mon, 28 Jul 2025 20:17:21 +0100 Subject: [PATCH 01/18] Implement Rng trait and RangeSample conversion for random number generation --- src/random/random_core.rs | 139 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 src/random/random_core.rs diff --git a/src/random/random_core.rs b/src/random/random_core.rs new file mode 100644 index 0000000..a4c214a --- /dev/null +++ b/src/random/random_core.rs @@ -0,0 +1,139 @@ +use std::f64::consts::PI; +use std::ops::Range; + +/// Trait implemented by random number generators. +pub trait Rng { + /// Generate the next random `u64` value. + fn next_u64(&mut self) -> u64; + + /// Generate a value uniformly in the given range. + fn random_range(&mut self, range: Range) -> T + where + T: RangeSample, + { + T::from_u64(self.next_u64(), &range) + } + + /// Generate a boolean with probability 0.5 of being `true`. + fn gen_bool(&mut self) -> bool { + self.random_range(0..2usize) == 1 + } + + /// Sample from a normal distribution using the Box-Muller transform. + fn normal(&mut self, mean: f64, sd: f64) -> f64 { + let u1 = self.random_range(0.0..1.0); + let u2 = self.random_range(0.0..1.0); + mean + sd * (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos() + } +} + +/// Conversion from a raw `u64` into a type within a range. +pub trait RangeSample: Sized { + fn from_u64(value: u64, range: &Range) -> Self; +} + +impl RangeSample for usize { + fn from_u64(value: u64, range: &Range) -> Self { + let span = range.end - range.start; + (value as usize % span) + range.start + } +} + +impl RangeSample for f64 { + fn from_u64(value: u64, range: &Range) -> Self { + let span = range.end - range.start; + range.start + (value as f64 / u64::MAX as f64) * span + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::random::{CryptoRng, Prng, SliceRandom}; + + #[test] + fn test_prng_determinism() { + let mut a = Prng::new(42); + let mut b = Prng::new(42); + for _ in 0..5 { + assert_eq!(a.next_u64(), b.next_u64()); + } + } + + #[test] + fn test_random_range_f64() { + let mut rng = Prng::new(1); + for _ in 0..10 { + let v = rng.random_range(-1.0..1.0); + assert!(v >= -1.0 && v < 1.0); + } + } + + #[test] + fn test_shuffle_slice() { + let mut rng = Prng::new(3); + let mut arr = [1, 2, 3, 4, 5]; + let orig = arr.clone(); + arr.shuffle(&mut rng); + assert_eq!(arr.len(), orig.len()); + let mut sorted = arr.to_vec(); + sorted.sort(); + assert_eq!(sorted, orig.to_vec()); + } + + #[test] + fn test_random_range_usize() { + let mut rng = Prng::new(9); + for _ in 0..100 { + let v = rng.random_range(10..20); + assert!(v >= 10 && v < 20); + } + } + + #[test] + fn test_gen_bool_balance() { + let mut rng = Prng::new(123); + let mut trues = 0; + for _ in 0..1000 { + if rng.gen_bool() { + trues += 1; + } + } + let ratio = trues as f64 / 1000.0; + assert!(ratio > 0.4 && ratio < 0.6); + } + + #[test] + fn test_normal_distribution() { + let mut rng = Prng::new(7); + let mut sum = 0.0; + let mut sum_sq = 0.0; + let mean = 5.0; + let sd = 2.0; + let n = 5000; + for _ in 0..n { + let val = rng.normal(mean, sd); + sum += val; + sum_sq += val * val; + } + let sample_mean = sum / n as f64; + let sample_var = sum_sq / n as f64 - sample_mean * sample_mean; + assert!((sample_mean - mean).abs() < 0.1); + assert!((sample_var - sd * sd).abs() < 0.2 * sd * sd); + } + + #[test] + fn test_crypto_rng_nonzero() { + let mut rng = CryptoRng::new(); + let mut all_same = true; + let mut prev = rng.next_u64(); + for _ in 0..5 { + let val = rng.next_u64(); + if val != prev { + all_same = false; + } + prev = val; + } + assert!(!all_same, "CryptoRng produced identical values"); + } +} From 6fd796cceb55f3fd12a1df2a06a641539cdd108c Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Mon, 28 Jul 2025 20:17:35 +0100 Subject: [PATCH 02/18] Add SliceRandom trait for shuffling slices using RNG --- src/random/seq.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/random/seq.rs diff --git a/src/random/seq.rs b/src/random/seq.rs new file mode 100644 index 0000000..621869c --- /dev/null +++ b/src/random/seq.rs @@ -0,0 +1,16 @@ +use crate::random::Rng; + +/// Trait for randomizing slices. +pub trait SliceRandom { + /// Shuffle the slice in place using the provided RNG. + fn shuffle(&mut self, rng: &mut R); +} + +impl SliceRandom for [T] { + fn shuffle(&mut self, rng: &mut R) { + for i in (1..self.len()).rev() { + let j = rng.random_range(0..(i + 1)); + self.swap(i, j); + } + } +} From d75bd7a08f856122f57e1c0e7f95c50aa1cbe559 Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Mon, 28 Jul 2025 20:17:59 +0100 Subject: [PATCH 03/18] Add XorShift64-based pseudo random number generator implementation --- src/random/prng.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/random/prng.rs diff --git a/src/random/prng.rs b/src/random/prng.rs new file mode 100644 index 0000000..fd2fafc --- /dev/null +++ b/src/random/prng.rs @@ -0,0 +1,41 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::random::Rng; + +/// Simple XorShift64-based pseudo random number generator. +#[derive(Clone)] +pub struct Prng { + state: u64, +} + +impl Prng { + /// Create a new generator from the given seed. + pub fn new(seed: u64) -> Self { + Self { state: seed } + } + + /// Create a generator seeded from the current time. + pub fn from_entropy() -> Self { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + Self::new(nanos) + } +} + +impl Rng for Prng { + fn next_u64(&mut self) -> u64 { + let mut x = self.state; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.state = x; + x + } +} + +/// Convenience constructor using system entropy. +pub fn rng() -> Prng { + Prng::from_entropy() +} From 28793e5b07f0fe7ed439d060b532df37909b3637 Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Mon, 28 Jul 2025 20:19:01 +0100 Subject: [PATCH 04/18] Add CryptoRng for cryptographically secure random number generation --- src/random/crypto.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/random/crypto.rs diff --git a/src/random/crypto.rs b/src/random/crypto.rs new file mode 100644 index 0000000..1bd6d8f --- /dev/null +++ b/src/random/crypto.rs @@ -0,0 +1,32 @@ +use std::fs::File; +use std::io::Read; + +use crate::random::Rng; + +/// Cryptographically secure RNG sourcing randomness from `/dev/urandom`. +pub struct CryptoRng { + file: File, +} + +impl CryptoRng { + /// Open `/dev/urandom` and create a new generator. + pub fn new() -> Self { + let file = File::open("/dev/urandom").expect("failed to open /dev/urandom"); + Self { file } + } +} + +impl Rng for CryptoRng { + fn next_u64(&mut self) -> u64 { + let mut buf = [0u8; 8]; + self.file + .read_exact(&mut buf) + .expect("failed reading from /dev/urandom"); + u64::from_ne_bytes(buf) + } +} + +/// Convenience constructor for [`CryptoRng`]. +pub fn crypto_rng() -> CryptoRng { + CryptoRng::new() +} From 5a5baf9716ce158cd125e34c7022d7217c400b72 Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Mon, 28 Jul 2025 20:19:12 +0100 Subject: [PATCH 05/18] Add initial implementation of random module with submodules and prelude exports --- src/random/mod.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/random/mod.rs diff --git a/src/random/mod.rs b/src/random/mod.rs new file mode 100644 index 0000000..b40a343 --- /dev/null +++ b/src/random/mod.rs @@ -0,0 +1,14 @@ +pub mod crypto; +pub mod prng; +pub mod random_core; +pub mod seq; + +pub use crypto::{crypto_rng, CryptoRng}; +pub use prng::{rng, Prng}; +pub use random_core::{RangeSample, Rng}; +pub use seq::SliceRandom; + +pub mod prelude { + pub use super::seq::SliceRandom; + pub use super::{crypto_rng, rng, CryptoRng, Prng, RangeSample, Rng}; +} From 252c8a3d298b39f873f86318b7d53bdab3ff56ac Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Mon, 28 Jul 2025 20:23:59 +0100 Subject: [PATCH 06/18] Refactor KMeans module to use inbuilt random --- src/compute/models/k_means.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/compute/models/k_means.rs b/src/compute/models/k_means.rs index 83412e3..0766075 100644 --- a/src/compute/models/k_means.rs +++ b/src/compute/models/k_means.rs @@ -1,7 +1,6 @@ use crate::compute::stats::mean_vertical; use crate::matrix::Matrix; -use rand::rng; -use rand::seq::SliceRandom; +use crate::random::prelude::*; pub struct KMeans { pub centroids: Matrix, // (k, n_features) @@ -193,7 +192,11 @@ mod tests { break; } } - assert!(matches_data_point, "Centroid {} (empty cluster) does not match any data point", c); + assert!( + matches_data_point, + "Centroid {} (empty cluster) does not match any data point", + c + ); } } break; @@ -360,5 +363,4 @@ mod tests { assert_eq!(predicted_label.len(), 1); assert!(predicted_label[0] < k); } - } From 4a1843183a036343337a43d4a53357f950a8002d Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Mon, 28 Jul 2025 20:36:52 +0100 Subject: [PATCH 07/18] Add documentation for the random module --- src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 510f36d..871819f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,3 +11,6 @@ pub mod utils; /// Documentation for the [`crate::compute`] module. pub mod compute; + +/// Documentation for the [`crate::random`] module. +pub mod random; From 5934b163f5237f266a6fd2db7e6d3af237d9c1a2 Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Mon, 28 Jul 2025 20:37:08 +0100 Subject: [PATCH 08/18] Refactor random number generation to use rustframe's random module --- examples/game_of_life.rs | 6 +++--- src/compute/models/dense_nn.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index 020a0a5..e596cda 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -5,8 +5,8 @@ //! To modify the behaviour of the example, please change the constants at the top of this file. //! By default, -use rand::{self, Rng}; use rustframe::matrix::{BoolMatrix, BoolOps, IntMatrix, Matrix}; +use rustframe::random::{rng, Rng}; use std::{thread, time}; const BOARD_SIZE: usize = 20; // Size of the board (50x50) @@ -265,7 +265,7 @@ pub fn generate_glider(board: &mut BoolMatrix, board_size: usize) { // Initialize with a Glider pattern. // It demonstrates how to set specific cells in the matrix. // This demonstrates `IndexMut` for `current_board[(r, c)] = true;`. - let mut rng = rand::rng(); + let mut rng = rng(); let r_offset = rng.random_range(0..(board_size - 3)); let c_offset = rng.random_range(0..(board_size - 3)); if board.rows() >= r_offset + 3 && board.cols() >= c_offset + 3 { @@ -281,7 +281,7 @@ pub fn generate_pulsar(board: &mut BoolMatrix, board_size: usize) { // Initialize with a Pulsar pattern. // This demonstrates how to set specific cells in the matrix. // This demonstrates `IndexMut` for `current_board[(r, c)] = true;`. - let mut rng = rand::rng(); + let mut rng = rng(); let r_offset = rng.random_range(0..(board_size - 17)); let c_offset = rng.random_range(0..(board_size - 17)); if board.rows() >= r_offset + 17 && board.cols() >= c_offset + 17 { diff --git a/src/compute/models/dense_nn.rs b/src/compute/models/dense_nn.rs index cdb2e32..ab5c05c 100644 --- a/src/compute/models/dense_nn.rs +++ b/src/compute/models/dense_nn.rs @@ -1,6 +1,6 @@ use crate::compute::models::activations::{drelu, relu, sigmoid}; use crate::matrix::{Matrix, SeriesOps}; -use rand::prelude::*; +use crate::random::prelude::*; /// Supported activation functions #[derive(Clone)] @@ -46,7 +46,7 @@ pub enum InitializerKind { impl InitializerKind { pub fn initialize(&self, rows: usize, cols: usize) -> Matrix { - let mut rng = rand::rng(); + let mut rng = rng(); let fan_in = rows; let fan_out = cols; let limit = match self { From b4520b0d30ec8af823f7c6a0788a429e47fad580 Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Mon, 28 Jul 2025 20:37:24 +0100 Subject: [PATCH 09/18] Update README to reflect built-in random number generation utilities --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e6fab01..8d0b05b 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Rustframe is an educational project, and is not intended for production use. It - **[Coming Soon]** _DataFrame_ - Multi-type data structure for heterogeneous data, with labeled columns and typed row indices. -- **[Coming Soon]** _Random number utils_ - Random number generation utilities for statistical sampling and simulations. (Currently using the [`rand`](https://crates.io/crates/rand) crate.) +- **Random number utils** - Built-in pseudo and cryptographically secure generators for simulations. #### Matrix and Frame functionality From cd13d98110716089e014674a26785a52c31f278d Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Mon, 28 Jul 2025 20:37:37 +0100 Subject: [PATCH 10/18] Remove rand dependency from Cargo.toml --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4339319..58d82f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ crate-type = ["cdylib", "lib"] [dependencies] chrono = "^0.4.10" criterion = { version = "0.5", features = ["html_reports"], optional = true } -rand = "^0.9.1" [features] bench = ["dep:criterion"] From 289c70d9e93335ff25caaa88c399311f2140607a Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Mon, 28 Jul 2025 23:11:17 +0100 Subject: [PATCH 11/18] Refactor tests to remove unused random number generator tests and enhance range sample validation --- src/random/random_core.rs | 101 +++++++++----------------------------- 1 file changed, 22 insertions(+), 79 deletions(-) diff --git a/src/random/random_core.rs b/src/random/random_core.rs index a4c214a..8c3a02c 100644 --- a/src/random/random_core.rs +++ b/src/random/random_core.rs @@ -49,91 +49,34 @@ impl RangeSample for f64 { #[cfg(test)] mod tests { use super::*; - use crate::random::{CryptoRng, Prng, SliceRandom}; #[test] - fn test_prng_determinism() { - let mut a = Prng::new(42); - let mut b = Prng::new(42); - for _ in 0..5 { - assert_eq!(a.next_u64(), b.next_u64()); + fn test_range_sample_usize_boundary() { + assert_eq!(::from_u64(0, &(0..1)), 0); + assert_eq!(::from_u64(u64::MAX, &(0..1)), 0); + } + + #[test] + fn test_range_sample_f64_boundary() { + let v0 = ::from_u64(0, &(0.0..1.0)); + let vmax = ::from_u64(u64::MAX, &(0.0..1.0)); + assert!(v0 >= 0.0 && v0 < 1.0); + assert!(vmax > 0.999999999999 && vmax <= 1.0); + } + + #[test] + fn test_range_sample_usize_varied() { + for i in 0..5 { + let v = ::from_u64(i, &(10..15)); + assert!(v >= 10 && v < 15); } } #[test] - fn test_random_range_f64() { - let mut rng = Prng::new(1); - for _ in 0..10 { - let v = rng.random_range(-1.0..1.0); - assert!(v >= -1.0 && v < 1.0); + fn test_range_sample_f64_span() { + for val in [0, u64::MAX / 2, u64::MAX] { + let f = ::from_u64(val, &(2.0..4.0)); + assert!(f >= 2.0 && f <= 4.0); } } - - #[test] - fn test_shuffle_slice() { - let mut rng = Prng::new(3); - let mut arr = [1, 2, 3, 4, 5]; - let orig = arr.clone(); - arr.shuffle(&mut rng); - assert_eq!(arr.len(), orig.len()); - let mut sorted = arr.to_vec(); - sorted.sort(); - assert_eq!(sorted, orig.to_vec()); - } - - #[test] - fn test_random_range_usize() { - let mut rng = Prng::new(9); - for _ in 0..100 { - let v = rng.random_range(10..20); - assert!(v >= 10 && v < 20); - } - } - - #[test] - fn test_gen_bool_balance() { - let mut rng = Prng::new(123); - let mut trues = 0; - for _ in 0..1000 { - if rng.gen_bool() { - trues += 1; - } - } - let ratio = trues as f64 / 1000.0; - assert!(ratio > 0.4 && ratio < 0.6); - } - - #[test] - fn test_normal_distribution() { - let mut rng = Prng::new(7); - let mut sum = 0.0; - let mut sum_sq = 0.0; - let mean = 5.0; - let sd = 2.0; - let n = 5000; - for _ in 0..n { - let val = rng.normal(mean, sd); - sum += val; - sum_sq += val * val; - } - let sample_mean = sum / n as f64; - let sample_var = sum_sq / n as f64 - sample_mean * sample_mean; - assert!((sample_mean - mean).abs() < 0.1); - assert!((sample_var - sd * sd).abs() < 0.2 * sd * sd); - } - - #[test] - fn test_crypto_rng_nonzero() { - let mut rng = CryptoRng::new(); - let mut all_same = true; - let mut prev = rng.next_u64(); - for _ in 0..5 { - let val = rng.next_u64(); - if val != prev { - all_same = false; - } - prev = val; - } - assert!(!all_same, "CryptoRng produced identical values"); - } } From 113831dc8c842d51f53707971f7a019c9e2da665 Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Mon, 28 Jul 2025 23:11:26 +0100 Subject: [PATCH 12/18] Add comprehensive tests for CryptoRng functionality and distribution properties --- src/random/crypto.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/src/random/crypto.rs b/src/random/crypto.rs index 1bd6d8f..7aaf866 100644 --- a/src/random/crypto.rs +++ b/src/random/crypto.rs @@ -30,3 +30,85 @@ impl Rng for CryptoRng { pub fn crypto_rng() -> CryptoRng { CryptoRng::new() } + +#[cfg(test)] +mod tests { + use super::*; + use crate::random::Rng; + use std::collections::HashSet; + + #[test] + fn test_crypto_rng_nonzero() { + let mut rng = CryptoRng::new(); + let mut all_same = true; + let mut prev = rng.next_u64(); + for _ in 0..5 { + let val = rng.next_u64(); + if val != prev { + all_same = false; + } + prev = val; + } + assert!(!all_same, "CryptoRng produced identical values"); + } + + #[test] + fn test_crypto_rng_variation_large() { + let mut rng = CryptoRng::new(); + let mut values = HashSet::new(); + for _ in 0..100 { + values.insert(rng.next_u64()); + } + assert!(values.len() > 90, "CryptoRng output not varied enough"); + } + + #[test] + fn test_crypto_rng_random_range_uniform() { + let mut rng = CryptoRng::new(); + let mut counts = [0usize; 10]; + for _ in 0..1000 { + let v = rng.random_range(0..10usize); + counts[v] += 1; + } + for &c in &counts { + assert!( + (c as isize - 100).abs() < 50, + "Crypto RNG counts far from uniform: {c}" + ); + } + } + + #[test] + fn test_crypto_normal_distribution() { + let mut rng = CryptoRng::new(); + let mean = 0.0; + let sd = 1.0; + let n = 2000; + let mut sum = 0.0; + let mut sum_sq = 0.0; + for _ in 0..n { + let val = rng.normal(mean, sd); + sum += val; + sum_sq += val * val; + } + let sample_mean = sum / n as f64; + let sample_var = sum_sq / n as f64 - sample_mean * sample_mean; + assert!(sample_mean.abs() < 0.1); + assert!((sample_var - 1.0).abs() < 0.2); + } + + #[test] + fn test_two_instances_different_values() { + let mut a = CryptoRng::new(); + let mut b = CryptoRng::new(); + let va = a.next_u64(); + let vb = b.next_u64(); + assert_ne!(va, vb); + } + + #[test] + fn test_crypto_rng_helper_function() { + let mut rng = crypto_rng(); + let _ = rng.next_u64(); + } +} From afcb29e7166dd7fd50291e7a36482f30e336d32a Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Mon, 28 Jul 2025 23:11:54 +0100 Subject: [PATCH 13/18] Add extensive tests for Prng functionality, including range checks and distribution properties --- src/random/prng.rs | 117 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/src/random/prng.rs b/src/random/prng.rs index fd2fafc..4c9dee5 100644 --- a/src/random/prng.rs +++ b/src/random/prng.rs @@ -39,3 +39,120 @@ impl Rng for Prng { pub fn rng() -> Prng { Prng::from_entropy() } + +#[cfg(test)] +mod tests { + use super::*; + use crate::random::Rng; + + #[test] + fn test_prng_determinism() { + let mut a = Prng::new(42); + let mut b = Prng::new(42); + for _ in 0..5 { + assert_eq!(a.next_u64(), b.next_u64()); + } + } + + #[test] + fn test_random_range_f64() { + let mut rng = Prng::new(1); + for _ in 0..10 { + let v = rng.random_range(-1.0..1.0); + assert!(v >= -1.0 && v < 1.0); + } + } + + #[test] + fn test_random_range_usize() { + let mut rng = Prng::new(9); + for _ in 0..100 { + let v = rng.random_range(10..20); + assert!(v >= 10 && v < 20); + } + } + + #[test] + fn test_gen_bool_balance() { + let mut rng = Prng::new(123); + let mut trues = 0; + for _ in 0..1000 { + if rng.gen_bool() { + trues += 1; + } + } + let ratio = trues as f64 / 1000.0; + assert!(ratio > 0.4 && ratio < 0.6); + } + + #[test] + fn test_normal_distribution() { + let mut rng = Prng::new(7); + let mut sum = 0.0; + let mut sum_sq = 0.0; + let mean = 5.0; + let sd = 2.0; + let n = 5000; + for _ in 0..n { + let val = rng.normal(mean, sd); + sum += val; + sum_sq += val * val; + } + let sample_mean = sum / n as f64; + let sample_var = sum_sq / n as f64 - sample_mean * sample_mean; + assert!((sample_mean - mean).abs() < 0.1); + assert!((sample_var - sd * sd).abs() < 0.2 * sd * sd); + } + + #[test] + fn test_prng_from_entropy_unique() { + use std::{collections::HashSet, thread, time::Duration}; + let mut seen = HashSet::new(); + for _ in 0..5 { + let mut rng = Prng::from_entropy(); + seen.insert(rng.next_u64()); + thread::sleep(Duration::from_micros(1)); + } + assert!(seen.len() > 1, "Entropy seeds produced identical outputs"); + } + + #[test] + fn test_prng_uniform_distribution() { + let mut rng = Prng::new(12345); + let mut counts = [0usize; 10]; + for _ in 0..10000 { + let v = rng.random_range(0..10usize); + counts[v] += 1; + } + for &c in &counts { + assert!( + (c as isize - 1000).abs() < 150, + "PRNG counts far from uniform: {c}" + ); + } + } + + #[test] + fn test_prng_different_seeds_different_output() { + let mut a = Prng::new(1); + let mut b = Prng::new(2); + let va = a.next_u64(); + let vb = b.next_u64(); + assert_ne!(va, vb); + } + + #[test] + fn test_prng_gen_bool_varies() { + let mut rng = Prng::new(99); + let mut seen_true = false; + let mut seen_false = false; + for _ in 0..100 { + if rng.gen_bool() { + seen_true = true; + } else { + seen_false = true; + } + } + assert!(seen_true && seen_false); + } +} From 3f56b378b26cc0f0ca534d1c922d17f81d9a86d9 Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Mon, 28 Jul 2025 23:12:20 +0100 Subject: [PATCH 14/18] Add unit tests for SliceRandom trait and shuffle functionality --- src/random/seq.rs | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/random/seq.rs b/src/random/seq.rs index 621869c..35c0d0b 100644 --- a/src/random/seq.rs +++ b/src/random/seq.rs @@ -14,3 +14,63 @@ impl SliceRandom for [T] { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::random::{CryptoRng, Prng}; + + #[test] + fn test_shuffle_slice() { + let mut rng = Prng::new(3); + let mut arr = [1, 2, 3, 4, 5]; + let orig = arr.clone(); + arr.shuffle(&mut rng); + assert_eq!(arr.len(), orig.len()); + let mut sorted = arr.to_vec(); + sorted.sort(); + assert_eq!(sorted, orig.to_vec()); + } + + #[test] + fn test_slice_shuffle_deterministic_with_prng() { + let mut rng1 = Prng::new(11); + let mut rng2 = Prng::new(11); + let mut a = [1u8, 2, 3, 4, 5, 6, 7, 8, 9]; + let mut b = a.clone(); + a.shuffle(&mut rng1); + b.shuffle(&mut rng2); + assert_eq!(a, b); + } + + #[test] + fn test_slice_shuffle_crypto_random_changes() { + let mut rng1 = CryptoRng::new(); + let mut rng2 = CryptoRng::new(); + let orig = [1u8, 2, 3, 4, 5, 6, 7, 8, 9]; + let mut a = orig.clone(); + let mut b = orig.clone(); + a.shuffle(&mut rng1); + b.shuffle(&mut rng2); + assert!(a != orig || b != orig, "Shuffles did not change order"); + assert_ne!(a, b, "Two Crypto RNG shuffles produced same order"); + } + + #[test] + fn test_shuffle_single_element_no_change() { + let mut rng = Prng::new(1); + let mut arr = [42]; + arr.shuffle(&mut rng); + assert_eq!(arr, [42]); + } + + #[test] + fn test_multiple_shuffles_different_results() { + let mut rng = Prng::new(5); + let mut arr1 = [1, 2, 3, 4]; + let mut arr2 = [1, 2, 3, 4]; + arr1.shuffle(&mut rng); + arr2.shuffle(&mut rng); + assert_ne!(arr1, arr2); + } +} From 2ea83727a1eb63e4b32aa4e65b0f32d0e3d68d29 Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Tue, 29 Jul 2025 00:36:05 +0100 Subject: [PATCH 15/18] enhance unittests for all random functionalities --- src/random/crypto.rs | 54 +++++++++++++++++++++++++++++ src/random/prng.rs | 72 +++++++++++++++++++++++++++++++++++++++ src/random/random_core.rs | 16 +++++++++ src/random/seq.rs | 29 ++++++++++++++++ 4 files changed, 171 insertions(+) diff --git a/src/random/crypto.rs b/src/random/crypto.rs index 7aaf866..a6367de 100644 --- a/src/random/crypto.rs +++ b/src/random/crypto.rs @@ -111,4 +111,58 @@ mod tests { let mut rng = crypto_rng(); let _ = rng.next_u64(); } + + #[test] + fn test_crypto_normal_zero_sd() { + let mut rng = CryptoRng::new(); + for _ in 0..5 { + let v = rng.normal(10.0, 0.0); + assert_eq!(v, 10.0); + } + } + + #[test] + fn test_crypto_shuffle_empty_slice() { + use crate::random::SliceRandom; + let mut rng = CryptoRng::new(); + let mut arr: [u8; 0] = []; + arr.shuffle(&mut rng); + assert!(arr.is_empty()); + } + + #[test] + fn test_crypto_chi_square_uniform() { + let mut rng = CryptoRng::new(); + let mut counts = [0usize; 10]; + let samples = 10000; + for _ in 0..samples { + let v = rng.random_range(0..10usize); + counts[v] += 1; + } + let expected = samples as f64 / 10.0; + let chi2: f64 = counts + .iter() + .map(|&c| { + let diff = c as f64 - expected; + diff * diff / expected + }) + .sum(); + assert!(chi2 < 40.0, "chi-square statistic too high: {chi2}"); + } + + #[test] + fn test_crypto_monobit() { + let mut rng = CryptoRng::new(); + let mut ones = 0usize; + let samples = 1000; + for _ in 0..samples { + ones += rng.next_u64().count_ones() as usize; + } + let total_bits = samples * 64; + let ratio = ones as f64 / total_bits as f64; + assert!( + (ratio - 0.5).abs() < 0.02, + "bit ratio far from 0.5: {ratio}" + ); + } } diff --git a/src/random/prng.rs b/src/random/prng.rs index 4c9dee5..2b0e304 100644 --- a/src/random/prng.rs +++ b/src/random/prng.rs @@ -155,4 +155,76 @@ mod tests { } assert!(seen_true && seen_false); } + + #[test] + fn test_random_range_single_usize() { + let mut rng = Prng::new(42); + for _ in 0..10 { + let v = rng.random_range(5..6); + assert_eq!(v, 5); + } + } + + #[test] + fn test_random_range_single_f64() { + let mut rng = Prng::new(42); + for _ in 0..10 { + let v = rng.random_range(1.234..1.235); + assert!(v >= 1.234 && v < 1.235); + } + } + + #[test] + fn test_prng_normal_zero_sd() { + let mut rng = Prng::new(7); + for _ in 0..5 { + let v = rng.normal(3.0, 0.0); + assert_eq!(v, 3.0); + } + } + + #[test] + fn test_random_range_extreme_usize() { + let mut rng = Prng::new(5); + for _ in 0..10 { + let v = rng.random_range(0..usize::MAX); + assert!(v < usize::MAX); + } + } + + #[test] + fn test_prng_chi_square_uniform() { + let mut rng = Prng::new(12345); + let mut counts = [0usize; 10]; + let samples = 10000; + for _ in 0..samples { + let v = rng.random_range(0..10usize); + counts[v] += 1; + } + let expected = samples as f64 / 10.0; + let chi2: f64 = counts + .iter() + .map(|&c| { + let diff = c as f64 - expected; + diff * diff / expected + }) + .sum(); + assert!(chi2 < 20.0, "chi-square statistic too high: {chi2}"); + } + + #[test] + fn test_prng_monobit() { + let mut rng = Prng::new(42); + let mut ones = 0usize; + let samples = 1000; + for _ in 0..samples { + ones += rng.next_u64().count_ones() as usize; + } + let total_bits = samples * 64; + let ratio = ones as f64 / total_bits as f64; + assert!( + (ratio - 0.5).abs() < 0.01, + "bit ratio far from 0.5: {ratio}" + ); + } } diff --git a/src/random/random_core.rs b/src/random/random_core.rs index 8c3a02c..5db5617 100644 --- a/src/random/random_core.rs +++ b/src/random/random_core.rs @@ -79,4 +79,20 @@ mod tests { assert!(f >= 2.0 && f <= 4.0); } } + + #[test] + fn test_range_sample_usize_single_value() { + for val in [0, 1, u64::MAX] { + let n = ::from_u64(val, &(5..6)); + assert_eq!(n, 5); + } + } + + #[test] + fn test_range_sample_f64_negative_range() { + for val in [0, u64::MAX / 3, u64::MAX] { + let f = ::from_u64(val, &(-2.0..2.0)); + assert!(f >= -2.0 && f <= 2.0); + } + } } diff --git a/src/random/seq.rs b/src/random/seq.rs index 35c0d0b..8df863f 100644 --- a/src/random/seq.rs +++ b/src/random/seq.rs @@ -73,4 +73,33 @@ mod tests { arr2.shuffle(&mut rng); assert_ne!(arr1, arr2); } + + #[test] + fn test_shuffle_empty_slice() { + let mut rng = Prng::new(1); + let mut arr: [i32; 0] = []; + arr.shuffle(&mut rng); + assert!(arr.is_empty()); + } + + #[test] + fn test_shuffle_three_uniform() { + use std::collections::HashMap; + let mut rng = Prng::new(123); + let mut counts: HashMap<[u8; 3], usize> = HashMap::new(); + for _ in 0..6000 { + let mut arr = [1u8, 2, 3]; + arr.shuffle(&mut rng); + *counts.entry(arr).or_insert(0) += 1; + } + let expected = 1000.0; + let chi2: f64 = counts + .values() + .map(|&c| { + let diff = c as f64 - expected; + diff * diff / expected + }) + .sum(); + assert!(chi2 < 30.0, "shuffle chi-square too high: {chi2}"); + } } From 3207254564726781ab91e0c8fe40cbf249e14390 Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Tue, 29 Jul 2025 00:36:14 +0100 Subject: [PATCH 16/18] Add examples for random number generation and statistical tests --- examples/random_demo.rs | 67 ++++++++++++++++++++++++++++++++++++++++ examples/random_stats.rs | 57 ++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 examples/random_demo.rs create mode 100644 examples/random_stats.rs diff --git a/examples/random_demo.rs b/examples/random_demo.rs new file mode 100644 index 0000000..1010d51 --- /dev/null +++ b/examples/random_demo.rs @@ -0,0 +1,67 @@ +use rustframe::random::{crypto_rng, rng, Rng, SliceRandom}; + +/// Demonstrates basic usage of the random number generators. +/// +/// It showcases uniform ranges, booleans, normal distribution, +/// shuffling and the cryptographically secure generator. +fn main() { + basic_usage(); + println!("\n-----\n"); + normal_demo(); + println!("\n-----\n"); + shuffle_demo(); +} + +fn basic_usage() { + println!("Basic PRNG usage\n----------------"); + let mut prng = rng(); + println!("random u64 : {}", prng.next_u64()); + println!("range [10,20): {}", prng.random_range(10..20)); + println!("bool : {}", prng.gen_bool()); +} + +fn normal_demo() { + println!("Normal distribution\n-------------------"); + let mut prng = rng(); + for _ in 0..3 { + let v = prng.normal(0.0, 1.0); + println!("sample: {:.3}", v); + } +} + +fn shuffle_demo() { + println!("Slice shuffling\n----------------"); + let mut prng = rng(); + let mut data = [1, 2, 3, 4, 5]; + data.shuffle(&mut prng); + println!("shuffled: {:?}", data); + + let mut secure = crypto_rng(); + let byte = secure.random_range(0..256usize); + println!("crypto byte: {}", byte); +} + +#[cfg(test)] +mod tests { + use super::*; + use rustframe::random::{CryptoRng, Prng}; + + #[test] + fn test_basic_usage_range_bounds() { + let mut rng = Prng::new(1); + for _ in 0..50 { + let v = rng.random_range(5..10); + assert!(v >= 5 && v < 10); + } + } + + #[test] + fn test_crypto_byte_bounds() { + let mut rng = CryptoRng::new(); + for _ in 0..50 { + let v = rng.random_range(0..256usize); + assert!(v < 256); + } + } +} + diff --git a/examples/random_stats.rs b/examples/random_stats.rs new file mode 100644 index 0000000..6aeef34 --- /dev/null +++ b/examples/random_stats.rs @@ -0,0 +1,57 @@ +use rustframe::random::{crypto_rng, rng, Rng}; + +/// Demonstrates simple statistical checks on random number generators. +fn main() { + chi_square_demo(); + println!("\n-----\n"); + monobit_demo(); +} + +fn chi_square_demo() { + println!("Chi-square test on PRNG"); + let mut rng = rng(); + let mut counts = [0usize; 10]; + let samples = 10000; + for _ in 0..samples { + let v = rng.random_range(0..10usize); + counts[v] += 1; + } + let expected = samples as f64 / 10.0; + let chi2: f64 = counts + .iter() + .map(|&c| { + let diff = c as f64 - expected; + diff * diff / expected + }) + .sum(); + println!("counts: {:?}", counts); + println!("chi-square: {:.3}", chi2); +} + +fn monobit_demo() { + println!("Monobit test on crypto RNG"); + let mut rng = crypto_rng(); + let mut ones = 0usize; + let samples = 1000; + for _ in 0..samples { + ones += rng.next_u64().count_ones() as usize; + } + let ratio = ones as f64 / (samples as f64 * 64.0); + println!("ones ratio: {:.4}", ratio); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_chi_square_demo_runs() { + chi_square_demo(); + } + + #[test] + fn test_monobit_demo_runs() { + monobit_demo(); + } +} + From 750adc72e9003016afb10975be6974856d2b802c Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Tue, 29 Jul 2025 21:42:47 +0100 Subject: [PATCH 17/18] Add missing #[cfg(test)] attribute to tests module in activations.rs --- src/compute/models/activations.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/compute/models/activations.rs b/src/compute/models/activations.rs index 661fe52..5391a2b 100644 --- a/src/compute/models/activations.rs +++ b/src/compute/models/activations.rs @@ -25,6 +25,7 @@ pub fn dleaky_relu(x: &Matrix) -> Matrix { x.map(|v| if v > 0.0 { 1.0 } else { 0.01 }) } +#[cfg(test)] mod tests { use super::*; From ef322fc6a235ffc32f9ed1e1092865044cbad58a Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Tue, 29 Jul 2025 22:15:45 +0100 Subject: [PATCH 18/18] Refactor assertions in tests to simplify error messages for KMeans, CryptoRng, and Prng modules --- src/compute/models/k_means.rs | 7 ++----- src/random/crypto.rs | 12 ++++-------- src/random/prng.rs | 15 ++++++--------- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/compute/models/k_means.rs b/src/compute/models/k_means.rs index 0766075..05e8c6d 100644 --- a/src/compute/models/k_means.rs +++ b/src/compute/models/k_means.rs @@ -192,11 +192,8 @@ mod tests { break; } } - assert!( - matches_data_point, - "Centroid {} (empty cluster) does not match any data point", - c - ); + // "Centroid {} (empty cluster) does not match any data point",c + assert!(matches_data_point); } } break; diff --git a/src/random/crypto.rs b/src/random/crypto.rs index a6367de..9cfa725 100644 --- a/src/random/crypto.rs +++ b/src/random/crypto.rs @@ -71,10 +71,8 @@ mod tests { counts[v] += 1; } for &c in &counts { - assert!( - (c as isize - 100).abs() < 50, - "Crypto RNG counts far from uniform: {c}" - ); + // "Crypto RNG counts far from uniform: {c}" + assert!((c as isize - 100).abs() < 50); } } @@ -160,9 +158,7 @@ mod tests { } let total_bits = samples * 64; let ratio = ones as f64 / total_bits as f64; - assert!( - (ratio - 0.5).abs() < 0.02, - "bit ratio far from 0.5: {ratio}" - ); + // "bit ratio far from 0.5: {ratio}" + assert!((ratio - 0.5).abs() < 0.02); } } diff --git a/src/random/prng.rs b/src/random/prng.rs index 2b0e304..067d163 100644 --- a/src/random/prng.rs +++ b/src/random/prng.rs @@ -125,10 +125,8 @@ mod tests { counts[v] += 1; } for &c in &counts { - assert!( - (c as isize - 1000).abs() < 150, - "PRNG counts far from uniform: {c}" - ); + // "PRNG counts far from uniform: {c}" + assert!((c as isize - 1000).abs() < 150); } } @@ -209,7 +207,8 @@ mod tests { diff * diff / expected }) .sum(); - assert!(chi2 < 20.0, "chi-square statistic too high: {chi2}"); + // "chi-square statistic too high: {chi2}" + assert!(chi2 < 20.0); } #[test] @@ -222,9 +221,7 @@ mod tests { } let total_bits = samples * 64; let ratio = ones as f64 / total_bits as f64; - assert!( - (ratio - 0.5).abs() < 0.01, - "bit ratio far from 0.5: {ratio}" - ); + // "bit ratio far from 0.5: {ratio}" + assert!((ratio - 0.5).abs() < 0.01); } }