use std::fmt;
use std::sync::Arc;
use crate::vtree::{VarId, Vtree, VtreeArena, VtreeIdx};
use crate::candidates::CandidateSet;
use crate::cnf::{CnfFormula, Local, ShowMask, ShowSet};
use crate::config::{ComponentPolicy, ConstructionBudget, RunConfig};
use crate::decompose::{BuildLimits, SelectionCtx};
use crate::diagnostics::diag;
use crate::error::VitriError;
use crate::spec::{
BALANCED_SPEC, BuildRequest, ParsedSpec, SelectionRecord, VtreeArtifacts,
build_one_vtree_artifacts, parse_vtree_spec,
};
#[derive(Clone, Debug)]
pub struct ComponentVtree {
pub vtree: Arc<Vtree>,
pub clause_indices: Vec<usize>,
pub local_to_outer: Vec<VarId>,
}
pub(crate) struct LocalView {
pub formula: CnfFormula,
pub show: Option<ShowSet<Local>>,
pub local_to_outer: Vec<VarId>,
}
pub(crate) fn local_view(
formula: &CnfFormula,
clause_indices: &[usize],
show: Option<&ShowMask>,
) -> LocalView {
let (sub, local_to_outer) = formula.extract_component(clause_indices);
LocalView {
show: show.map(|m| m.restrict(&local_to_outer)),
formula: sub,
local_to_outer,
}
}
#[derive(Debug)]
pub struct VtreeBuild {
pub vtree: Arc<Vtree>,
pub components: Option<Vec<ComponentVtree>>,
pub selections: Vec<SelectionRecord>,
pub candidate_sets: Vec<CandidateSet>,
pub limits: crate::decompose::BuildLimitsReport,
pub construction_ms: u64,
}
fn is_structural_spec(spec: &ParsedSpec<'_>) -> bool {
spec.family.is_structural()
}
pub fn build_vtree(
formula: &CnfFormula,
config: &RunConfig,
selection: &SelectionCtx,
) -> Result<VtreeBuild, VitriError> {
config.validate()?;
selection.goatd.validate()?;
build_vtree_anchored(
formula,
&config.anchored(std::time::Instant::now()),
selection,
)
}
pub(crate) fn build_vtree_anchored(
formula: &CnfFormula,
config: &RunConfig,
selection: &SelectionCtx,
) -> Result<VtreeBuild, VitriError> {
if formula.num_vars == 0 {
return Err(VitriError::input(
"the formula declares 0 variables; a vtree has at least one leaf, so there is \
nothing to build one over",
));
}
let started = std::time::Instant::now();
let _metered = matches!(
config.construction_budget,
ConstructionBudget::Deterministic { .. }
)
.then(|| crate::decompose::meter::arm(started));
let limits = BuildLimits {
deadline: config.construction_deadline(started),
budget_ms: config.budget_ms,
candidates: config.candidates,
};
let mut parsed = parse_vtree_spec(&config.vtree_spec)?;
parsed.inherit(config.reading);
let request = BuildRequest {
formula,
spec: &parsed,
ctx: selection,
limits: &limits,
};
let mut built = build_vtree_split(request, config.components, &mut ())?;
built.construction_ms = started.elapsed().as_millis() as u64;
Ok(built)
}
pub(crate) trait BuildObserver {
fn cached_vtree_reused(&mut self) {}
fn component_show_mask(&mut self, _mask: &ShowMask) {}
}
impl BuildObserver for () {}
#[derive(PartialEq, Eq, Hash)]
struct ComponentKey {
num_vars: u32,
clauses: Vec<Vec<(u32, bool)>>,
show: Option<crate::cnf::ShowMask>,
}
impl ComponentKey {
fn new(sub: &CnfFormula, show: Option<crate::cnf::ShowMask>) -> Self {
let mut clauses: Vec<Vec<(u32, bool)>> = sub
.clauses
.iter()
.map(|c| {
let mut lits: Vec<(u32, bool)> =
c.literals.iter().map(|l| (l.var.0, l.positive)).collect();
lits.sort_unstable();
lits
})
.collect();
clauses.sort_unstable();
ComponentKey {
num_vars: sub.num_vars,
clauses,
show,
}
}
}
const TINY_COMPONENT_MAX_VARS: u32 = 30;
const fn is_tiny_component(num_vars: u32) -> bool {
num_vars <= TINY_COMPONENT_MAX_VARS
}
fn assert_one_leaf_per_var(vtree: &Vtree, num_vars: u32, what: fmt::Arguments<'_>) {
assert_eq!(
vtree.num_leaves(),
num_vars,
"{what}: leaf count ({}) ≠ num_vars ({}) — vtree builder produced a malformed vtree",
vtree.num_leaves(),
num_vars,
);
}
fn tiny_component_artifacts(
sub: &CnfFormula,
request: crate::decompose::ConversionRequest<'_>,
) -> VtreeArtifacts {
let (vtree, selection) = match crate::decompose::vtree_from_minfill(
sub,
crate::decompose::INTERNAL_ELIMINATION_SEED,
request,
) {
Ok(b) => (
b.vtree,
SelectionRecord {
winning_spec: Some(crate::decompose::MINFILL_SPEC.to_string()),
scores: None,
td_meta: b.td.meta,
},
),
Err(_) => (
Arc::new(Vtree::balanced(sub.num_vars)),
SelectionRecord {
winning_spec: Some(BALANCED_SPEC.to_string()),
scores: None,
td_meta: None,
},
),
};
VtreeArtifacts {
vtree,
selection,
candidate_set: CandidateSet::default(),
limits: crate::decompose::BuildLimitsReport::default(),
}
}
pub(crate) fn build_vtree_split<O: BuildObserver>(
req: BuildRequest<'_>,
policy: ComponentPolicy,
observer: &mut O,
) -> Result<VtreeBuild, VitriError> {
if !policy.is_whole()
&& is_structural_spec(req.spec)
&& let Some(comps) = req.formula.detect_components()
{
diag!(
"[components] {} independent sub-problems detected",
comps.len()
);
return build_per_component(req, &comps, observer);
}
crate::score::agg::set_component(None);
let built = build_one_vtree_artifacts(req)?;
assert_one_leaf_per_var(
&built.vtree,
req.formula.num_vars,
format_args!("vtree for spec {:?}", req.spec.raw),
);
Ok(VtreeBuild {
vtree: built.vtree,
components: None,
selections: vec![built.selection],
candidate_sets: vec![built.candidate_set],
limits: built.limits,
construction_ms: 0,
})
}
fn build_per_component<O: BuildObserver>(
req: BuildRequest<'_>,
comps: &[Vec<usize>],
observer: &mut O,
) -> Result<VtreeBuild, VitriError> {
let BuildRequest {
formula,
spec,
ctx,
limits,
} = req;
let mut comp_vtrees = Vec::new();
let mut candidate_sets: Vec<CandidateSet> = Vec::new();
let mut selections: Vec<SelectionRecord> = Vec::new();
let mut limits_report = crate::decompose::BuildLimitsReport::default();
let mut in_component = vec![false; formula.num_vars as usize];
let mut vtree_cache: std::collections::HashMap<ComponentKey, VtreeArtifacts> =
std::collections::HashMap::new();
let mut clauses_left: usize = comps.iter().map(|c| c.len()).sum();
for (index, comp_indices) in comps.iter().enumerate() {
crate::score::agg::set_component(Some(index));
let comp_deadline = limits
.deadline
.map(|d| crate::budget::pro_rata_deadline(d, comp_indices.len(), clauses_left));
clauses_left = clauses_left.saturating_sub(comp_indices.len());
let LocalView {
formula: sub_formula,
show: comp_show,
local_to_outer,
} = local_view(formula, comp_indices, ctx.objective.show_mask());
for &v in &local_to_outer {
in_component[v.idx()] = true;
}
let tiny = is_tiny_component(sub_formula.num_vars);
let local_show = if tiny {
None
} else {
comp_show.map(|s| s.mask(sub_formula.num_vars))
};
let key = ComponentKey::new(&sub_formula, local_show.clone());
let artifacts = if let Some(cached) = vtree_cache.get(&key) {
observer.cached_vtree_reused();
cached.clone()
} else {
let built = if tiny {
tiny_component_artifacts(
&sub_formula,
crate::decompose::ConversionRequest {
spec: Some(crate::decompose::MINFILL_SPEC),
reading: spec.reading,
effort_scale: crate::budget::vtree_effort_scale(limits.budget_ms),
deadline: limits.deadline,
real_deadline: None,
trace: ctx.conversion.trace,
},
)
} else {
let comp_ctx = SelectionCtx {
objective: ctx
.objective
.with_mask(local_show.clone().map(std::rc::Rc::new)),
..ctx.clone()
};
let comp_limits = BuildLimits {
deadline: comp_deadline,
..limits.clone()
};
if let Some(local) = &local_show {
observer.component_show_mask(local);
}
build_one_vtree_artifacts(BuildRequest {
formula: &sub_formula,
spec,
ctx: &comp_ctx,
limits: &comp_limits,
})?
};
limits_report.absorb(built.limits.clone());
vtree_cache.insert(key, built.clone());
built
};
let VtreeArtifacts {
vtree: sub_vtree,
selection: sub_selection,
candidate_set: sub_candidates,
limits: _,
} = artifacts;
assert_one_leaf_per_var(
&sub_vtree,
sub_formula.num_vars,
format_args!("component vtree for spec {:?}", spec.raw),
);
comp_vtrees.push(ComponentVtree {
vtree: sub_vtree,
clause_indices: comp_indices.clone(),
local_to_outer,
});
selections.push(sub_selection);
candidate_sets.push(sub_candidates);
}
let free_vars: Vec<VarId> = (0..formula.num_vars)
.map(VarId)
.filter(|v| !in_component[v.idx()])
.collect();
let full_vtree = Arc::new(graft_component_vtrees(
&comp_vtrees,
&free_vars,
formula.num_vars,
));
assert_one_leaf_per_var(
&full_vtree,
formula.num_vars,
format_args!("grafted full vtree"),
);
Ok(VtreeBuild {
vtree: full_vtree,
components: Some(comp_vtrees),
selections,
candidate_sets,
limits: limits_report,
construction_ms: 0,
})
}
fn graft_component_vtrees(
components: &[ComponentVtree],
free_vars: &[VarId],
total_vars: u32,
) -> Vtree {
let mut nodes = VtreeArena::new();
let mut subtree_roots: Vec<VtreeIdx> = Vec::new();
for comp in components {
subtree_roots.push(nodes.graft(&comp.vtree, |local| comp.local_to_outer[local.idx()]));
}
for &var in free_vars {
subtree_roots.push(nodes.leaf(var));
}
assert!(
!subtree_roots.is_empty(),
"graft_component_vtrees: no components or free vars"
);
let mut root = subtree_roots[0];
for &next_root in &subtree_roots[1..] {
root = nodes.internal(root, next_root);
}
Vtree::from_nodes(nodes.into_nodes(), root, total_vars)
}
#[cfg(test)]
mod tests;