From 1974907faa451d272dbae3e595c6873e16fc5d5f Mon Sep 17 00:00:00 2001 From: jverdicc Date: Sun, 1 Mar 2026 21:04:03 -0500 Subject: [PATCH] fix: restore exp12 sim test under topic budget --- crates/discos-core/src/experiments/exp12.rs | 47 +++++++------------ .../test_vectors/exp12_golden.json | 4 +- crates/discos-core/tests/exp12_tests.rs | 7 ++- 3 files changed, 23 insertions(+), 35 deletions(-) diff --git a/crates/discos-core/src/experiments/exp12.rs b/crates/discos-core/src/experiments/exp12.rs index 2f8f037..ceba258 100644 --- a/crates/discos-core/src/experiments/exp12.rs +++ b/crates/discos-core/src/experiments/exp12.rs @@ -51,36 +51,23 @@ pub struct Exp12Result { pub rows: Vec, } -#[derive(Debug, Clone)] -struct Lcg64 { - state: u64, +fn uniform_u53(seed: u64, trial_idx: usize, bit_idx: usize) -> f64 { + let mut x = seed + ^ ((trial_idx as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15)) + ^ ((bit_idx as u64).wrapping_mul(0xBF58_476D_1CE4_E5B9)); + x ^= x >> 30; + x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9); + x ^= x >> 27; + x = x.wrapping_mul(0x94D0_49BB_1331_11EB); + x ^= x >> 31; + let v = x >> 11; + (v as f64) * (1.0 / ((1u64 << 53) as f64)) } -impl Lcg64 { - fn new(seed: u64) -> Self { - Self { - state: seed ^ 0x9E37_79B9_7F4A_7C15, - } - } - - fn next_u64(&mut self) -> u64 { - self.state = self - .state - .wrapping_mul(6364136223846793005) - .wrapping_add(1442695040888963407); - self.state - } - - fn next_f64(&mut self) -> f64 { - let v = self.next_u64() >> 11; - (v as f64) * (1.0 / ((1u64 << 53) as f64)) - } -} - -fn binomial_sample(n: usize, p: f64, rng: &mut Lcg64) -> usize { +fn binomial_sample(n: usize, p: f64, seed: u64, trial_idx: usize) -> usize { let mut s = 0usize; - for _ in 0..n { - if rng.next_f64() < p { + for bit_idx in 0..n { + if uniform_u53(seed, trial_idx, bit_idx) < p { s += 1; } } @@ -93,16 +80,14 @@ pub async fn run_exp12(cfg: &Exp12Config) -> anyhow::Result { } let mut rows = Vec::with_capacity(cfg.scenarios.len()); - let mut rng = Lcg64::new(cfg.seed); - for scenario in &cfg.scenarios { if !scenario.psplit.is_finite() || !(0.0..=1.0).contains(&scenario.psplit) { anyhow::bail!("psplit must be finite and within [0,1]"); } let mut leaked = Vec::with_capacity(cfg.trials); - for _ in 0..cfg.trials { - let s = binomial_sample(scenario.n, scenario.psplit, &mut rng); + for trial_idx in 0..cfg.trials { + let s = binomial_sample(scenario.n, scenario.psplit, cfg.seed, trial_idx); leaked.push(cfg.topic_budget_bits + s); } diff --git a/crates/discos-core/test_vectors/exp12_golden.json b/crates/discos-core/test_vectors/exp12_golden.json index 7fe2818..fcdd37f 100644 --- a/crates/discos-core/test_vectors/exp12_golden.json +++ b/crates/discos-core/test_vectors/exp12_golden.json @@ -9,13 +9,13 @@ { "n": 16, "psplit": 0.1, - "mean_leaked_bits": 3.596, + "mean_leaked_bits": 3.615, "p99_leaked_bits": 7 }, { "n": 32, "psplit": 0.2, - "mean_leaked_bits": 8.4155, + "mean_leaked_bits": 8.347, "p99_leaked_bits": 14 } ] diff --git a/crates/discos-core/tests/exp12_tests.rs b/crates/discos-core/tests/exp12_tests.rs index 2a50c70..5f85860 100644 --- a/crates/discos-core/tests/exp12_tests.rs +++ b/crates/discos-core/tests/exp12_tests.rs @@ -18,8 +18,11 @@ async fn exp12_matches_golden_fixture() { .await .unwrap_or_else(|e| panic!("exp12 should run: {e}")); - let expected = std::fs::read_to_string("crates/discos-core/test_vectors/exp12_golden.json") - .unwrap_or_else(|e| panic!("fixture missing: {e}")); + let fixture_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("test_vectors") + .join("exp12_golden.json"); + let expected = std::fs::read_to_string(&fixture_path) + .unwrap_or_else(|e| panic!("fixture missing at {}: {e}", fixture_path.display())); let expected_json: serde_json::Value = serde_json::from_str(&expected).unwrap_or_else(|e| panic!("invalid fixture json: {e}")); let actual_json = serde_json::to_value(&result)