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;