dratify 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dratify/__init__.py +68 -0
- dratify/_nativeinfo.py +21 -0
- dratify/cnf.py +340 -0
- dratify/lits.py +114 -0
- dratify/proof.py +692 -0
- dratify-0.1.0.dist-info/METADATA +136 -0
- dratify-0.1.0.dist-info/RECORD +10 -0
- dratify-0.1.0.dist-info/WHEEL +5 -0
- dratify-0.1.0.dist-info/licenses/LICENSE +202 -0
- dratify-0.1.0.dist-info/top_level.txt +1 -0
dratify/__init__.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Check a DRAT/DRUP proof of unsatisfiability, inside your Python process.
|
|
3
|
+
|
|
4
|
+
A SAT solver that answers "unsatisfiable" is asking to be trusted. A DRAT proof
|
|
5
|
+
is how it stops asking: the solver logs every clause it derives, and a checker
|
|
6
|
+
that shares no code with it replays the log and confirms the empty clause
|
|
7
|
+
really follows.
|
|
8
|
+
|
|
9
|
+
Emitting those proofs is common -- PySAT exposes them from six solver families
|
|
10
|
+
via ``with_proof=True`` / ``get_proof()``. *Checking* them from Python has not
|
|
11
|
+
been possible: ``drat-trim`` is C you have to compile and shell out to, and the
|
|
12
|
+
one checker on PyPI installs only on Linux x86-64 under Python <= 3.10.
|
|
13
|
+
|
|
14
|
+
>>> from dratify import parse_dimacs, check_proof
|
|
15
|
+
>>> formula = parse_dimacs(open("problem.cnf").read())
|
|
16
|
+
>>> result = check_proof(formula, open("proof.drat").read())
|
|
17
|
+
>>> result.ok
|
|
18
|
+
True
|
|
19
|
+
|
|
20
|
+
Two engines, and that is the point
|
|
21
|
+
----------------------------------
|
|
22
|
+
The pure-Python checker has zero dependencies and runs anywhere Python does.
|
|
23
|
+
The optional Rust accelerator (``pip install dratify[native]``) is roughly 18x
|
|
24
|
+
faster. Neither is the "real" one: proof checking is the one domain where two
|
|
25
|
+
independent implementations agreeing *is* the evidence, and these two have been
|
|
26
|
+
differentially tested against each other, on acceptances and on rejections.
|
|
27
|
+
|
|
28
|
+
Pass ``engine="python"``, ``engine="native"``, or ``engine="auto"`` (the
|
|
29
|
+
default: native when installed).
|
|
30
|
+
|
|
31
|
+
RUP and RAT are both checked. Checking is forward -- every step is verified,
|
|
32
|
+
rather than working backwards from the empty clause.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from .cnf import CNF
|
|
38
|
+
from .lits import from_dimacs, to_dimacs
|
|
39
|
+
from .proof import (
|
|
40
|
+
DRATChecker,
|
|
41
|
+
MemoryProof,
|
|
42
|
+
ProofWriter,
|
|
43
|
+
check_proof,
|
|
44
|
+
)
|
|
45
|
+
from .cnf import parse_dimacs, parse_dimacs_file, write_dimacs
|
|
46
|
+
from . import _nativeinfo
|
|
47
|
+
|
|
48
|
+
__version__ = "0.1.0"
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"CNF",
|
|
52
|
+
"DRATChecker",
|
|
53
|
+
"MemoryProof",
|
|
54
|
+
"ProofWriter",
|
|
55
|
+
"check_proof",
|
|
56
|
+
"parse_dimacs",
|
|
57
|
+
"parse_dimacs_file",
|
|
58
|
+
"write_dimacs",
|
|
59
|
+
"from_dimacs",
|
|
60
|
+
"to_dimacs",
|
|
61
|
+
"native_available",
|
|
62
|
+
"__version__",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def native_available() -> bool:
|
|
67
|
+
"""True when the optional Rust checker is installed."""
|
|
68
|
+
return _nativeinfo.available()
|
dratify/_nativeinfo.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Availability of the optional Rust checker."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
BUILD_HINT = (
|
|
7
|
+
"engine='native' was requested but the Rust checker is not installed. "
|
|
8
|
+
"The Rust accelerator is not yet published as a Python wheel; use "
|
|
9
|
+
"engine='python' or engine='auto' "
|
|
10
|
+
"(the pure-Python checker is complete and is never going away -- it is "
|
|
11
|
+
"the independent implementation that makes agreement meaningful)."
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def available() -> bool:
|
|
16
|
+
"""True when the Rust checker can be imported."""
|
|
17
|
+
try:
|
|
18
|
+
import sable_native
|
|
19
|
+
except ImportError:
|
|
20
|
+
return False
|
|
21
|
+
return hasattr(sable_native, "check_proof")
|
dratify/cnf.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2026 Carlo Perassi. Licensed under the Apache License 2.0.
|
|
3
|
+
"""Clause and formula containers, plus DIMACS CNF input/output.
|
|
4
|
+
|
|
5
|
+
Two distinct objects live here and they are deliberately different:
|
|
6
|
+
|
|
7
|
+
``Clause``
|
|
8
|
+
The solver's runtime clause. Mutable, carries the two watched literals in
|
|
9
|
+
slots 0 and 1, and holds the learnt-clause bookkeeping (LBD, activity,
|
|
10
|
+
deletion mark). Never used for storage or I/O.
|
|
11
|
+
|
|
12
|
+
``CNF``
|
|
13
|
+
An immutable-ish *formula*: a variable count plus a list of clauses, each a
|
|
14
|
+
tuple of internal literals. This is what the parser produces, what the
|
|
15
|
+
encoders build, what the proof checker consumes and what the brute-force
|
|
16
|
+
reference solver evaluates. It has no solver state at all.
|
|
17
|
+
|
|
18
|
+
Keeping them apart means the encoders and the checker can be tested without
|
|
19
|
+
instantiating a solver, and the solver never has to worry about I/O concerns
|
|
20
|
+
such as tautology filtering or duplicate literals -- ``CNF.add`` normalises on
|
|
21
|
+
the way in.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import io
|
|
27
|
+
from typing import Iterable, Iterator, Sequence
|
|
28
|
+
|
|
29
|
+
from .lits import from_dimacs, to_dimacs
|
|
30
|
+
|
|
31
|
+
__all__ = ["Clause", "CNF", "parse_dimacs", "parse_dimacs_file", "write_dimacs"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# --------------------------------------------------------------------------
|
|
35
|
+
# runtime clause
|
|
36
|
+
# --------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Clause:
|
|
40
|
+
"""A runtime clause with two watched literals in ``lits[0]`` and ``lits[1]``.
|
|
41
|
+
|
|
42
|
+
Invariant maintained by the solver: if the clause is not satisfied at a
|
|
43
|
+
lower level, ``lits[0]`` and ``lits[1]`` are either unassigned or assigned
|
|
44
|
+
true, or the clause is conflicting / unit. See
|
|
45
|
+
:meth:`sable.solver.Solver._propagate` for the exact restoration logic.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
__slots__ = ("lits", "learnt", "lbd", "act", "deleted", "seen")
|
|
49
|
+
|
|
50
|
+
def __init__(self, lits: Sequence[int], learnt: bool = False, lbd: int = 0) -> None:
|
|
51
|
+
self.lits: list[int] = list(lits)
|
|
52
|
+
self.learnt = learnt
|
|
53
|
+
self.lbd = lbd
|
|
54
|
+
self.act = 0.0
|
|
55
|
+
self.deleted = False
|
|
56
|
+
self.seen = False # scratch flag for subsumption / DB reduction
|
|
57
|
+
|
|
58
|
+
def __len__(self) -> int:
|
|
59
|
+
return len(self.lits)
|
|
60
|
+
|
|
61
|
+
def __iter__(self) -> Iterator[int]:
|
|
62
|
+
return iter(self.lits)
|
|
63
|
+
|
|
64
|
+
def __getitem__(self, i: int) -> int:
|
|
65
|
+
return self.lits[i]
|
|
66
|
+
|
|
67
|
+
def __setitem__(self, i: int, v: int) -> None:
|
|
68
|
+
self.lits[i] = v
|
|
69
|
+
|
|
70
|
+
def __repr__(self) -> str: # pragma: no cover - debugging aid
|
|
71
|
+
kind = "learnt" if self.learnt else "orig"
|
|
72
|
+
body = " ".join(str(to_dimacs(l)) for l in self.lits)
|
|
73
|
+
return f"<Clause {kind} lbd={self.lbd} [{body}]>"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# --------------------------------------------------------------------------
|
|
77
|
+
# formula
|
|
78
|
+
# --------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CNF:
|
|
82
|
+
"""A CNF formula over variables ``0 .. nvars-1``.
|
|
83
|
+
|
|
84
|
+
``add`` performs three normalisations, all of them satisfiability
|
|
85
|
+
preserving and all of them required by the rest of the toolkit:
|
|
86
|
+
|
|
87
|
+
* duplicate literals are collapsed (``x v x = x``);
|
|
88
|
+
* tautologies (``x v ~x``) are dropped entirely;
|
|
89
|
+
* the variable count grows to cover every literal seen.
|
|
90
|
+
|
|
91
|
+
The empty clause is kept: it is the canonical certificate of
|
|
92
|
+
unsatisfiability and both the checker and the solver rely on being able to
|
|
93
|
+
represent it.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
__slots__ = ("nvars", "clauses", "comments", "_names", "header_mismatch")
|
|
97
|
+
|
|
98
|
+
def __init__(self, nvars: int = 0) -> None:
|
|
99
|
+
self.nvars = nvars
|
|
100
|
+
self.clauses: list[tuple[int, ...]] = []
|
|
101
|
+
self.comments: list[str] = []
|
|
102
|
+
#: (declared_clauses, parsed_clauses) when a `p cnf` header disagreed
|
|
103
|
+
#: with what was actually read, else None.
|
|
104
|
+
#:
|
|
105
|
+
#: Tolerating a stray `%` line or a missing final `0` is being liberal
|
|
106
|
+
#: in what you accept. A clause count that disagrees with the header is
|
|
107
|
+
#: not that -- it means the formula in memory is not the formula in the
|
|
108
|
+
#: file. SATLIB's `dubois100.cnf` declares 800 clauses and parses to
|
|
109
|
+
#: 396 here, which flipped it from unsatisfiable to satisfiable, and
|
|
110
|
+
#: the benchmark compared two solvers on it for weeks without noticing
|
|
111
|
+
#: because both were fed the *re-serialised* formula and so agreed.
|
|
112
|
+
self.header_mismatch: tuple[int, int] | None = None
|
|
113
|
+
self._names: dict[int, str] = {}
|
|
114
|
+
|
|
115
|
+
# -- construction -------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def new_var(self, name: str | None = None) -> int:
|
|
118
|
+
"""Allocate a fresh variable and return its 0-based index."""
|
|
119
|
+
v = self.nvars
|
|
120
|
+
self.nvars += 1
|
|
121
|
+
if name is not None:
|
|
122
|
+
self._names[v] = name
|
|
123
|
+
return v
|
|
124
|
+
|
|
125
|
+
def new_vars(self, n: int) -> list[int]:
|
|
126
|
+
return [self.new_var() for _ in range(n)]
|
|
127
|
+
|
|
128
|
+
def name(self, v: int) -> str:
|
|
129
|
+
return self._names.get(v, f"x{v}")
|
|
130
|
+
|
|
131
|
+
def set_name(self, v: int, name: str) -> None:
|
|
132
|
+
self._names[v] = name
|
|
133
|
+
|
|
134
|
+
def add(self, lits: Iterable[int]) -> bool:
|
|
135
|
+
"""Add a clause of internal literals. Returns False if it was a tautology."""
|
|
136
|
+
seen: set[int] = set()
|
|
137
|
+
out: list[int] = []
|
|
138
|
+
for l in lits:
|
|
139
|
+
if l < 0:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"internal literals are non-negative; got {l} "
|
|
142
|
+
"(use lits.from_dimacs for signed input)"
|
|
143
|
+
)
|
|
144
|
+
if l in seen:
|
|
145
|
+
continue
|
|
146
|
+
if (l ^ 1) in seen:
|
|
147
|
+
return False # tautology
|
|
148
|
+
seen.add(l)
|
|
149
|
+
out.append(l)
|
|
150
|
+
v = l >> 1
|
|
151
|
+
if v >= self.nvars:
|
|
152
|
+
self.nvars = v + 1
|
|
153
|
+
self.clauses.append(tuple(out))
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
def add_dimacs(self, dimacs_lits: Iterable[int]) -> bool:
|
|
157
|
+
"""Add a clause given as signed 1-based DIMACS literals."""
|
|
158
|
+
return self.add(from_dimacs(d) for d in dimacs_lits)
|
|
159
|
+
|
|
160
|
+
def extend(self, clauses: Iterable[Iterable[int]]) -> None:
|
|
161
|
+
for c in clauses:
|
|
162
|
+
self.add(c)
|
|
163
|
+
|
|
164
|
+
# -- queries ------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def nclauses(self) -> int:
|
|
168
|
+
return len(self.clauses)
|
|
169
|
+
|
|
170
|
+
def __len__(self) -> int:
|
|
171
|
+
return len(self.clauses)
|
|
172
|
+
|
|
173
|
+
def __iter__(self):
|
|
174
|
+
return iter(self.clauses)
|
|
175
|
+
|
|
176
|
+
def literals(self) -> Iterator[int]:
|
|
177
|
+
for c in self.clauses:
|
|
178
|
+
yield from c
|
|
179
|
+
|
|
180
|
+
def occurrence_counts(self) -> list[int]:
|
|
181
|
+
counts = [0] * (2 * self.nvars)
|
|
182
|
+
for c in self.clauses:
|
|
183
|
+
for l in c:
|
|
184
|
+
counts[l] += 1
|
|
185
|
+
return counts
|
|
186
|
+
|
|
187
|
+
def stats(self) -> dict[str, int | float]:
|
|
188
|
+
sizes = [len(c) for c in self.clauses]
|
|
189
|
+
total = sum(sizes)
|
|
190
|
+
return {
|
|
191
|
+
"vars": self.nvars,
|
|
192
|
+
"clauses": len(self.clauses),
|
|
193
|
+
"literals": total,
|
|
194
|
+
"max_len": max(sizes) if sizes else 0,
|
|
195
|
+
"min_len": min(sizes) if sizes else 0,
|
|
196
|
+
"avg_len": (total / len(sizes)) if sizes else 0.0,
|
|
197
|
+
"unit": sum(1 for s in sizes if s == 1),
|
|
198
|
+
"binary": sum(1 for s in sizes if s == 2),
|
|
199
|
+
"ternary": sum(1 for s in sizes if s == 3),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# -- evaluation ---------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
def is_satisfied_by(self, model: Sequence[bool]) -> bool:
|
|
205
|
+
"""True iff every clause has a true literal under ``model``."""
|
|
206
|
+
for c in self.clauses:
|
|
207
|
+
for l in c:
|
|
208
|
+
v = l >> 1
|
|
209
|
+
if v < len(model) and (model[v] != bool(l & 1)):
|
|
210
|
+
break
|
|
211
|
+
else:
|
|
212
|
+
return False
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
def falsified_clauses(self, model: Sequence[bool]) -> list[tuple[int, ...]]:
|
|
216
|
+
"""All clauses with no true literal (used for error messages)."""
|
|
217
|
+
bad = []
|
|
218
|
+
for c in self.clauses:
|
|
219
|
+
for l in c:
|
|
220
|
+
v = l >> 1
|
|
221
|
+
if v < len(model) and (model[v] != bool(l & 1)):
|
|
222
|
+
break
|
|
223
|
+
else:
|
|
224
|
+
bad.append(c)
|
|
225
|
+
return bad
|
|
226
|
+
|
|
227
|
+
def copy(self) -> "CNF":
|
|
228
|
+
f = CNF(self.nvars)
|
|
229
|
+
f.clauses = list(self.clauses)
|
|
230
|
+
f.comments = list(self.comments)
|
|
231
|
+
f._names = dict(self._names)
|
|
232
|
+
return f
|
|
233
|
+
|
|
234
|
+
# -- serialisation ------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
def to_dimacs(self) -> str:
|
|
237
|
+
buf = io.StringIO()
|
|
238
|
+
write_dimacs(self, buf)
|
|
239
|
+
return buf.getvalue()
|
|
240
|
+
|
|
241
|
+
def save(self, path: str) -> None:
|
|
242
|
+
with open(path, "w", encoding="ascii") as fh:
|
|
243
|
+
write_dimacs(self, fh)
|
|
244
|
+
|
|
245
|
+
def __repr__(self) -> str: # pragma: no cover - debugging aid
|
|
246
|
+
return f"<CNF vars={self.nvars} clauses={len(self.clauses)}>"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# --------------------------------------------------------------------------
|
|
250
|
+
# DIMACS I/O
|
|
251
|
+
# --------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def parse_dimacs(text: str, strict: bool = False) -> CNF:
|
|
255
|
+
"""Parse DIMACS CNF text.
|
|
256
|
+
|
|
257
|
+
Tolerates the real-world deviations that appear in benchmark archives:
|
|
258
|
+
clauses split across lines, a missing or wrong header, ``%`` / trailing
|
|
259
|
+
``0`` terminators from SATLIB files, and blank lines. With ``strict=True``
|
|
260
|
+
a header mismatch raises instead of being repaired.
|
|
261
|
+
"""
|
|
262
|
+
f = CNF()
|
|
263
|
+
declared_vars = declared_clauses = None
|
|
264
|
+
pending: list[int] = []
|
|
265
|
+
for lineno, raw in enumerate(text.splitlines(), 1):
|
|
266
|
+
line = raw.strip()
|
|
267
|
+
if not line:
|
|
268
|
+
continue
|
|
269
|
+
head = line[0]
|
|
270
|
+
if head == "c":
|
|
271
|
+
f.comments.append(line[1:].strip())
|
|
272
|
+
continue
|
|
273
|
+
if head == "%":
|
|
274
|
+
break # SATLIB terminator
|
|
275
|
+
if head == "p":
|
|
276
|
+
parts = line.split()
|
|
277
|
+
if len(parts) < 4 or parts[1] != "cnf":
|
|
278
|
+
raise ValueError(f"line {lineno}: malformed header {line!r}")
|
|
279
|
+
declared_vars = int(parts[2])
|
|
280
|
+
declared_clauses = int(parts[3])
|
|
281
|
+
if declared_vars > f.nvars:
|
|
282
|
+
f.nvars = declared_vars
|
|
283
|
+
continue
|
|
284
|
+
for tok in line.split():
|
|
285
|
+
try:
|
|
286
|
+
d = int(tok)
|
|
287
|
+
except ValueError:
|
|
288
|
+
raise ValueError(f"line {lineno}: bad token {tok!r}") from None
|
|
289
|
+
if d == 0:
|
|
290
|
+
f.add_dimacs(pending)
|
|
291
|
+
pending = []
|
|
292
|
+
else:
|
|
293
|
+
pending.append(d)
|
|
294
|
+
if pending:
|
|
295
|
+
if strict:
|
|
296
|
+
raise ValueError("input ends with an unterminated clause")
|
|
297
|
+
f.add_dimacs(pending)
|
|
298
|
+
if declared_vars is not None and declared_clauses is not None:
|
|
299
|
+
if declared_vars != f.nvars or declared_clauses != len(f.clauses):
|
|
300
|
+
if strict:
|
|
301
|
+
raise ValueError(
|
|
302
|
+
f"header says {declared_vars} vars / {declared_clauses} "
|
|
303
|
+
f"clauses, found {f.nvars} / {len(f.clauses)}"
|
|
304
|
+
)
|
|
305
|
+
f.header_mismatch = (declared_clauses, len(f.clauses))
|
|
306
|
+
return f
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def parse_dimacs_file(path: str, strict: bool = False) -> CNF:
|
|
310
|
+
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
|
311
|
+
return parse_dimacs(fh.read(), strict=strict)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def write_dimacs(f: CNF, out) -> None:
|
|
315
|
+
"""Write ``f`` in DIMACS CNF format to a text stream."""
|
|
316
|
+
for c in f.comments:
|
|
317
|
+
out.write(f"c {c}\n")
|
|
318
|
+
out.write(f"p cnf {f.nvars} {len(f.clauses)}\n")
|
|
319
|
+
for c in f.clauses:
|
|
320
|
+
out.write(" ".join(str(to_dimacs(l)) for l in c))
|
|
321
|
+
out.write(" 0\n")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# --------------------------------------------------------------------------
|
|
325
|
+
# model helpers
|
|
326
|
+
# --------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def model_to_dimacs(model: Sequence[bool]) -> list[int]:
|
|
330
|
+
"""Convert a boolean model into the signed literal list DIMACS expects."""
|
|
331
|
+
return [(i + 1) if b else -(i + 1) for i, b in enumerate(model)]
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def model_from_dimacs(lits: Sequence[int], nvars: int) -> list[bool]:
|
|
335
|
+
model = [False] * nvars
|
|
336
|
+
for d in lits:
|
|
337
|
+
if d == 0:
|
|
338
|
+
continue
|
|
339
|
+
model[abs(d) - 1] = d > 0
|
|
340
|
+
return model
|
dratify/lits.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2026 Carlo Perassi. Licensed under the Apache License 2.0.
|
|
3
|
+
"""Literal and truth-value representation.
|
|
4
|
+
|
|
5
|
+
SABLE uses the standard "doubled index" literal encoding found in MiniSat,
|
|
6
|
+
Glucose and CaDiCaL:
|
|
7
|
+
|
|
8
|
+
variable v 0-based integer in [0, nvars)
|
|
9
|
+
positive literal 2*v
|
|
10
|
+
negative literal 2*v + 1
|
|
11
|
+
|
|
12
|
+
The encoding makes negation a single XOR (``lit ^ 1``), makes the variable a
|
|
13
|
+
single shift (``lit >> 1``) and lets every per-literal table be a flat array of
|
|
14
|
+
length ``2 * nvars`` indexed directly by the literal. This is why the solver
|
|
15
|
+
never has to branch on the sign of a literal in the propagation inner loop,
|
|
16
|
+
which is where roughly 80% of a CDCL solver's time is spent.
|
|
17
|
+
|
|
18
|
+
Truth values are represented by the three-valued domain
|
|
19
|
+
|
|
20
|
+
U = 0 undefined
|
|
21
|
+
T = 1 true
|
|
22
|
+
F = 2 false
|
|
23
|
+
|
|
24
|
+
stored in a ``bytearray`` of length ``2 * nvars``. Both a literal and its
|
|
25
|
+
negation get an entry, kept complementary at all times by
|
|
26
|
+
:meth:`sable.solver.Solver._assign`. Reading ``val[lit]`` is then a single
|
|
27
|
+
array index with no sign test. ``flip(x)`` maps T<->F and leaves U alone.
|
|
28
|
+
|
|
29
|
+
DIMACS literals (the signed non-zero integers used by the standard file
|
|
30
|
+
format, where variables are 1-based) are converted at the I/O boundary only;
|
|
31
|
+
nothing inside the solver ever sees a signed literal.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"U",
|
|
38
|
+
"T",
|
|
39
|
+
"F",
|
|
40
|
+
"flip",
|
|
41
|
+
"neg",
|
|
42
|
+
"var_of",
|
|
43
|
+
"is_neg",
|
|
44
|
+
"mk_lit",
|
|
45
|
+
"from_dimacs",
|
|
46
|
+
"to_dimacs",
|
|
47
|
+
"lit_str",
|
|
48
|
+
"clause_str",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
# --------------------------------------------------------------------------
|
|
52
|
+
# three-valued logic
|
|
53
|
+
# --------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
U = 0 # undefined
|
|
56
|
+
T = 1 # true
|
|
57
|
+
F = 2 # false
|
|
58
|
+
|
|
59
|
+
#: ``_FLIP[x]`` is the negation of truth value ``x`` (U stays U).
|
|
60
|
+
_FLIP = (U, F, T)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def flip(x: int) -> int:
|
|
64
|
+
"""Negate a three-valued truth value: T<->F, U->U."""
|
|
65
|
+
return _FLIP[x]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# --------------------------------------------------------------------------
|
|
69
|
+
# literals
|
|
70
|
+
# --------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def neg(lit: int) -> int:
|
|
74
|
+
"""Return the complement of ``lit``."""
|
|
75
|
+
return lit ^ 1
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def var_of(lit: int) -> int:
|
|
79
|
+
"""Return the 0-based variable underlying ``lit``."""
|
|
80
|
+
return lit >> 1
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def is_neg(lit: int) -> bool:
|
|
84
|
+
"""True when ``lit`` is a negative (complemented) literal."""
|
|
85
|
+
return bool(lit & 1)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def mk_lit(var: int, negated: bool = False) -> int:
|
|
89
|
+
"""Build the literal for 0-based ``var``, complemented iff ``negated``."""
|
|
90
|
+
return (var << 1) | (1 if negated else 0)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def from_dimacs(d: int) -> int:
|
|
94
|
+
"""Convert a signed 1-based DIMACS literal into internal encoding."""
|
|
95
|
+
if d == 0:
|
|
96
|
+
raise ValueError("0 is not a DIMACS literal (it is the clause terminator)")
|
|
97
|
+
v = abs(d) - 1
|
|
98
|
+
return (v << 1) | (1 if d < 0 else 0)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def to_dimacs(lit: int) -> int:
|
|
102
|
+
"""Convert an internal literal back into signed 1-based DIMACS form."""
|
|
103
|
+
v = (lit >> 1) + 1
|
|
104
|
+
return -v if (lit & 1) else v
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def lit_str(lit: int) -> str:
|
|
108
|
+
"""Human-readable form of a literal, e.g. ``x3`` or ``~x3``."""
|
|
109
|
+
return ("~x" if (lit & 1) else "x") + str(lit >> 1)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def clause_str(lits) -> str:
|
|
113
|
+
"""Human-readable form of a clause given as internal literals."""
|
|
114
|
+
return "(" + " v ".join(lit_str(l) for l in lits) + ")"
|