use deep_causality_discovery::{BrcdConfig, BrcdLoaderConfig, CdlBuilder, CdlConfigBuilder};
use deep_causality_rand::{Distribution, Normal, Rng, Xoshiro256};
use std::fs;
use std::io::Write;
use std::time::Instant;
const N_VARS: usize = 30;
const N_ROWS: usize = 800;
const SEED: u64 = 0xB0C5_CACE;
const REPS: usize = 3;
fn main() {
println!();
println!("BRCD evaluation harness — cold (learn) vs warm (cached) CPDAG resolution");
println!(
"Dataset: {N_VARS} vars, {N_ROWS} normal + {N_ROWS} anomalous rows, planted root cause."
);
println!("Median of {REPS} reps; fresh cache per cold rep. Times are wall-clock (indicative).");
println!();
let mut dir = std::env::temp_dir();
dir.push(format!("dcl_brcd_cache_{SEED:016x}"));
fs::create_dir_all(&dir).expect("create temp dir");
let normal_path = dir.join("normal.csv").to_str().unwrap().to_string();
let anomalous_path = dir.join("anomalous.csv").to_str().unwrap().to_string();
let cache_path = dir.join("cpdag_cache.csv").to_str().unwrap().to_string();
let cache_key = format!("{cache_path}.key");
let rc = N_VARS - 5; write_dataset(&normal_path, 0.0, rc, SEED ^ 0x1111);
write_dataset(&anomalous_path, 5.0, rc, SEED ^ 0x2222);
println!("Wrote dataset to {}", dir.display());
println!("Planted root cause: v{rc}");
println!();
let mut cold_ms: Vec<f64> = Vec::new();
let mut cold_ranks: Option<Vec<Vec<usize>>> = None;
for rep in 0..REPS {
let _ = fs::remove_file(&cache_path);
let _ = fs::remove_file(&cache_key);
let cfg = make_config(&normal_path, &anomalous_path, &cache_path);
let (ms, ranks) = run_once(&cfg);
println!(" cold rep {rep}: {ms:.1} ms");
cold_ms.push(ms);
cold_ranks = Some(ranks);
}
assert!(
fs::metadata(&cache_path).is_ok(),
"cache CSV should exist after a cold run"
);
let mut warm_ms: Vec<f64> = Vec::new();
let mut warm_ranks: Option<Vec<Vec<usize>>> = None;
for rep in 0..REPS {
let cfg = make_config(&normal_path, &anomalous_path, &cache_path);
let (ms, ranks) = run_once(&cfg);
println!(" warm rep {rep}: {ms:.1} ms");
warm_ms.push(ms);
warm_ranks = Some(ranks);
}
let cold = median_ms(&mut cold_ms);
let warm = median_ms(&mut warm_ms);
let delta = cold - warm;
let speedup = cold / warm;
println!();
println!("============================================================================");
println!("COLD vs WARM (median of {REPS} reps)");
println!("============================================================================");
println!(" cold (BOSS learns + persists + rank): {cold:>10.1} ms");
println!(" warm (cache load + rank): {warm:>10.1} ms");
println!(" delta (structure-learning avoided): {delta:>10.1} ms");
println!(" speedup: {speedup:>10.2} x");
println!("============================================================================");
let cold_ranks = cold_ranks.expect("cold ranks");
let warm_ranks = warm_ranks.expect("warm ranks");
assert_eq!(
cold_ranks, warm_ranks,
"warm (cached) ranking must equal cold (learned) ranking — cache must be correct"
);
assert!(
warm < cold,
"warm run ({warm:.1} ms) must be cheaper than cold run ({cold:.1} ms)"
);
let top_cold = cold_ranks.first().map(|v| v.as_slice());
println!();
println!("Cache correctness: warm ranking == cold ranking (asserted).");
println!("Top-ranked candidate set (both): {top_cold:?}");
println!(
"Reading: the warm run avoids BOSS structure learning entirely, so it is {speedup:.1}x\n\
cheaper than the cold run while producing the identical ranking."
);
}
fn write_dataset(path: &str, perturb: f64, rc: usize, seed: u64) {
let mut rng = Xoshiro256::from_seed(seed);
let eps = Normal::new(0.0_f64, 1.0).unwrap();
let mut srng = Xoshiro256::from_seed(0xDA15_5EED);
let mut parents: Vec<Vec<usize>> = vec![Vec::new(); N_VARS];
let mut weight: Vec<Vec<f64>> = vec![Vec::new(); N_VARS];
for i in 0..N_VARS {
for j in 0..i {
if srng.random_range(0.0..1.0) < 0.25 {
parents[i].push(j);
let sign = if srng.random_range(0.0..1.0) < 0.5 {
-1.0
} else {
1.0
};
weight[i].push(sign * (0.6 + srng.random_range(0.0..1.0)));
}
}
}
let mut header = String::new();
for i in 0..N_VARS {
if i > 0 {
header.push(',');
}
header.push_str(&format!("v{i}"));
}
let mut out = String::with_capacity(N_ROWS * N_VARS * 8);
out.push_str(&header);
out.push('\n');
for _ in 0..N_ROWS {
let mut x = vec![0.0_f64; N_VARS];
for i in 0..N_VARS {
let mut mean = if i == rc { perturb } else { 0.0 };
for (k, &j) in parents[i].iter().enumerate() {
mean += weight[i][k] * x[j];
}
x[i] = mean + eps.sample(&mut rng);
}
for (i, v) in x.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&format!("{v:.6}"));
}
out.push('\n');
}
let mut f = fs::File::create(path).expect("create dataset csv");
f.write_all(out.as_bytes()).expect("write dataset csv");
}
fn make_config(normal: &str, anomalous: &str, cache: &str) -> BrcdLoaderConfig<f64> {
CdlConfigBuilder::build_brcd_config()
.with_normal_path(normal)
.with_anomalous_path(anomalous)
.with_brcd_config(BrcdConfig::<f64>::continuous(SEED))
.with_cpdag_cache_path(cache)
.build()
.expect("dataset files exist")
}
fn run_once(config: &BrcdLoaderConfig<f64>) -> (f64, Vec<Vec<usize>>) {
let t = Instant::now();
let effect = CdlBuilder::build_brcd(config)
.brcd_load_input()
.brcd_discover();
let ms = t.elapsed().as_secs_f64() * 1e3;
let cdl = effect.inner.expect("pipeline succeeds");
(ms, cdl.state.brcd_result.ranks().to_vec())
}
fn median_ms(samples: &mut [f64]) -> f64 {
samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
let m = samples.len() / 2;
if samples.len() % 2 == 1 {
samples[m]
} else {
0.5 * (samples[m - 1] + samples[m])
}
}