-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmod.rs
More file actions
390 lines (360 loc) · 15.8 KB
/
Copy pathmod.rs
File metadata and controls
390 lines (360 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
//! CNF formula types.
//!
//! - `Literal`: a variable with a polarity
//! - `Clause`: a disjunction of literals
//! - `CnfFormula`: a conjunction of clauses
//!
//! plus the header metadata a competition instance declares (`Mode`,
//! `WeightTable`, `CnfMeta`). Reading those types out of DIMACS text, and
//! writing them back, is `dimacs`; splitting a formula at its independent
//! components is `components`.
mod components;
mod dimacs;
mod literal;
pub(crate) mod show_set;
pub(crate) mod space;
pub(crate) mod weights;
pub(crate) use literal::EquivFold;
/// A variable identifier and a literal over it — the two types every other CNF
/// type is built from.
pub use literal::{Literal, VarId};
/// The projection show set and the mask derived from it.
pub use show_set::{ShowMask, ShowSet};
/// The marker types that say which formula's variables a numbering is
/// expressed over.
pub use space::{Local, Original, Reduced, Space};
/// The literal weights a weighted instance declares, and the resolved
/// per-variable table every stage reads them through.
pub use weights::{WeightTable, Weights};
pub(crate) use dimacs::{DimacsHeader, parse_weight, rational_string, write_dimacs};
/// Parse a DIMACS weight token into an exact rational.
pub use dimacs::parse_rational_weight;
/// The independent components of a clause slice, for a caller holding clauses
/// it has not wrapped in a formula.
pub use components::detect_components_in;
/// A clause: a disjunction of literals.
///
/// Each variable must appear at most once. A variable carrying both polarities
/// makes the clause a tautology, which [`CnfFormula::from_dimacs`] drops on the
/// way in; a clause built directly is taken at its word, so a programmatic
/// caller owes the uniqueness itself.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Clause {
/// The disjuncts. Order matches the source DIMACS line after
/// normalization (sorted by variable, deduplicated) once parsed via
/// [`CnfFormula::from_dimacs`]; callers constructing a `Clause` directly
/// are not required to sort.
pub literals: Vec<Literal>,
}
impl Clause {
/// Wraps `literals` as a `Clause`. Debug builds assert the
/// at-most-once-per-variable invariant documented on [`Clause`]; release
/// builds trust the caller and skip the O(k²) check.
pub fn new(literals: Vec<Literal>) -> Self {
debug_assert!(
{
let mut ok = true;
for i in 0..literals.len() {
for j in (i + 1)..literals.len() {
if literals[i].var == literals[j].var {
ok = false;
break;
}
}
if !ok {
break;
}
}
ok
},
"Clause::new: duplicate variable in literals {literals:?}",
);
Clause { literals }
}
}
/// A `Clause` derefs to its literal slice, so `&Clause` coerces to `&[Literal]`
/// wherever a plain literal slice is expected — code that needs only the
/// literals takes no dependency on this type.
impl std::ops::Deref for Clause {
type Target = [Literal];
#[inline(always)]
fn deref(&self) -> &[Literal] {
&self.literals
}
}
/// What preprocessing must preserve.
///
/// The first four are the MCC 2026 tracks and are the only tokens a `c t` line
/// may carry: `mc` (Track 1), `wmc` (weighted, Track 2 and the WMC sub-case of
/// Track 4), `pmc` (projected, Track 3), `pwmc` (projected weighted, Track 4).
/// Defaults to `Mc` when no `c t` line is present.
///
/// [`Compile`](Self::Compile) is a fifth mode that no header can declare — it is
/// reachable only by asking for it explicitly (`--mode compile`,
/// [`crate::config::RunConfig::mode`]).
///
/// `#[non_exhaustive]`: a caller matching on this must carry a `_` arm. A track
/// added to a later competition is a new variant here, and it should not break
/// a build that already handles the tracks it knows.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Mode {
/// Track 1: plain unweighted, unprojected model counting.
#[default]
Mc,
/// Track 2 (and the weighted half of Track 4): counts are weighted by
/// per-literal weights from `c p weight` lines.
Wmc,
/// Track 3: counting is projected onto the `c p show` variable set.
Pmc,
/// Track 4: both projected (onto `c p show`) and weighted (via
/// `c p weight`).
Pwmc,
/// Preserve the function, not just a count: the reduced formula plus the
/// record reconstruct the original Boolean function over the original
/// variables. Only stages with a recorded reconstruction run — forced-literal
/// propagation, equivalent-literal substitution, free-variable removal — so
/// preprocessing is weaker than any counting mode's.
Compile,
}
impl Mode {
/// Every mode, in the order a message or a `--help` line offers them.
///
/// The vocabulary itself is [`Mode::token`]'s match, which the compiler
/// keeps exhaustive; this fixes the ORDER and is what [`Mode::names`] and
/// [`Mode::parse_mode`] read, so an offer, a rejection and a parse cannot
/// disagree about which spellings exist.
const ALL: &'static [Mode] = &[Mode::Mc, Mode::Wmc, Mode::Pmc, Mode::Pwmc, Mode::Compile];
/// Every `--mode` token, in table order — for a shell over this crate that
/// offers the vocabulary it will accept rather than keeping a copy.
pub fn names() -> impl Iterator<Item = &'static str> {
Mode::ALL.iter().map(|m| m.token())
}
/// Parses a `c t <type>` token: any [`Mode::names`] entry except
/// `compile`, which `c t` cannot name — that header names a competition
/// track, and [`Mode::Compile`] is not one.
pub(crate) fn parse_track(s: &str) -> Option<Self> {
Mode::parse_mode(s).filter(|m| *m != Mode::Compile)
}
/// Parses a `--mode` token: any [`Mode::names`] entry. The inverse of
/// [`Mode::token`] by construction — it is that spelling looked up.
pub fn parse_mode(s: &str) -> Option<Self> {
Mode::ALL.iter().copied().find(|m| m.token() == s)
}
/// The token naming this mode, the exact inverse of
/// [`Mode::parse_mode`].
pub fn token(self) -> &'static str {
match self {
Mode::Mc => "mc",
Mode::Wmc => "wmc",
Mode::Pmc => "pmc",
Mode::Pwmc => "pwmc",
Mode::Compile => "compile",
}
}
/// True for the two tracks whose count is weighted (`Wmc`, `Pwmc`).
///
/// False for `Compile`, which carries any declared weights through untouched
/// rather than counting under them — so a site asking "does this file declare
/// weights" must read [`CnfMeta::declared_weights`], not this.
pub fn is_weighted(self) -> bool {
matches!(self, Mode::Wmc | Mode::Pwmc)
}
/// True for the two tracks whose count is projected onto a `show` variable
/// set (`Pmc`, `Pwmc`). False for `Compile`, for the same reason as
/// [`Self::is_weighted`].
pub fn is_projected(self) -> bool {
matches!(self, Mode::Pmc | Mode::Pwmc)
}
}
/// CNF header metadata parsed from MCC `c t` / `c p show` / `c p weight`
/// meta-comment lines, returned alongside the [`CnfFormula`] by
/// [`CnfFormula::from_dimacs`]. All fields default to the Track-1
/// (plain MC) interpretation, so a file without these lines yields
/// `CnfMeta::default()`. The metadata is expressed over ORIGINAL DIMACS
/// variable ids and must be threaded and remapped explicitly by the caller as
/// preprocessing renumbers variables.
#[derive(Clone, Debug, Default)]
pub struct CnfMeta {
/// The track a `c t` line named, read through [`CnfMeta::declared_track`],
/// or resolved to `Mode::Mc` through [`CnfMeta::mode`]. `None` when no
/// `c t` line is present.
track: Option<Mode>,
/// The show set for projected counting, read through
/// [`CnfMeta::declared_show_vars`]; the projected-out set is
/// `all_vars \ show_vars`. `None` when no `c p show` line is present.
show_vars: Option<ShowSet<Original>>,
/// Literal weights for weighted counting, read through
/// [`CnfMeta::declared_weights`]; `None` when no `c p weight` line is
/// present.
pub(crate) weights: Option<WeightTable>,
}
impl CnfMeta {
/// Builds metadata for a formula with `num_vars` declared variables.
///
/// `show_vars` is typed in the formula's [`Original`] variable space and
/// therefore contains 0-based ids. `weights` remains the sparse table of
/// explicit signed 1-based DIMACS literal declarations; omitted literals
/// are not materialized. `None` means the corresponding declaration was
/// absent, for `track` as well as for the other two, while
/// `Some(ShowSet::empty())` and `Some` of an empty [`WeightTable`] preserve
/// explicit empty declarations.
///
/// Passing the formula's declared count is mandatory: metadata assembled
/// in-process is range-checked exactly as metadata read from DIMACS is.
///
/// # Errors
///
/// [`VitriError::Input`](crate::error::VitriError::Input) naming a shown
/// variable or weight literal outside the formula's declared variable
/// space.
pub fn from_parts(
num_vars: u32,
track: Option<Mode>,
show_vars: Option<ShowSet<Original>>,
weights: Option<WeightTable>,
) -> Result<Self, crate::error::VitriError> {
if let Some(var) = show_vars
.as_ref()
.and_then(|show| show.iter_vars().find(|var| var.0 >= num_vars))
{
return Err(crate::error::VitriError::input(format!(
"show variable {} exceeds declared variable count {num_vars}",
var.to_dimacs()
)));
}
if let Some(weights) = &weights {
weights.validate_num_vars(num_vars)?;
}
Ok(CnfMeta {
track,
show_vars,
weights,
})
}
/// The track this file's `c t` line named, or `None` when it carried no
/// such line.
///
/// Declaring a track and counting under one are different questions, as
/// they are for the show set and the weights below. [`Self::mode`] answers
/// the second, resolving an absent line to [`Mode::Mc`]; this answers the
/// first, and is the only thing that tells `c t mc` apart from a file
/// carrying no header at all. A consumer whose own defaults differ between
/// a competition instance and a plain CNF reads this one, not `mode`.
pub fn declared_track(&self) -> Option<Mode> {
self.track
}
/// The counting track to read this file as: the one its `c t` line named,
/// or [`Mode::Mc`] when it carried no such line.
pub fn mode(&self) -> Mode {
self.track.unwrap_or_default()
}
/// The show set this file declares, or `None` when it carried no
/// `c p show` line.
///
/// Declaring a show set and counting under one are different questions: a
/// projected mode (`pmc`/`pwmc`) uses this set as the projection its
/// preprocessing preserves; [`Mode::Compile`] carries a declared set through
/// without projecting, keying on this rather than [`Mode::is_projected`];
/// an unprojected mode ignores it (see
/// [`ResolvedMode::notices`](crate::config::ResolvedMode::notices)).
///
/// An empty set is still a declaration: `c p show 0` projects onto
/// nothing (count is 1 or 0), unlike an unprojected count over the same
/// clauses.
pub fn declared_show_vars(&self) -> Option<&ShowSet<Original>> {
self.show_vars.as_ref()
}
/// The literal weights this file declares, or `None` when it carried no
/// `c p weight` line.
///
/// Declaring weights and counting under them are different questions, as
/// they are for the show set above: a weighted mode (`wmc`/`pwmc`) counts
/// under this table, [`Mode::Compile`] renumbers it onto the reduced
/// formula and carries it through rather than folding it into the lift,
/// and an unweighted mode ignores it (see
/// [`ResolvedMode::notices`](crate::config::ResolvedMode::notices)).
pub fn declared_weights(&self) -> Option<&WeightTable> {
self.weights.as_ref()
}
}
/// A CNF formula: a conjunction of clauses.
///
/// `PartialEq`/`Eq` are structural (same declared `num_vars`, same clauses in
/// the same order) — an identity check, not semantic equivalence.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CnfFormula {
/// Declared variable count from the DIMACS `p cnf <vars> <clauses>`
/// header, and the whole variable space: a file naming an id above it is
/// rejected by [`CnfFormula::from_dimacs`], so this is never
/// lower than the widest id in `clauses`.
pub num_vars: u32,
/// The conjuncts. May exceed the header's advisory clause count — extra
/// clauses beyond the declared total are accepted, not truncated.
pub clauses: Vec<Clause>,
}
impl CnfFormula {
/// The refutation over `num_vars` variables: one empty clause, so nothing
/// satisfies it, and the declared variable space intact, so a caller's
/// numbering still reads over it.
pub(crate) fn contradiction(num_vars: u32) -> Self {
CnfFormula {
num_vars,
clauses: vec![Clause::new(vec![])],
}
}
/// Whether this formula carries a refutation —
/// [`contains_empty_clause`] over its own clauses.
pub(crate) fn is_refuted(&self) -> bool {
contains_empty_clause(&self.clauses)
}
}
/// The result of equivalence-preserving unit propagation.
///
/// `residual` has every forced literal propagated to fixpoint and no longer
/// carries the unit clauses that established those assignments. The pair
/// `(residual, forced)` therefore describes the original function; the
/// residual alone does not. A contradiction is represented by one empty
/// clause in `residual`, as it is everywhere else in this crate.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnitPropagation {
/// The clauses left after propagation.
pub residual: CnfFormula,
/// Every forced literal, including units derived during propagation.
pub forced: Vec<Literal>,
}
/// Propagate every unit clause in `formula` to fixpoint.
///
/// This is the reusable CNF operation used by Vitri's own preprocessing. It is
/// also useful to an embedding compiler that conditions a derived formula and
/// needs the residual plus the assignments it must account for.
pub fn propagate_units(formula: &CnfFormula) -> UnitPropagation {
let (clauses, forced) =
crate::preprocess::unit_propagation::propagate(&formula.clauses, formula.num_vars);
UnitPropagation {
residual: CnfFormula {
num_vars: formula.num_vars,
clauses,
},
forced,
}
}
/// Whether `clauses` contains the empty clause — the form in which every pass
/// that derives a contradiction reports one, and the one spelling of the
/// question, so that a clause slice and a whole formula cannot answer it
/// differently.
pub(crate) fn contains_empty_clause(clauses: &[Clause]) -> bool {
clauses.iter().any(|c| c.literals.is_empty())
}
/// Derived per-variable views of a clause set: occurrence lists, appearance
/// mask, frequency tables. Here rather than under a consumer because both the
/// preprocessing passes and vtree construction read them, and they are
/// functions of the clauses alone.
pub(crate) mod occ;
/// Disjoint-set helper, used here to group the variables a clause set connects
/// into independent components.
mod union_find;
/// Whole-formula shape statistics and the `coloring_like` predicate over them,
/// read by the Arjun bounded-variable-addition policy and by the vtree
/// portfolio's candidate gates.
pub(crate) mod stats;