cdclkit 0.1.7__tar.gz → 0.2.0__tar.gz
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.
- {cdclkit-0.1.7 → cdclkit-0.2.0}/PKG-INFO +14 -5
- {cdclkit-0.1.7 → cdclkit-0.2.0}/README.md +11 -2
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit/__init__.py +1 -1
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit/cli.py +45 -28
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit/encodings.py +92 -27
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit/model.py +111 -15
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit/mus.py +48 -19
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit/native.py +5 -3
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit/pipeline.py +25 -5
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit/portfolio.py +48 -20
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit/preprocess.py +12 -2
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit/pyeq.py +158 -27
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit/solver.py +99 -15
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit.egg-info/PKG-INFO +14 -5
- cdclkit-0.2.0/cdclkit.egg-info/requires.txt +4 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/pyproject.toml +3 -3
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_adaptive_proof.py +160 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_encodings.py +568 -0
- cdclkit-0.2.0/tests/test_mus.py +364 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_native_solver.py +88 -1
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_portfolio.py +26 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_preprocess.py +109 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_pyeq.py +388 -1
- cdclkit-0.1.7/cdclkit.egg-info/requires.txt +0 -4
- cdclkit-0.1.7/tests/test_mus.py +0 -151
- {cdclkit-0.1.7 → cdclkit-0.2.0}/LICENSE +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit/__main__.py +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit/brute.py +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit/heap.py +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit/py.typed +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit.egg-info/SOURCES.txt +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit.egg-info/dependency_links.txt +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit.egg-info/entry_points.txt +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/cdclkit.egg-info/top_level.txt +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/setup.cfg +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_agent_docs.py +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_brute.py +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_cli_defaults.py +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_core.py +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_integration.py +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_native.py +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_packaging.py +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_pipeline.py +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_proof.py +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_readme.py +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_solver.py +0 -0
- {cdclkit-0.1.7 → cdclkit-0.2.0}/tests/test_target_scaling.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cdclkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A CDCL SAT solver, preprocessor, encoding library and modelling layer, written from scratch in readable Python. Every answer comes with a certificate.
|
|
5
5
|
Author: Carlo Perassi
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -23,9 +23,9 @@ Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
|
26
|
-
Requires-Dist: dratify>=0.1.
|
|
26
|
+
Requires-Dist: dratify>=0.1.11
|
|
27
27
|
Provides-Extra: native
|
|
28
|
-
Requires-Dist: cdclkit-native==0.
|
|
28
|
+
Requires-Dist: cdclkit-native==0.2.0; extra == "native"
|
|
29
29
|
Dynamic: license-file
|
|
30
30
|
|
|
31
31
|
# cdclkit
|
|
@@ -140,8 +140,8 @@ checking a refactored Python function against the original.
|
|
|
140
140
|
| `pyeq` | **experimental** — bounded equivalence of two Python integer functions |
|
|
141
141
|
|
|
142
142
|
An optional Rust engine ([`cdclkit-native`](https://pypi.org/project/cdclkit-native/))
|
|
143
|
-
|
|
144
|
-
propagations on every instance — and about **20x faster**: the geometric mean
|
|
143
|
+
runs a search **bit-exact** with the Python one — identical conflicts, decisions
|
|
144
|
+
and propagations on every instance — and about **20x faster**: the geometric mean
|
|
145
145
|
of per-instance ratios over the 17 benchmark instances is 20.1x, ranging from
|
|
146
146
|
11x to 51x. That comes from two checkpoints recorded 34 seconds apart at the
|
|
147
147
|
same commit, one sample per instance — good enough for the mean, not for any
|
|
@@ -199,6 +199,15 @@ otherwise.
|
|
|
199
199
|
(0 of 105). See `experiments/pyeq-llm-refactor/report.md` for the full
|
|
200
200
|
negative result.
|
|
201
201
|
- Every performance figure comes from one machine.
|
|
202
|
+
- Bit-exactness stops at the search. The two preprocessors differ: native has no
|
|
203
|
+
blocked clause elimination and visits clauses in another order. So once
|
|
204
|
+
preprocessing runs, on `--adaptive` past its probe for example, an engine can
|
|
205
|
+
give a different model, conflict count and proof. No verdict has differed, and
|
|
206
|
+
every model satisfied its formula. Proof streams differ on search-only routes
|
|
207
|
+
too, and each one replays.
|
|
208
|
+
- Preprocessed proofs delete a unit clause and then rely on it. They check under
|
|
209
|
+
the rule dratify documents, which ignores such deletions, and which drat-trim
|
|
210
|
+
applies by default. A checker honouring every deletion to the letter rejects them.
|
|
202
211
|
|
|
203
212
|
## Documentation
|
|
204
213
|
|
|
@@ -110,8 +110,8 @@ checking a refactored Python function against the original.
|
|
|
110
110
|
| `pyeq` | **experimental** — bounded equivalence of two Python integer functions |
|
|
111
111
|
|
|
112
112
|
An optional Rust engine ([`cdclkit-native`](https://pypi.org/project/cdclkit-native/))
|
|
113
|
-
|
|
114
|
-
propagations on every instance — and about **20x faster**: the geometric mean
|
|
113
|
+
runs a search **bit-exact** with the Python one — identical conflicts, decisions
|
|
114
|
+
and propagations on every instance — and about **20x faster**: the geometric mean
|
|
115
115
|
of per-instance ratios over the 17 benchmark instances is 20.1x, ranging from
|
|
116
116
|
11x to 51x. That comes from two checkpoints recorded 34 seconds apart at the
|
|
117
117
|
same commit, one sample per instance — good enough for the mean, not for any
|
|
@@ -169,6 +169,15 @@ otherwise.
|
|
|
169
169
|
(0 of 105). See `experiments/pyeq-llm-refactor/report.md` for the full
|
|
170
170
|
negative result.
|
|
171
171
|
- Every performance figure comes from one machine.
|
|
172
|
+
- Bit-exactness stops at the search. The two preprocessors differ: native has no
|
|
173
|
+
blocked clause elimination and visits clauses in another order. So once
|
|
174
|
+
preprocessing runs, on `--adaptive` past its probe for example, an engine can
|
|
175
|
+
give a different model, conflict count and proof. No verdict has differed, and
|
|
176
|
+
every model satisfied its formula. Proof streams differ on search-only routes
|
|
177
|
+
too, and each one replays.
|
|
178
|
+
- Preprocessed proofs delete a unit clause and then rely on it. They check under
|
|
179
|
+
the rule dratify documents, which ignores such deletions, and which drat-trim
|
|
180
|
+
applies by default. A checker honouring every deletion to the letter rejects them.
|
|
172
181
|
|
|
173
182
|
## Documentation
|
|
174
183
|
|
|
@@ -52,7 +52,7 @@ from .preprocess import Preprocessor, preprocess
|
|
|
52
52
|
from dratify.proof import DRATChecker, MemoryProof, ProofWriter, check_proof
|
|
53
53
|
from .solver import Config, SAT, Solver, Stats, UNKNOWN, UNSAT
|
|
54
54
|
|
|
55
|
-
__version__ = "0.
|
|
55
|
+
__version__ = "0.2.0"
|
|
56
56
|
|
|
57
57
|
__all__ = [
|
|
58
58
|
"CNF",
|
|
@@ -90,6 +90,8 @@ def _emit_model(model: Sequence[bool], out=None, per_line: int = 20) -> None:
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
def cmd_solve(args) -> int:
|
|
93
|
+
if args.conflicts is not None and args.conflicts < 0:
|
|
94
|
+
raise ValueError(f"--conflicts must not be negative, got {args.conflicts}")
|
|
93
95
|
f = parse_dimacs_file(args.file)
|
|
94
96
|
print(f"c cdclkit :: {args.file}")
|
|
95
97
|
print(f"c formula: {f.nvars} vars, {f.nclauses} clauses")
|
|
@@ -144,20 +146,15 @@ def cmd_solve(args) -> int:
|
|
|
144
146
|
config=cfg,
|
|
145
147
|
jobs=jobs,
|
|
146
148
|
proof=proof_sink,
|
|
149
|
+
max_conflicts=args.conflicts,
|
|
147
150
|
)
|
|
148
151
|
print(pr.report())
|
|
149
152
|
print(f"c {pr.conflicts} conflicts in {pr.seconds:.3f}s")
|
|
153
|
+
if pr.sat is None:
|
|
154
|
+
print("s UNKNOWN")
|
|
155
|
+
return EXIT_UNKNOWN
|
|
150
156
|
if pr.sat:
|
|
151
|
-
|
|
152
|
-
base = original if original is not None else f
|
|
153
|
-
if base.falsified_clauses(pr.model):
|
|
154
|
-
print("c MODEL CHECK FAILED")
|
|
155
|
-
return EXIT_ERROR
|
|
156
|
-
print("c model verified against the input formula")
|
|
157
|
-
print("s SATISFIABLE")
|
|
158
|
-
if not args.no_model:
|
|
159
|
-
_emit_model(pr.model)
|
|
160
|
-
return EXIT_SAT
|
|
157
|
+
return _finish_sat(args, pr.model, pre, original)
|
|
161
158
|
print("s UNSATISFIABLE")
|
|
162
159
|
# The proof covers preprocessing *and* the search, so it is checked
|
|
163
160
|
# against the original formula, not the reduced one.
|
|
@@ -173,7 +170,8 @@ def cmd_solve(args) -> int:
|
|
|
173
170
|
if engine is None:
|
|
174
171
|
return EXIT_ERROR
|
|
175
172
|
pr = solve_portfolio(f, jobs=jobs, engine=engine,
|
|
176
|
-
want_proof=proof_sink is not None
|
|
173
|
+
want_proof=proof_sink is not None,
|
|
174
|
+
max_conflicts=args.conflicts)
|
|
177
175
|
if not pr.finished:
|
|
178
176
|
print("s UNKNOWN")
|
|
179
177
|
return EXIT_UNKNOWN
|
|
@@ -197,25 +195,39 @@ def cmd_solve(args) -> int:
|
|
|
197
195
|
print("s UNKNOWN")
|
|
198
196
|
return EXIT_UNKNOWN
|
|
199
197
|
if result:
|
|
200
|
-
|
|
201
|
-
if pre is not None:
|
|
202
|
-
model = pre.reconstruct(model)
|
|
203
|
-
if args.check_model:
|
|
204
|
-
base = original if original is not None else f
|
|
205
|
-
bad = base.falsified_clauses(model)
|
|
206
|
-
if bad:
|
|
207
|
-
print(f"c MODEL CHECK FAILED: {len(bad)} clauses unsatisfied")
|
|
208
|
-
return EXIT_ERROR
|
|
209
|
-
print("c model verified against the input formula")
|
|
210
|
-
print("s SATISFIABLE")
|
|
211
|
-
if not args.no_model:
|
|
212
|
-
_emit_model(model)
|
|
213
|
-
return EXIT_SAT
|
|
198
|
+
return _finish_sat(args, s.model, pre, original)
|
|
214
199
|
|
|
215
200
|
print("s UNSATISFIABLE")
|
|
216
201
|
return _finish_unsat(args, original, mem_proof, proof_sink)
|
|
217
202
|
|
|
218
203
|
|
|
204
|
+
def _finish_sat(args, model, pre, original) -> int:
|
|
205
|
+
"""Reconstruct, check if asked, then print the model.
|
|
206
|
+
|
|
207
|
+
Shared by every route, for the reason `_finish_unsat` is. The adaptive
|
|
208
|
+
branch had its own copy that returned before the only `pre.reconstruct`, so
|
|
209
|
+
`--preprocess --adaptive` printed a model of the reduced formula as the
|
|
210
|
+
answer: `v -1` for `(x1)`, exit 10.
|
|
211
|
+
|
|
212
|
+
`--self-check` checks a model as well as a proof. Evaluating the clauses
|
|
213
|
+
costs one pass over the input, far less than the replay an UNSAT answer
|
|
214
|
+
gets, and a self-check that only looks at one of the two verdicts leaves
|
|
215
|
+
the other on trust.
|
|
216
|
+
"""
|
|
217
|
+
if pre is not None:
|
|
218
|
+
model = pre.reconstruct(model)
|
|
219
|
+
if original is not None: # kept for exactly --check-model and --self-check
|
|
220
|
+
bad = original.falsified_clauses(model)
|
|
221
|
+
if bad:
|
|
222
|
+
print(f"c MODEL CHECK FAILED: {len(bad)} clauses unsatisfied")
|
|
223
|
+
return EXIT_ERROR
|
|
224
|
+
print("c model verified against the input formula")
|
|
225
|
+
print("s SATISFIABLE")
|
|
226
|
+
if not args.no_model:
|
|
227
|
+
_emit_model(model)
|
|
228
|
+
return EXIT_SAT
|
|
229
|
+
|
|
230
|
+
|
|
219
231
|
def _finish_unsat(args, formula, mem_proof, proof_sink) -> int:
|
|
220
232
|
"""Self-check or write out the proof, then report.
|
|
221
233
|
|
|
@@ -456,7 +468,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
456
468
|
s.add_argument("file")
|
|
457
469
|
s.add_argument("--proof", help="write a DRAT proof here (UNSAT only)")
|
|
458
470
|
s.add_argument("--self-check", action="store_true",
|
|
459
|
-
help="keep the proof in memory and verify it before reporting
|
|
471
|
+
help="keep the proof in memory and verify it before reporting "
|
|
472
|
+
"UNSAT; verify the model against the input before "
|
|
473
|
+
"reporting SAT")
|
|
460
474
|
s.add_argument("--check-model", action="store_true",
|
|
461
475
|
help="verify the model against the input before reporting SAT")
|
|
462
476
|
s.add_argument("--preprocess", action="store_true")
|
|
@@ -464,7 +478,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
464
478
|
help="probe briefly, then preprocess only if the instance "
|
|
465
479
|
"turns out to be hard enough to repay it")
|
|
466
480
|
s.add_argument("--prep-rounds", type=int, default=3)
|
|
467
|
-
s.add_argument("--conflicts", type=int, default=None,
|
|
481
|
+
s.add_argument("--conflicts", type=int, default=None,
|
|
482
|
+
help="conflict budget; under -j, per worker. Out of budget is "
|
|
483
|
+
"s UNKNOWN, exit 0")
|
|
468
484
|
# Every search default comes from Config() rather than a literal. They were
|
|
469
485
|
# literals, and --restart drifted to "glucose" while the library moved to
|
|
470
486
|
# "luby" -- so the CLI quietly ran the policy the CHANGELOG says was
|
|
@@ -521,7 +537,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
521
537
|
mu.add_argument("--method", default="deletion",
|
|
522
538
|
choices=["deletion", "quickxplain", "core"])
|
|
523
539
|
mu.add_argument("--verify", action="store_true",
|
|
524
|
-
help="check that the result is unsatisfiable and minimal"
|
|
540
|
+
help="check that the result is unsatisfiable and minimal; a "
|
|
541
|
+
"core need not be, so --method core can exit 1 here")
|
|
525
542
|
mu.set_defaults(func=cmd_mus)
|
|
526
543
|
|
|
527
544
|
st = sub.add_parser("stats", help="formula statistics")
|
|
@@ -101,6 +101,12 @@ class Encoder:
|
|
|
101
101
|
halves the clause count, and :meth:`tseitin` does apply it when you tell it
|
|
102
102
|
the polarity; the individual ``*_gate`` helpers stay complete because they
|
|
103
103
|
are also used to *define* variables the caller may reference either way.
|
|
104
|
+
|
|
105
|
+
A constraint may name variables the sink has not seen, as ``from_dimacs``
|
|
106
|
+
makes easy: anything that needs an auxiliary variable first grows the sink to
|
|
107
|
+
cover its own inputs. It cannot know about inputs of a *later* constraint, so
|
|
108
|
+
a program that numbers its own variables should declare them before the first
|
|
109
|
+
constraint rather than leave them to be taken.
|
|
104
110
|
"""
|
|
105
111
|
|
|
106
112
|
#: Cap on direct XOR expansion, which emits 2^(k-1) clauses. 16 inputs is
|
|
@@ -143,6 +149,23 @@ class Encoder:
|
|
|
143
149
|
for c in clauses:
|
|
144
150
|
self.add(c)
|
|
145
151
|
|
|
152
|
+
def _declare(self, lits: Iterable[int]) -> None:
|
|
153
|
+
"""Grow the sink to cover `lits`, before asking it for a fresh variable.
|
|
154
|
+
|
|
155
|
+
Its next variable is otherwise whatever it has not seen, which may be one
|
|
156
|
+
of these: the binary encoding of two undeclared inputs read `x1 -> ~x1`.
|
|
157
|
+
"""
|
|
158
|
+
top = max(lits, default=-1) >> 1
|
|
159
|
+
while self.sink.nvars <= top:
|
|
160
|
+
self.sink.new_var()
|
|
161
|
+
|
|
162
|
+
def _declare_expr(self, expr) -> None:
|
|
163
|
+
if isinstance(expr, tuple):
|
|
164
|
+
for e in expr[1:]:
|
|
165
|
+
self._declare_expr(e)
|
|
166
|
+
elif not isinstance(expr, bool):
|
|
167
|
+
self._declare((expr,))
|
|
168
|
+
|
|
146
169
|
@property
|
|
147
170
|
def true_lit(self) -> int:
|
|
148
171
|
"""A literal that is forced true, created on first use."""
|
|
@@ -160,6 +183,7 @@ class Encoder:
|
|
|
160
183
|
|
|
161
184
|
def and_gate(self, lits: Sequence[int], out: int | None = None) -> int:
|
|
162
185
|
"""``out <-> AND(lits)``. Returns ``out``."""
|
|
186
|
+
self._declare(lits if out is None else [*lits, out])
|
|
163
187
|
lits = self._simplify_and(lits)
|
|
164
188
|
if lits is None:
|
|
165
189
|
return self.false_lit
|
|
@@ -185,6 +209,7 @@ class Encoder:
|
|
|
185
209
|
|
|
186
210
|
def xor_gate(self, a: int, b: int, out: int | None = None) -> int:
|
|
187
211
|
"""``out <-> a XOR b``."""
|
|
212
|
+
self._declare((a, b) if out is None else (a, b, out))
|
|
188
213
|
key = ("xor", min(a, b), max(a, b))
|
|
189
214
|
if out is None and key in self._cache:
|
|
190
215
|
return self._cache[key]
|
|
@@ -205,6 +230,7 @@ class Encoder:
|
|
|
205
230
|
on the chain is as strong as on the direct form as long as literals are
|
|
206
231
|
fixed from the ends inwards.
|
|
207
232
|
"""
|
|
233
|
+
self._declare(lits)
|
|
208
234
|
if not lits:
|
|
209
235
|
if value:
|
|
210
236
|
self.add([]) # empty XOR is false; asserting true is a contradiction
|
|
@@ -225,6 +251,7 @@ class Encoder:
|
|
|
225
251
|
|
|
226
252
|
def ite(self, c: int, t: int, e: int, out: int | None = None) -> int:
|
|
227
253
|
"""``out <-> (c ? t : e)``, the primitive every BDD encoding needs."""
|
|
254
|
+
self._declare((c, t, e) if out is None else (c, t, e, out))
|
|
228
255
|
if t == e:
|
|
229
256
|
return t
|
|
230
257
|
key = ("ite", c, t, e)
|
|
@@ -275,7 +302,7 @@ class Encoder:
|
|
|
275
302
|
|
|
276
303
|
("and", e1, e2, ...) ("or", ...) ("xor", e1, e2)
|
|
277
304
|
("not", e) ("ite", c, t, e) ("imp", a, b)
|
|
278
|
-
("iff", a, b) <int literal>
|
|
305
|
+
("iff", a, b) <int literal> True False
|
|
279
306
|
|
|
280
307
|
``polarity`` is +1 when the expression only ever needs to be *forced
|
|
281
308
|
true* (so only the ``inputs -> out`` direction is required), -1 for the
|
|
@@ -284,33 +311,40 @@ class Encoder:
|
|
|
284
311
|
occurs with one polarity can always be set to the value its definition
|
|
285
312
|
prescribes.
|
|
286
313
|
"""
|
|
314
|
+
self._declare_expr(expr)
|
|
315
|
+
return self._tseitin(expr, polarity)
|
|
316
|
+
|
|
317
|
+
def _tseitin(self, expr, polarity: int) -> int:
|
|
318
|
+
# before the int test, because True is the int 1 and 1 is a literal
|
|
319
|
+
if isinstance(expr, bool):
|
|
320
|
+
return self.true_lit if expr else self.false_lit
|
|
287
321
|
if isinstance(expr, int):
|
|
288
322
|
return expr
|
|
289
323
|
op = expr[0]
|
|
290
324
|
if op == "not":
|
|
291
|
-
return neg(self.
|
|
325
|
+
return neg(self._tseitin(expr[1], -polarity))
|
|
292
326
|
if op == "and":
|
|
293
|
-
kids = [self.
|
|
327
|
+
kids = [self._tseitin(e, polarity) for e in expr[1:]]
|
|
294
328
|
return self._and_pol(kids, polarity)
|
|
295
329
|
if op == "or":
|
|
296
|
-
kids = [self.
|
|
330
|
+
kids = [self._tseitin(e, polarity) for e in expr[1:]]
|
|
297
331
|
return neg(self._and_pol([neg(k) for k in kids], -polarity))
|
|
298
332
|
if op == "imp":
|
|
299
|
-
a = self.
|
|
300
|
-
b = self.
|
|
333
|
+
a = self._tseitin(expr[1], -polarity)
|
|
334
|
+
b = self._tseitin(expr[2], polarity)
|
|
301
335
|
return neg(self._and_pol([a, neg(b)], -polarity))
|
|
302
336
|
if op == "iff":
|
|
303
|
-
a = self.
|
|
304
|
-
b = self.
|
|
337
|
+
a = self._tseitin(expr[1], 0)
|
|
338
|
+
b = self._tseitin(expr[2], 0)
|
|
305
339
|
return neg(self.xor_gate(a, b))
|
|
306
340
|
if op == "xor":
|
|
307
|
-
a = self.
|
|
308
|
-
b = self.
|
|
341
|
+
a = self._tseitin(expr[1], 0)
|
|
342
|
+
b = self._tseitin(expr[2], 0)
|
|
309
343
|
return self.xor_gate(a, b)
|
|
310
344
|
if op == "ite":
|
|
311
|
-
c = self.
|
|
312
|
-
t = self.
|
|
313
|
-
e = self.
|
|
345
|
+
c = self._tseitin(expr[1], 0)
|
|
346
|
+
t = self._tseitin(expr[2], polarity)
|
|
347
|
+
e = self._tseitin(expr[3], polarity)
|
|
314
348
|
return self.ite(c, t, e)
|
|
315
349
|
raise ValueError(f"unknown operator {op!r}")
|
|
316
350
|
|
|
@@ -334,14 +368,15 @@ class Encoder:
|
|
|
334
368
|
|
|
335
369
|
def assert_expr(self, expr) -> None:
|
|
336
370
|
"""Force an expression tree to be true, with polarity optimisation."""
|
|
371
|
+
self._declare_expr(expr)
|
|
337
372
|
if isinstance(expr, tuple) and expr[0] == "and":
|
|
338
373
|
for e in expr[1:]:
|
|
339
374
|
self.assert_expr(e)
|
|
340
375
|
return
|
|
341
376
|
if isinstance(expr, tuple) and expr[0] == "or":
|
|
342
|
-
self.add([self.
|
|
377
|
+
self.add([self._tseitin(e, +1) for e in expr[1:]])
|
|
343
378
|
return
|
|
344
|
-
self.add([self.
|
|
379
|
+
self.add([self._tseitin(expr, +1)])
|
|
345
380
|
|
|
346
381
|
# ------------------------------------------------------------ at-most-one
|
|
347
382
|
|
|
@@ -367,6 +402,7 @@ class Encoder:
|
|
|
367
402
|
n = len(lits)
|
|
368
403
|
if n <= 1:
|
|
369
404
|
return
|
|
405
|
+
self._declare(lits)
|
|
370
406
|
bits = max(1, (n - 1).bit_length())
|
|
371
407
|
code = [self.new_lit() for _ in range(bits)]
|
|
372
408
|
for i, x in enumerate(lits):
|
|
@@ -387,6 +423,7 @@ class Encoder:
|
|
|
387
423
|
if len(lits) <= group:
|
|
388
424
|
self.amo_pairwise(lits)
|
|
389
425
|
return
|
|
426
|
+
self._declare(lits)
|
|
390
427
|
commanders: list[int] = []
|
|
391
428
|
for i in range(0, len(lits), group):
|
|
392
429
|
g = lits[i : i + group]
|
|
@@ -444,6 +481,7 @@ class Encoder:
|
|
|
444
481
|
for x in lits:
|
|
445
482
|
self.add([neg(x)])
|
|
446
483
|
return
|
|
484
|
+
self._declare(lits)
|
|
447
485
|
s = [[self.new_lit() for _ in range(k)] for _ in range(n - 1)]
|
|
448
486
|
self.add([neg(lits[0]), s[0][0]])
|
|
449
487
|
for j in range(1, k):
|
|
@@ -465,9 +503,12 @@ class Encoder:
|
|
|
465
503
|
|
|
466
504
|
def at_most_k(self, lits: Sequence[int], k: int, method: str = "auto") -> None:
|
|
467
505
|
n = len(lits)
|
|
506
|
+
if k < 0: # before `k >= n`, which would call -1 of nothing vacuous
|
|
507
|
+
self.add([])
|
|
508
|
+
return
|
|
468
509
|
if k >= n:
|
|
469
510
|
return
|
|
470
|
-
if k
|
|
511
|
+
if k == 0:
|
|
471
512
|
for x in lits:
|
|
472
513
|
self.add([neg(x)])
|
|
473
514
|
return
|
|
@@ -517,6 +558,7 @@ class Encoder:
|
|
|
517
558
|
"""
|
|
518
559
|
if len(weights) != len(lits):
|
|
519
560
|
raise ValueError("weights and literals must have equal length")
|
|
561
|
+
self._declare(lits)
|
|
520
562
|
ws: list[int] = []
|
|
521
563
|
ls: list[int] = []
|
|
522
564
|
b = bound
|
|
@@ -595,20 +637,24 @@ class Totalizer:
|
|
|
595
637
|
|
|
596
638
|
``max_count`` truncates the counter: outputs above the cut are never
|
|
597
639
|
created, since a bound of k never needs to distinguish k+2 from k+3. The
|
|
598
|
-
truncated version is still arc consistent for the bound it was built for
|
|
640
|
+
truncated version is still arc consistent for the bound it was built for,
|
|
641
|
+
and it cannot express anything beyond: ``sum <= k`` for k >= max_count and
|
|
642
|
+
``sum >= k`` for k > max_count raise ``ValueError``, unless the inputs alone
|
|
643
|
+
decide them.
|
|
599
644
|
"""
|
|
600
645
|
|
|
601
646
|
def __init__(self, enc: Encoder, lits: Sequence[int], max_count: int | None = None):
|
|
602
647
|
self.enc = enc
|
|
603
648
|
self.inputs = list(lits)
|
|
649
|
+
enc._declare(self.inputs)
|
|
604
650
|
self.max_count = len(self.inputs) if max_count is None else min(
|
|
605
651
|
max_count, len(self.inputs)
|
|
606
652
|
)
|
|
607
653
|
self.outputs = self._build(self.inputs)
|
|
608
654
|
|
|
609
655
|
def _build(self, lits: Sequence[int]) -> list[int]:
|
|
610
|
-
if len(lits)
|
|
611
|
-
return
|
|
656
|
+
if len(lits) <= 1:
|
|
657
|
+
return list(lits)
|
|
612
658
|
mid = len(lits) // 2
|
|
613
659
|
a = self._build(lits[:mid])
|
|
614
660
|
b = self._build(lits[mid:])
|
|
@@ -653,17 +699,26 @@ class Totalizer:
|
|
|
653
699
|
return None
|
|
654
700
|
if k < 0:
|
|
655
701
|
return self.enc.false_lit
|
|
656
|
-
|
|
657
|
-
return None
|
|
702
|
+
self._require_output(k)
|
|
658
703
|
return neg(self.outputs[k])
|
|
659
704
|
|
|
660
705
|
def at_least_lit(self, k: int) -> int | None:
|
|
661
706
|
if k <= 0:
|
|
662
707
|
return None
|
|
663
|
-
if k > len(self.inputs)
|
|
708
|
+
if k > len(self.inputs):
|
|
664
709
|
return self.enc.false_lit
|
|
710
|
+
self._require_output(k - 1)
|
|
665
711
|
return self.outputs[k - 1]
|
|
666
712
|
|
|
713
|
+
def _require_output(self, i: int) -> None:
|
|
714
|
+
# Missing here means this counter stopped short, and says nothing about
|
|
715
|
+
# the bound. Reading it as "vacuous" dropped a constraint, and as
|
|
716
|
+
# "impossible" made a feasible problem unsatisfiable, with a valid proof.
|
|
717
|
+
if i >= len(self.outputs):
|
|
718
|
+
raise ValueError(
|
|
719
|
+
f"this counter was truncated with max_count={self.max_count}, so "
|
|
720
|
+
f"it cannot count to {i + 1} of its {len(self.inputs)} inputs")
|
|
721
|
+
|
|
667
722
|
def assert_at_most(self, k: int) -> None:
|
|
668
723
|
l = self.at_most_lit(k)
|
|
669
724
|
if l is not None:
|
|
@@ -754,6 +809,8 @@ class Optimiser:
|
|
|
754
809
|
def solve_with_bound(self, bound: int | None, permanent: bool = False):
|
|
755
810
|
"""Solve demanding ``count <= bound``. Returns a model or None."""
|
|
756
811
|
assumptions: list[int] = []
|
|
812
|
+
if bound is not None and bound < 0 and self.totalizer is None:
|
|
813
|
+
return None # nothing to count, so a count of 0, which is not <= -1
|
|
757
814
|
if bound is not None and self.totalizer is not None:
|
|
758
815
|
lit = self.totalizer.at_most_lit(bound)
|
|
759
816
|
if lit is not None:
|
|
@@ -772,21 +829,29 @@ class Optimiser:
|
|
|
772
829
|
|
|
773
830
|
Every iteration produces a real model, so stopping early still leaves
|
|
774
831
|
:attr:`best` holding the best solution found -- the property that
|
|
775
|
-
matters when there is a time budget.
|
|
832
|
+
matters when there is a time budget. It only ever improves, whatever a
|
|
833
|
+
later call asks for. `max_iterations` applies to this call, so a loop
|
|
834
|
+
can resume with the same budget each time; :attr:`iterations` is the
|
|
835
|
+
total so far.
|
|
776
836
|
"""
|
|
777
837
|
bound = target
|
|
838
|
+
if bound is None and self.best is not None:
|
|
839
|
+
bound = self.best[0] - 1
|
|
840
|
+
done = 0
|
|
778
841
|
while True:
|
|
779
842
|
model = self.solve_with_bound(bound)
|
|
780
843
|
if model is None:
|
|
781
844
|
break
|
|
782
845
|
self.iterations += 1
|
|
846
|
+
done += 1
|
|
783
847
|
n = self.count(model)
|
|
784
|
-
self.best
|
|
785
|
-
|
|
786
|
-
on_improve
|
|
848
|
+
if self.best is None or n < self.best[0]:
|
|
849
|
+
self.best = (n, model)
|
|
850
|
+
if on_improve is not None:
|
|
851
|
+
on_improve(n, model)
|
|
787
852
|
if n == 0:
|
|
788
853
|
break
|
|
789
|
-
if max_iterations and
|
|
854
|
+
if max_iterations and done >= max_iterations:
|
|
790
855
|
break
|
|
791
856
|
bound = n - 1
|
|
792
857
|
return self.result()
|