tididi_cnf/
formula.rs

1//! What the compiler adds on the formula side: reading DIMACS text or clauses,
2//! and the two formula operations the case splits share.
3//!
4//! The formula itself is vitri's: [`vitri::cnf`] defines `VarId`, `Literal`,
5//! `Clause`, `CnfFormula`, the header metadata and the DIMACS reader and
6//! writer, and this compiler uses those types directly. [`parse_dimacs`] is
7//! the crate's one way in for DIMACS text, and [`formula_from_clauses`] for
8//! clauses given as DIMACS integers. The case splits read the
9//! per-variable occurrence count to pick a branching variable, and condition
10//! on one literal by appending it as a unit clause.
11
12use num_rational::BigRational;
13use vitri::cnf::{Clause, CnfFormula, CnfMeta, Literal, ShowSet, VarId, WeightTable};
14
15use crate::Error;
16
17/// Read DIMACS text into a formula and the header lines that came with it.
18///
19/// The formula is vitri's [`CnfFormula`](vitri::cnf::CnfFormula), and the
20/// [`CnfMeta`](vitri::cnf::CnfMeta) holds the `c t` task line, the `c p show`
21/// set and the `c p weight` table. The pair converts into an
22/// [`Input`](crate::Input) for [`count`](crate::count), and the formula is
23/// what [`compile_cnf`](crate::compile::compile_cnf) compiles.
24///
25/// Every CNF this crate reads, the binary's instance included, arrives through
26/// here, and the reading itself is [`vitri::cnf::CnfFormula::from_dimacs`]: the
27/// line kinds accepted, the ids refused and what each header line means are
28/// decided in one place.
29///
30/// ```
31/// let text = "c t mc\np cnf 2 1\n1 -2 0\n";
32/// let (formula, _header) = tididi_cnf::parse_dimacs(text.as_bytes())?;
33/// assert_eq!((formula.num_vars(), formula.clauses().len()), (2, 1));
34/// # Ok::<(), tididi_cnf::Error>(())
35/// ```
36///
37/// # Errors
38///
39/// [`Error::Input`](crate::Error::Input), with vitri's sentence naming the
40/// line and what is wrong with it, for text that is not DIMACS: a malformed
41/// problem line, a non-integer token in clause data, a weight or show token
42/// that names no rational or no variable, or any id above the `p cnf` declared
43/// variable count.
44pub fn parse_dimacs<R: std::io::BufRead>(
45    reader: R,
46) -> Result<(CnfFormula, CnfMeta), Error> {
47    CnfFormula::from_dimacs(reader).map_err(|error| Error::Input(error.to_string()))
48}
49
50/// Build a formula from clauses of DIMACS integers, with the header lines a
51/// projected or weighted count reads.
52///
53/// The formula has `num_vars` variables, and each clause is its literals
54/// without the closing `0`: `3` is variable 3 and `-3` its negation. `show`
55/// is the `c p show` set, the variables a projected count projects onto, and
56/// `None` declares none. `weights` are the `c p weight` lines as
57/// `(literal, weight)` pairs, read as the competition's format reads its lines:
58/// a literal they leave out weighs `1 - w` when the other literal of its
59/// variable is given a `w` strictly between 0 and 1, and 1 when neither is
60/// given, and a literal given twice takes its last weight. The pair is what [`parse_dimacs`]
61/// returns for the same formula written as DIMACS text, so it goes to
62/// [`count`](crate::count) or [`compile_cnf`](crate::compile::compile_cnf)
63/// the same way, and the run is the one that text makes.
64///
65/// ```
66/// use num_rational::BigRational;
67/// use tididi_cnf::{count, formula_from_clauses, Count, Limits, Mode};
68///
69/// // (x1 ∨ x2) ∧ (¬x1 ∨ x3) ∧ (¬x2 ∨ ¬x3), projected onto x1 and x2, where
70/// // x1 weighs 1/4, ¬x1 weighs 3/4, and x2 weighs 1/2, so ¬x2 weighs 1/2.
71/// let clauses = [vec![1, 2], vec![-1, 3], vec![-2, -3]];
72/// let show: &[u32] = &[1, 2];
73/// let quarters = |n: i32| BigRational::new(n.into(), 4.into());
74/// let weights = [(1, quarters(1)), (-1, quarters(3)), (2, quarters(2))];
75/// let formula = formula_from_clauses(3, &clauses, Some(show), &weights)?;
76/// let answer = count(formula, Some(Mode::Pwmc), &Limits::default())?;
77/// // x1 ¬x2 weighs 1/4 · 1/2, and ¬x1 x2 weighs 3/4 · 1/2.
78/// assert_eq!(answer.count(), &Count::Rational(BigRational::new(1.into(), 2.into())));
79/// # Ok::<(), tididi_cnf::Error>(())
80/// ```
81///
82/// # Errors
83///
84/// [`Error::Input`](crate::Error::Input) for a clause literal, shown variable
85/// or weight literal that names no variable, such as `0`, or a variable past
86/// `num_vars`, for a clause that names a variable twice, and for a literal
87/// given alone at a weight outside `(0, 1)`, which leaves its partner none. The DIMACS reader
88/// merges a repeated literal and drops a clause holding both literals of a
89/// variable; clauses given as data are taken as they are, so they hold each
90/// variable once.
91pub fn formula_from_clauses<C: AsRef<[i32]>>(
92    num_vars: u32,
93    clauses: impl IntoIterator<Item = C>,
94    show: Option<&[u32]>,
95    weights: &[(i32, BigRational)],
96) -> Result<(CnfFormula, CnfMeta), Error> {
97    let input = |error: vitri::VitriError| Error::Input(error.to_string());
98    let clauses = clauses
99        .into_iter()
100        .zip(1..)
101        .map(|(clause, n)| clause_from_dimacs(clause.as_ref(), n))
102        .collect::<Result<Vec<_>, _>>()?;
103    let formula = CnfFormula::new(num_vars, clauses).map_err(input)?;
104    let show = show.map(ShowSet::from_dimacs_ids).transpose().map_err(input)?;
105    // No pairs declare no weights, as a file with no `c p weight` line does.
106    let weights = (!weights.is_empty())
107        .then(|| WeightTable::from_dimacs_pairs(weights.iter().cloned(), num_vars))
108        .transpose()
109        .map_err(input)?;
110    let meta = CnfMeta::from_parts(num_vars, None, show, weights).map_err(input)?;
111    Ok((formula, meta))
112}
113
114/// Clause `n` of [`formula_from_clauses`], its literals in the order the
115/// DIMACS reader leaves them: by variable.
116fn clause_from_dimacs(literals: &[i32], n: usize) -> Result<Clause, Error> {
117    let mut clause = literals
118        .iter()
119        .map(|&literal| {
120            let var = VarId::try_from_dimacs(literal).ok_or_else(|| {
121                Error::Input(format!("clause {n} holds {literal}, which names no variable"))
122            })?;
123            Ok(Literal::new(var, literal > 0))
124        })
125        .collect::<Result<Vec<_>, Error>>()?;
126    clause.sort_unstable_by_key(|literal| literal.var);
127    if let Some(pair) = clause.windows(2).find(|pair| pair[0].var == pair[1].var) {
128        return Err(Error::Input(format!(
129            "clause {n} names variable {} twice; a clause holds each variable once",
130            pair[0].var.get()
131        )));
132    }
133    Ok(Clause::new(clause))
134}
135
136/// How many literals of each variable of `formula` sit in clauses of at least
137/// `min_len` literals, indexed by `VarId::idx`.
138pub(crate) fn occurrence_counts(formula: &CnfFormula, min_len: usize) -> Vec<usize> {
139    let mut occ = vec![0; formula.num_vars() as usize];
140    for clause in formula.clauses().iter().filter(|c| c.literals.len() >= min_len) {
141        for lit in &clause.literals {
142            occ[lit.var.idx()] += 1;
143        }
144    }
145    occ
146}
147
148/// The variable, among those `eligible` accepts by index, with the most
149/// literals in clauses of at least `min_len` literals, the lowest index on a
150/// tie. `None` when no eligible variable occurs in such a clause.
151pub(crate) fn most_frequent_var(
152    formula: &CnfFormula,
153    min_len: usize,
154    eligible: impl Fn(usize) -> bool,
155) -> Option<VarId> {
156    let mut best: Option<(usize, usize)> = None;
157    for (idx, n) in occurrence_counts(formula, min_len).into_iter().enumerate() {
158        if n > 0 && best.is_none_or(|(_, b)| n > b) && eligible(idx) {
159            best = Some((idx, n));
160        }
161    }
162    best.map(|(idx, _)| VarId::from_idx(idx))
163}
164
165/// `formula` with the unit clause `lit` appended. No propagation, so variable
166/// ids and `num_vars` do not move.
167pub(crate) fn with_unit(formula: &CnfFormula, lit: Literal) -> CnfFormula {
168    let mut clauses = formula.clone().into_clauses();
169    clauses.push(Clause::new(vec![lit]));
170    CnfFormula::new(formula.num_vars(), clauses)
171        .expect("a unit over the formula's own variables stays in its space")
172}
173
174#[cfg(test)]
175#[path = "tests/formula.rs"]
176mod tests;