use crate::asv::AsvResult;
use crate::cache::mask_to_coalition;
use crate::error::CausasvError;
use crate::graph::{Dag, NodeId};
pub(crate) fn compute_dp_ind(n: usize, parents_mask: &[u64]) -> Result<Vec<u64>, CausasvError> {
let total_masks = 1usize << n;
let mut dp_ind = vec![0u64; total_masks];
dp_ind[0] = 1;
for mask in 1..total_masks {
let mask64 = mask as u64;
let mut bits = mask64;
while bits != 0 {
let bit = bits & bits.wrapping_neg();
let i = bit.trailing_zeros() as usize;
bits ^= bit;
if parents_mask[i] & mask64 == 0 {
let prev = dp_ind[mask ^ (1usize << i)];
dp_ind[mask] = dp_ind[mask].checked_add(prev).ok_or_else(|| {
CausasvError::Overflow(format!(
"dp_ind[{mask:#b}] overflowed u64 — use approx for this DAG"
))
})?;
}
}
}
Ok(dp_ind)
}
fn value_cached_dense<F>(cache: &mut [f64], value_fn: &F, mask: u64) -> Result<f64, CausasvError>
where
F: Fn(&[NodeId]) -> Result<f64, CausasvError>,
{
let v = cache[mask as usize];
if !v.is_nan() {
return Ok(v);
}
let coalition = mask_to_coalition(mask);
let v = value_fn(&coalition)?;
cache[mask as usize] = v;
Ok(v)
}
pub(crate) fn dag_exact_asv<F>(
dag: &Dag,
value_fn: F,
parents_mask: &[u64],
) -> Result<AsvResult, CausasvError>
where
F: Fn(&[NodeId]) -> Result<f64, CausasvError>,
{
let n = dag.node_count();
if n > 20 {
return Err(CausasvError::InvalidConfig(format!(
"exact_dag requires n ≤ 20 (2^n DP states), got {n}"
)));
}
let total_masks = 1usize << n;
let full_mask = total_masks - 1;
let full64 = full_mask as u64;
let mut dp_fwd = vec![0u64; total_masks];
dp_fwd[0] = 1;
for mask in 0..total_masks {
if dp_fwd[mask] == 0 {
continue;
}
let mask64 = mask as u64;
let mut not_placed = (!mask64) & full64;
while not_placed != 0 {
let bit = not_placed & not_placed.wrapping_neg();
let i = bit.trailing_zeros() as usize;
not_placed ^= bit;
if parents_mask[i] & mask64 == parents_mask[i] {
let next = mask | (1 << i);
dp_fwd[next] = dp_fwd[next].checked_add(dp_fwd[mask]).ok_or_else(|| {
CausasvError::Overflow(format!(
"dp_fwd[{mask:#b}] overflowed u64 — use approx for this DAG"
))
})?;
}
}
}
let dp_ind = compute_dp_ind(n, parents_mask)?;
let total = dp_ind[full_mask] as f64;
let mut phi = vec![0.0f64; n];
let mut cache = vec![f64::NAN; total_masks];
for (mask, &fwd_count) in dp_fwd.iter().enumerate() {
if fwd_count == 0 {
continue; }
let mask64 = mask as u64;
let w_prefix = fwd_count as f64;
let v_s = value_cached_dense(&mut cache, &value_fn, mask64)?;
let mut not_placed = (!mask64) & full64;
while not_placed != 0 {
let bit = not_placed & not_placed.wrapping_neg();
let i = bit.trailing_zeros() as usize;
not_placed ^= bit;
if parents_mask[i] & mask64 != parents_mask[i] {
continue; }
let with_i_mask = mask | (1usize << i);
let suffix_mask = full_mask ^ with_i_mask; let w_suffix = dp_ind[suffix_mask] as f64;
let mask_with_i = mask64 | (1u64 << i);
phi[i] += w_prefix
* w_suffix
* (value_cached_dense(&mut cache, &value_fn, mask_with_i)? - v_s);
}
}
let values = (0..n).map(|i| (NodeId(i as u32), phi[i] / total)).collect();
Ok(AsvResult {
values,
n_samples: total_masks,
seed: None,
is_exact: true,
effective_sample_size: None,
converged: None,
stderr: None,
n_order_ideals: None,
state_ratio: None,
memory_mb: None,
fallback_from: None,
fallback_reason: None,
method_used: None,
})
}