use std::collections::{BTreeSet, HashMap};
use crate::asv::AsvResult;
use crate::cache::{value_cached, vec_to_mask};
use crate::error::CausasvError;
use crate::graph::{Dag, NodeId};
pub(crate) fn find_rooted_tree_root(dag: &Dag) -> Result<NodeId, CausasvError> {
let in_deg = dag.in_degrees();
let roots: Vec<NodeId> = in_deg
.iter()
.enumerate()
.filter(|&(_, d)| *d == 0)
.map(|(i, _)| NodeId(i as u32))
.collect();
if roots.len() != 1 {
return Err(CausasvError::NotRootedTree);
}
let all_unit = in_deg
.iter()
.enumerate()
.filter(|(i, _)| NodeId(*i as u32) != roots[0])
.all(|(_, &d)| d == 1);
if !all_unit {
return Err(CausasvError::NotRootedTree);
}
Ok(roots[0])
}
pub(crate) fn subtree_sizes(dag: &Dag, root: NodeId) -> Vec<usize> {
let mut sizes = vec![0usize; dag.node_count()];
dfs_sizes(dag, root, &mut sizes);
sizes
}
fn dfs_sizes(dag: &Dag, node: NodeId, sizes: &mut Vec<usize>) {
sizes[node.0 as usize] = 1;
for &child in dag.children_raw(node) {
dfs_sizes(dag, child, sizes);
sizes[node.0 as usize] += sizes[child.0 as usize];
}
}
fn subtree_ideal_counts(dag: &Dag, root: NodeId) -> Vec<u64> {
let mut counts = vec![0u64; dag.node_count()];
dfs_ideal_counts(dag, root, &mut counts);
counts
}
fn dfs_ideal_counts(dag: &Dag, node: NodeId, counts: &mut Vec<u64>) {
let mut product: u64 = 1;
for &child in dag.children_raw(node) {
dfs_ideal_counts(dag, child, counts);
product = product.saturating_mul(counts[child.0 as usize]);
}
counts[node.0 as usize] = 1u64.saturating_add(product);
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct TreeExactCostEstimate {
pub(crate) feasible: bool,
pub(crate) max_cartesian_terms: u64,
pub(crate) cartesian_term_budget: u64,
pub(crate) estimated_total_terms: u64,
pub(crate) total_term_budget: u64,
}
pub(crate) fn estimate_tree_exact_cost(
dag: &Dag,
root: NodeId,
cartesian_term_budget: u64,
total_term_budget: u64,
) -> TreeExactCostEstimate {
let ideal_counts = subtree_ideal_counts(dag, root);
let mut max_cartesian_terms = 0u64;
let mut estimated_total_terms = 0u64;
accumulate_cost(
dag,
root,
&ideal_counts,
1,
&mut max_cartesian_terms,
&mut estimated_total_terms,
);
TreeExactCostEstimate {
feasible: max_cartesian_terms <= cartesian_term_budget
&& estimated_total_terms <= total_term_budget,
max_cartesian_terms,
cartesian_term_budget,
estimated_total_terms,
total_term_budget,
}
}
fn accumulate_cost(
dag: &Dag,
node: NodeId,
ideal_counts: &[u64],
running_product: u64,
max_cartesian_terms: &mut u64,
estimated_total_terms: &mut u64,
) {
*max_cartesian_terms = (*max_cartesian_terms).max(running_product);
*estimated_total_terms = (*estimated_total_terms).saturating_add(running_product);
let children = dag.children_raw(node);
let m = children.len();
if m > 0 {
let child_counts: Vec<u64> = children
.iter()
.map(|&c| ideal_counts[c.0 as usize])
.collect();
let mut prefix = vec![1u64; m + 1];
for i in 0..m {
prefix[i + 1] = prefix[i].saturating_mul(child_counts[i]);
}
let mut suffix = vec![1u64; m + 1];
for i in (0..m).rev() {
suffix[i] = suffix[i + 1].saturating_mul(child_counts[i]);
}
for (j, &child) in children.iter().enumerate() {
let side_product = prefix[j].saturating_mul(suffix[j + 1]);
let child_running_product = running_product.saturating_mul(side_product);
accumulate_cost(
dag,
child,
ideal_counts,
child_running_product,
max_cartesian_terms,
estimated_total_terms,
);
}
}
}
fn ancestors_of(dag: &Dag, target: NodeId, root: NodeId) -> Vec<NodeId> {
if target == root {
return vec![];
}
let mut path = Vec::new();
find_path(dag, root, target, &mut path);
path.pop(); path
}
fn find_path(dag: &Dag, current: NodeId, target: NodeId, path: &mut Vec<NodeId>) -> bool {
path.push(current);
if current == target {
return true;
}
for &child in dag.children_raw(current) {
if find_path(dag, child, target, path) {
return true;
}
}
path.pop();
false
}
pub(crate) fn enumerate_order_ideals(dag: &Dag, roots: &[NodeId]) -> Vec<Vec<NodeId>> {
if roots.is_empty() {
return vec![vec![]];
}
let per_tree: Vec<Vec<Vec<NodeId>>> = roots.iter().map(|&r| tree_ideals(dag, r)).collect();
cartesian_product_vecs(&per_tree)
.into_iter()
.map(|mut v| {
v.sort_unstable();
v
})
.collect()
}
fn tree_ideals(dag: &Dag, root: NodeId) -> Vec<Vec<NodeId>> {
let children = dag.children_raw(root);
let children_ideals: Vec<Vec<Vec<NodeId>>> =
children.iter().map(|&c| tree_ideals(dag, c)).collect();
let mut result = vec![vec![]]; for combo in cartesian_product_vecs(&children_ideals) {
let mut ideal = Vec::with_capacity(1 + combo.len());
ideal.push(root);
ideal.extend_from_slice(&combo);
ideal.sort_unstable();
result.push(ideal);
}
result
}
fn cartesian_product_vecs(groups: &[Vec<Vec<NodeId>>]) -> Vec<Vec<NodeId>> {
if groups.is_empty() {
return vec![vec![]];
}
let mut result = vec![vec![]];
for group in groups {
let mut new_result = Vec::new();
for prev in &result {
for item in group {
let mut combined = prev.clone();
combined.extend_from_slice(item);
new_result.push(combined);
}
}
result = new_result;
}
result
}
fn log_lin_ext_of_s(dag: &Dag, s: &[NodeId], root: NodeId, log_fact: &[f64]) -> f64 {
if s.is_empty() {
return 0.0; }
let s_set: BTreeSet<NodeId> = s.iter().copied().collect();
let mut sub_sizes = vec![0usize; dag.node_count()];
dfs_sizes_in_s(dag, root, &s_set, &mut sub_sizes);
let sum_log: f64 = s
.iter()
.map(|&v| (sub_sizes[v.0 as usize] as f64).ln())
.sum();
log_fact[s.len()] - sum_log
}
fn dfs_sizes_in_s(dag: &Dag, node: NodeId, s_set: &BTreeSet<NodeId>, sizes: &mut Vec<usize>) {
if !s_set.contains(&node) {
return;
}
sizes[node.0 as usize] = 1;
for &child in dag.children_raw(node) {
if s_set.contains(&child) {
dfs_sizes_in_s(dag, child, s_set, sizes);
sizes[node.0 as usize] += sizes[child.0 as usize];
}
}
}
pub(crate) fn tree_exact_asv<F>(dag: &Dag, value_fn: F) -> Result<AsvResult, CausasvError>
where
F: Fn(&[NodeId]) -> Result<f64, CausasvError>,
{
let root = find_rooted_tree_root(dag)?;
let sizes = subtree_sizes(dag, root);
let n = dag.node_count();
if n > 64 {
return Err(CausasvError::InvalidConfig(format!(
"bitmask coalitions require n ≤ 64, got {n}"
)));
}
let mut log_fact = vec![0.0f64; n + 1];
for k in 1..=n {
log_fact[k] = log_fact[k - 1] + (k as f64).ln();
}
let log_s: Vec<f64> = sizes.iter().map(|&s| (s as f64).ln()).collect();
let mut phi = vec![0.0f64; n];
let mut cache = HashMap::<u64, f64>::new();
for i in dag.all_nodes() {
let anc = ancestors_of(dag, i, root);
let d = anc.len();
let mut ideals_per_level: Vec<Vec<Vec<NodeId>>> = Vec::with_capacity(d);
for k in 0..d {
let a_k = anc[k];
let on_path = if k + 1 < d { anc[k + 1] } else { i };
let side: Vec<NodeId> = dag
.children_raw(a_k)
.iter()
.copied()
.filter(|&c| c != on_path)
.collect();
ideals_per_level.push(enumerate_order_ideals(dag, &side));
}
for combo in cartesian_product_vecs(&ideals_per_level) {
let mut s_vec: Vec<NodeId> = anc.iter().copied().chain(combo).collect();
s_vec.sort_unstable();
let mut s_plus_i = s_vec.clone();
let pos = s_plus_i.partition_point(|&v| v < i);
s_plus_i.insert(pos, i);
let log_ls = log_lin_ext_of_s(dag, &s_vec, root, &log_fact);
let n_comp = n - s_vec.len() - 1;
let sum_log_si: f64 = s_plus_i.iter().map(|&v| log_s[v.0 as usize]).sum();
let w = (log_ls + log_fact[n_comp] + sum_log_si - log_fact[n]).exp();
let s_mask = vec_to_mask(&s_vec);
let si_mask = s_mask | (1u64 << i.0);
phi[i.0 as usize] += w
* (value_cached(&mut cache, &value_fn, si_mask)?
- value_cached(&mut cache, &value_fn, s_mask)?);
}
}
let values = dag.all_nodes().map(|v| (v, phi[v.0 as usize])).collect();
Ok(AsvResult {
values,
n_samples: 0,
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,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn add_balanced_subtree(dag: &mut Dag, parent: NodeId, depth: usize, prefix: &str) {
if depth == 0 {
return;
}
let left = dag.add_node(&format!("{prefix}l"));
let right = dag.add_node(&format!("{prefix}r"));
dag.add_edge(parent, left).unwrap();
dag.add_edge(parent, right).unwrap();
add_balanced_subtree(dag, left, depth - 1, &format!("{prefix}l"));
add_balanced_subtree(dag, right, depth - 1, &format!("{prefix}r"));
}
fn star_of_balanced_subtrees(num_branches: usize, depth: usize) -> Dag {
let mut dag = Dag::new();
let root = dag.add_node("root");
for b in 0..num_branches {
let branch_root = dag.add_node(&format!("b{b}"));
dag.add_edge(root, branch_root).unwrap();
add_balanced_subtree(&mut dag, branch_root, depth, &format!("b{b}"));
}
dag
}
#[test]
fn subtree_ideal_count_matches_known_small_values() {
let mut dag = Dag::new();
let leaf = dag.add_node("leaf");
assert_eq!(subtree_ideal_counts(&dag, leaf)[leaf.0 as usize], 2);
let dag = star_of_balanced_subtrees(1, 2);
let root = find_rooted_tree_root(&dag).unwrap();
let counts = subtree_ideal_counts(&dag, root);
assert_eq!(counts[root.0 as usize], 27);
}
#[test]
fn estimate_tree_exact_cost_cheap_case_is_feasible() {
let dag = star_of_balanced_subtrees(1, 3); let root = find_rooted_tree_root(&dag).unwrap();
let estimate = estimate_tree_exact_cost(&dag, root, 1_000_000, 10_000_000);
assert!(estimate.feasible);
}
#[test]
fn estimate_tree_exact_cost_flags_issue_36_reproduction_shape() {
let dag = star_of_balanced_subtrees(4, 3);
assert_eq!(dag.node_count(), 61);
let root = find_rooted_tree_root(&dag).unwrap();
let estimate = estimate_tree_exact_cost(&dag, root, 1_000_000, 10_000_000);
let i0 = 2u64;
let i1 = 1 + i0 * i0;
let i2 = 1 + i1 * i1;
let i3 = 1 + i2 * i2;
assert_eq!(i3, 677);
let expected_deepest_leaf_cost = i3.pow(3) * i2 * i1 * i0;
assert_eq!(estimate.max_cartesian_terms, expected_deepest_leaf_cost);
assert!(!estimate.feasible);
assert!(estimate.max_cartesian_terms > estimate.cartesian_term_budget);
}
#[test]
fn estimate_tree_exact_cost_never_hangs_on_a_much_larger_bushy_tree() {
let dag = star_of_balanced_subtrees(6, 4);
let root = find_rooted_tree_root(&dag).unwrap();
let estimate = estimate_tree_exact_cost(&dag, root, 1_000_000, 10_000_000);
assert!(!estimate.feasible);
assert!(estimate.max_cartesian_terms >= 1_000_000);
}
}