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(());