use std::collections::{HashMap, VecDeque};
use crate::asv::AsvResult;
use crate::cache::value_cached;
use crate::error::CausasvError;
use crate::graph::{Dag, NodeId};
pub struct ExactDagConfig {
pub max_nodes: usize,
pub memory_limit_bytes: usize,
}
impl Default for ExactDagConfig {
fn default() -> Self {
Self {
max_nodes: 28,
memory_limit_bytes: 2 * 1024 * 1024 * 1024,
}
}
}
pub(crate) fn dag_exact_asv_sparse<F>(
dag: &Dag,
value_fn: F,
config: &ExactDagConfig,
) -> Result<(AsvResult, usize, f64, f64), CausasvError>
where
F: Fn(&[NodeId]) -> Result<f64, CausasvError>,
{
let n = dag.node_count();
if n > config.max_nodes {
return Err(CausasvError::InvalidConfig(format!(
"exact_dag_sparse: n={n} exceeds max_nodes={}; use approximate method for large DAGs",
config.max_nodes
)));
}
if n > 63 {
return Err(CausasvError::InvalidConfig(format!(
"bitmask representation requires n ≤ 63 for sparse DP, got {n}"
)));
}
let parents_mask: Vec<u64> = (0..n)
.map(|i| {
dag.parents_raw(NodeId(i as u32))
.iter()
.fold(0u64, |m, &p| m | (1u64 << p.0))
})
.collect();
let full_mask: u64 = (1u64 << n) - 1;
let mut dp_fwd: HashMap<u64, u64> = HashMap::new();
dp_fwd.insert(0, 1);
let mut queue: VecDeque<u64> = VecDeque::new();
queue.push_back(0);
while let Some(s) = queue.pop_front() {
let fwd = *dp_fwd.get(&s).unwrap();
for (i, &pmask) in parents_mask.iter().enumerate() {
if s & (1u64 << i) != 0 {
continue; }
if pmask & s == pmask {
let s_next = s | (1u64 << i);
let entry = dp_fwd.entry(s_next).or_insert(0);
let is_new = *entry == 0;
*entry = entry.checked_add(fwd).ok_or_else(|| {
CausasvError::Overflow(format!(
"dp_fwd[{s_next:#b}] overflowed u64 — use approx for this DAG"
))
})?;
if is_new {
queue.push_back(s_next);
if dp_fwd.len() * 80 > config.memory_limit_bytes {
return Err(CausasvError::InvalidConfig(format!(
"exact_dag_sparse: exceeded memory limit ({} bytes) after {} order ideals; \
try approximate method",
config.memory_limit_bytes,
dp_fwd.len()
)));
}
}
}
}
}
let n_order_ideals = dp_fwd.len();
let state_ratio = n_order_ideals as f64 / (1u64 << n) as f64;
let mut dp_ind: HashMap<u64, u64> = HashMap::new();
let total = dp_ind_lazy(full_mask, &parents_mask, n, &mut dp_ind)? as f64;
let mut phi = vec![0.0f64; n];
let mut value_cache: HashMap<u64, f64> = HashMap::new();
for (&s, &fwd_count) in &dp_fwd {
if fwd_count == 0 {
continue;
}
let w_prefix = fwd_count as f64;
for (i, &pmask) in parents_mask.iter().enumerate() {
if s & (1u64 << i) != 0 {
continue; }
if pmask & s != pmask {
continue; }
let suffix_mask = full_mask ^ (s | (1u64 << i));
let w_suffix = dp_ind_lazy(suffix_mask, &parents_mask, n, &mut dp_ind)? as f64;
let s_with_i = s | (1u64 << i);
phi[i] += w_prefix
* w_suffix
* (value_cached(&mut value_cache, &value_fn, s_with_i)?
- value_cached(&mut value_cache, &value_fn, s)?);
}
}
let values = (0..n).map(|i| (NodeId(i as u32), phi[i] / total)).collect();
let memory_mb = ((dp_fwd.len() + dp_ind.len()) * 80) as f64 / (1024.0 * 1024.0);
let result = AsvResult {
values,
n_samples: n_order_ideals,
seed: None,
is_exact: true,
effective_sample_size: None,
converged: None,
stderr: None,
n_order_ideals: Some(n_order_ideals),
state_ratio: Some(state_ratio),
memory_mb: Some(memory_mb),
};
Ok((result, n_order_ideals, state_ratio, memory_mb))
}
fn dp_ind_lazy(
mask: u64,
parents_mask: &[u64],
n: usize,
cache: &mut HashMap<u64, u64>,
) -> Result<u64, CausasvError> {
if mask == 0 {
return Ok(1);
}
if let Some(&v) = cache.get(&mask) {
return Ok(v);
}
let mut result = 0u64;
for (i, &pmask) in parents_mask.iter().enumerate().take(n) {
if mask & (1u64 << i) == 0 {
continue; }
if pmask & mask == 0 {
let sub = dp_ind_lazy(mask ^ (1u64 << i), parents_mask, n, cache)?;
result = result.checked_add(sub).ok_or_else(|| {
CausasvError::Overflow(format!(
"dp_ind[{mask:#b}] overflowed u64 — use approx for this DAG"
))
})?;
}
}
cache.insert(mask, result);
Ok(result)
}