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;