tididi_cnf/
config.rs

1//! What a run is asked for, and the configuration record it runs on.
2//!
3//! [`Mode`] and [`Limits`] are what a caller of [`crate::count`] chooses;
4//! `Settings` is the rest of what the command line can set, and
5//! `Settings::refuse_inert` refuses whichever of it the run's mode would
6//! ignore. `Config` is the record every stage below the driver reads, built
7//! from the limits, the settings and vitri's environment settings by
8//! `Config::new`; [`crate::vtree::VtreeOptions`] is the per-request half of it
9//! that travels with one vtree construction. Nothing here reads argv, the
10//! environment or a file: `cli` fills `Limits` and `Settings` from the command
11//! line, `frontend::request` reads vitri's environment settings, and the
12//! stages read the resulting `Config`.
13
14/// The `--preprocess` level.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub(crate) enum PreprocessLevel {
17    /// Pass the input formula through without simplification.
18    None,
19    /// Run vitri's full simplification pipeline.
20    Full,
21}
22
23impl PreprocessLevel {
24    /// Every level, in the order `--help` lists them.
25    pub(crate) const ALL: [PreprocessLevel; 2] = [PreprocessLevel::None, PreprocessLevel::Full];
26
27    /// The level's name, as `--preprocess` spells it.
28    pub(crate) fn token(self) -> &'static str {
29        match self {
30            PreprocessLevel::None => "none",
31            PreprocessLevel::Full => "full",
32        }
33    }
34}
35
36/// The limits a run works under: the time it may take and the address space
37/// it may use.
38///
39/// These are the binary's `--time-limit-ms` and `--mem-limit-mb`, with the
40/// same meaning. `Limits::default()` sets neither.
41///
42/// ```
43/// let limits = tididi_cnf::Limits { time_limit_ms: Some(60_000), ..Default::default() };
44/// assert_eq!(limits.mem_limit_mb, 0);
45/// ```
46#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
47pub struct Limits {
48    /// The time the whole run may take, in milliseconds, counted from the
49    /// moment it starts. Every stage budget is a share of it, and a run that
50    /// reaches it stops with [`Error::Incomplete`](crate::Error::Incomplete).
51    /// `None` sets no limit, and the stages use fixed budgets of their own.
52    pub time_limit_ms: Option<u64>,
53    /// Lower the process's address-space limit (`RLIMIT_AS`) to this many MiB
54    /// while the run lasts, as `ulimit -v` does, so the stages that adapt to
55    /// memory pressure can see the limit: a compile that runs out of memory
56    /// splits the formula instead of failing. The run only ever lowers the
57    /// limit, never raises it, and puts back the one it found when it
58    /// returns. `0` leaves it unchanged, the library's default. The binary's
59    /// `--mem-limit-mb` has a default of its own, which `tididi --help`
60    /// prints. A WebAssembly build has no such limit and refuses any other
61    /// value with [`Error::Usage`](crate::Error::Usage).
62    pub mem_limit_mb: u64,
63}
64
65/// What the command line sets beyond the mode and the limits: `--vtree`,
66/// `--preprocess`, whether `--save-tdd` asks for the circuit, and the
67/// `TIDIDI_SAT_PRUNE` switch. The library's [`crate::count`] leaves all four
68/// unset, so none of them can be inert there.
69#[derive(Clone, Debug, Default)]
70pub(crate) struct Settings {
71    /// `--vtree`: compile on this spec instead of the default construction.
72    pub(crate) vtree: Option<String>,
73    /// `--preprocess`, when it was on the command line.
74    pub(crate) preprocess: Option<PreprocessLevel>,
75    /// Whether `--save-tdd` was on the command line. The run must then be a
76    /// plain compile, whose circuit the command line writes out.
77    pub(crate) save_tdd: bool,
78    /// `TIDIDI_SAT_PRUNE`, when it is set: `Some(false)` for `off`,
79    /// `Some(true)` for `on`. The weighted count refuses it set either way,
80    /// since its compiles never prune.
81    pub(crate) sat_prune: Option<bool>,
82}
83
84impl Settings {
85    /// Refuse a setting the run's `mode` or its input would ignore. Every
86    /// such check is here, and the driver makes it once, as soon as the
87    /// instance's header, or a bundle's record, has decided the mode and
88    /// before any preprocessing. Each check is on the setting's presence, and
89    /// its message names the setting and what made it inert: the input being
90    /// a vitri bundle (`bundle`), or what made the run that mode, the `--mode`
91    /// it was given (`flag`) or the header lines of `meta`.
92    pub(crate) fn refuse_inert(
93        &self,
94        flag: Option<Mode>,
95        meta: &vitri::cnf::CnfMeta,
96        mode: Mode,
97        bundle: bool,
98    ) -> Result<(), crate::Error> {
99        // A bundle brings the formula vitri preprocessed and the vtree vitri
100        // built for it, so the run neither preprocesses nor builds a vtree.
101        if bundle && self.vtree.is_some() {
102            return Err(crate::Error::Usage(
103                "--vtree applies to a DIMACS file, but the input is a vitri bundle, which \
104                 brings its own vtree. Drop --vtree, or give the original formula to compile \
105                 on that construction."
106                    .to_owned(),
107            ));
108        }
109        if bundle && self.preprocess.is_some() {
110            return Err(crate::Error::Usage(
111                "--preprocess applies to a DIMACS file, but the input is a vitri bundle, whose \
112                 formula vitri has already preprocessed. Drop --preprocess."
113                    .to_owned(),
114            ));
115        }
116        let cause = || match (flag, Mode::declared(meta)) {
117            (Some(flag), _) => format!("--mode {flag}"),
118            _ if bundle => "the bundle's preprocess.json".to_owned(),
119            (None, Some((_, lines))) => lines,
120            (None, None) => "the default".to_owned(),
121        };
122        // Only a compile builds the circuit over every original variable
123        // whose count is the one reported.
124        if self.save_tdd && mode != Mode::Compile {
125            return Err(crate::Error::Usage(format!(
126                "--save-tdd needs mode compile, but {} asks for a count (this run's \
127                 mode is {mode}). A counting run compiles a preprocessed formula whose diagram \
128                 does not carry the reported count, so there is no faithful circuit to save. \
129                 Drop --save-tdd to compute the count.",
130                cause()
131            )));
132        }
133        // `--preprocess` sets the simplify stage of the frontend the compile,
134        // MC and WMC share. The projected counts preprocess through the
135        // projection chain instead, which always simplifies.
136        if self.preprocess.is_some() && mode.projected() {
137            return Err(crate::Error::Usage(format!(
138                "--preprocess applies to the compile, mc and wmc modes, but {} makes \
139                 this run a projected count (mode {mode}), whose projection chain always simplifies. \
140                 Drop --preprocess.",
141                cause()
142            )));
143        }
144        // The weighted count has one construction, its cascade on vitri's
145        // portfolio.
146        if self.vtree.is_some() && mode == Mode::Wmc {
147            return Err(crate::Error::Usage(format!(
148                "--vtree applies to the compile, mc, pmc and pwmc modes, but {} makes \
149                 this run a weighted count (mode wmc), which always compiles through its cascade \
150                 on portfolio. Drop --vtree.",
151                cause()
152            )));
153        }
154        // The weighted count compiles with a weight store, which the SAT-pruning
155        // pass cannot read, so its compiles never prune. The projected
156        // weighted count compiles without one and prunes.
157        if self.sat_prune.is_some() && mode == Mode::Wmc {
158            return Err(crate::Error::Environment(format!(
159                "TIDIDI_SAT_PRUNE is set, but it applies to the compile, mc, pmc and pwmc \
160                 modes, and {} makes this run a weighted count (mode wmc), whose \
161                 compiles never run SAT pruning. Unset TIDIDI_SAT_PRUNE.",
162                cause()
163            )));
164        }
165        Ok(())
166    }
167}
168
169/// The run's settings: what the caller asked for, the mode the instance
170/// resolved, `source_structure_profile`, which the driver measures on the
171/// instance, and what the run's vtree constructions share,
172/// `portfolio_history` and `vtree_time`.
173pub(crate) struct Config {
174    /// `--vtree`, when the caller gave it. Without it the model and weighted
175    /// counts run their cascades; with it the model count compiles on it
176    /// directly and the weighted count refuses it.
177    pub(crate) vtree: Option<String>,
178    /// The portfolio's record of the vtrees it has built, shared by every
179    /// vtree request of the run.
180    pub(crate) portfolio_history: vitri::decompose::PortfolioBuildHistory,
181    /// The time the run's vtree constructions take, which every stage that
182    /// builds one adds to and the record reports as `vtree_ms`.
183    pub(crate) vtree_time: crate::budget::VtreeTime,
184    /// vitri's structure profile of the source formula before preprocessing,
185    /// measured once by the driver and copied into every vtree-selection
186    /// request of the run.
187    pub(crate) source_structure_profile: Option<vitri::score::StructureProfile>,
188    /// Whether the frontend simplifies the formula before the compile.
189    pub(crate) preprocess_level: PreprocessLevel,
190    /// What the run answers, as `driver::parse_input` resolves it from the
191    /// mode the caller named and the instance header through
192    /// [`Mode::resolve`]. Read, never re-derived, below the driver.
193    pub(crate) mode: Mode,
194    /// The run's time limit in ms, [`Limits::time_limit_ms`]. Internal budgets
195    /// (Arjun, the Shannon split, SAT pruning) are shares of it; without it the
196    /// absolute defaults apply.
197    pub(crate) time_limit_ms: Option<u64>,
198    /// Whether the run's compiles may run the SAT-pruning pass: on unless
199    /// `TIDIDI_SAT_PRUNE=off`, a diagnostic off-switch for paired on/off
200    /// measurements. The driver hands the run's compiles a SAT-pruning pass
201    /// ([`crate::compile::sat_prune::SatPrune`]) only when it is set.
202    pub(crate) sat_prune: bool,
203    /// vitri's `VITRI_*` settings, read and checked from the environment as
204    /// the run starts: the base of every vitri request the run makes.
205    pub(crate) vitri: vitri::RunConfig,
206    /// The vtree a vitri bundle came with, when the input is one: the tree
207    /// the run compiles the bundle's formula on, or how vitri's preprocessing
208    /// settled the instance. `None` when the input is a formula, whose vtrees
209    /// the run builds.
210    pub(crate) input_vtree: Option<crate::frontend::FrontendVtree>,
211}
212
213impl Config {
214    /// The record a run of `mode` starts from: `limits`, `settings` and
215    /// vitri's settings as the caller gave them, the preprocessing defaulting
216    /// to [`PreprocessLevel::Full`], and the vtree a bundle came with.
217    pub(crate) fn new(
218        limits: &Limits,
219        settings: &Settings,
220        vitri: vitri::RunConfig,
221        mode: Mode,
222        input_vtree: Option<crate::frontend::FrontendVtree>,
223    ) -> Config {
224        Config {
225            vtree: settings.vtree.clone(),
226            portfolio_history: vitri::decompose::PortfolioBuildHistory::default(),
227            vtree_time: crate::budget::VtreeTime::default(),
228            source_structure_profile: None,
229            preprocess_level: settings.preprocess.unwrap_or(PreprocessLevel::Full),
230            mode,
231            time_limit_ms: limits.time_limit_ms,
232            sat_prune: settings.sat_prune.unwrap_or(true),
233            vitri,
234            input_vtree,
235        }
236    }
237
238    /// The vtree the run compiles on: `--vtree`, or `portfolio` without it.
239    pub(crate) fn vtree_spec(&self) -> &str {
240        self.vtree.as_deref().unwrap_or("portfolio")
241    }
242
243    /// The tree the run's main compile is on, as its log lines name it: the
244    /// bundle's, or the construction [`Config::vtree_spec`] names.
245    pub(crate) fn vtree_name(&self) -> &str {
246        if self.input_vtree.is_some() { "bundle" } else { self.vtree_spec() }
247    }
248
249    /// A bundle's vtree, when the input is a bundle whose preprocessing left
250    /// a formula to compile.
251    pub(crate) fn bundle_tree(&self) -> Option<&crate::vtree::VtreeBuild> {
252        match &self.input_vtree {
253            Some(crate::frontend::FrontendVtree::Built(built)) => Some(built),
254            _ => None,
255        }
256    }
257}
258
259/// What a run answers: one of the four counting tasks, or a compile.
260///
261/// The instance header declares it: a `c t mc|wmc|pmc|pwmc` line names the
262/// count, and a `c p show` line makes it projected. A mode given explicitly —
263/// the binary's `--mode`, or the `mode` argument of [`crate::count`] — must
264/// agree with that header, and without either the run is a
265/// [`Compile`](Mode::Compile). The two are merged once, when the instance is
266/// read; every dispatch reads the result and nothing re-derives it.
267///
268/// It prints as the token the header, `--mode` and the stats record spell it
269/// with: `compile`, `mc`, `wmc`, `pmc` or `pwmc`.
270#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
271pub enum Mode {
272    /// Compile the formula into a diagram over every original variable, and
273    /// count its models.
274    Compile,
275    /// Count the models: exact, an integer.
276    Mc,
277    /// Weigh the models by the `c p weight` table: in a bounded-precision log
278    /// domain.
279    Wmc,
280    /// Count the assignments to the `c p show` variables that extend to a
281    /// model: exact, an integer.
282    Pmc,
283    /// Weigh those assignments by the `c p weight` table: exact, a rational.
284    Pwmc,
285}
286
287impl std::fmt::Display for Mode {
288    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289        f.write_str(self.token())
290    }
291}
292
293/// The mode a token names, spelled as it prints, so a caller that takes the
294/// mode as text (the C and Python bindings) reads it with the binary's
295/// vocabulary.
296///
297/// ```
298/// use tididi_cnf::Mode;
299///
300/// assert_eq!("pwmc".parse::<Mode>()?, Mode::Pwmc);
301/// assert!("weighted".parse::<Mode>().is_err());
302/// # Ok::<(), tididi_cnf::Error>(())
303/// ```
304impl std::str::FromStr for Mode {
305    type Err = crate::Error;
306
307    /// [`Error::Usage`](crate::Error::Usage), listing the tokens, for any
308    /// other spelling.
309    fn from_str(token: &str) -> Result<Self, crate::Error> {
310        one_of("mode", token, &Mode::ALL, Mode::token).map_err(crate::Error::Usage)
311    }
312}
313
314impl Mode {
315    /// Every mode, in the order `--help` lists them.
316    pub(crate) const ALL: [Mode; 5] = [Mode::Compile, Mode::Mc, Mode::Wmc, Mode::Pmc, Mode::Pwmc];
317
318    /// The mode named as vitri's `mode` is, `None` for a vitri mode this
319    /// crate does not answer.
320    pub(crate) fn of_vitri(mode: vitri::cnf::Mode) -> Option<Self> {
321        Mode::ALL.into_iter().find(|ours| ours.vitri() == mode)
322    }
323
324    /// The vitri mode of the same name, whose token is this mode's.
325    fn vitri(self) -> vitri::cnf::Mode {
326        match self {
327            Mode::Compile => vitri::cnf::Mode::Compile,
328            Mode::Mc => vitri::cnf::Mode::Mc,
329            Mode::Wmc => vitri::cnf::Mode::Wmc,
330            Mode::Pmc => vitri::cnf::Mode::Pmc,
331            Mode::Pwmc => vitri::cnf::Mode::Pwmc,
332        }
333    }
334
335    /// The mode's name, as `--mode`, the record's `mode` and the `c t` header
336    /// spell it: vitri's token, so all three spell a mode the same way.
337    pub(crate) fn token(self) -> &'static str {
338        self.vitri().token()
339    }
340
341    /// The count one `c t` line and a `c p show` line declare: the `c t` type,
342    /// made projected by a show line.
343    fn from_header(header_type: Option<vitri::cnf::Mode>, show: bool) -> Option<Self> {
344        let (weighted, projected) = match header_type {
345            Some(header_type) => (header_type.is_weighted(), header_type.is_projected() || show),
346            None if show => (false, true),
347            None => return None,
348        };
349        Some(match (weighted, projected) {
350            (true, true) => Mode::Pwmc,
351            (true, false) => Mode::Wmc,
352            (false, true) => Mode::Pmc,
353            (false, false) => Mode::Mc,
354        })
355    }
356
357    /// The mode the instance header declares, and the header lines that
358    /// declare it, or `None` when it declares none.
359    ///
360    /// A `c t <type>` line names a count, and a `c p show` line makes the count
361    /// projected; `c p weight` lines alone declare nothing. A show line without
362    /// a `c t` line reads as a projected count, which `--mode pwmc` may make
363    /// weighted.
364    pub(crate) fn declared(meta: &vitri::cnf::CnfMeta) -> Option<(Self, String)> {
365        let header_type = meta.declared_track();
366        let show = meta.declared_show_vars().is_some();
367        let mode = Self::from_header(header_type, show)?;
368        let lines = match header_type {
369            Some(header_type) if show && !header_type.is_projected() => {
370                format!("the instance's `c t {}` and `c p show` lines", header_type.token())
371            }
372            Some(header_type) => format!("the instance's `c t {}` line", header_type.token()),
373            None => "the instance's `c p show` line".to_owned(),
374        };
375        Some((mode, lines))
376    }
377
378    /// Merge the mode the caller asked for (`--mode`, or the `mode` argument
379    /// of [`crate::count`]) with the instance header. Without one the header
380    /// decides, and a header that declares nothing is a plain compile. With
381    /// one the header must agree: a mode the header contradicts is a usage
382    /// error naming both. A lone `c p show` line agrees with either projected
383    /// mode.
384    ///
385    /// Asking for mode mc or pmc also refuses an instance with `c p weight`
386    /// lines, which the unweighted count would ignore: a mode asked for
387    /// refuses input that mode would silently drop, as mc refuses a `c p show`
388    /// line. A header-driven count (`c t mc` or `c t pmc`, no mode asked for)
389    /// keeps reading past the weights, and mode compile, whose circuit has no
390    /// use for weights, ignores them.
391    ///
392    /// The messages name the mode, not the flag, so they read the same to the
393    /// binary's user and to a library caller.
394    pub(crate) fn resolve(flag: Option<Self>, meta: &vitri::cnf::CnfMeta) -> Result<Self, crate::Error> {
395        let mode = Self::merge(flag, meta)?;
396        let weighted_instead = match flag {
397            Some(Mode::Mc) => "mode wmc to count with the weights (mode pwmc for a projected count)",
398            Some(Mode::Pmc) => "mode pwmc to count with the weights",
399            _ => return Ok(mode),
400        };
401        if meta.declared_weights().is_some() {
402            return Err(crate::Error::Usage(format!(
403                "mode {} would ignore the instance's `c p weight` lines. Ask for \
404                 {weighted_instead}.",
405                mode.token()
406            )));
407        }
408        Ok(mode)
409    }
410
411    /// [`Mode::resolve`] without the weight check: the flag against the
412    /// header's `c t` and `c p show` lines.
413    fn merge(flag: Option<Self>, meta: &vitri::cnf::CnfMeta) -> Result<Self, crate::Error> {
414        let declared = Self::declared(meta);
415        let (flag, (header, lines)) = match (flag, declared) {
416            (None, None) => return Ok(Mode::Compile),
417            (None, Some((header, _))) => return Ok(header),
418            (Some(flag), None) => return Ok(flag),
419            (Some(flag), Some(declared)) => (flag, declared),
420        };
421        let show_only = meta.declared_track().is_none();
422        if flag == header || (show_only && flag.projected()) {
423            return Ok(flag);
424        }
425        let what = if show_only {
426            "makes the count projected (mode pmc or pwmc)".to_owned()
427        } else {
428            format!("declares mode {}", header.token())
429        };
430        Err(crate::Error::Usage(format!(
431            "mode {} contradicts {lines}, which {what}. Leave the mode unset to answer \
432             what the header declares.",
433            flag.token()
434        )))
435    }
436
437    pub(crate) fn projected(self) -> bool {
438        self.vitri().is_projected()
439    }
440}
441
442/// The item of `all` whose `token` is `value`, or else a message that names
443/// `what` and lists every token. Modes, preprocessing levels and output
444/// formats are all read through it.
445pub(crate) fn one_of<T: Copy>(what: &str, value: &str, all: &[T], token: fn(T) -> &'static str) -> Result<T, String> {
446    all.iter()
447        .copied()
448        .find(|item| token(*item) == value)
449        .ok_or_else(|| format!("unknown {what} {value:?}; valid: {}.", spelled(all, token, ", ")))
450}
451
452/// Every spelling of a vocabulary, in its order, joined by `separator`.
453pub(crate) fn spelled<T: Copy>(all: &[T], token: fn(T) -> &'static str, separator: &str) -> String {
454    all.iter().map(|item| token(*item)).collect::<Vec<_>>().join(separator)
455}
456
457#[cfg(test)]
458#[path = "tests/config.rs"]
459mod tests;