diff --git a/Cargo.lock b/Cargo.lock index ee9c3f9..d1f7c3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -403,7 +403,6 @@ dependencies = [ "log", "nom 8.0.0", "num", - "once_cell", "petgraph", "rand", "rand_distr", diff --git a/ddnnife/Cargo.toml b/ddnnife/Cargo.toml index 7d42076..00aa134 100644 --- a/ddnnife/Cargo.toml +++ b/ddnnife/Cargo.toml @@ -7,9 +7,6 @@ edition = "2024" license = "LGPL-3.0-or-later" workspace = ".." -[features] -deterministic = [] - [dependencies] bimap = "0.6" bitvec = "1" @@ -20,7 +17,6 @@ itertools = "0.14" log = { workspace = true } nom = "8" num = { workspace = true } -once_cell = "1" petgraph = "0.8" rand = "0.9" rand_distr = "0.5" diff --git a/ddnnife/src/config.rs b/ddnnife/src/config.rs new file mode 100644 index 0000000..d2cb9b1 --- /dev/null +++ b/ddnnife/src/config.rs @@ -0,0 +1,61 @@ +use std::sync::OnceLock; +use std::sync::atomic::{AtomicBool, Ordering}; + +/// Global flag to control deterministic behavior at runtime. +/// +/// Defaults to `false`, meaning ddnnife will use non-deterministic operations. +#[cfg(not(test))] +static DETERMINISTIC: AtomicBool = AtomicBool::new(false); + +#[cfg(test)] +static DETERMINISTIC: AtomicBool = AtomicBool::new(true); + +/// Global value for seeding random number generators. +/// +/// Only has an effect when [DETERMINISTIC] is `true`. +static DETERMINISTIC_SEED: OnceLock = OnceLock::new(); + +/// Enables or disables deterministic operations. +/// +/// Actually using deterministic operations requires a seed to be set. +/// See [set_seed]. +#[inline] +pub fn set_deterministic(enable: bool) { + DETERMINISTIC.store(enable, Ordering::Relaxed); +} + +/// Returns whether deterministic mode is currently enabled. +#[inline] +pub fn is_deterministic() -> bool { + DETERMINISTIC.load(Ordering::Relaxed) +} + +/// Sets the seed to use for deterministic operations. +/// Does **not** implicitly enable determinism. +/// +/// Can only be called once. +/// +/// # Panics +/// +/// Panics when called more than once. +#[inline] +pub fn set_seed(seed: u64) { + DETERMINISTIC_SEED + .set(seed) + .expect("Seed can only be set once"); +} + +/// Returns the seed to use for random number generators. +/// +/// `None` when no seed has been set yet. +#[inline] +#[cfg(not(test))] +pub fn get_seed() -> Option { + DETERMINISTIC_SEED.get().copied() +} + +#[inline] +#[cfg(test)] +pub fn get_seed() -> Option { + Some(42) +} diff --git a/ddnnife/src/ddnnf/anomalies/config_creation.rs b/ddnnife/src/ddnnf/anomalies/config_creation.rs index 001f175..40ce4fc 100644 --- a/ddnnife/src/ddnnf/anomalies/config_creation.rs +++ b/ddnnife/src/ddnnf/anomalies/config_creation.rs @@ -1,23 +1,19 @@ -use std::{ - cmp::min, - collections::HashMap, - sync::{Arc, Mutex}, -}; - +use crate::Ddnnf; +use crate::NodeType::*; use itertools::Itertools; use num::{BigInt, BigRational, ToPrimitive, Zero}; -use once_cell::sync::Lazy; use rand::SeedableRng; use rand::seq::SliceRandom; use rand_distr::{Binomial, Distribution, weighted::WeightedAliasIndex}; use rand_pcg::{Lcg64Xsh32, Pcg32}; +use std::{ + cmp::min, + collections::HashMap, + sync::{LazyLock, RwLock}, +}; -use crate::Ddnnf; -use crate::NodeType::*; - -#[allow(clippy::type_complexity)] -static ENUMERATION_CACHE: Lazy, usize>>>> = - Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); +static ENUMERATION_CACHE: LazyLock, usize>>> = + LazyLock::new(|| RwLock::new(HashMap::new())); impl Ddnnf { /// Creates satisfiable complete configurations for a d-DNNF and given assumptions. @@ -43,7 +39,7 @@ impl Ddnnf { assumptions.sort_unstable_by_key(|f| f.abs()); if self.execute_query(assumptions) > BigInt::ZERO { - let last_stop = match ENUMERATION_CACHE.lock().unwrap().get(assumptions) { + let last_stop = match ENUMERATION_CACHE.read().unwrap().get(assumptions) { Some(&x) => x, None => 0, }; @@ -59,7 +55,7 @@ impl Ddnnf { sample.sort_unstable_by_key(|f| f.abs()); } - ENUMERATION_CACHE.lock().unwrap().insert( + ENUMERATION_CACHE.write().unwrap().insert( assumptions.to_vec(), (min(self.rt(), BigInt::from(last_stop + amount)) % self.rt()) .to_usize() diff --git a/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sample_merger/similarity_merger.rs b/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sample_merger/similarity_merger.rs index aeb9bed..a91a1ff 100644 --- a/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sample_merger/similarity_merger.rs +++ b/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sample_merger/similarity_merger.rs @@ -3,7 +3,7 @@ use super::super::t_iterator::TInteractionIter; use super::{Config, Sample}; use super::{OrMerger, SampleMerger}; use crate::int_hash::IntSet; -use crate::util::rng; +use crate::rand::rng; use rand::prelude::SliceRandom; use std::cmp::{Ordering, min}; use streaming_iterator::StreamingIterator; diff --git a/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sample_merger/zipping_merger.rs b/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sample_merger/zipping_merger.rs index 4d4d24a..ab9295c 100644 --- a/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sample_merger/zipping_merger.rs +++ b/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sample_merger/zipping_merger.rs @@ -5,6 +5,9 @@ use super::super::{ use super::{AndMerger, SampleMerger}; use super::{Config, Sample}; use crate::Ddnnf; +use crate::config; +use crate::rand::rng; +use rand::seq::SliceRandom; use std::cmp::min; use std::collections::HashSet; use streaming_iterator::StreamingIterator; @@ -101,22 +104,16 @@ impl ZippingMerger<'_> { }); // Deterministic sampling requires the same iteration order between runs. - // As the HashSets used for the rest of the algorithm do not provide such a determinsitic order, - // we collect, sort and (determinsitically) shuffle the generated interactions. - #[cfg(feature = "deterministic")] - { - use crate::util::rng; - use rand::seq::SliceRandom; - + // As the HashSets used for the rest of the algorithm do not provide such a deterministic order, + // we collect, sort and (deterministically) shuffle the generated interactions. + if config::is_deterministic() { let mut interactions: Vec> = interactions.into_iter().collect(); interactions.sort_unstable(); interactions.shuffle(&mut rng()); - interactions + } else { + interactions.into_iter().collect() } - - #[cfg(not(feature = "deterministic"))] - interactions.into_iter().collect() } /// Generates a set of interactions inside a sample ordered by interactions size. diff --git a/ddnnife/src/ddnnf/anomalies/t_wise_sampling/t_wise_sampler.rs b/ddnnife/src/ddnnf/anomalies/t_wise_sampling/t_wise_sampler.rs index c680f39..8d5f7a8 100644 --- a/ddnnife/src/ddnnf/anomalies/t_wise_sampling/t_wise_sampler.rs +++ b/ddnnife/src/ddnnf/anomalies/t_wise_sampling/t_wise_sampler.rs @@ -5,7 +5,7 @@ use super::{Sample, SamplingResult, SatWrapper}; use crate::NodeType; use crate::ddnnf::extended_ddnnf::ExtendedDdnnf; use crate::int_hash::{self, IntMap, IntSet}; -use crate::util::rng; +use crate::rand::rng; use crate::{Ddnnf, DdnnfKind}; use itertools::Itertools; use rand::prelude::SliceRandom; diff --git a/ddnnife/src/lib.rs b/ddnnife/src/lib.rs index ffc84e0..ef5352b 100644 --- a/ddnnife/src/lib.rs +++ b/ddnnife/src/lib.rs @@ -1,10 +1,12 @@ +pub mod config; +pub mod ddnnf; pub mod int_hash; pub mod parser; +pub mod rand; pub mod util; -pub use crate::parser::c2d_lexer; -pub use crate::parser::d4_lexer; -pub mod ddnnf; pub use crate::ddnnf::{Ddnnf, DdnnfKind, node::*}; +pub use crate::parser::c2d_lexer; +pub use crate::parser::d4_lexer; pub mod cnf; diff --git a/ddnnife/src/rand.rs b/ddnnife/src/rand.rs new file mode 100644 index 0000000..d62c3c9 --- /dev/null +++ b/ddnnife/src/rand.rs @@ -0,0 +1,29 @@ +use crate::config; +use rand::{SeedableRng, rngs::SmallRng}; +use std::sync::{LazyLock, OnceLock, RwLock, RwLockWriteGuard}; + +static RNG: LazyLock> = LazyLock::new(|| RwLock::new(SmallRng::from_os_rng())); +static RNG_DETERMINISTIC: OnceLock> = OnceLock::new(); + +/// Returns a handle to a random number generator. +/// +/// Uses a small but **not** cryptographically safe RNG. +/// +/// Depending on the corresponding [config] entries, a deterministic RNG +/// as specified by the seed or an actually random RNG is used. +#[inline] +pub fn rng<'a>() -> RwLockWriteGuard<'a, SmallRng> { + if config::is_deterministic() { + RNG_DETERMINISTIC + .get_or_init(|| { + RwLock::new(SmallRng::seed_from_u64( + config::get_seed() + .expect("Using deterministic operations requires a seed to be set"), + )) + }) + .write() + .unwrap() + } else { + RNG.write().unwrap() + } +} diff --git a/ddnnife/src/util.rs b/ddnnife/src/util.rs index b93c2ef..90e2ff7 100644 --- a/ddnnife/src/util.rs +++ b/ddnnife/src/util.rs @@ -1,9 +1,5 @@ -use rand::Rng; use std::iter; -#[cfg(any(feature = "deterministic", test))] -use rand::prelude::{SeedableRng, StdRng}; - pub fn format_vec_separated_by( vals: impl Iterator, separator: &str, @@ -27,18 +23,6 @@ where .join(";") } -#[cfg(any(feature = "deterministic", test))] -#[inline] -pub fn rng() -> impl Rng { - StdRng::seed_from_u64(42) -} - -#[cfg(not(any(feature = "deterministic", test)))] -#[inline] -pub fn rng() -> impl Rng { - rand::rng() -} - pub fn zip_assumptions_variables<'a>( assumptions: &'a [i32], variables: &'a [i32], diff --git a/ddnnife_cli/Cargo.toml b/ddnnife_cli/Cargo.toml index ba14bce..7c1a3a6 100644 --- a/ddnnife_cli/Cargo.toml +++ b/ddnnife_cli/Cargo.toml @@ -11,9 +11,6 @@ workspace = ".." name = "ddnnife" path = "src/main.rs" -[features] -deterministic = ["ddnnife/deterministic"] - [dependencies] clap = { workspace = true } csv = { workspace = true } diff --git a/ddnnife_cli/src/main.rs b/ddnnife_cli/src/main.rs index 4d21145..ceec2c2 100644 --- a/ddnnife_cli/src/main.rs +++ b/ddnnife_cli/src/main.rs @@ -3,6 +3,7 @@ mod stream; use crate::stream::{Query, handle_query, stream}; use clap::{Parser, Subcommand}; use ddnnife::DdnnfKind; +use ddnnife::config; use ddnnife::ddnnf::Ddnnf; use ddnnife::ddnnf::anomalies::t_wise_sampling::Sample; use ddnnife::ddnnf::statistics::Statistics; @@ -45,6 +46,10 @@ struct Args { /// Logging level for outputting warnings and other information. #[arg(short, long, default_value_t=log::LevelFilter::Info)] logging: log::LevelFilter, + + /// Enable deterministic behavior for random operations using the given seed. + #[arg(long)] + seed: Option, } #[derive(Debug, Clone, Subcommand)] @@ -205,13 +210,19 @@ enum Operation { } fn main() -> io::Result<()> { - // Parse the + // Parse the CLI arguments. let cli = Args::parse(); pretty_env_logger::formatted_builder() .filter_level(cli.logging) .init(); + // Enable deterministic mode when a seed is given. + if let Some(seed) = cli.seed { + config::set_seed(seed); + config::set_deterministic(true); + } + let time = Instant::now(); let mut ddnnf = if let Some(path) = &cli.input {