From 5f661ed7bfb4dbe74d6b844e8419179a22733569 Mon Sep 17 00:00:00 2001 From: ShryukGrandhi <85529990+ShryukGrandhi@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:21:19 -0700 Subject: [PATCH 1/4] fix(datasets): decouple datasets feature from std_rand (#416) Signed-off-by: ShryukGrandhi <85529990+ShryukGrandhi@users.noreply.github.com> --- CHANGELOG.md | 1 + Cargo.toml | 5 +++-- src/dataset/generator.rs | 27 ++++++++++++++++++++++++--- src/readers/io_testing.rs | 15 ++++++++++++--- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d0dc5d7..818c2adf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - In the `...Regressor`s the features are always shuffled even when `mtry` equals the number of attributes. This changes the constructed trees. +- The `datasets` feature no longer enables `std_rand` (#416). The toy datasets now compile without `rand/std` and the OS RNG. The generators `make_blobs`, `make_circles` and `make_moons` now use the crate RNG. With `std_rand` the RNG takes its seed from OS entropy, as before. Without `std_rand` the RNG uses a fixed seed, thus the output is the same for each call. To keep the previous behaviour, enable `std_rand` together with `datasets`. ## [0.6.16] ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 4ec3a4b5..671d075d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,8 +47,9 @@ serde = ["dep:serde", "dep:typetag"] ndarray-bindings = ["dep:ndarray"] # Sample datasets (iris, boston, digits, ...) and dataset generators. -# Enables std_rand and serde because datasets ship serialized bundles. -datasets = ["std_rand", "serde"] +# Enables serde because datasets ship serialized bundles. Does not enable +# std_rand: the generators use the crate RNG (SmallRng without std_rand). +datasets = ["serde"] # Use the standard library RNG (StdRng / thread_rng) instead of SmallRng. # Required for non-deterministic seeding without an explicit seed. diff --git a/src/dataset/generator.rs b/src/dataset/generator.rs index 37f9c236..0ed329af 100644 --- a/src/dataset/generator.rs +++ b/src/dataset/generator.rs @@ -1,9 +1,14 @@ //! # Dataset Generators //! +//! The generators get their RNG from `get_rng_impl(None)`. With the `std_rand` +//! feature the RNG takes its seed from OS entropy. Without `std_rand` the RNG +//! uses a fixed seed (or `getrandom` with the `js` feature), thus the +//! `datasets` feature does not need `rand/std`. use rand::distr::Distribution; use rand::distr::Uniform; use crate::dataset::Dataset; +use crate::rand_custom::get_rng_impl; /// Sample from N(mean, std) via Box-Muller transform using only rand 0.10 #[inline] @@ -24,7 +29,7 @@ pub fn make_blobs( ) -> Dataset { let center_box = Uniform::new(-10.0f32, 10.0f32).expect("Invalid uniform range"); let cluster_std = 1.0f32; - let mut rng = rand::rng(); + let mut rng = get_rng_impl(None); // Pre-compute cluster centers (one mean per feature per cluster) let centers: Vec> = (0..num_centers) @@ -70,7 +75,7 @@ pub fn make_circles(num_samples: usize, factor: f32, noise: f32) -> Dataset = Vec::with_capacity(num_samples * 2); let mut y: Vec = Vec::with_capacity(num_samples); @@ -107,7 +112,7 @@ pub fn make_moons(num_samples: usize, noise: f32) -> Dataset { let linspace_out = linspace(0.0, std::f32::consts::PI, num_samples_out); let linspace_in = linspace(0.0, std::f32::consts::PI, num_samples_in); - let mut rng = rand::rng(); + let mut rng = get_rng_impl(None); let mut x: Vec = Vec::with_capacity(num_samples * 2); let mut y: Vec = Vec::with_capacity(num_samples); @@ -194,4 +199,20 @@ mod tests { assert_eq!(dataset.num_features, 2); assert_eq!(dataset.num_samples, 10); } + + #[cfg(not(any(feature = "std_rand", feature = "js")))] + #[cfg_attr( + all(target_arch = "wasm32", not(target_os = "wasi")), + wasm_bindgen_test::wasm_bindgen_test + )] + #[test] + fn test_generators_without_std_rand_are_reproducible() { + // without std_rand the generators use a fixed-seed SmallRng + assert_eq!(make_blobs(10, 2, 3).data, make_blobs(10, 2, 3).data); + assert_eq!( + make_circles(10, 0.5, 0.05).data, + make_circles(10, 0.5, 0.05).data + ); + assert_eq!(make_moons(10, 0.05).data, make_moons(10, 0.05).data); + } } diff --git a/src/readers/io_testing.rs b/src/readers/io_testing.rs index 0bd0e07f..90504e43 100644 --- a/src/readers/io_testing.rs +++ b/src/readers/io_testing.rs @@ -1,13 +1,16 @@ //! This module contains functionality to test IO. It has both functions that write //! to the file-system for end-to-end tests, but also abstractions to avoid this by //! reading from strings instead. -use rand::distr::{Alphanumeric, SampleString}; use std::fs; use std::io::Bytes; use std::io::Read; use std::io::{Chain, IoSliceMut, Take, Write}; +use std::sync::atomic::{AtomicUsize, Ordering}; -/// Writing out a temporary csv file at a random location and cleaning +/// Counter that makes each temporary file name unique in the process. +static TEMPORARY_FILE_COUNTER: AtomicUsize = AtomicUsize::new(0); + +/// Writing out a temporary csv file at a unique location and cleaning /// it up on `Drop`. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct TemporaryTextFile { @@ -16,7 +19,13 @@ pub struct TemporaryTextFile { impl TemporaryTextFile { pub fn new(contents: &str) -> std::io::Result { let test_text_file = TemporaryTextFile { - random_path: Alphanumeric.sample_string(&mut rand::rng(), 16), + // Process id and counter give a unique name without an OS RNG, + // thus the tests do not need the `std_rand` feature. + random_path: format!( + "smartcore_tmp_{}_{}", + std::process::id(), + TEMPORARY_FILE_COUNTER.fetch_add(1, Ordering::Relaxed) + ), }; string_to_file(contents, &test_text_file.random_path)?; Ok(test_text_file) From d8d0837871bf4a382397d36bfeeec0900d09a63a Mon Sep 17 00:00:00 2001 From: ShryukGrandhi <85529990+ShryukGrandhi@users.noreply.github.com> Date: Fri, 23 Oct 2026 05:10:20 -0700 Subject: [PATCH 2/4] feat(datasets): add seeded generator variants and document determinism (#416) Add make_blobs_with_seed, make_circles_with_seed and make_moons_with_seed. Document the std_rand dependency of the generator output, label the CHANGELOG entry as breaking, test the std_rand randomness, and write TemporaryTextFile into the OS temporary directory. Signed-off-by: ShryukGrandhi <85529990+ShryukGrandhi@users.noreply.github.com> --- CHANGELOG.md | 7 ++- Cargo.toml | 2 + src/dataset/generator.rs | 92 +++++++++++++++++++++++++++++++++++++-- src/readers/io_testing.rs | 14 +++--- 4 files changed, 106 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 818c2adf..fafb8174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - In the `...Regressor`s the features are always shuffled even when `mtry` equals the number of attributes. This changes the constructed trees. -- The `datasets` feature no longer enables `std_rand` (#416). The toy datasets now compile without `rand/std` and the OS RNG. The generators `make_blobs`, `make_circles` and `make_moons` now use the crate RNG. With `std_rand` the RNG takes its seed from OS entropy, as before. Without `std_rand` the RNG uses a fixed seed, thus the output is the same for each call. To keep the previous behaviour, enable `std_rand` together with `datasets`. + +### Changed (breaking) +- The `datasets` feature no longer enables `std_rand` (#416). The toy datasets now compile without `rand/std` and the OS RNG. The generators `make_blobs`, `make_circles` and `make_moons` now use the crate RNG. With `std_rand` the RNG takes its seed from OS entropy, as before. Without `std_rand` the RNG uses a fixed seed, thus the output is the same for each call. To keep the previous behaviour, enable `std_rand` together with `datasets`. This change must go in a minor version bump. + +### Added +- `make_blobs_with_seed`, `make_circles_with_seed` and `make_moons_with_seed` take a `seed: Option` for the generator RNG. With `Some(seed)` the output is reproducible with all features. ## [0.6.16] ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 671d075d..aa1130b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,8 @@ ndarray-bindings = ["dep:ndarray"] # Sample datasets (iris, boston, digits, ...) and dataset generators. # Enables serde because datasets ship serialized bundles. Does not enable # std_rand: the generators use the crate RNG (SmallRng without std_rand). +# Without std_rand, make_blobs/make_circles/make_moons use a fixed seed and give +# the same output for each call. Add std_rand for non-deterministic generators. datasets = ["serde"] # Use the standard library RNG (StdRng / thread_rng) instead of SmallRng. diff --git a/src/dataset/generator.rs b/src/dataset/generator.rs index 0ed329af..99bffa3e 100644 --- a/src/dataset/generator.rs +++ b/src/dataset/generator.rs @@ -21,15 +21,32 @@ fn sample_normal(mean: f32, std: f32, rng: &mut impl rand::Rng) -> f32 { } /// Generate `num_centers` clusters of normally distributed points +/// +/// The RNG comes from `get_rng_impl(None)`. With the `std_rand` feature the output +/// is random for each call. Without `std_rand` (and without `js`) the RNG uses a +/// fixed seed, thus each call gives the same output. Use [`make_blobs_with_seed`] to +/// select the seed. #[must_use] pub fn make_blobs( num_samples: usize, num_features: usize, num_centers: usize, +) -> Dataset { + make_blobs_with_seed(num_samples, num_features, num_centers, None) +} + +/// Same as [`make_blobs`], but uses `seed` for the RNG. With `Some(seed)` the output is +/// reproducible with all features. With `None` the behaviour is the same as [`make_blobs`]. +#[must_use] +pub fn make_blobs_with_seed( + num_samples: usize, + num_features: usize, + num_centers: usize, + seed: Option, ) -> Dataset { let center_box = Uniform::new(-10.0f32, 10.0f32).expect("Invalid uniform range"); let cluster_std = 1.0f32; - let mut rng = get_rng_impl(None); + let mut rng = get_rng_impl(seed); // Pre-compute cluster centers (one mean per feature per cluster) let centers: Vec> = (0..num_centers) @@ -63,8 +80,25 @@ pub fn make_blobs( } /// Make a large circle containing a smaller circle in 2d. +/// +/// The RNG comes from `get_rng_impl(None)`. With the `std_rand` feature the output +/// is random for each call. Without `std_rand` (and without `js`) the RNG uses a +/// fixed seed, thus each call gives the same output. Use [`make_circles_with_seed`] to +/// select the seed. #[must_use] pub fn make_circles(num_samples: usize, factor: f32, noise: f32) -> Dataset { + make_circles_with_seed(num_samples, factor, noise, None) +} + +/// Same as [`make_circles`], but uses `seed` for the RNG. With `Some(seed)` the output is +/// reproducible with all features. With `None` the behaviour is the same as [`make_circles`]. +#[must_use] +pub fn make_circles_with_seed( + num_samples: usize, + factor: f32, + noise: f32, + seed: Option, +) -> Dataset { if !(0.0..1.0).contains(&factor) { panic!("'factor' has to be between 0 and 1."); } @@ -75,7 +109,7 @@ pub fn make_circles(num_samples: usize, factor: f32, noise: f32) -> Dataset = Vec::with_capacity(num_samples * 2); let mut y: Vec = Vec::with_capacity(num_samples); @@ -104,15 +138,31 @@ pub fn make_circles(num_samples: usize, factor: f32, noise: f32) -> Dataset Dataset { + make_moons_with_seed(num_samples, noise, None) +} + +/// Same as [`make_moons`], but uses `seed` for the RNG. With `Some(seed)` the output is +/// reproducible with all features. With `None` the behaviour is the same as [`make_moons`]. +#[must_use] +pub fn make_moons_with_seed( + num_samples: usize, + noise: f32, + seed: Option, +) -> Dataset { let num_samples_out = num_samples / 2; let num_samples_in = num_samples - num_samples_out; let linspace_out = linspace(0.0, std::f32::consts::PI, num_samples_out); let linspace_in = linspace(0.0, std::f32::consts::PI, num_samples_in); - let mut rng = get_rng_impl(None); + let mut rng = get_rng_impl(seed); let mut x: Vec = Vec::with_capacity(num_samples * 2); let mut y: Vec = Vec::with_capacity(num_samples); @@ -215,4 +265,40 @@ mod tests { ); assert_eq!(make_moons(10, 0.05).data, make_moons(10, 0.05).data); } + + #[cfg(feature = "std_rand")] + #[test] + fn test_generators_with_std_rand_are_random() { + // with std_rand the generators take the seed from OS entropy + assert_ne!(make_blobs(100, 2, 3).data, make_blobs(100, 2, 3).data); + assert_ne!( + make_circles(100, 0.5, 0.05).data, + make_circles(100, 0.5, 0.05).data + ); + assert_ne!(make_moons(100, 0.05).data, make_moons(100, 0.05).data); + } + + #[cfg_attr( + all(target_arch = "wasm32", not(target_os = "wasi")), + wasm_bindgen_test::wasm_bindgen_test + )] + #[test] + fn test_generators_with_seed_are_reproducible() { + assert_eq!( + make_blobs_with_seed(10, 2, 3, Some(42)).data, + make_blobs_with_seed(10, 2, 3, Some(42)).data + ); + assert_ne!( + make_blobs_with_seed(10, 2, 3, Some(1)).data, + make_blobs_with_seed(10, 2, 3, Some(2)).data + ); + assert_eq!( + make_circles_with_seed(10, 0.5, 0.05, Some(42)).data, + make_circles_with_seed(10, 0.5, 0.05, Some(42)).data + ); + assert_eq!( + make_moons_with_seed(10, 0.05, Some(42)).data, + make_moons_with_seed(10, 0.05, Some(42)).data + ); + } } diff --git a/src/readers/io_testing.rs b/src/readers/io_testing.rs index 90504e43..e7cad3ed 100644 --- a/src/readers/io_testing.rs +++ b/src/readers/io_testing.rs @@ -21,11 +21,15 @@ impl TemporaryTextFile { let test_text_file = TemporaryTextFile { // Process id and counter give a unique name without an OS RNG, // thus the tests do not need the `std_rand` feature. - random_path: format!( - "smartcore_tmp_{}_{}", - std::process::id(), - TEMPORARY_FILE_COUNTER.fetch_add(1, Ordering::Relaxed) - ), + // The file is in the OS temporary directory, not in the working directory. + random_path: std::env::temp_dir() + .join(format!( + "smartcore_tmp_{}_{}", + std::process::id(), + TEMPORARY_FILE_COUNTER.fetch_add(1, Ordering::Relaxed) + )) + .to_string_lossy() + .into_owned(), }; string_to_file(contents, &test_text_file.random_path)?; Ok(test_text_file) From 23ceda76912605c8c89864861d60c75836ef6e51 Mon Sep 17 00:00:00 2001 From: ShryukGrandhi <85529990+ShryukGrandhi@users.noreply.github.com> Date: Fri, 23 Oct 2026 07:15:42 -0700 Subject: [PATCH 3/4] ci: run tests for the datasets-only feature build (#416) Signed-off-by: ShryukGrandhi <85529990+ShryukGrandhi@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7715aed7..bf7bad03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: - name: Stable Build run: cargo build --no-default-features ${{ matrix.features }} - name: Tests - if: matrix.features == '--features ndarray-bindings' + if: matrix.features == '--features ndarray-bindings' || matrix.features == '--features datasets' run: cargo test --no-default-features ${{ matrix.features }} msrv: From ab2f6f8d9209285dda115b84b74714130a318c68 Mon Sep 17 00:00:00 2001 From: ShryukGrandhi <85529990+ShryukGrandhi@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:15:15 -0700 Subject: [PATCH 4/4] docs(datasets): document make_circles panic, test None seed, align CHANGELOG (#416) Signed-off-by: ShryukGrandhi <85529990+ShryukGrandhi@users.noreply.github.com> --- CHANGELOG.md | 6 +++--- src/dataset/generator.rs | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fafb8174..75dba18c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - In the `...Regressor`s the features are always shuffled even when `mtry` equals the number of attributes. This changes the constructed trees. -### Changed (breaking) -- The `datasets` feature no longer enables `std_rand` (#416). The toy datasets now compile without `rand/std` and the OS RNG. The generators `make_blobs`, `make_circles` and `make_moons` now use the crate RNG. With `std_rand` the RNG takes its seed from OS entropy, as before. Without `std_rand` the RNG uses a fixed seed, thus the output is the same for each call. To keep the previous behaviour, enable `std_rand` together with `datasets`. This change must go in a minor version bump. - ### Added - `make_blobs_with_seed`, `make_circles_with_seed` and `make_moons_with_seed` take a `seed: Option` for the generator RNG. With `Some(seed)` the output is reproducible with all features. +### Changed +- **Breaking**: the `datasets` feature no longer enables `std_rand` (#416). The toy datasets now compile without `rand/std` and the OS RNG. The generators `make_blobs`, `make_circles` and `make_moons` now use the crate RNG. With `std_rand` the RNG takes its seed from OS entropy, as before. Without `std_rand` the RNG uses a fixed seed, thus the output is the same for each call. To keep the previous behaviour, enable `std_rand` together with `datasets`. This change must go in a minor version bump. + ## [0.6.16] ### Fixed - `predict`, `predict_proba`, `predict_oob`, `predict_matrix` and `decision_function` now return `Err(Failed)` instead of panicking when `fit` has not run (#469, #470). Models return the same unfitted error even after deserialization, when the state fields are `None`. The guarded methods cover the decision trees, the random forests, the extra-trees regressor, the KNN classifier and regressor, `SVC`, `MultiClassSVC`, `SVR`, the linear models, the naive Bayes classifiers and `XGRegressor`. diff --git a/src/dataset/generator.rs b/src/dataset/generator.rs index 99bffa3e..00876482 100644 --- a/src/dataset/generator.rs +++ b/src/dataset/generator.rs @@ -85,6 +85,10 @@ pub fn make_blobs_with_seed( /// is random for each call. Without `std_rand` (and without `js`) the RNG uses a /// fixed seed, thus each call gives the same output. Use [`make_circles_with_seed`] to /// select the seed. +/// +/// # Panics +/// +/// Panics if `factor` is not in the range `0..1`. #[must_use] pub fn make_circles(num_samples: usize, factor: f32, noise: f32) -> Dataset { make_circles_with_seed(num_samples, factor, noise, None) @@ -92,6 +96,10 @@ pub fn make_circles(num_samples: usize, factor: f32, noise: f32) -> Dataset