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: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions ddnnife/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ edition = "2024"
license = "LGPL-3.0-or-later"
workspace = ".."

[features]
deterministic = []

[dependencies]
bimap = "0.6"
bitvec = "1"
Expand All @@ -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"
Expand Down
61 changes: 61 additions & 0 deletions ddnnife/src/config.rs
Original file line number Diff line number Diff line change
@@ -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<u64> = 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<u64> {
DETERMINISTIC_SEED.get().copied()
}

#[inline]
#[cfg(test)]
pub fn get_seed() -> Option<u64> {
Some(42)
}
26 changes: 11 additions & 15 deletions ddnnife/src/ddnnf/anomalies/config_creation.rs
Original file line number Diff line number Diff line change
@@ -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<Arc<Mutex<HashMap<Vec<i32>, usize>>>> =
Lazy::new(|| Arc::new(Mutex::new(HashMap::new())));
static ENUMERATION_CACHE: LazyLock<RwLock<HashMap<Vec<i32>, usize>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));

impl Ddnnf {
/// Creates satisfiable complete configurations for a d-DNNF and given assumptions.
Expand All @@ -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,
};
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Vec<i32>> = 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 5 additions & 3 deletions ddnnife/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
29 changes: 29 additions & 0 deletions ddnnife/src/rand.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use crate::config;
use rand::{SeedableRng, rngs::SmallRng};
use std::sync::{LazyLock, OnceLock, RwLock, RwLockWriteGuard};

static RNG: LazyLock<RwLock<SmallRng>> = LazyLock::new(|| RwLock::new(SmallRng::from_os_rng()));
static RNG_DETERMINISTIC: OnceLock<RwLock<SmallRng>> = 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()
}
}
16 changes: 0 additions & 16 deletions ddnnife/src/util.rs
Original file line number Diff line number Diff line change
@@ -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<T: ToString>(
vals: impl Iterator<Item = T>,
separator: &str,
Expand All @@ -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],
Expand Down
3 changes: 0 additions & 3 deletions ddnnife_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ workspace = ".."
name = "ddnnife"
path = "src/main.rs"

[features]
deterministic = ["ddnnife/deterministic"]

[dependencies]
clap = { workspace = true }
csv = { workspace = true }
Expand Down
13 changes: 12 additions & 1 deletion ddnnife_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<u64>,
}

#[derive(Debug, Clone, Subcommand)]
Expand Down Expand Up @@ -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 {
Expand Down