use std::collections::{HashMap, HashSet};
use crate::cnf::CnfFormula;
use crate::vtree::{Vtree, VtreeIdx};
use super::{
child_boundary_features, clause_high_lca, clause_lca_counts, context_width_from_high_lca,
node_depths, outside_context_tables, sorted_bounds, subtree_tables,
vtree_crossing_clauses_per_node,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Feature {
InsideWidth,
OutsideWidth,
TightWidth,
CrossingClauses,
LocalClauses,
LeftInsideWidth,
RightInsideWidth,
LeftOutsideWidth,
RightOutsideWidth,
LeftTightWidth,
RightTightWidth,
LeftCrossingClauses,
RightCrossingClauses,
SubtreeLeaves,
SubtreeClauses,
LeftSubtreeLeaves,
RightSubtreeLeaves,
LeftSubtreeClauses,
RightSubtreeClauses,
Depth,
SubtreeHeight,
ChildTightSum,
ChildTightProduct,
ChildTightOverlap,
ChildTightImbalance,
ChildTightUniqueSum,
ChildOutsideOverlap,
ChildOutsideUnion,
ChildOutsideSymmetricDifference,
LocalJoinDensitySubtree,
LocalJoinDensityTotal,
SignedSplitDistinct,
UnsignedSplitDistinct,
SignedSplitEntropyBits,
CutRank,
TwinIn,
TwinOut,
Below,
}
pub(super) const FEATURE_NAMES: [(&str, Feature); 38] = [
("inside_width", Feature::InsideWidth),
("outside_width", Feature::OutsideWidth),
("tight_width", Feature::TightWidth),
("crossing_clauses", Feature::CrossingClauses),
("local_clauses", Feature::LocalClauses),
("left_inside_width", Feature::LeftInsideWidth),
("right_inside_width", Feature::RightInsideWidth),
("left_outside_width", Feature::LeftOutsideWidth),
("right_outside_width", Feature::RightOutsideWidth),
("left_tight_width", Feature::LeftTightWidth),
("right_tight_width", Feature::RightTightWidth),
("left_crossing_clauses", Feature::LeftCrossingClauses),
("right_crossing_clauses", Feature::RightCrossingClauses),
("subtree_leaves", Feature::SubtreeLeaves),
("subtree_clauses", Feature::SubtreeClauses),
("left_subtree_leaves", Feature::LeftSubtreeLeaves),
("right_subtree_leaves", Feature::RightSubtreeLeaves),
("left_subtree_clauses", Feature::LeftSubtreeClauses),
("right_subtree_clauses", Feature::RightSubtreeClauses),
("depth", Feature::Depth),
("subtree_height", Feature::SubtreeHeight),
("child_tight_sum", Feature::ChildTightSum),
("child_tight_product", Feature::ChildTightProduct),
("child_tight_overlap", Feature::ChildTightOverlap),
("child_tight_imbalance", Feature::ChildTightImbalance),
("child_tight_unique_sum", Feature::ChildTightUniqueSum),
("child_outside_overlap", Feature::ChildOutsideOverlap),
("child_outside_union", Feature::ChildOutsideUnion),
(
"child_outside_symmetric_difference",
Feature::ChildOutsideSymmetricDifference,
),
(
"local_join_density_subtree",
Feature::LocalJoinDensitySubtree,
),
("local_join_density_total", Feature::LocalJoinDensityTotal),
("signed_split_distinct", Feature::SignedSplitDistinct),
("unsigned_split_distinct", Feature::UnsignedSplitDistinct),
("signed_split_entropy_bits", Feature::SignedSplitEntropyBits),
("cutrank", Feature::CutRank),
("twin_in", Feature::TwinIn),
("twin_out", Feature::TwinOut),
("below", Feature::Below),
];
impl Feature {
pub(super) fn from_name(name: &str) -> Option<Feature> {
FEATURE_NAMES
.iter()
.find(|(known, _)| *known == name)
.map(|&(_, feature)| feature)
}
pub(super) fn is_from_split(self) -> bool {
matches!(
self,
Feature::LocalJoinDensitySubtree
| Feature::LocalJoinDensityTotal
| Feature::SignedSplitDistinct
| Feature::UnsignedSplitDistinct
| Feature::SignedSplitEntropyBits
)
}
pub(super) fn is_from_cut(self) -> bool {
matches!(
self,
Feature::CutRank | Feature::TwinIn | Feature::TwinOut | Feature::Below
)
}
}
struct CutTables {
density_subtree: Vec<f64>,
density_total: Vec<f64>,
signed_distinct: Vec<u32>,
unsigned_distinct: Vec<u32>,
signed_entropy_bits: Vec<f64>,
cutrank: Vec<u32>,
twin_in: Vec<u32>,
twin_out: Vec<u32>,
below: Vec<u32>,
has_cut: Vec<bool>,
}
const CUTRANK_CAP: usize = 4096;
impl CutTables {
fn build(
vtree: &Vtree,
formula: &CnfFormula,
subtree_clauses: &[u64],
subtree_leaves: &[u32],
clause_at: &[u32],
split: bool,
cut: bool,
) -> CutTables {
let nodes = vtree.num_nodes();
let zeros = || vec![0u32; nodes];
let reals = || vec![0f64; nodes];
let mut tables = CutTables {
density_subtree: reals(),
density_total: reals(),
signed_distinct: zeros(),
unsigned_distinct: zeros(),
signed_entropy_bits: reals(),
cutrank: zeros(),
twin_in: zeros(),
twin_out: zeros(),
below: zeros(),
has_cut: vec![false; nodes],
};
let (entry, exit) = super::subtree_intervals(vtree);
if split {
tables.fill_split(vtree, formula, subtree_clauses, clause_at, &entry, &exit);
}
if cut {
tables.fill_cut(vtree, formula, subtree_leaves, &entry, &exit);
}
tables
}
fn fill_split(
&mut self,
vtree: &Vtree,
formula: &CnfFormula,
subtree_clauses: &[u64],
clause_at: &[u32],
entry: &[u32],
exit: &[u32],
) {
let total_clauses: u64 = clause_at.iter().map(|&load| u64::from(load)).sum();
let clauses_at = super::clause_lca_members(vtree, formula);
let mut signed: HashMap<(Vec<i32>, Vec<i32>), u32> = HashMap::new();
let mut unsigned: HashSet<(Vec<u32>, Vec<u32>)> = HashSet::new();
for (node, left, _right) in vtree.internal_bottomup() {
let t = node.idx();
let clause_ids = &clauses_at[t];
if clause_ids.is_empty() {
continue;
}
let load = clause_ids.len() as u64;
signed.clear();
unsigned.clear();
let mut left_adjacency: Vec<Vec<usize>> = Vec::with_capacity(clause_ids.len());
let mut right_adjacency: Vec<Vec<usize>> = Vec::with_capacity(clause_ids.len());
for &clause_idx in clause_ids {
let mut left_literals = Vec::new();
let mut right_literals = Vec::new();
for lit in &formula.clauses[clause_idx].literals {
let leaf = vtree.leaf_of(lit.var).idx();
let inside = entry[left.idx()] <= entry[leaf] && entry[leaf] < exit[left.idx()];
if inside {
left_literals.push(lit.to_dimacs());
} else {
right_literals.push(lit.to_dimacs());
}
}
for side in [&mut left_literals, &mut right_literals] {
side.sort_unstable();
side.dedup();
}
let variables = |literals: &[i32]| {
let mut vars: Vec<u32> = literals.iter().map(|l| l.unsigned_abs()).collect();
vars.sort_unstable();
vars.dedup();
vars
};
let left_vars = variables(&left_literals);
let right_vars = variables(&right_literals);
unsigned.insert((left_vars.clone(), right_vars.clone()));
*signed
.entry((left_literals, right_literals))
.or_insert(0u32) += 1;
left_adjacency.push(left_vars.into_iter().map(|v| v as usize).collect());
right_adjacency.push(right_vars.into_iter().map(|v| v as usize).collect());
}
let mut counts: Vec<u32> = signed.values().copied().collect();
counts.sort_unstable();
let entropy: f64 = counts
.iter()
.map(|&count| {
let share = f64::from(count) / load as f64;
-share * share.log2()
})
.sum();
let matched = super::maximum_matching_size(&left_adjacency)
.min(super::maximum_matching_size(&right_adjacency));
let matched_f = f64::from(matched);
self.density_subtree[t] = matched_f * load as f64 / subtree_clauses[t] as f64;
self.density_total[t] = matched_f * load as f64 / total_clauses.max(1) as f64;
self.signed_distinct[t] = signed.len() as u32;
self.unsigned_distinct[t] = unsigned.len() as u32;
self.signed_entropy_bits[t] = entropy;
}
}
fn fill_cut(
&mut self,
vtree: &Vtree,
formula: &CnfFormula,
subtree_leaves: &[u32],
entry: &[u32],
exit: &[u32],
) {
let declared = formula.num_vars as usize;
let space = declared.max(vtree.num_vars() as usize);
if space == 0 {
return;
}
let mut place = vec![u32::MAX; space];
for (leaf, var) in vtree.leaf_bottomup() {
place[var.idx()] = entry[leaf.idx()];
}
let mut adjacency: Vec<Vec<u32>> = vec![Vec::new(); space];
let mut clause_vars: Vec<u32> = Vec::new();
for clause in &formula.clauses {
clause_vars.clear();
clause_vars.extend(clause.literals.iter().map(|lit| lit.var.0));
clause_vars.sort_unstable();
clause_vars.dedup();
for &x in &clause_vars {
for &y in &clause_vars {
if x != y {
adjacency[x as usize].push(y);
}
}
}
}
for neighbours in adjacency.iter_mut() {
neighbours.sort_unstable();
neighbours.dedup();
}
let mut leaves: Vec<(u32, u32)> = vtree
.leaf_bottomup()
.map(|(leaf, var)| (entry[leaf.idx()], var.0))
.collect();
leaves.sort_unstable();
let mut rows: HashSet<Vec<u32>> = HashSet::new();
let mut columns: HashSet<Vec<u32>> = HashSet::new();
let mut restricted: Vec<u32> = Vec::new();
let mut reached: Vec<u32> = Vec::new();
let mut seen = vec![false; space];
let mut scratch = RankScratch::default();
for (node, _left, _right) in vtree.internal_bottomup() {
let t = node.idx();
let (lo, hi) = (entry[t], exit[t]);
if subtree_leaves[t] as usize == declared {
continue;
}
let inside = |v: u32| {
let at = place[v as usize];
lo <= at && at < hi
};
rows.clear();
columns.clear();
reached.clear();
let first = leaves.partition_point(|&(at, _)| at < lo);
let last = leaves.partition_point(|&(at, _)| at < hi);
for &(_, v) in &leaves[first..last] {
let neighbours = &adjacency[v as usize];
if neighbours.is_empty() {
continue;
}
restricted.clear();
restricted.extend(neighbours.iter().copied().filter(|&n| !inside(n)));
if restricted.is_empty() {
continue;
}
for &n in &restricted {
if !seen[n as usize] {
seen[n as usize] = true;
reached.push(n);
}
}
if !rows.contains(&restricted) {
rows.insert(restricted.clone());
}
}
for &u in &reached {
seen[u as usize] = false;
restricted.clear();
restricted.extend(adjacency[u as usize].iter().copied().filter(|&n| inside(n)));
if !columns.contains(&restricted) {
columns.insert(restricted.clone());
}
}
self.below[t] = subtree_leaves[t];
self.twin_in[t] = rows.len() as u32;
self.twin_out[t] = columns.len() as u32;
self.cutrank[t] = scratch.rank(rows.iter(), space);
self.has_cut[t] = true;
}
}
}
#[derive(Default)]
struct RankScratch {
at: Vec<u32>,
basis: Vec<Vec<u64>>,
used: Vec<usize>,
row: Vec<u64>,
}
impl RankScratch {
fn rank<'a, I>(&mut self, rows: I, space: usize) -> u32
where
I: Iterator<Item = &'a Vec<u32>>,
{
let RankScratch {
at,
basis,
used,
row,
} = self;
let words = space.div_ceil(64);
for &bit in used.iter() {
at[bit] = u32::MAX;
}
used.clear();
if at.len() < space {
at.resize(space, u32::MAX);
}
let mut rank = 0usize;
for members in rows {
row.clear();
row.resize(words, 0);
for &v in members {
row[v as usize / 64] |= 1 << (v as usize % 64);
}
while let Some(pivot) = leading_bit(row) {
let slot = at[pivot];
if slot == u32::MAX {
at[pivot] = rank as u32;
used.push(pivot);
match basis.get_mut(rank) {
Some(vector) => {
vector.clear();
vector.extend_from_slice(row);
}
None => basis.push(row.clone()),
}
rank += 1;
break;
}
for (word, other) in row.iter_mut().zip(&basis[slot as usize]) {
*word ^= other;
}
}
if rank >= CUTRANK_CAP {
return CUTRANK_CAP as u32;
}
}
rank as u32
}
}
fn leading_bit(row: &[u64]) -> Option<usize> {
row.iter()
.rposition(|&word| word != 0)
.map(|index| index * 64 + (63 - row[index].leading_zeros() as usize))
}
pub(super) struct Tables {
ctx_in: Vec<u32>,
ctx_out: Vec<u32>,
cross: Vec<u32>,
tight: Vec<u32>,
clause_at: Vec<u32>,
subtree_clauses: Vec<u64>,
subtree_leaves: Vec<u32>,
subtree_height: Vec<u32>,
depth: Vec<u32>,
sibling_overlap: Vec<u32>,
tight_unique_sum: Vec<u32>,
cut: Option<CutTables>,
}
impl Tables {
pub(super) fn build(vtree: &Vtree, formula: &CnfFormula, split: bool, cut: bool) -> Tables {
let clause_at = clause_lca_counts(vtree, formula);
let high_lca = clause_high_lca(vtree, formula);
let ctx_in = context_width_from_high_lca(vtree, &high_lca, None);
let outside = outside_context_tables(vtree, formula);
let cross = vtree_crossing_clauses_per_node(vtree, formula);
let tight: Vec<u32> = (0..vtree.num_nodes())
.map(|i| {
let idx = VtreeIdx(i as u32);
sorted_bounds(
ctx_in[i],
outside.widths[i],
cross[i],
vtree.node(idx).is_leaf(),
)[0]
})
.collect();
let subtree = subtree_tables(vtree, &clause_at);
let boundaries =
child_boundary_features(vtree, &tight, &outside.widths, &outside.sibling_overlap);
let cut = (split || cut).then(|| {
CutTables::build(
vtree,
formula,
&subtree.clauses,
&subtree.leaves,
&clause_at,
split,
cut,
)
});
Tables {
ctx_in,
ctx_out: outside.widths,
cross,
tight,
clause_at,
subtree_clauses: subtree.clauses,
subtree_leaves: subtree.leaves,
subtree_height: subtree.height,
depth: node_depths(vtree),
sibling_overlap: outside.sibling_overlap,
tight_unique_sum: boundaries.tight_unique_sum,
cut,
}
}
fn cut(&self) -> &CutTables {
self.cut
.as_ref()
.expect("the split and cut tables are built when a model reads one of their columns")
}
pub(super) fn has_cut_row(&self, node: VtreeIdx) -> bool {
self.cut.as_ref().is_some_and(|cut| cut.has_cut[node.idx()])
}
pub(super) fn cost_tables(&self) -> super::UnifiedCostTables<'_> {
super::UnifiedCostTables {
clause_at: &self.clause_at,
ctx_in: &self.ctx_in,
ctx_out: &self.ctx_out,
sibling_overlap: &self.sibling_overlap,
cross: &self.cross,
}
}
pub(super) fn clause_at(&self) -> &[u32] {
&self.clause_at
}
pub(super) fn value(
&self,
feature: Feature,
node: VtreeIdx,
left: VtreeIdx,
right: VtreeIdx,
) -> f64 {
let t = node.idx();
let l = left.idx();
let r = right.idx();
let overlap = self.sibling_overlap[t];
match feature {
Feature::InsideWidth => f64::from(self.ctx_in[t]),
Feature::OutsideWidth => f64::from(self.ctx_out[t]),
Feature::TightWidth => f64::from(self.tight[t]),
Feature::CrossingClauses => f64::from(self.cross[t]),
Feature::LocalClauses => f64::from(self.clause_at[t]),
Feature::LeftInsideWidth => f64::from(self.ctx_in[l]),
Feature::RightInsideWidth => f64::from(self.ctx_in[r]),
Feature::LeftOutsideWidth => f64::from(self.ctx_out[l]),
Feature::RightOutsideWidth => f64::from(self.ctx_out[r]),
Feature::LeftTightWidth => f64::from(self.tight[l]),
Feature::RightTightWidth => f64::from(self.tight[r]),
Feature::LeftCrossingClauses => f64::from(self.cross[l]),
Feature::RightCrossingClauses => f64::from(self.cross[r]),
Feature::SubtreeLeaves => f64::from(self.subtree_leaves[t]),
Feature::SubtreeClauses => self.subtree_clauses[t] as f64,
Feature::LeftSubtreeLeaves => f64::from(self.subtree_leaves[l]),
Feature::RightSubtreeLeaves => f64::from(self.subtree_leaves[r]),
Feature::LeftSubtreeClauses => self.subtree_clauses[l] as f64,
Feature::RightSubtreeClauses => self.subtree_clauses[r] as f64,
Feature::Depth => f64::from(self.depth[t]),
Feature::SubtreeHeight => f64::from(self.subtree_height[t]),
Feature::ChildTightSum => {
if self.clause_at[t] == 0 {
0.0
} else {
f64::from(self.tight[l]) + f64::from(self.tight[r])
}
}
Feature::ChildTightProduct => f64::from(self.tight[l]) * f64::from(self.tight[r]),
Feature::ChildTightOverlap => {
f64::from(self.tight[l] + self.tight[r] - self.tight_unique_sum[t])
}
Feature::ChildTightImbalance => f64::from(self.tight[l].abs_diff(self.tight[r])),
Feature::ChildTightUniqueSum => f64::from(self.tight_unique_sum[t]),
Feature::ChildOutsideOverlap => f64::from(overlap),
Feature::ChildOutsideUnion => f64::from(self.ctx_out[l] + self.ctx_out[r] - overlap),
Feature::ChildOutsideSymmetricDifference => {
f64::from(self.ctx_out[l] + self.ctx_out[r] - 2 * overlap)
}
Feature::LocalJoinDensitySubtree => self.cut().density_subtree[t],
Feature::LocalJoinDensityTotal => self.cut().density_total[t],
Feature::SignedSplitDistinct => f64::from(self.cut().signed_distinct[t]),
Feature::UnsignedSplitDistinct => f64::from(self.cut().unsigned_distinct[t]),
Feature::SignedSplitEntropyBits => self.cut().signed_entropy_bits[t],
Feature::CutRank => f64::from(self.cut().cutrank[t]),
Feature::TwinIn => f64::from(self.cut().twin_in[t]),
Feature::TwinOut => f64::from(self.cut().twin_out[t]),
Feature::Below => f64::from(self.cut().below[t]),
}
}
}
#[cfg(test)]
mod tests;