cdclkit 0.1.0__tar.gz → 0.1.1__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.0 → cdclkit-0.1.1}/PKG-INFO +4 -3
- {cdclkit-0.1.0 → cdclkit-0.1.1}/README.md +1 -2
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit/__init__.py +1 -1
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit/pipeline.py +1 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit/portfolio.py +3 -1
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit.egg-info/PKG-INFO +4 -3
- cdclkit-0.1.1/cdclkit.egg-info/requires.txt +4 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/pyproject.toml +7 -1
- {cdclkit-0.1.0 → cdclkit-0.1.1}/tests/test_native_solver.py +79 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/tests/test_packaging.py +33 -0
- cdclkit-0.1.0/cdclkit.egg-info/requires.txt +0 -1
- {cdclkit-0.1.0 → cdclkit-0.1.1}/LICENSE +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit/__main__.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit/brute.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit/cli.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit/encodings.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit/heap.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit/model.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit/mus.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit/native.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit/preprocess.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit/pyeq.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit/solver.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit.egg-info/SOURCES.txt +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit.egg-info/dependency_links.txt +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit.egg-info/entry_points.txt +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/cdclkit.egg-info/top_level.txt +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/setup.cfg +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/tests/test_core.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/tests/test_encodings.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/tests/test_integration.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/tests/test_mus.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/tests/test_native.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/tests/test_pipeline.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/tests/test_portfolio.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/tests/test_preprocess.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/tests/test_proof.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/tests/test_pyeq.py +0 -0
- {cdclkit-0.1.0 → cdclkit-0.1.1}/tests/test_solver.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.1
|
|
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,6 +23,8 @@ Requires-Python: >=3.10
|
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
25
|
Requires-Dist: dratify>=0.1.1
|
|
26
|
+
Provides-Extra: native
|
|
27
|
+
Requires-Dist: cdclkit-native==0.1.1; extra == "native"
|
|
26
28
|
Dynamic: license-file
|
|
27
29
|
|
|
28
30
|
# cdclkit
|
|
@@ -77,8 +79,7 @@ puzzle, graph colouring, bounded model checking, circuit equivalence.
|
|
|
77
79
|
| `portfolio` | parallel configurations |
|
|
78
80
|
| `pyeq` | **experimental** — bounded equivalence of two Python integer functions |
|
|
79
81
|
|
|
80
|
-
An optional Rust engine (
|
|
81
|
-
on PyPI yet) is roughly 18x faster
|
|
82
|
+
An optional Rust engine (`pip install cdclkit[native]`) is roughly 18x faster
|
|
82
83
|
and **bit-exact** with the Python one: identical conflicts, decisions and
|
|
83
84
|
propagations on every instance. The pure-Python path has zero third-party
|
|
84
85
|
dependencies and is the one that must never break.
|
|
@@ -50,8 +50,7 @@ puzzle, graph colouring, bounded model checking, circuit equivalence.
|
|
|
50
50
|
| `portfolio` | parallel configurations |
|
|
51
51
|
| `pyeq` | **experimental** — bounded equivalence of two Python integer functions |
|
|
52
52
|
|
|
53
|
-
An optional Rust engine (
|
|
54
|
-
on PyPI yet) is roughly 18x faster
|
|
53
|
+
An optional Rust engine (`pip install cdclkit[native]`) is roughly 18x faster
|
|
55
54
|
and **bit-exact** with the Python one: identical conflicts, decisions and
|
|
56
55
|
propagations on every instance. The pure-Python path has zero third-party
|
|
57
56
|
dependencies and is the one that must never break.
|
|
@@ -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.1"
|
|
56
56
|
|
|
57
57
|
__all__ = [
|
|
58
58
|
"CNF",
|
|
@@ -92,6 +92,7 @@ def _solve(f: CNF, cfg: Config, budget: int | None, engine: str,
|
|
|
92
92
|
cla_decay=cfg.cla_decay, luby_base=float(cfg.luby_base),
|
|
93
93
|
first_reduce=cfg.first_reduce, reduce_inc=cfg.reduce_inc,
|
|
94
94
|
glue_keep=cfg.glue_keep, block_restart=cfg.block_restart,
|
|
95
|
+
rnd_freq=cfg.rnd_freq, rnd_seed=cfg.rnd_seed,
|
|
95
96
|
)
|
|
96
97
|
for c in f.clauses:
|
|
97
98
|
if not s.add_clause(list(c)):
|
|
@@ -235,6 +235,7 @@ def _worker_native(index, nvars, clauses, cfg_kwargs, want_proof):
|
|
|
235
235
|
cla_decay=cfg.cla_decay, luby_base=float(cfg.luby_base),
|
|
236
236
|
first_reduce=cfg.first_reduce, reduce_inc=cfg.reduce_inc,
|
|
237
237
|
glue_keep=cfg.glue_keep, block_restart=cfg.block_restart,
|
|
238
|
+
rnd_freq=cfg.rnd_freq, rnd_seed=cfg.rnd_seed,
|
|
238
239
|
)
|
|
239
240
|
if want_proof:
|
|
240
241
|
s.enable_proof() # must precede the first clause
|
|
@@ -416,7 +417,8 @@ def _config_tuples(configs):
|
|
|
416
417
|
var_decay=c.var_decay, var_decay_max=c.var_decay_max,
|
|
417
418
|
cla_decay=c.cla_decay, luby_base=float(c.luby_base),
|
|
418
419
|
first_reduce=c.first_reduce, reduce_inc=c.reduce_inc,
|
|
419
|
-
glue_keep=c.glue_keep, block_restart=c.block_restart
|
|
420
|
+
glue_keep=c.glue_keep, block_restart=c.block_restart,
|
|
421
|
+
rnd_freq=c.rnd_freq, rnd_seed=c.rnd_seed)
|
|
420
422
|
for c in configs
|
|
421
423
|
]
|
|
422
424
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cdclkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
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,6 +23,8 @@ Requires-Python: >=3.10
|
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
25
|
Requires-Dist: dratify>=0.1.1
|
|
26
|
+
Provides-Extra: native
|
|
27
|
+
Requires-Dist: cdclkit-native==0.1.1; extra == "native"
|
|
26
28
|
Dynamic: license-file
|
|
27
29
|
|
|
28
30
|
# cdclkit
|
|
@@ -77,8 +79,7 @@ puzzle, graph colouring, bounded model checking, circuit equivalence.
|
|
|
77
79
|
| `portfolio` | parallel configurations |
|
|
78
80
|
| `pyeq` | **experimental** — bounded equivalence of two Python integer functions |
|
|
79
81
|
|
|
80
|
-
An optional Rust engine (
|
|
81
|
-
on PyPI yet) is roughly 18x faster
|
|
82
|
+
An optional Rust engine (`pip install cdclkit[native]`) is roughly 18x faster
|
|
82
83
|
and **bit-exact** with the Python one: identical conflicts, decisions and
|
|
83
84
|
propagations on every instance. The pure-Python path has zero third-party
|
|
84
85
|
dependencies and is the one that must never break.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cdclkit"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.1"
|
|
8
8
|
description = "A CDCL SAT solver, preprocessor, encoding library and modelling layer, written from scratch in readable Python. Every answer comes with a certificate."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -31,6 +31,12 @@ classifiers = [
|
|
|
31
31
|
# test run here as well as by its own suite.
|
|
32
32
|
dependencies = ["dratify>=0.1.1"]
|
|
33
33
|
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
# The Rust accelerator, as abi3 wheels for Linux and macOS. Pinned exactly:
|
|
36
|
+
# the two are bit-exact by construction and a mismatched pair is a divergence
|
|
37
|
+
# nobody would look for. tests/test_packaging.py enforces the pin.
|
|
38
|
+
native = ["cdclkit-native==0.1.1"]
|
|
39
|
+
|
|
34
40
|
[project.scripts]
|
|
35
41
|
cdclkit = "cdclkit.__main__:main"
|
|
36
42
|
|
|
@@ -160,6 +160,85 @@ class TestDefaultsAgree(unittest.TestCase):
|
|
|
160
160
|
self.assertEqual(py.stats.propagations, rs.propagations)
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
@requires_native
|
|
164
|
+
class TestRandomBranchingAgrees(unittest.TestCase):
|
|
165
|
+
"""`rnd_freq` and `rnd_seed` must reach the native engine and match.
|
|
166
|
+
|
|
167
|
+
These two knobs had no Rust counterpart at all: the native side accepted
|
|
168
|
+
the rest of the configuration and silently dropped these, so a Python run
|
|
169
|
+
at rnd_freq=0.02 took 4,987 conflicts where the native one took 3,171.
|
|
170
|
+
|
|
171
|
+
That was not a cosmetic gap. `portfolio.py` builds worker diversity from
|
|
172
|
+
exactly these two fields, and sets a per-worker seed specifically "so
|
|
173
|
+
duplicated recipes still diverge" -- which on the native path they did not,
|
|
174
|
+
because every worker got the same search. Duplicate recipes became
|
|
175
|
+
bit-identical duplicate workers doing the same work in parallel.
|
|
176
|
+
|
|
177
|
+
The existing bit-exactness tests could not have caught this: none of them
|
|
178
|
+
set either knob.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def _native(self, f, rnd_freq, rnd_seed):
|
|
182
|
+
n = native.require()
|
|
183
|
+
d = Config()
|
|
184
|
+
rs = n.Solver(
|
|
185
|
+
f.nvars, restart=d.restart, ccmin=d.ccmin,
|
|
186
|
+
phase_saving=d.phase_saving, init_phase=d.init_phase,
|
|
187
|
+
target_phase=d.target_phase, target_reset=d.target_reset,
|
|
188
|
+
walk_flips=d.walk_flips, walk_interval=d.walk_interval,
|
|
189
|
+
walk_patience=d.walk_patience,
|
|
190
|
+
walk_min_conflicts=d.walk_min_conflicts,
|
|
191
|
+
var_decay=d.var_decay, var_decay_max=d.var_decay_max,
|
|
192
|
+
cla_decay=d.cla_decay, luby_base=float(d.luby_base),
|
|
193
|
+
first_reduce=d.first_reduce, reduce_inc=d.reduce_inc,
|
|
194
|
+
glue_keep=d.glue_keep, block_restart=d.block_restart,
|
|
195
|
+
rnd_freq=rnd_freq, rnd_seed=rnd_seed)
|
|
196
|
+
for c in f.clauses:
|
|
197
|
+
rs.add_clause(list(c))
|
|
198
|
+
rs.solve(None)
|
|
199
|
+
return rs.conflicts, rs.decisions, rs.propagations
|
|
200
|
+
|
|
201
|
+
def _python(self, f, rnd_freq, rnd_seed):
|
|
202
|
+
from cdclkit.solver import Solver as PySolver
|
|
203
|
+
|
|
204
|
+
cfg = Config()
|
|
205
|
+
cfg.rnd_freq = rnd_freq
|
|
206
|
+
cfg.rnd_seed = rnd_seed
|
|
207
|
+
py = PySolver(f.nvars, config=cfg)
|
|
208
|
+
py.add_cnf(f)
|
|
209
|
+
py.solve()
|
|
210
|
+
return py.stats.conflicts, py.stats.decisions, py.stats.propagations
|
|
211
|
+
|
|
212
|
+
def test_engines_agree_across_frequencies_and_seeds(self):
|
|
213
|
+
f = php(6)
|
|
214
|
+
for freq, seed in [(0.0, 91648253), (0.02, 91648253), (0.02, 12345),
|
|
215
|
+
(0.10, 7), (0.05, 999983)]:
|
|
216
|
+
with self.subTest(rnd_freq=freq, rnd_seed=seed):
|
|
217
|
+
self.assertEqual(
|
|
218
|
+
self._python(f, freq, seed), self._native(f, freq, seed),
|
|
219
|
+
f"engines diverge at rnd_freq={freq}, rnd_seed={seed}")
|
|
220
|
+
|
|
221
|
+
def test_a_zero_frequency_does_not_touch_the_random_stream(self):
|
|
222
|
+
"""The short-circuit is load-bearing, not an optimisation.
|
|
223
|
+
|
|
224
|
+
Python tests `rnd_freq > 0.0` before drawing, so the default
|
|
225
|
+
configuration never advances the PRNG. If the native side drew first,
|
|
226
|
+
the walk -- which shares the stream -- would desynchronise and the two
|
|
227
|
+
engines would diverge on instances long enough to reach it.
|
|
228
|
+
"""
|
|
229
|
+
f = php(6)
|
|
230
|
+
self.assertEqual(self._python(f, 0.0, 91648253),
|
|
231
|
+
self._native(f, 0.0, 91648253))
|
|
232
|
+
|
|
233
|
+
def test_the_seed_actually_changes_the_search(self):
|
|
234
|
+
"""A knob that is wired up but inert would pass the tests above."""
|
|
235
|
+
f = php(6)
|
|
236
|
+
runs = {self._native(f, 0.05, seed) for seed in (1, 7, 12345, 999983)}
|
|
237
|
+
self.assertGreater(len(runs), 1,
|
|
238
|
+
"every seed produced an identical search, so the "
|
|
239
|
+
"seed is not reaching the engine")
|
|
240
|
+
|
|
241
|
+
|
|
163
242
|
@requires_native
|
|
164
243
|
class TestTier2Faithfulness(unittest.TestCase):
|
|
165
244
|
"""Bit-exact agreement with the Python solver."""
|
|
@@ -157,6 +157,39 @@ class TestPublicAPISurface(unittest.TestCase):
|
|
|
157
157
|
self.assertNotIn(internal, cdclkit.__all__)
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
class TestWorkflowsAreWellFormed(unittest.TestCase):
|
|
161
|
+
"""Every top-level line in a workflow must be a known key or a comment.
|
|
162
|
+
|
|
163
|
+
A comment in release.yml lost its leading "# " during an edit. The result
|
|
164
|
+
was still valid YAML -- it simply became a mapping key -- so a
|
|
165
|
+
`yaml.safe_load` check passed it, and GitHub then refused the whole file
|
|
166
|
+
with "this run likely failed because of a workflow file issue". Zero jobs
|
|
167
|
+
ran, and the tag had to be deleted and re-cut.
|
|
168
|
+
|
|
169
|
+
Deliberately does not import yaml: the dependency-free test path is the one
|
|
170
|
+
that must never break, and this failure mode does not need a parser.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
#: https://docs.github.com/actions/reference/workflow-syntax-for-github-actions
|
|
174
|
+
ALLOWED = {"name", "on", "permissions", "env", "defaults", "concurrency",
|
|
175
|
+
"jobs", "run-name"}
|
|
176
|
+
|
|
177
|
+
def test_no_stray_top_level_keys(self):
|
|
178
|
+
workflows = sorted((ROOT / ".github" / "workflows").glob("*.yml"))
|
|
179
|
+
self.assertTrue(workflows, "no workflows found")
|
|
180
|
+
for wf in workflows:
|
|
181
|
+
with self.subTest(workflow=wf.name):
|
|
182
|
+
for n, line in enumerate(wf.read_text().splitlines(), 1):
|
|
183
|
+
if not line or line[0] in " \t#":
|
|
184
|
+
continue
|
|
185
|
+
key = line.split(":", 1)[0].strip()
|
|
186
|
+
self.assertIn(
|
|
187
|
+
key, self.ALLOWED,
|
|
188
|
+
f"{wf.name}:{n} starts a top-level key {key!r} that "
|
|
189
|
+
f"GitHub does not recognise -- most likely a comment "
|
|
190
|
+
f"that lost its '# '")
|
|
191
|
+
|
|
192
|
+
|
|
160
193
|
class TestLicenceConsistency(unittest.TestCase):
|
|
161
194
|
"""The tree is Apache-2.0; every place that states a licence must agree.
|
|
162
195
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
dratify>=0.1.1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|