Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experiment: Replace global mutable variables for random number generators #1

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
9 changes: 4 additions & 5 deletions rosomaxa/src/utils/iterators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use crate::utils::*;
use std::collections::HashMap;
use std::fmt::{Debug, Formatter};
use std::hash::Hash;
use std::sync::Arc;

/// An iterator which collects items into group.
pub trait CollectGroupBy: Iterator {
Expand Down Expand Up @@ -47,12 +46,12 @@ pub struct SelectionSamplingIterator<I: Iterator> {
needed: usize,
size: usize,
iterator: I,
random: Arc<dyn Random + Send + Sync>,
random: DefaultPureRandom,
}

impl<I: Iterator> SelectionSamplingIterator<I> {
/// Creates a new instance of `SelectionSamplingIterator`.
pub fn new(iterator: I, amount: usize, random: Arc<dyn Random + Send + Sync>) -> Self {
pub fn new(iterator: I, amount: usize, random: DefaultPureRandom) -> Self {
assert!(amount > 0);
Self {
// NOTE relying on lower bound size hint!
Expand Down Expand Up @@ -131,7 +130,7 @@ pub trait SelectionSamplingSearch: Iterator {
fn sample_search<'a, T, R, FM, FI, FC>(
self,
sample_size: usize,
random: Arc<dyn Random + Send + Sync>,
random: &mut DefaultPureRandom,
mut map_fn: FM,
index_fn: FI,
compare_fn: FC,
Expand Down Expand Up @@ -159,7 +158,7 @@ pub trait SelectionSamplingSearch: Iterator {
// keeps track data to track properly right range limit if best is found at last
let (orig_right, last_probe_idx) = (state.right, take.min(sample_size - 1));

state = SelectionSamplingIterator::new(iterator, sample_size, random.clone())
state = SelectionSamplingIterator::new(iterator, sample_size, random.new_pure_random())
.enumerate()
.fold(state, |mut acc, (probe_idx, item)| {
let item_idx = index_fn(&item);
Expand Down
3 changes: 3 additions & 0 deletions rosomaxa/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ pub use self::noise::*;
mod parallel;
pub use self::parallel::*;

mod pure_random;
pub use self::pure_random::*;

mod random;
pub use self::random::*;

Expand Down
13 changes: 13 additions & 0 deletions rosomaxa/src/utils/parallel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#[path = "../../tests/unit/utils/parallel_test.rs"]
mod parallel_test;

pub use self::actual::into_map_reduce;
pub use self::actual::map_reduce;
pub use self::actual::parallel_collect;
pub use self::actual::parallel_foreach_mut;
Expand Down Expand Up @@ -70,6 +71,18 @@ mod actual {
source.par_iter().map(map_op).reduce(default_op, reduce_op)
}

/// Performs map reduce operations in parallel.
pub fn into_map_reduce<T, FM, FR, FD, R>(source: Vec<T>, map_op: FM, default_op: FD, reduce_op: FR) -> R
where
T: Send + Sync,
FM: Fn(T) -> R + Sync + Send,
FR: Fn(R, R) -> R + Sync + Send,
FD: Fn() -> R + Sync + Send,
R: Send,
{
source.into_par_iter().map(map_op).reduce(default_op, reduce_op)
}

/// Performs mutable foreach in parallel.
pub fn parallel_foreach_mut<T, F>(source: &mut [T], action: F)
where
Expand Down
131 changes: 131 additions & 0 deletions rosomaxa/src/utils/pure_random.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use rand::prelude::*;
use rand::Error;


/// Provides the way to use randomized values in generic way.
pub trait PureRandom {
/// Creates a new RNG, different for every call.
fn new_pure_random(&mut self) -> Self;

/// Produces integral random value, uniformly distributed on the closed interval [min, max]
fn uniform_int(&mut self, min: i32, max: i32) -> i32;

/// Produces real random value, uniformly distributed on the closed interval [min, max)
fn uniform_real(&mut self, min: f64, max: f64) -> f64;
/// Flips a coin and returns true if it is "heads", false otherwise.
fn is_head_not_tails(&mut self) -> bool;

/// Tests probability value in (0., 1.) range.
fn is_hit(&mut self, probability: f64) -> bool;

/// Returns an index from collected with probability weight.
/// Uses exponential distribution where the weights are the rate of the distribution (lambda)
/// and selects the smallest sampled value.
fn weighted(&mut self, weights: &[usize]) -> usize;
}

/// A default random implementation.
pub struct DefaultPureRandom {
rng: SmallRng,
}

impl DefaultPureRandom {
/// This behaves like the trait `Copy`, but we don't implement the trait, as usually this behaviour
/// is not desired for random number generators
///
/// In general, we probably don't want to use this method too often as the return value will
/// behave exactly as the original value. If we have a single (global) instance of this struct,
/// it should probably be mutable, so that newly constructed instances can be different for each call.
#[inline]
pub fn generate_copy(&self) -> Self {
Self { rng: self.rng.clone() }
}

/// Creates an instance of `DefaultPureRandom` with repeatable (predictable) random generation.
#[inline]
pub fn with_seed(seed: u64) -> Self {
Self { rng: SmallRng::seed_from_u64(seed) }
}

/// Creates an instance of `DefaultPureRandom` with reproducible behavior.
#[inline]
pub fn for_tests() -> Self {
Self { rng: SmallRng::seed_from_u64(1234567890) }
}

/// Creates a randomly initialized instance of `DefaultPureRandom`.
#[inline]
pub fn new_random() -> Self {
Self { rng: SmallRng::from_rng(thread_rng()).expect("cannot get RNG from thread rng") }
}
}

impl PureRandom for DefaultPureRandom {
#[inline]
fn new_pure_random(&mut self) -> Self {
Self { rng: SmallRng::seed_from_u64(self.next_u64())}
}
#[inline]
fn uniform_int(&mut self, min: i32, max: i32) -> i32 {
if min == max {
return min;
}

assert!(min < max);
self.rng.gen_range(min..max + 1)
}

#[inline]
fn uniform_real(&mut self, min: f64, max: f64) -> f64 {
if (min - max).abs() < f64::EPSILON {
return min;
}

assert!(min < max);
self.rng.gen_range(min..max)
}

#[inline]
fn is_head_not_tails(&mut self) -> bool {
self.rng.gen_bool(0.5)
}

#[inline]
fn is_hit(&mut self, probability: f64) -> bool {
self.gen_bool(probability.clamp(0., 1.))
}

#[inline]
fn weighted(&mut self, weights: &[usize]) -> usize {
weights
.iter()
.zip(0_usize..)
.map(|(&weight, index)| (-self.uniform_real(0., 1.).ln() / weight as f64, index))
.min_by(|a, b| a.0.partial_cmp(&b.0).unwrap())
.unwrap()
.1
}
}

/// Reimplementing RngCore helps to set breakpoints and also hides the usage of SmallRng.
impl RngCore for DefaultPureRandom {
#[inline]
fn next_u32(&mut self) -> u32 {
self.rng.next_u32()
}

#[inline]
fn next_u64(&mut self) -> u64 {
self.rng.next_u64()
}

#[inline]
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.rng.fill_bytes(dest)
}

#[inline]
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> {
self.rng.try_fill_bytes(dest)
}
}
21 changes: 11 additions & 10 deletions rosomaxa/tests/unit/utils/iterators_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ use super::*;
use crate::utils::DefaultRandom;

mod selection_sampling {
use std::sync::Arc;
use super::*;

#[test]
fn can_sample_from_large_range() {
let random = Arc::new(DefaultRandom::default());
let random = DefaultPureRandom::for_tests();
let amount = 5;

let numbers = SelectionSamplingIterator::new(0..100, amount, random).collect::<Vec<_>>();
Expand All @@ -25,7 +26,7 @@ mod selection_sampling {
#[test]
fn can_sample_from_same_range() {
let amount = 5;
let random = Arc::new(DefaultRandom::default());
let random = DefaultPureRandom::for_tests();

let numbers = SelectionSamplingIterator::new(0..amount, amount, random).collect::<Vec<_>>();

Expand All @@ -44,6 +45,7 @@ mod selection_sampling {
}

mod range_sampling {
use std::sync::Arc;
use super::*;
use crate::prelude::RandomGen;

Expand Down Expand Up @@ -111,7 +113,6 @@ mod range_sampling {

mod sampling_search {
use super::*;
use crate::Environment;
use std::cell::RefCell;
use std::sync::RwLock;

Expand Down Expand Up @@ -142,7 +143,7 @@ mod sampling_search {
let total_size = 1000;
let sample_size = 8;
let target = 10;
let random = Environment::default().random;
let mut random = DefaultPureRandom::for_tests();

let mut results = (0..100)
.map(|_| {
Expand All @@ -156,7 +157,7 @@ mod sampling_search {

let idx = data
.iter()
.sample_search(sample_size, random.clone(), map_fn, |item| item.idx, compare_fn)
.sample_search(sample_size, &mut random, map_fn, |item| item.idx, compare_fn)
.unwrap()
.idx;
let count = *counter.read().unwrap();
Expand All @@ -181,22 +182,22 @@ mod sampling_search {
66, 47, 96, 82, 34, 20, 23, 94, 11, 18, 89, 79, 47, 77, 30,
48, 8, 45, 11, 21, 54, 15, 26, 23, 37, 58, 27, 31, 11, 60,
],
4, 12, 96,
4, 11, 97,
),
case02_at_end: (
vec![
66, 47, 96, 82, 34, 20, 23, 94, 11, 18, 89, 79, 47, 77, 30,
48, 8, 45, 11, 21, 54, 15, 26, 23, 37, 58, 27, 31, 11, 60,
76, 36, 93, 15, 21, 40, 97, 77, 35, 86, 61, 71, 7, 32, 29,
],
4, 7, 86,
4, 8, 86,
),
case03_wave: (
vec![
2, 5, 6, 10, 18, 24, 25, 29, 34, 35, 37, 38, 40, 43, 45, 53, 55, 60, 61, 63, 68,
69, 71, 73, 77, 80, 81, 82, 84, 91, 96, 93, 90, 86, 80, 72, 71, 65, 62, 56, 55, 52,
],
8, 13, 96,
8, 16, 93,
),
}

Expand All @@ -206,14 +207,14 @@ mod sampling_search {
expected_counter: usize,
expected_value: i32,
) {
let random = Arc::new(DefaultRandom::new_repeatable());
let mut random = DefaultPureRandom::for_tests();
let counter = RefCell::new(0);
let value = sequence
.into_iter()
.enumerate()
.sample_search(
sample_size,
random.clone(),
&mut random,
|(_idx, i)| {
*counter.borrow_mut() += 1;
//println!("{} probe: {i} at {idx}", counter.borrow());
Expand Down
6 changes: 3 additions & 3 deletions vrp-core/src/construction/clustering/vicinity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,10 @@ fn get_check_insertion_fn(
actor_filter: Arc<dyn Fn(&Actor) -> bool + Send + Sync>,
) -> impl Fn(&Job) -> Result<(), i32> {
move |job: &Job| -> Result<(), i32> {
let eval_ctx = EvaluationContext {
let mut eval_ctx = EvaluationContext {
goal: &insertion_ctx.problem.goal,
job,
leg_selection: &LegSelection::Exhaustive,
leg_selection: LegSelection::Exhaustive,
result_selector: &BestResultSelector::default(),
};

Expand All @@ -199,7 +199,7 @@ fn get_check_insertion_fn(
.try_fold(Err(-1), |_, route_ctx| {
let result = eval_job_insertion_in_route(
&insertion_ctx,
&eval_ctx,
&mut eval_ctx,
route_ctx,
InsertionPosition::Any,
InsertionResult::make_failure(),
Expand Down
14 changes: 7 additions & 7 deletions vrp-core/src/construction/heuristics/evaluators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub struct EvaluationContext<'a> {
/// A job which is about to be inserted.
pub job: &'a Job,
/// A leg selection mode.
pub leg_selection: &'a LegSelection,
pub leg_selection: LegSelection,
/// A result selector.
pub result_selector: &'a (dyn ResultSelector + Send + Sync),
}
Expand All @@ -39,7 +39,7 @@ pub enum InsertionPosition {
/// at given position constraint.
pub fn eval_job_insertion_in_route(
insertion_ctx: &InsertionContext,
eval_ctx: &EvaluationContext,
eval_ctx: &mut EvaluationContext,
route_ctx: &RouteContext,
position: InsertionPosition,
alternative: InsertionResult,
Expand Down Expand Up @@ -84,7 +84,7 @@ pub fn eval_job_insertion_in_route(
/// Evaluates possibility to preform insertion in route context only.
/// NOTE: doesn't evaluate constraints on route level.
pub fn eval_job_constraint_in_route(
eval_ctx: &EvaluationContext,
eval_ctx: &mut EvaluationContext,
route_ctx: &RouteContext,
position: InsertionPosition,
route_costs: InsertionCost,
Expand All @@ -98,7 +98,7 @@ pub fn eval_job_constraint_in_route(

pub(crate) fn eval_single_constraint_in_route(
insertion_ctx: &InsertionContext,
eval_ctx: &EvaluationContext,
eval_ctx: &mut EvaluationContext,
route_ctx: &RouteContext,
single: &Arc<Single>,
position: InsertionPosition,
Expand All @@ -119,7 +119,7 @@ pub(crate) fn eval_single_constraint_in_route(
}

fn eval_single(
eval_ctx: &EvaluationContext,
eval_ctx: &mut EvaluationContext,
route_ctx: &RouteContext,
single: &Arc<Single>,
position: InsertionPosition,
Expand Down Expand Up @@ -151,7 +151,7 @@ fn eval_single(
}

fn eval_multi(
eval_ctx: &EvaluationContext,
eval_ctx: &mut EvaluationContext,
route_ctx: &RouteContext,
multi: &Arc<Multi>,
position: InsertionPosition,
Expand Down Expand Up @@ -244,7 +244,7 @@ fn analyze_insertion_in_route(
init
}
}
None => eval_ctx.leg_selection.sample_best(
None => eval_ctx.leg_selection.generate_copy().sample_best(
route_ctx,
eval_ctx.job,
init.index,
Expand Down
Loading