tididi_cnf/run.rs
1//! One call per task: [`count`], what it reads, and what it answers.
2//!
3//! [`count`] is the run the `tididi` binary makes, minus the printing: the
4//! binary parses its command line into the same mode and [`Limits`], calls the
5//! same `driver::solve`, and prints the [`Answer`] it gets back.
6
7use std::path::PathBuf;
8use std::sync::{Mutex, PoisonError};
9
10use num_bigint::BigUint;
11use num_rational::BigRational;
12use tididi::diagram::SignedLog;
13use tididi::Tdd;
14use vitri::cnf::{CnfFormula, CnfMeta};
15
16use crate::config::{Limits, Mode, Settings};
17use crate::output::Stats;
18use crate::Error;
19
20/// The instance a run reads: a DIMACS file, or a formula already parsed with
21/// its header.
22///
23/// Every form [`count`] takes converts into this, so a path (`&str`, `String`,
24/// `&Path`, `PathBuf`) or the pair [`parse_dimacs`](crate::parse_dimacs) and
25/// [`formula_from_clauses`](crate::formula_from_clauses) return can be passed
26/// as it is.
27#[derive(Debug)]
28pub enum Input {
29 /// A DIMACS file, read when the run starts. Its reading counts against
30 /// the time limit, as it does in the binary.
31 ///
32 /// A directory is read as the bundle `vitri --out-dir` writes. The run
33 /// compiles the bundle's `reduced.cnf` on its `vtree.vtree`, split into
34 /// the components `components.json` lists, and preprocesses nothing; its
35 /// mode is the one in `preprocess.json`, and a different `mode` is
36 /// refused. The answer is the original formula's: the record's lift is
37 /// applied to the count, and in mode compile the diagram is expanded back
38 /// onto the original variables.
39 File(PathBuf),
40 /// A formula and the header lines that came with it: the `c t` mode, the
41 /// `c p show` set and the `c p weight` table.
42 Formula(CnfFormula, CnfMeta),
43}
44
45impl From<&str> for Input {
46 fn from(path: &str) -> Self {
47 Input::File(path.into())
48 }
49}
50
51impl From<String> for Input {
52 fn from(path: String) -> Self {
53 Input::File(path.into())
54 }
55}
56
57impl From<&std::path::Path> for Input {
58 fn from(path: &std::path::Path) -> Self {
59 Input::File(path.into())
60 }
61}
62
63impl From<PathBuf> for Input {
64 fn from(path: PathBuf) -> Self {
65 Input::File(path)
66 }
67}
68
69impl From<(CnfFormula, CnfMeta)> for Input {
70 fn from((formula, meta): (CnfFormula, CnfMeta)) -> Self {
71 Input::Formula(formula, meta)
72 }
73}
74
75/// A count, in the domain its task produces.
76///
77/// It prints as the binary's answer line prints it: an integer in decimal, a
78/// rational as `<numerator>/<denominator>`, and a log-domain value as
79/// `<mantissa>e<exponent>` (`0` for zero).
80#[derive(Clone, Debug, PartialEq)]
81pub enum Count {
82 /// An exact integer: the model count ([`Mode::Mc`], [`Mode::Compile`]) and
83 /// the projected model count ([`Mode::Pmc`]).
84 Integer(BigUint),
85 /// An exact rational: the projected weighted count ([`Mode::Pwmc`]).
86 Rational(BigRational),
87 /// A value in the bounded-precision log domain, its sign and the natural
88 /// log of its magnitude: the weighted count ([`Mode::Wmc`]), which stays in
89 /// that domain because exact rationals grow without bound on large
90 /// instances.
91 Log(SignedLog),
92}
93
94impl Count {
95 /// The count as the nearest `f64`, for printing or plotting.
96 ///
97 /// Lossy: a large integer or rational is rounded to an `f64`'s precision,
98 /// and a magnitude beyond `f64`'s range, as a log-domain value's can be,
99 /// is infinite.
100 ///
101 /// ```
102 /// let count = tididi_cnf::Count::Rational(num_rational::BigRational::new(91.into(), 125.into()));
103 /// assert_eq!(count.to_f64(), 0.728);
104 /// ```
105 pub fn to_f64(&self) -> f64 {
106 use num_traits::ToPrimitive;
107 match self {
108 Count::Integer(count) => count.to_f64().unwrap_or(f64::INFINITY),
109 // A rational with a nonzero denominator always converts.
110 Count::Rational(count) => count.to_f64().unwrap_or(f64::NAN),
111 // Zero is sign 0 with a magnitude of exp(-inf) = 0.
112 Count::Log(count) => f64::from(count.sign) * count.ln_abs.exp(),
113 }
114 }
115}
116
117impl std::fmt::Display for Count {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 match self {
120 Count::Integer(count) => write!(f, "{count}"),
121 // Always both parts, so a whole number still reads as a rational.
122 Count::Rational(count) => write!(f, "{}/{}", count.numer(), count.denom()),
123 Count::Log(count) => {
124 if count.sign == 0 {
125 return f.write_str("0");
126 }
127 // Decompose 10^log10 into mantissa·10^exp with mantissa in
128 // [1, 10): the magnitude can be far outside what an f64 holds.
129 let log10 = count.log10_abs();
130 let exp = log10.floor();
131 let mantissa = 10f64.powf(log10 - exp);
132 let sign = if count.sign < 0 { "-" } else { "" };
133 write!(f, "{sign}{mantissa}e{}", exp as i64)
134 }
135 }
136 }
137}
138
139/// What a run answered: the mode it ran, whether the formula has a model, the
140/// count, and the run's record.
141#[derive(Debug)]
142pub struct Answer {
143 pub(crate) mode: Mode,
144 pub(crate) satisfiable: bool,
145 pub(crate) count: Count,
146 pub(crate) diagram: Option<Tdd>,
147 pub(crate) stats: Stats,
148}
149
150impl Answer {
151 /// The mode the run resolved to, from the header and the mode it was
152 /// given.
153 pub fn mode(&self) -> Mode {
154 self.mode
155 }
156
157 /// Whether the formula has a model. This is the run's verdict, not read
158 /// off the count: a zero weight, or weights that cancel, make the weighted
159 /// count of a satisfiable formula zero.
160 pub fn satisfiable(&self) -> bool {
161 self.satisfiable
162 }
163
164 /// The count: over every variable the formula declares, or over the
165 /// `c p show` variables on the projected modes.
166 pub fn count(&self) -> &Count {
167 &self.count
168 }
169
170 /// The compiled diagram, over every variable the formula declares, in the
171 /// [`Mode::Compile`] mode; `None` in the counting modes, which count
172 /// without keeping one. [`tididi::io::save_tdd`] writes it, as the
173 /// binary's `--save-tdd` does.
174 pub fn diagram(&self) -> Option<&Tdd> {
175 self.diagram.as_ref()
176 }
177
178 /// The run's record: sizes, timings, which stage answered, as the binary's
179 /// `--stats` writes it.
180 pub fn stats(&self) -> &Stats {
181 &self.stats
182 }
183}
184
185/// A run that stopped without an answer: why, and the run's record when it
186/// got far enough to open one — what it was doing when it stopped, which is
187/// the row worth reading. The record is boxed so a failure stays as small as
188/// the error.
189pub(crate) struct Failure {
190 pub(crate) error: Error,
191 pub(crate) stats: Option<Box<Stats>>,
192}
193
194/// Count, or compile, one instance: the task `mode` names, or the one its
195/// header declares, under `limits`.
196///
197/// `input` is a DIMACS file path or a parsed [`Input::Formula`]. The mode is
198/// the header's when `mode` is `None` — a `c t mc|wmc|pmc|pwmc` line, made
199/// projected by a `c p show` line — and a plain compile when the header
200/// declares none; a `mode` given must agree with the header. The run is the
201/// binary's without its printing: the same preprocessing, vtree selection and
202/// fallbacks, the same answer, and the same record in [`Answer::stats`]. Its
203/// progress lines go to stderr, as the binary's do. Calls from several
204/// threads run one at a time: a call waits for the one in progress to return.
205///
206/// # Errors
207///
208/// - [`Error::Input`]: the file cannot be opened, or is not DIMACS; or a
209/// supplied formula's metadata names a show or weight variable outside
210/// that formula's declared variable space.
211/// - [`Error::Usage`]: `mode` contradicts the header, or asks for an
212/// unweighted count ([`Mode::Mc`] or [`Mode::Pmc`]) of an instance with
213/// `c p weight` lines, which that count would ignore.
214/// - [`Error::Environment`]: a vitri setting in the environment is invalid.
215/// Each call reads the environment as it is when the call starts.
216/// - [`Error::Stage`], [`Error::VtreeSpec`]: preprocessing or vtree
217/// construction failed.
218/// - [`Error::Incomplete`]: the run ended without a count: at the time limit,
219/// on a refused allocation (reading the count off the diagram included), or
220/// when every stage gave up.
221///
222/// # Example
223///
224/// ```
225/// use tididi_cnf::{Count, Limits, Mode};
226///
227/// // (x1 ∨ x2) ∧ (¬x1 ∨ x3) ∧ (¬x2 ∨ ¬x3), with no header: a model count
228/// // needs the mode.
229/// let answer = tididi_cnf::count("tests/data/simple.cnf", Some(Mode::Mc), &Limits::default())?;
230/// assert_eq!(answer.count(), &Count::Integer(2u32.into()));
231/// # Ok::<(), tididi_cnf::Error>(())
232/// ```
233pub fn count(input: impl Into<Input>, mode: Option<Mode>, limits: &Limits) -> Result<Answer, Error> {
234 let _one_at_a_time = ONE_RUN.lock().unwrap_or_else(PoisonError::into_inner);
235 crate::driver::solve(input.into(), mode, limits, &Settings::default())
236 .map_err(|failure| failure.error)
237}
238
239/// Held for the whole of every [`count`]: the memory limit and the memory
240/// readings belong to the process, and preprocessing goes through vitri, which
241/// is not known to be safe to enter from two threads at once.
242static ONE_RUN: Mutex<()> = Mutex::new(());