Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 24 additions & 3 deletions src/dataset/generator.rs
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -24,7 +29,7 @@ pub fn make_blobs(
) -> Dataset<f32, f32> {
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<Vec<f32>> = (0..num_centers)
Expand Down Expand Up @@ -70,7 +75,7 @@ pub fn make_circles(num_samples: usize, factor: f32, noise: f32) -> Dataset<f32,
let linspace_out = linspace(0.0, 2.0 * std::f32::consts::PI, num_samples_out);
let linspace_in = linspace(0.0, 2.0 * std::f32::consts::PI, num_samples_in);

let mut rng = rand::rng();
let mut rng = get_rng_impl(None);

let mut x: Vec<f32> = Vec::with_capacity(num_samples * 2);
let mut y: Vec<f32> = Vec::with_capacity(num_samples);
Expand Down Expand Up @@ -107,7 +112,7 @@ pub fn make_moons(num_samples: usize, noise: f32) -> Dataset<f32, u32> {
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<f32> = Vec::with_capacity(num_samples * 2);
let mut y: Vec<f32> = Vec::with_capacity(num_samples);
Expand Down Expand Up @@ -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);
}
}
15 changes: 12 additions & 3 deletions src/readers/io_testing.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -16,7 +19,13 @@ pub struct TemporaryTextFile {
impl TemporaryTextFile {
pub fn new(contents: &str) -> std::io::Result<Self> {
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)
Expand Down
Loading