tididi_cnf/vtree.rs
1//! Vtree construction for the compiler, and the descriptors of a formula's
2//! independent components.
3//!
4//! A vtree is a binary tree whose leaves are the formula's variables. The
5//! diagram the compiler builds follows its shape, so the vtree decides how
6//! large the diagram grows. vitri builds every one, from a spec in its
7//! [vtree grammar](https://github.com/Tractables/vitri/blob/main/docs/vtrees.md),
8//! the one the binary's `--vtree` takes; `"portfolio"`, the default, builds a
9//! catalog of candidates and keeps the best-scoring one.
10//!
11//! [`build_vtree`] builds one tree for the whole formula, and
12//! [`build_vtree_with_components`] one per independent component as well when
13//! the spec reads the formula's structure; [`VtreeOptions`] are the options of
14//! one request. A component is a group of clauses that shares no variable with
15//! the rest: [`Component`] names its clauses and variables, and
16//! [`ComponentVtree`] pairs it with the tree built for it.
17//!
18//! Every construction asks vitri, and what it asks is decided here: the
19//! selection context a request carries, the request itself and the conversion
20//! of the trees that come back. The run's frontends (`crate::frontend`), which
21//! have vitri build a vtree beside its preprocessing, use the same selection
22//! context and conversion.
23
24use std::sync::Arc;
25use vitri::cnf::CnfFormula;
26use tididi::vtree::{VarId, Vtree};
27
28use crate::config::Config;
29use crate::convert::{diagram_var, vtree_from_vitri};
30use crate::Error;
31
32/// The options of one vtree request.
33///
34/// The default ranks the candidates of a portfolio spec by how evenly they
35/// spread the clauses over the tree, with no deadline. Nothing about the
36/// construction is read from ambient state apart from vitri's `VITRI_*`
37/// vtree-selection settings, which every build reads from the environment.
38#[derive(Clone, Debug)]
39pub struct VtreeOptions {
40 pub(crate) peak_active: bool,
41 pub(crate) deadline: Option<std::time::Instant>,
42 /// The portfolio's record of earlier builds. Cloned options share it, so
43 /// the builds of one run's cascade see one another; a fresh constructor
44 /// starts an independent history.
45 pub(crate) portfolio_history: vitri::decompose::PortfolioBuildHistory,
46 pub(crate) source_structure_profile: Option<vitri::score::StructureProfile>,
47 /// Build a portfolio of this one entry of vitri's built-in catalog
48 /// ([`PORTFOLIO_CATALOG`]), every other entry skipped; `None` builds the
49 /// default portfolio. The model count's stall recompile asks for one. Read
50 /// in one place, [`resolve_vitri_selection_ctx`].
51 pub(crate) only_portfolio_entry: Option<&'static str>,
52}
53
54/// Clause-balance selection, no deadline, a fresh portfolio history, the
55/// default portfolio.
56impl Default for VtreeOptions {
57 fn default() -> Self {
58 Self {
59 peak_active: false,
60 deadline: None,
61 portfolio_history: vitri::decompose::PortfolioBuildHistory::default(),
62 source_structure_profile: None,
63 only_portfolio_entry: None,
64 }
65 }
66}
67
68impl VtreeOptions {
69 /// Selection that ranks the candidates by their peak context width over
70 /// every variable; the projected counts ask for it when there is a show
71 /// set.
72 pub fn by_peak_width() -> Self {
73 Self {
74 peak_active: true,
75 ..Self::default()
76 }
77 }
78
79 /// Stop construction at `deadline`. `None` leaves it unbounded. A build
80 /// that reaches it still returns a vtree: the candidate under way gets one
81 /// short attempt and the rest are not started.
82 #[must_use]
83 pub fn with_deadline(mut self, deadline: Option<std::time::Instant>) -> Self {
84 self.deadline = deadline;
85 self
86 }
87
88 /// The run's portfolio history and the structure profile of its input.
89 #[must_use]
90 pub(crate) fn with_run_policy(mut self, config: &Config) -> Self {
91 self.portfolio_history = config.portfolio_history.clone();
92 self.source_structure_profile = config.source_structure_profile;
93 self
94 }
95}
96
97/// What a build returns: the vtree over every variable of the formula, and
98/// the component trees when the build split the formula.
99#[derive(Clone, Debug)]
100pub struct VtreeBuild {
101 /// The vtree over the formula's variables `1..=num_vars`.
102 pub root: Arc<Vtree>,
103 /// One tree per independent component, when the spec reads the formula's
104 /// structure and the formula has two or more components; `None` when the
105 /// build did not split.
106 /// [`ComponentPlan::from_components`](crate::compile::ComponentPlan::from_components)
107 /// turns it into the plan [`compile_cnf`](crate::compile::compile_cnf)
108 /// takes.
109 pub components: Option<Vec<ComponentVtree>>,
110}
111
112impl VtreeBuild {
113 /// Attach freshly built trees to the run's engine before any diagram is
114 /// built on them.
115 pub(crate) fn bind(mut self, engine: &tididi::Engine) -> Self {
116 self.root = engine.bind_vtree(Arc::unwrap_or_clone(self.root));
117 self.components = self.components.map(|components| {
118 components.into_iter().map(|mut component| {
119 component.vtree = engine.bind_vtree(Arc::unwrap_or_clone(component.vtree));
120 component
121 }).collect()
122 });
123 self
124 }
125}
126
127/// An independent component of a formula: its clauses, and the formula
128/// variables it renumbers.
129///
130/// A component is compiled as a formula of its own over the local variables
131/// `1..=k`, `k` being `local_to_global.len()`: local variable `i + 1` stands
132/// for the formula variable `local_to_global[i]`.
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct Component {
135 /// The component's clauses, as indices into the formula's clauses.
136 pub clause_indices: Vec<usize>,
137 /// The formula variable of each local variable, ascending: entry `i` is
138 /// the one local variable `i + 1` stands for.
139 pub local_to_global: Vec<VarId>,
140}
141
142/// A component and the vtree built for it, whose leaves are the component's
143/// local variables.
144#[derive(Clone, Debug)]
145pub struct ComponentVtree {
146 /// The vtree over the local variables `1..=component.local_to_global.len()`.
147 pub vtree: Arc<Vtree>,
148 /// The clauses and variables the vtree is for.
149 pub component: Component,
150}
151
152/// Accept a spec of vitri's vtree grammar, the one `--vtree` takes.
153pub(crate) fn validate_vtree_spec(spec: &str) -> Result<(), Error> {
154 vitri::spec::validate_vtree_spec(spec).map_err(|error| Error::VtreeSpec(error.to_string()))
155}
156
157/// The construction family a vtree spec names, its text before any
158/// parameters: `hypergraph-bisect` for `hypergraph-bisect:imbalance=0.40`. A
159/// parameter that names another construction
160/// (`flowcutter-primal:binarize=hypergraph-bisect`) stays a parameter.
161pub(crate) fn spec_family(spec: &str) -> &str {
162 spec.split_once(':').map_or(spec, |(family, _)| family)
163}
164
165/// The vitri request for one construction in the caller's window: the caller
166/// has carved the absolute construction deadline out of its run, so vitri
167/// spends all of what remains of it rather than dividing it again.
168///
169/// `spec` goes to vitri as written; the caller supplies every policy that has
170/// a vitri representation.
171fn vitri_vtree_build_config(
172 spec: &str,
173 deadline: Option<std::time::Instant>,
174 components: vitri::config::ComponentPolicy,
175) -> vitri::RunConfig {
176 vitri::RunConfig {
177 deadline,
178 construction_budget: vitri::config::ConstructionBudget::WholeRemaining,
179 vtree_spec: spec.to_owned(),
180 components,
181 candidates: 1,
182 ..vitri::RunConfig::default()
183 }
184}
185
186/// Translate the caller's [`VtreeOptions`] into vitri's selection context.
187fn vitri_selection_ctx(ctx: &VtreeOptions) -> vitri::decompose::SelectionCtx {
188 let mut selection = match ctx.peak_active {
189 true => vitri::decompose::SelectionCtx::peak(),
190 false => vitri::decompose::SelectionCtx::plain(),
191 };
192 selection.source_profile = ctx.source_structure_profile;
193 selection.portfolio.build_history = ctx.portfolio_history.clone();
194 selection
195}
196
197/// The base names of vitri's built-in portfolio catalog, the names its
198/// `PortfolioKnobs::skip` takes. vitri publishes the catalog's candidate specs
199/// but not this list; a test holds the two equal.
200pub(crate) const PORTFOLIO_CATALOG: [&str; 7] = [
201 "flowcutter-incidence",
202 "flowcutter-primal",
203 "goatd-incidence",
204 "goatd-primal",
205 "force",
206 "hypergraph-bisect",
207 "guided-bisect",
208];
209
210/// The selection context for `ctx` with the `VITRI_*` selection settings
211/// applied: every construction's, standalone or in a frontend.
212///
213/// A request for one catalog entry ([`VtreeOptions::only_portfolio_entry`])
214/// skips every other entry, set after the environment's settings, so that
215/// `VITRI_PORTFOLIO_SKIP` can neither add an entry back nor take the one
216/// asked for away.
217///
218/// # Panics
219///
220/// When the entry asked for is not in [`PORTFOLIO_CATALOG`]: the crate asks
221/// only for entries it names.
222pub(crate) fn resolve_vitri_selection_ctx(
223 ctx: &VtreeOptions,
224) -> Result<vitri::decompose::SelectionCtx, vitri::VitriError> {
225 let mut selection = vitri_selection_ctx(ctx).with_env_defaults()?;
226 if let Some(only) = ctx.only_portfolio_entry {
227 assert!(PORTFOLIO_CATALOG.contains(&only), "{only:?} is not a portfolio catalog entry");
228 selection.portfolio.skip = PORTFOLIO_CATALOG.into_iter().filter(|&entry| entry != only).collect();
229 }
230 Ok(selection)
231}
232
233/// Convert one vitri construction result into this crate's types.
234///
235/// Both standalone construction and the whole-run frontend use this exact
236/// conversion, so component numbering and tree ownership cannot drift between
237/// the two entry points.
238pub(crate) fn vtree_build_from_vitri(built: vitri::component::VtreeBuild) -> VtreeBuild {
239 let vitri::component::VtreeBuild {
240 vtree,
241 components,
242 selections: _,
243 candidate_sets: _,
244 limits: _,
245 cached_components: _,
246 // A frontend adds it to the run's vtree time; a stage times its own
247 // builds (`budget::VtreeTime`).
248 construction_ms: _,
249 } = built;
250
251 let components = components.map(|components| {
252 components
253 .into_iter()
254 .map(|component| ComponentVtree {
255 vtree: Arc::new(vtree_from_vitri(&component.vtree)),
256 component: Component {
257 clause_indices: component.clause_indices,
258 local_to_global: component
259 .local_to_outer
260 .into_iter()
261 .map(diagram_var)
262 .collect(),
263 },
264 })
265 .collect()
266 });
267
268 VtreeBuild { root: Arc::new(vtree_from_vitri(&vtree)), components }
269}
270
271/// Every standalone construction goes through here, so its selection context
272/// is resolved in one place. vitri validates `spec`.
273fn build(
274 formula: &CnfFormula,
275 spec: &str,
276 options: &VtreeOptions,
277 components: vitri::config::ComponentPolicy,
278) -> Result<VtreeBuild, Error> {
279 let selection = resolve_vitri_selection_ctx(options)
280 .map_err(|error| Error::Environment(error.to_string()))?;
281 // A deadline that has already passed is vitri's to answer, the same way it
282 // answers one that passes mid-build: the first candidate gets one short
283 // attempt and the rest are never started, so the build still returns a
284 // vtree and the compile behind it decides against the wall.
285 let config = vitri_vtree_build_config(spec, options.deadline, components);
286 vitri::component::build_vtree(formula, &config, &selection)
287 .map(vtree_build_from_vitri)
288 .map_err(|error| Error::VtreeSpec(format!("vtree construction for {spec:?} failed: {error}")))
289}
290
291/// Build one vtree for the whole formula from a spec, such as `"portfolio"`.
292///
293/// # Errors
294///
295/// - [`Error::VtreeSpec`]: `spec` is not a spec of vitri's grammar, or the
296/// construction failed, as it does for a formula with no variables.
297/// - [`Error::Environment`]: a `VITRI_*` vtree-selection setting in the
298/// environment is invalid.
299pub fn build_vtree(formula: &CnfFormula, spec: &str, options: &VtreeOptions) -> Result<Arc<Vtree>, Error> {
300 Ok(build(formula, spec, options, vitri::config::ComponentPolicy::Whole)?.root)
301}
302
303/// Build a vtree for a formula from a spec, and one per independent component
304/// when the spec reads the formula's structure and the formula has two or
305/// more components.
306///
307/// [`VtreeOptions::default`] ranks a portfolio's candidates by clause balance;
308/// [`VtreeOptions::by_peak_width`] by their peak context width over every
309/// variable.
310///
311/// # Errors
312///
313/// As [`build_vtree`].
314pub fn build_vtree_with_components(
315 formula: &CnfFormula,
316 spec: &str,
317 options: &VtreeOptions,
318) -> Result<VtreeBuild, Error> {
319 build(formula, spec, options, vitri::config::ComponentPolicy::Split)
320}
321
322/// A stage's reading of a construction that failed: it says why on stderr
323/// and gives up, as a compile that stops does, so the stage's own fallback
324/// runs. A panic instead would reach the recovery around the compile, which
325/// takes any panic for a refused allocation and splits the formula.
326pub(crate) fn built_or_give_up<T>(built: Result<T, Error>) -> Option<T> {
327 built.map_err(|error| eprintln!("[vtree] gave up: {error}")).ok()
328}
329
330/// The components a streaming compile on `spec` takes `formula` in, as
331/// [`build_vtree_with_components`] splits it.
332///
333/// Empty for a formula with no clauses; for a spec vitri builds from the
334/// formula's structure ([`vitri::spec::is_structural_spec`]), whose formula
335/// splits into two or more independent components, one per component;
336/// otherwise the whole formula as one. `spec` is one the run has validated.
337pub(crate) fn split_components(formula: &CnfFormula, spec: &str) -> Vec<Component> {
338 if formula.clauses().is_empty() {
339 return vec![];
340 }
341
342 let structural = vitri::spec::is_structural_spec(spec).expect("the run validated its vtree spec");
343 if structural
344 && let Some(groups) = formula.detect_components()
345 {
346 eprintln!("[vtree] {} independent components", groups.len());
347 return groups
348 .into_iter()
349 .map(|clause_indices| Component {
350 local_to_global: formula
351 .component_vars(&clause_indices)
352 .into_iter()
353 .map(diagram_var)
354 .collect(),
355 clause_indices,
356 })
357 .collect();
358 }
359
360 vec![whole_formula_component(formula)]
361}
362
363/// The whole formula as one component, with the identity variable map.
364pub(crate) fn whole_formula_component(formula: &CnfFormula) -> Component {
365 Component {
366 clause_indices: (0..formula.clauses().len()).collect(),
367 local_to_global: (1..=formula.num_vars()).map(VarId).collect(),
368 }
369}
370
371#[cfg(test)]
372#[path = "tests/vtree.rs"]
373mod tests;