cdclkit 0.1.2__tar.gz → 0.1.3__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.2 → cdclkit-0.1.3}/PKG-INFO +78 -11
- {cdclkit-0.1.2 → cdclkit-0.1.3}/README.md +75 -8
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit/__init__.py +1 -1
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit/brute.py +5 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit/cli.py +50 -9
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit/encodings.py +2 -2
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit/pipeline.py +53 -10
- cdclkit-0.1.3/cdclkit/py.typed +0 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit/pyeq.py +49 -6
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit/solver.py +18 -7
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit.egg-info/PKG-INFO +78 -11
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit.egg-info/SOURCES.txt +5 -0
- cdclkit-0.1.3/cdclkit.egg-info/requires.txt +4 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/pyproject.toml +8 -3
- cdclkit-0.1.3/tests/test_adaptive_proof.py +161 -0
- cdclkit-0.1.3/tests/test_agent_docs.py +86 -0
- cdclkit-0.1.3/tests/test_brute.py +144 -0
- cdclkit-0.1.3/tests/test_cli_defaults.py +67 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/tests/test_core.py +7 -7
- {cdclkit-0.1.2 → cdclkit-0.1.3}/tests/test_encodings.py +4 -2
- {cdclkit-0.1.2 → cdclkit-0.1.3}/tests/test_mus.py +9 -5
- {cdclkit-0.1.2 → cdclkit-0.1.3}/tests/test_native.py +4 -4
- {cdclkit-0.1.2 → cdclkit-0.1.3}/tests/test_native_solver.py +97 -5
- {cdclkit-0.1.2 → cdclkit-0.1.3}/tests/test_packaging.py +32 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/tests/test_pipeline.py +3 -3
- {cdclkit-0.1.2 → cdclkit-0.1.3}/tests/test_portfolio.py +5 -5
- {cdclkit-0.1.2 → cdclkit-0.1.3}/tests/test_preprocess.py +5 -5
- {cdclkit-0.1.2 → cdclkit-0.1.3}/tests/test_proof.py +20 -11
- {cdclkit-0.1.2 → cdclkit-0.1.3}/tests/test_pyeq.py +23 -2
- {cdclkit-0.1.2 → cdclkit-0.1.3}/tests/test_solver.py +7 -7
- cdclkit-0.1.2/cdclkit.egg-info/requires.txt +0 -4
- {cdclkit-0.1.2 → cdclkit-0.1.3}/LICENSE +0 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit/__main__.py +0 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit/heap.py +0 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit/model.py +0 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit/mus.py +0 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit/native.py +0 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit/portfolio.py +0 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit/preprocess.py +0 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit.egg-info/dependency_links.txt +0 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit.egg-info/entry_points.txt +0 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/cdclkit.egg-info/top_level.txt +0 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/setup.cfg +0 -0
- {cdclkit-0.1.2 → cdclkit-0.1.3}/tests/test_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cdclkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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
|
|
@@ -22,9 +22,9 @@ Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
|
-
Requires-Dist: dratify>=0.1.
|
|
25
|
+
Requires-Dist: dratify>=0.1.4
|
|
26
26
|
Provides-Extra: native
|
|
27
|
-
Requires-Dist: cdclkit-native==0.1.
|
|
27
|
+
Requires-Dist: cdclkit-native==0.1.3; extra == "native"
|
|
28
28
|
Dynamic: license-file
|
|
29
29
|
|
|
30
30
|
# cdclkit
|
|
@@ -36,7 +36,7 @@ Every answer comes with a certificate, and the certificate gets checked.
|
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
pip install cdclkit # pure Python
|
|
39
|
-
pip install "cdclkit[native]" # plus the Rust engine, ~
|
|
39
|
+
pip install "cdclkit[native]" # plus the Rust engine, ~20x faster
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
Three packages, and no third-party code:
|
|
@@ -59,6 +59,56 @@ A solver that says "unsatisfiable" and offers nothing else is asking to be
|
|
|
59
59
|
trusted. This one hands you the proof — and the checker that reads it shares no
|
|
60
60
|
code with the solver that wrote it.
|
|
61
61
|
|
|
62
|
+
## Why you would want this
|
|
63
|
+
|
|
64
|
+
**Use this if:**
|
|
65
|
+
|
|
66
|
+
- ✅ You have constraints and need any assignment satisfying all of them
|
|
67
|
+
- ✅ You need to know **why** something is infeasible, not just that it is
|
|
68
|
+
- ✅ You need to *verify* an "impossible" answer rather than trust it
|
|
69
|
+
- ✅ You want to read the solver, or teach how CDCL works
|
|
70
|
+
- ✅ You want zero third-party dependencies
|
|
71
|
+
|
|
72
|
+
**Use something else if:**
|
|
73
|
+
|
|
74
|
+
- ❌ You need raw speed on industrial instances → [PySAT](https://pypi.org/project/python-sat/) ships kissat and CaDiCaL
|
|
75
|
+
- ❌ Your problem is numeric optimisation over real numbers → an LP or MIP solver
|
|
76
|
+
- ❌ You need Windows → never tested here, and the classifiers say so
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
You have a pile of constraints and need any assignment satisfying all of them:
|
|
81
|
+
|
|
82
|
+
- **Rostering and scheduling** — six nurses, three shifts, nobody works two
|
|
83
|
+
nights running, everyone gets a weekend off a month.
|
|
84
|
+
- **Configuration** — which package versions can coexist, which hardware
|
|
85
|
+
options are compatible.
|
|
86
|
+
- **Assignment** — exams to rooms and slots with no clashes, frequencies to
|
|
87
|
+
transmitters that must not interfere.
|
|
88
|
+
- **Puzzles** — Sudoku, nonograms, the zebra puzzle. Genuinely the same shape
|
|
89
|
+
as the three above.
|
|
90
|
+
|
|
91
|
+
The alternative is writing backtracking search yourself. That is slow to write,
|
|
92
|
+
and what you produce in an afternoon will be far slower than a solver with
|
|
93
|
+
thirty years of engineering in the same loop.
|
|
94
|
+
|
|
95
|
+
Two things you get that a hand-rolled search does not:
|
|
96
|
+
|
|
97
|
+
- **When there is no solution, you learn why.** A minimal unsatisfiable subset
|
|
98
|
+
names the handful of constraints that actually conflict, instead of reporting
|
|
99
|
+
"infeasible" and leaving you to bisect a thousand of them.
|
|
100
|
+
- **When it says "no", you can check it.** A "yes" verifies itself in linear
|
|
101
|
+
time; a "no" is an assertion about every one of 2^n assignments. This one
|
|
102
|
+
hands you a proof.
|
|
103
|
+
|
|
104
|
+
**When not to.** Numeric optimisation over real numbers wants an LP or MIP
|
|
105
|
+
solver, not this. Raw throughput on millions of clauses wants
|
|
106
|
+
[PySAT](https://pypi.org/project/python-sat/), which ships kissat and CaDiCaL
|
|
107
|
+
as binary wheels and will be much faster.
|
|
108
|
+
|
|
109
|
+
New to this? [docs/tutorial/](docs/tutorial/) starts from boolean logic and
|
|
110
|
+
assumes nothing else.
|
|
111
|
+
|
|
62
112
|
## Quick start
|
|
63
113
|
|
|
64
114
|
```python
|
|
@@ -73,7 +123,8 @@ Note the shape: `solve()` returns a **tuple**, so `if solve(f):` is always
|
|
|
73
123
|
true. Unpack it.
|
|
74
124
|
|
|
75
125
|
Worked examples live in `examples/` — Sudoku with a uniqueness proof, the zebra
|
|
76
|
-
puzzle, graph colouring, bounded model checking, circuit equivalence
|
|
126
|
+
puzzle, graph colouring, bounded model checking, circuit equivalence, and
|
|
127
|
+
checking a refactored Python function against the original.
|
|
77
128
|
|
|
78
129
|
## What's in it
|
|
79
130
|
|
|
@@ -88,9 +139,12 @@ puzzle, graph colouring, bounded model checking, circuit equivalence.
|
|
|
88
139
|
| `pyeq` | **experimental** — bounded equivalence of two Python integer functions |
|
|
89
140
|
|
|
90
141
|
An optional Rust engine ([`cdclkit-native`](https://pypi.org/project/cdclkit-native/))
|
|
91
|
-
is
|
|
92
|
-
|
|
93
|
-
|
|
142
|
+
is **bit-exact** with the Python one — identical conflicts, decisions and
|
|
143
|
+
propagations on every instance — and about **20x faster**: the geometric mean
|
|
144
|
+
of per-instance ratios over the 17 benchmark instances is 20.1x, ranging from
|
|
145
|
+
11x to 51x. Regenerate it with `make history` (the `python` and `native`
|
|
146
|
+
checkpoints in `bench/history.jsonl`); see [BENCHMARKS.md](BENCHMARKS.md) for
|
|
147
|
+
the method. The pure-Python path has zero third-party
|
|
94
148
|
dependencies and is the one that must never break.
|
|
95
149
|
|
|
96
150
|
## Relationship to dratify
|
|
@@ -122,13 +176,18 @@ of what is being measured.
|
|
|
122
176
|
|
|
123
177
|
If you need raw speed, install [PySAT](https://pypi.org/project/python-sat/):
|
|
124
178
|
it ships kissat, CaDiCaL and Glucose as binary wheels on every platform. This
|
|
125
|
-
project is not trying to beat them
|
|
126
|
-
|
|
179
|
+
project is not trying to beat them, and will lose badly on large instances.
|
|
180
|
+
|
|
181
|
+
The difference worth naming is narrower than "readable": PySAT can emit DRUP
|
|
182
|
+
proofs and ships nothing to check them, and its solvers are third-party C++ in
|
|
183
|
+
a compiled extension. Here the proof gets checked by default, by a package you
|
|
184
|
+
can read. That matters if you are about to act on "no solution exists", and not
|
|
185
|
+
otherwise.
|
|
127
186
|
|
|
128
187
|
## Honest limitations
|
|
129
188
|
|
|
130
189
|
- **No Windows.** Never tested; the classifiers say so rather than implying support.
|
|
131
|
-
- Pure Python is ~
|
|
190
|
+
- Pure Python is ~20x slower than its own Rust port, which is itself far from
|
|
132
191
|
kissat. Not a tool for competition-scale instances.
|
|
133
192
|
- No inprocessing, no XOR/Gaussian reasoning. Parity families are a known
|
|
134
193
|
weakness and `bench/` includes one to keep that visible.
|
|
@@ -140,12 +199,20 @@ self-checking implementation you can audit.
|
|
|
140
199
|
|
|
141
200
|
## Documentation
|
|
142
201
|
|
|
202
|
+
- [docs/tutorial/](docs/tutorial/) — **start here if you are new to SAT.** A
|
|
203
|
+
tutorial for a working programmer who knows boolean logic and has never used
|
|
204
|
+
a solver: the theory you need, then Python, then Rust. Every example runs.
|
|
143
205
|
- [docs/ALGORITHMS.md](docs/ALGORITHMS.md) — the mathematics, from resolution
|
|
144
206
|
through first-UIP, LBD, DRAT, encodings and preprocessing, including a
|
|
145
207
|
section on what is deliberately absent.
|
|
146
208
|
- [docs/ROADMAP.md](docs/ROADMAP.md) — what is planned, in sprints.
|
|
147
209
|
- [docs/RELEASING.md](docs/RELEASING.md) — the release checklist.
|
|
148
210
|
|
|
211
|
+
## Generating code against this?
|
|
212
|
+
|
|
213
|
+
[AGENTS.md](AGENTS.md) lists the API's sharp edges — the mistakes that have
|
|
214
|
+
actually been made, not hypothetical ones. Worth reading before writing a line.
|
|
215
|
+
|
|
149
216
|
## Licence
|
|
150
217
|
|
|
151
218
|
Apache-2.0. See [LICENSE](LICENSE).
|
|
@@ -7,7 +7,7 @@ Every answer comes with a certificate, and the certificate gets checked.
|
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
9
|
pip install cdclkit # pure Python
|
|
10
|
-
pip install "cdclkit[native]" # plus the Rust engine, ~
|
|
10
|
+
pip install "cdclkit[native]" # plus the Rust engine, ~20x faster
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Three packages, and no third-party code:
|
|
@@ -30,6 +30,56 @@ A solver that says "unsatisfiable" and offers nothing else is asking to be
|
|
|
30
30
|
trusted. This one hands you the proof — and the checker that reads it shares no
|
|
31
31
|
code with the solver that wrote it.
|
|
32
32
|
|
|
33
|
+
## Why you would want this
|
|
34
|
+
|
|
35
|
+
**Use this if:**
|
|
36
|
+
|
|
37
|
+
- ✅ You have constraints and need any assignment satisfying all of them
|
|
38
|
+
- ✅ You need to know **why** something is infeasible, not just that it is
|
|
39
|
+
- ✅ You need to *verify* an "impossible" answer rather than trust it
|
|
40
|
+
- ✅ You want to read the solver, or teach how CDCL works
|
|
41
|
+
- ✅ You want zero third-party dependencies
|
|
42
|
+
|
|
43
|
+
**Use something else if:**
|
|
44
|
+
|
|
45
|
+
- ❌ You need raw speed on industrial instances → [PySAT](https://pypi.org/project/python-sat/) ships kissat and CaDiCaL
|
|
46
|
+
- ❌ Your problem is numeric optimisation over real numbers → an LP or MIP solver
|
|
47
|
+
- ❌ You need Windows → never tested here, and the classifiers say so
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
You have a pile of constraints and need any assignment satisfying all of them:
|
|
52
|
+
|
|
53
|
+
- **Rostering and scheduling** — six nurses, three shifts, nobody works two
|
|
54
|
+
nights running, everyone gets a weekend off a month.
|
|
55
|
+
- **Configuration** — which package versions can coexist, which hardware
|
|
56
|
+
options are compatible.
|
|
57
|
+
- **Assignment** — exams to rooms and slots with no clashes, frequencies to
|
|
58
|
+
transmitters that must not interfere.
|
|
59
|
+
- **Puzzles** — Sudoku, nonograms, the zebra puzzle. Genuinely the same shape
|
|
60
|
+
as the three above.
|
|
61
|
+
|
|
62
|
+
The alternative is writing backtracking search yourself. That is slow to write,
|
|
63
|
+
and what you produce in an afternoon will be far slower than a solver with
|
|
64
|
+
thirty years of engineering in the same loop.
|
|
65
|
+
|
|
66
|
+
Two things you get that a hand-rolled search does not:
|
|
67
|
+
|
|
68
|
+
- **When there is no solution, you learn why.** A minimal unsatisfiable subset
|
|
69
|
+
names the handful of constraints that actually conflict, instead of reporting
|
|
70
|
+
"infeasible" and leaving you to bisect a thousand of them.
|
|
71
|
+
- **When it says "no", you can check it.** A "yes" verifies itself in linear
|
|
72
|
+
time; a "no" is an assertion about every one of 2^n assignments. This one
|
|
73
|
+
hands you a proof.
|
|
74
|
+
|
|
75
|
+
**When not to.** Numeric optimisation over real numbers wants an LP or MIP
|
|
76
|
+
solver, not this. Raw throughput on millions of clauses wants
|
|
77
|
+
[PySAT](https://pypi.org/project/python-sat/), which ships kissat and CaDiCaL
|
|
78
|
+
as binary wheels and will be much faster.
|
|
79
|
+
|
|
80
|
+
New to this? [docs/tutorial/](docs/tutorial/) starts from boolean logic and
|
|
81
|
+
assumes nothing else.
|
|
82
|
+
|
|
33
83
|
## Quick start
|
|
34
84
|
|
|
35
85
|
```python
|
|
@@ -44,7 +94,8 @@ Note the shape: `solve()` returns a **tuple**, so `if solve(f):` is always
|
|
|
44
94
|
true. Unpack it.
|
|
45
95
|
|
|
46
96
|
Worked examples live in `examples/` — Sudoku with a uniqueness proof, the zebra
|
|
47
|
-
puzzle, graph colouring, bounded model checking, circuit equivalence
|
|
97
|
+
puzzle, graph colouring, bounded model checking, circuit equivalence, and
|
|
98
|
+
checking a refactored Python function against the original.
|
|
48
99
|
|
|
49
100
|
## What's in it
|
|
50
101
|
|
|
@@ -59,9 +110,12 @@ puzzle, graph colouring, bounded model checking, circuit equivalence.
|
|
|
59
110
|
| `pyeq` | **experimental** — bounded equivalence of two Python integer functions |
|
|
60
111
|
|
|
61
112
|
An optional Rust engine ([`cdclkit-native`](https://pypi.org/project/cdclkit-native/))
|
|
62
|
-
is
|
|
63
|
-
|
|
64
|
-
|
|
113
|
+
is **bit-exact** with the Python one — identical conflicts, decisions and
|
|
114
|
+
propagations on every instance — and about **20x faster**: the geometric mean
|
|
115
|
+
of per-instance ratios over the 17 benchmark instances is 20.1x, ranging from
|
|
116
|
+
11x to 51x. Regenerate it with `make history` (the `python` and `native`
|
|
117
|
+
checkpoints in `bench/history.jsonl`); see [BENCHMARKS.md](BENCHMARKS.md) for
|
|
118
|
+
the method. The pure-Python path has zero third-party
|
|
65
119
|
dependencies and is the one that must never break.
|
|
66
120
|
|
|
67
121
|
## Relationship to dratify
|
|
@@ -93,13 +147,18 @@ of what is being measured.
|
|
|
93
147
|
|
|
94
148
|
If you need raw speed, install [PySAT](https://pypi.org/project/python-sat/):
|
|
95
149
|
it ships kissat, CaDiCaL and Glucose as binary wheels on every platform. This
|
|
96
|
-
project is not trying to beat them
|
|
97
|
-
|
|
150
|
+
project is not trying to beat them, and will lose badly on large instances.
|
|
151
|
+
|
|
152
|
+
The difference worth naming is narrower than "readable": PySAT can emit DRUP
|
|
153
|
+
proofs and ships nothing to check them, and its solvers are third-party C++ in
|
|
154
|
+
a compiled extension. Here the proof gets checked by default, by a package you
|
|
155
|
+
can read. That matters if you are about to act on "no solution exists", and not
|
|
156
|
+
otherwise.
|
|
98
157
|
|
|
99
158
|
## Honest limitations
|
|
100
159
|
|
|
101
160
|
- **No Windows.** Never tested; the classifiers say so rather than implying support.
|
|
102
|
-
- Pure Python is ~
|
|
161
|
+
- Pure Python is ~20x slower than its own Rust port, which is itself far from
|
|
103
162
|
kissat. Not a tool for competition-scale instances.
|
|
104
163
|
- No inprocessing, no XOR/Gaussian reasoning. Parity families are a known
|
|
105
164
|
weakness and `bench/` includes one to keep that visible.
|
|
@@ -111,12 +170,20 @@ self-checking implementation you can audit.
|
|
|
111
170
|
|
|
112
171
|
## Documentation
|
|
113
172
|
|
|
173
|
+
- [docs/tutorial/](docs/tutorial/) — **start here if you are new to SAT.** A
|
|
174
|
+
tutorial for a working programmer who knows boolean logic and has never used
|
|
175
|
+
a solver: the theory you need, then Python, then Rust. Every example runs.
|
|
114
176
|
- [docs/ALGORITHMS.md](docs/ALGORITHMS.md) — the mathematics, from resolution
|
|
115
177
|
through first-UIP, LBD, DRAT, encodings and preprocessing, including a
|
|
116
178
|
section on what is deliberately absent.
|
|
117
179
|
- [docs/ROADMAP.md](docs/ROADMAP.md) — what is planned, in sprints.
|
|
118
180
|
- [docs/RELEASING.md](docs/RELEASING.md) — the release checklist.
|
|
119
181
|
|
|
182
|
+
## Generating code against this?
|
|
183
|
+
|
|
184
|
+
[AGENTS.md](AGENTS.md) lists the API's sharp edges — the mistakes that have
|
|
185
|
+
actually been made, not hypothetical ones. Worth reading before writing a line.
|
|
186
|
+
|
|
120
187
|
## Licence
|
|
121
188
|
|
|
122
189
|
Apache-2.0. See [LICENSE](LICENSE).
|
|
@@ -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.1.
|
|
55
|
+
__version__ = "0.1.3"
|
|
56
56
|
|
|
57
57
|
__all__ = [
|
|
58
58
|
"CNF",
|
|
@@ -112,6 +112,11 @@ def dpll(f: CNF, max_steps: int = 2_000_000) -> list[bool] | None:
|
|
|
112
112
|
steps[0] += 1
|
|
113
113
|
if steps[0] > max_steps:
|
|
114
114
|
raise RuntimeError("DPLL step budget exhausted")
|
|
115
|
+
# An empty clause cannot be satisfied by anything. `simplify` catches
|
|
116
|
+
# the ones this search *creates*, but one present in the input reached
|
|
117
|
+
# the split below and indexed cs[0][0] on an empty list.
|
|
118
|
+
if any(not c for c in cs):
|
|
119
|
+
return None
|
|
115
120
|
# unit propagation
|
|
116
121
|
while True:
|
|
117
122
|
unit = next((c[0] for c in cs if len(c) == 1), None)
|
|
@@ -105,7 +105,23 @@ def cmd_solve(args) -> int:
|
|
|
105
105
|
if getattr(args, "adaptive", False):
|
|
106
106
|
from .pipeline import solve_adaptive
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
# Everything below used to be dropped here: cfg, the proof sink and the
|
|
109
|
+
# conflict budget were all built above and none reached the pipeline.
|
|
110
|
+
# `--adaptive --self-check` printed "s UNSATISFIABLE" and checked
|
|
111
|
+
# nothing, which is the one thing this project claims not to do.
|
|
112
|
+
jobs = getattr(args, "jobs", None) or 1
|
|
113
|
+
if proof_sink is not None and jobs > 1:
|
|
114
|
+
print("c error: a proof cannot be logged from a parallel portfolio "
|
|
115
|
+
"(workers race and their steps interleave); drop -j or drop "
|
|
116
|
+
"--proof/--self-check")
|
|
117
|
+
return EXIT_ERROR
|
|
118
|
+
pr = solve_adaptive(
|
|
119
|
+
f,
|
|
120
|
+
engine="native" if _native_ok() else "python",
|
|
121
|
+
config=cfg,
|
|
122
|
+
jobs=jobs,
|
|
123
|
+
proof=proof_sink,
|
|
124
|
+
)
|
|
109
125
|
print(pr.report())
|
|
110
126
|
print(f"c {pr.conflicts} conflicts in {pr.seconds:.3f}s")
|
|
111
127
|
if pr.sat:
|
|
@@ -120,7 +136,10 @@ def cmd_solve(args) -> int:
|
|
|
120
136
|
_emit_model(pr.model)
|
|
121
137
|
return EXIT_SAT
|
|
122
138
|
print("s UNSATISFIABLE")
|
|
123
|
-
|
|
139
|
+
# The proof covers preprocessing *and* the search, so it is checked
|
|
140
|
+
# against the original formula, not the reduced one.
|
|
141
|
+
return _finish_unsat(args, original if original is not None else f,
|
|
142
|
+
mem_proof, proof_sink)
|
|
124
143
|
|
|
125
144
|
jobs = args.jobs
|
|
126
145
|
if jobs is not None and jobs > 1:
|
|
@@ -167,9 +186,23 @@ def cmd_solve(args) -> int:
|
|
|
167
186
|
return EXIT_SAT
|
|
168
187
|
|
|
169
188
|
print("s UNSATISFIABLE")
|
|
189
|
+
return _finish_unsat(args, original, mem_proof, proof_sink)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _finish_unsat(args, formula, mem_proof, proof_sink) -> int:
|
|
193
|
+
"""Self-check or write out the proof, then report.
|
|
194
|
+
|
|
195
|
+
Shared by the ordinary and the adaptive paths. It was inline in one of
|
|
196
|
+
them, which is how the adaptive path came to skip it entirely: the flag
|
|
197
|
+
was accepted, the branch returned early, and nothing said so.
|
|
198
|
+
|
|
199
|
+
`formula` is the *original* input. When the adaptive pipeline
|
|
200
|
+
preprocesses, its steps are logged first, so the combined proof is a
|
|
201
|
+
refutation of what the user handed in rather than of the reduced formula.
|
|
202
|
+
"""
|
|
170
203
|
if mem_proof is not None:
|
|
171
204
|
t0 = time.perf_counter()
|
|
172
|
-
res = check_proof(
|
|
205
|
+
res = check_proof(formula, mem_proof)
|
|
173
206
|
print(f"c proof self-check took {time.perf_counter()-t0:.3f}s "
|
|
174
207
|
f"({len(mem_proof.steps)} steps)")
|
|
175
208
|
for line in res.report().splitlines():
|
|
@@ -405,12 +438,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
405
438
|
"turns out to be hard enough to repay it")
|
|
406
439
|
s.add_argument("--prep-rounds", type=int, default=3)
|
|
407
440
|
s.add_argument("--conflicts", type=int, default=None, help="conflict budget")
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
s.add_argument("--
|
|
441
|
+
# Every search default comes from Config() rather than a literal. They were
|
|
442
|
+
# literals, and --restart drifted to "glucose" while the library moved to
|
|
443
|
+
# "luby" -- so the CLI quietly ran the policy the CHANGELOG says was
|
|
444
|
+
# replaced, and the headline measurement did not describe `cdclkit solve`.
|
|
445
|
+
_d = Config()
|
|
446
|
+
s.add_argument("--restart", default=_d.restart,
|
|
447
|
+
choices=["glucose", "luby", "none"])
|
|
448
|
+
s.add_argument("--var-decay", type=float, default=_d.var_decay)
|
|
449
|
+
s.add_argument("--ccmin", default=_d.ccmin, choices=["deep", "basic", "none"])
|
|
450
|
+
s.add_argument("--no-phase-saving", action="store_true",
|
|
451
|
+
help="phase saving is on by default"
|
|
452
|
+
if _d.phase_saving else "phase saving is off by default")
|
|
453
|
+
s.add_argument("--rnd-freq", type=float, default=_d.rnd_freq)
|
|
454
|
+
s.add_argument("--seed", type=int, default=_d.rnd_seed)
|
|
414
455
|
s.add_argument("--no-model", action="store_true", help="suppress the v lines")
|
|
415
456
|
s.add_argument("--jobs", "-j", type=int, default=None,
|
|
416
457
|
metavar="N",
|
|
@@ -103,12 +103,12 @@ class Encoder:
|
|
|
103
103
|
are also used to *define* variables the caller may reference either way.
|
|
104
104
|
"""
|
|
105
105
|
|
|
106
|
-
#: Cap on
|
|
106
|
+
#: Cap on direct XOR expansion, which emits 2^(k-1) clauses. 16 inputs is
|
|
107
107
|
#: 32768 clauses -- large but survivable, and the point of the direct form
|
|
108
108
|
#: is to cross-check the chain on small arities, not to replace it.
|
|
109
109
|
MAX_DIRECT_XOR_ARITY = 16
|
|
110
110
|
|
|
111
|
-
#: Cap on
|
|
111
|
+
#: Cap on expanded truth-table assertion, which enumerates 2^v rows over the
|
|
112
112
|
#: v variables an expression mentions. 12 is 4096 rows per conjunct.
|
|
113
113
|
MAX_EXPAND_ARITY = 12
|
|
114
114
|
|
|
@@ -31,6 +31,7 @@ import time
|
|
|
31
31
|
from typing import Sequence
|
|
32
32
|
|
|
33
33
|
from dratify.cnf import CNF
|
|
34
|
+
from dratify.lits import from_dimacs
|
|
34
35
|
from .solver import Config, Solver
|
|
35
36
|
|
|
36
37
|
__all__ = ["solve_adaptive", "PipelineResult"]
|
|
@@ -74,8 +75,27 @@ def _native_available() -> bool:
|
|
|
74
75
|
return native.available()
|
|
75
76
|
|
|
76
77
|
|
|
78
|
+
def _drain_native_proof(obj, proof) -> None:
|
|
79
|
+
"""Copy a native stage's logged steps into `proof`, in order.
|
|
80
|
+
|
|
81
|
+
Both the native Solver and the native Preprocessor accumulate steps and
|
|
82
|
+
hand them over at the end, where the Python versions write to a sink as
|
|
83
|
+
they go. Concatenating them preserves DRAT's only ordering requirement:
|
|
84
|
+
a clause must be justified by what precedes it, and preprocessing precedes
|
|
85
|
+
the search.
|
|
86
|
+
"""
|
|
87
|
+
if proof is None:
|
|
88
|
+
return
|
|
89
|
+
for kind, lits in obj.proof_steps():
|
|
90
|
+
internal = [from_dimacs(d) for d in lits]
|
|
91
|
+
if kind == "d":
|
|
92
|
+
proof.delete(internal)
|
|
93
|
+
else:
|
|
94
|
+
proof.add(internal)
|
|
95
|
+
|
|
96
|
+
|
|
77
97
|
def _solve(f: CNF, cfg: Config, budget: int | None, engine: str,
|
|
78
|
-
seconds: float | None = None):
|
|
98
|
+
seconds: float | None = None, proof=None):
|
|
79
99
|
"""Returns (status, model, conflicts). status None means budget exhausted."""
|
|
80
100
|
if engine == "native":
|
|
81
101
|
from . import native
|
|
@@ -94,15 +114,20 @@ def _solve(f: CNF, cfg: Config, budget: int | None, engine: str,
|
|
|
94
114
|
glue_keep=cfg.glue_keep, block_restart=cfg.block_restart,
|
|
95
115
|
rnd_freq=cfg.rnd_freq, rnd_seed=cfg.rnd_seed,
|
|
96
116
|
)
|
|
117
|
+
if proof is not None:
|
|
118
|
+
s.enable_proof() # must precede the first clause
|
|
97
119
|
for c in f.clauses:
|
|
98
120
|
if not s.add_clause(list(c)):
|
|
121
|
+
_drain_native_proof(s, proof)
|
|
99
122
|
return False, None, s.conflicts
|
|
100
123
|
res = s.solve(budget, seconds)
|
|
101
124
|
if res is None:
|
|
102
125
|
return None, None, s.conflicts
|
|
126
|
+
if not res:
|
|
127
|
+
_drain_native_proof(s, proof)
|
|
103
128
|
return res, (list(s.model) if res else None), s.conflicts
|
|
104
129
|
|
|
105
|
-
s = Solver(f.nvars, config=cfg)
|
|
130
|
+
s = Solver(f.nvars, config=cfg, proof=proof)
|
|
106
131
|
if not s.add_cnf(f):
|
|
107
132
|
return False, None, s.stats.conflicts
|
|
108
133
|
res = s.solve(max_conflicts=budget, deadline=(
|
|
@@ -112,17 +137,22 @@ def _solve(f: CNF, cfg: Config, budget: int | None, engine: str,
|
|
|
112
137
|
return res, (list(s.model) if res else None), s.stats.conflicts
|
|
113
138
|
|
|
114
139
|
|
|
115
|
-
def _preprocess(f: CNF, engine: str):
|
|
140
|
+
def _preprocess(f: CNF, engine: str, proof=None):
|
|
116
141
|
"""Returns (reduced_formula, unsat, reconstruct_fn, seconds)."""
|
|
117
142
|
t0 = time.perf_counter()
|
|
118
143
|
if engine == "native" and _native_available():
|
|
119
144
|
from . import native
|
|
120
145
|
|
|
121
146
|
n = native.require()
|
|
122
|
-
|
|
147
|
+
# with_proof is off by default; without it the native
|
|
148
|
+
# preprocessor logs nothing and the solver's proof ends up
|
|
149
|
+
# being about the *reduced* formula, which does not check
|
|
150
|
+
# against the original.
|
|
151
|
+
p = n.Preprocessor(f.nvars, with_proof=proof is not None)
|
|
123
152
|
for c in f.clauses:
|
|
124
153
|
p.add_clause(list(c))
|
|
125
154
|
p.run(3)
|
|
155
|
+
_drain_native_proof(p, proof)
|
|
126
156
|
red = CNF(f.nvars)
|
|
127
157
|
for c in p.reduced():
|
|
128
158
|
red.add(c)
|
|
@@ -131,7 +161,7 @@ def _preprocess(f: CNF, engine: str):
|
|
|
131
161
|
|
|
132
162
|
from .preprocess import Preprocessor
|
|
133
163
|
|
|
134
|
-
p = Preprocessor(f)
|
|
164
|
+
p = Preprocessor(f, proof=proof)
|
|
135
165
|
red = p.run()
|
|
136
166
|
return red, p.unsat, p.reconstruct, time.perf_counter() - t0
|
|
137
167
|
|
|
@@ -145,6 +175,7 @@ def solve_adaptive(
|
|
|
145
175
|
never_preprocess: bool = False,
|
|
146
176
|
jobs: int = 1,
|
|
147
177
|
seconds: float | None = None,
|
|
178
|
+
proof=None,
|
|
148
179
|
) -> PipelineResult:
|
|
149
180
|
"""Solve `f`, preprocessing only when a short probe says it is worth it.
|
|
150
181
|
|
|
@@ -162,13 +193,22 @@ def solve_adaptive(
|
|
|
162
193
|
cfg = config or Config()
|
|
163
194
|
if engine == "native" and not _native_available():
|
|
164
195
|
engine = "python"
|
|
196
|
+
if proof is not None and jobs > 1:
|
|
197
|
+
# Portfolio workers race; whichever finishes first is the one whose
|
|
198
|
+
# refutation is returned, and the others' steps would interleave into
|
|
199
|
+
# a stream that justifies nothing. Refuse rather than emit a proof
|
|
200
|
+
# that will not check.
|
|
201
|
+
raise ValueError(
|
|
202
|
+
"a proof cannot be logged from a parallel portfolio: workers race "
|
|
203
|
+
"and their steps would interleave. Use jobs=1 when proof is set.")
|
|
165
204
|
|
|
166
205
|
r = PipelineResult()
|
|
167
206
|
r.clauses_before = f.nclauses
|
|
168
207
|
t_start = time.perf_counter()
|
|
169
208
|
|
|
170
209
|
if not never_preprocess and not always_preprocess:
|
|
171
|
-
|
|
210
|
+
# The probe is a real solve and may refute outright, so it logs too.
|
|
211
|
+
status, model, conflicts = _solve(f, cfg, probe, engine, proof=proof)
|
|
172
212
|
r.probe_conflicts = conflicts
|
|
173
213
|
if status is not None:
|
|
174
214
|
r.sat, r.model, r.conflicts = status, model, conflicts
|
|
@@ -177,13 +217,14 @@ def solve_adaptive(
|
|
|
177
217
|
return r
|
|
178
218
|
|
|
179
219
|
if never_preprocess:
|
|
180
|
-
status, model, conflicts = _solve(f, cfg, None, engine, seconds
|
|
220
|
+
status, model, conflicts = _solve(f, cfg, None, engine, seconds,
|
|
221
|
+
proof=proof)
|
|
181
222
|
r.sat, r.model, r.conflicts = status, model, conflicts
|
|
182
223
|
r.seconds = time.perf_counter() - t_start
|
|
183
224
|
r.clauses_after = f.nclauses
|
|
184
225
|
return r
|
|
185
226
|
|
|
186
|
-
red, unsat, reconstruct, prep_s = _preprocess(f, engine)
|
|
227
|
+
red, unsat, reconstruct, prep_s = _preprocess(f, engine, proof=proof)
|
|
187
228
|
r.preprocessed = True
|
|
188
229
|
r.prep_seconds = prep_s
|
|
189
230
|
r.clauses_after = red.nclauses
|
|
@@ -199,12 +240,14 @@ def solve_adaptive(
|
|
|
199
240
|
# preprocess_workers=0 because this formula is *already* preprocessed;
|
|
200
241
|
# asking for preprocessing workers here would both redo the work and
|
|
201
242
|
# force the process-based path, paying ~60 ms of startup for nothing
|
|
202
|
-
pr = solve_portfolio(red, jobs=jobs, engine=engine,
|
|
243
|
+
pr = solve_portfolio(red, jobs=jobs, engine=engine,
|
|
244
|
+
preprocess_workers=0, timeout=seconds)
|
|
203
245
|
status = pr.sat
|
|
204
246
|
model = pr.model
|
|
205
247
|
conflicts = pr.stats.get("conflicts", 0)
|
|
206
248
|
else:
|
|
207
|
-
status, model, conflicts = _solve(red, cfg, None, engine, seconds
|
|
249
|
+
status, model, conflicts = _solve(red, cfg, None, engine, seconds,
|
|
250
|
+
proof=proof)
|
|
208
251
|
r.conflicts = conflicts + r.probe_conflicts
|
|
209
252
|
r.sat = status
|
|
210
253
|
if status:
|
|
File without changes
|