cdclkit 0.1.4__tar.gz → 0.1.6__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.4 → cdclkit-0.1.6}/PKG-INFO +3 -2
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/__init__.py +1 -1
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/cli.py +29 -2
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/native.py +8 -6
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/pyeq.py +10 -5
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/solver.py +20 -4
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit.egg-info/PKG-INFO +3 -2
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit.egg-info/SOURCES.txt +2 -1
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit.egg-info/requires.txt +1 -1
- {cdclkit-0.1.4 → cdclkit-0.1.6}/pyproject.toml +3 -2
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_adaptive_proof.py +45 -1
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_portfolio.py +6 -0
- cdclkit-0.1.6/tests/test_target_scaling.py +78 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/LICENSE +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/README.md +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/__main__.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/brute.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/encodings.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/heap.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/model.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/mus.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/pipeline.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/portfolio.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/preprocess.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/py.typed +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit.egg-info/dependency_links.txt +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit.egg-info/entry_points.txt +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit.egg-info/top_level.txt +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/setup.cfg +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_agent_docs.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_brute.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_cli_defaults.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_core.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_encodings.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_integration.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_mus.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_native.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_native_solver.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_packaging.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_pipeline.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_preprocess.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_proof.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_pyeq.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_readme.py +0 -0
- {cdclkit-0.1.4 → cdclkit-0.1.6}/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.6
|
|
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
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
21
|
Classifier: Programming Language :: Rust
|
|
21
22
|
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
22
23
|
Requires-Python: >=3.10
|
|
@@ -24,7 +25,7 @@ Description-Content-Type: text/markdown
|
|
|
24
25
|
License-File: LICENSE
|
|
25
26
|
Requires-Dist: dratify>=0.1.7
|
|
26
27
|
Provides-Extra: native
|
|
27
|
-
Requires-Dist: cdclkit-native==0.1.
|
|
28
|
+
Requires-Dist: cdclkit-native==0.1.6; extra == "native"
|
|
28
29
|
Dynamic: license-file
|
|
29
30
|
|
|
30
31
|
# cdclkit
|
|
@@ -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.6"
|
|
56
56
|
|
|
57
57
|
__all__ = [
|
|
58
58
|
"CNF",
|
|
@@ -47,6 +47,26 @@ def _native_ok() -> bool:
|
|
|
47
47
|
return native.available()
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
def _pick_engine() -> str | None:
|
|
51
|
+
"""The engine for `--adaptive` and `-j`; None after printing an error.
|
|
52
|
+
|
|
53
|
+
Native when it is installed: it is bit-exact with Python, so this changes
|
|
54
|
+
the speed and not the answer. CDCLKIT_ENGINE overrides it, which is how to
|
|
55
|
+
rule the native engine in or out of a suspect result. The plain sequential
|
|
56
|
+
path is always Python.
|
|
57
|
+
"""
|
|
58
|
+
from . import native
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
engine = native.engine_requested(
|
|
62
|
+
default="native" if _native_ok() else "python")
|
|
63
|
+
except RuntimeError as e:
|
|
64
|
+
print(f"c error: {e}", file=sys.stderr)
|
|
65
|
+
return None
|
|
66
|
+
print(f"c engine: {engine}")
|
|
67
|
+
return engine
|
|
68
|
+
|
|
69
|
+
|
|
50
70
|
EXIT_SAT = 10
|
|
51
71
|
EXIT_UNSAT = 20
|
|
52
72
|
EXIT_UNKNOWN = 0
|
|
@@ -115,9 +135,12 @@ def cmd_solve(args) -> int:
|
|
|
115
135
|
"(workers race and their steps interleave); drop -j or drop "
|
|
116
136
|
"--proof/--self-check")
|
|
117
137
|
return EXIT_ERROR
|
|
138
|
+
engine = _pick_engine()
|
|
139
|
+
if engine is None:
|
|
140
|
+
return EXIT_ERROR
|
|
118
141
|
pr = solve_adaptive(
|
|
119
142
|
f,
|
|
120
|
-
engine=
|
|
143
|
+
engine=engine,
|
|
121
144
|
config=cfg,
|
|
122
145
|
jobs=jobs,
|
|
123
146
|
proof=proof_sink,
|
|
@@ -146,7 +169,11 @@ def cmd_solve(args) -> int:
|
|
|
146
169
|
# Parallel portfolio. Only the winning worker's proof exists, and
|
|
147
170
|
# because workers share no clauses it is a complete refutation on its
|
|
148
171
|
# own -- see cdclkit/portfolio.py.
|
|
149
|
-
|
|
172
|
+
engine = _pick_engine()
|
|
173
|
+
if engine is None:
|
|
174
|
+
return EXIT_ERROR
|
|
175
|
+
pr = solve_portfolio(f, jobs=jobs, engine=engine,
|
|
176
|
+
want_proof=proof_sink is not None)
|
|
150
177
|
if not pr.finished:
|
|
151
178
|
print("s UNKNOWN")
|
|
152
179
|
return EXIT_UNKNOWN
|
|
@@ -13,10 +13,13 @@ The contract, in both directions:
|
|
|
13
13
|
full under a plain `python3` with no Rust toolchain anywhere. That property
|
|
14
14
|
is tested, not assumed -- it is what lets the port proceed in small commits
|
|
15
15
|
without the repo ever being in a broken state.
|
|
16
|
-
* **If it is present,
|
|
17
|
-
|
|
18
|
-
`
|
|
19
|
-
is
|
|
16
|
+
* **If it is present, the plain solver still does not use it.** `Solver`,
|
|
17
|
+
`solve()` and `cdclkit solve` are Python. `solve_adaptive()`,
|
|
18
|
+
`solve_portfolio()` and `cdclkit solve --adaptive` / `-j N` use it when it is
|
|
19
|
+
installed. That is safe only because the two engines are bit-exact and tested
|
|
20
|
+
to stay that way. A silent switch between two implementations is how
|
|
21
|
+
differential bugs hide, so the CLI prints which engine ran and
|
|
22
|
+
`CDCLKIT_ENGINE` (read by `engine_requested()`) overrides the choice.
|
|
20
23
|
|
|
21
24
|
Building it::
|
|
22
25
|
|
|
@@ -54,8 +57,7 @@ else: # pragma: no cover - depends on whether the module was built
|
|
|
54
57
|
#
|
|
55
58
|
# This is not the silent engine switch the rest of this module warns about.
|
|
56
59
|
# Two checkers must agree by construction -- if they ever disagree that is
|
|
57
|
-
# a bug worth surfacing, not a difference worth preserving.
|
|
58
|
-
# the thing that stays explicitly opt-in.
|
|
60
|
+
# a bug worth surfacing, not a difference worth preserving.
|
|
59
61
|
try:
|
|
60
62
|
import dratify as _dratify
|
|
61
63
|
_dratify.register_native(_native)
|
|
@@ -198,8 +198,11 @@ class BitVec:
|
|
|
198
198
|
return self.enc.and_gate(same)
|
|
199
199
|
|
|
200
200
|
def slt(self, other: "BitVec") -> int:
|
|
201
|
-
"""Signed less-than
|
|
202
|
-
|
|
201
|
+
"""Signed less-than: a < b iff a - b is negative.
|
|
202
|
+
|
|
203
|
+
Both operands are sign-extended by one bit before subtracting, so the
|
|
204
|
+
difference of two w-bit values always fits in w+1 bits and cannot
|
|
205
|
+
overflow. Its top bit is then the answer, with no overflow flag."""
|
|
203
206
|
enc = self.enc
|
|
204
207
|
w = max(self.width, other.width) + 1 # one extra bit kills the overflow case
|
|
205
208
|
a, b = self.extend(w), other.extend(w)
|
|
@@ -451,10 +454,12 @@ class _Compiler(ast.NodeVisitor):
|
|
|
451
454
|
self._fail(node, type(node).__name__)
|
|
452
455
|
|
|
453
456
|
def assign(self, name: str, value: BitVec) -> None:
|
|
454
|
-
"""
|
|
457
|
+
"""Bind `name` to `value` in the current environment, unconditionally.
|
|
455
458
|
|
|
456
|
-
|
|
457
|
-
|
|
459
|
+
Branches stay correct without a select here: an `if` runs each arm on
|
|
460
|
+
its own copy of the environment and merges them afterwards, with one
|
|
461
|
+
select per variable that differs between the arms (see the `ast.If`
|
|
462
|
+
case of `stmt`).
|
|
458
463
|
"""
|
|
459
464
|
self.env[name] = value
|
|
460
465
|
|
|
@@ -392,6 +392,11 @@ class Solver:
|
|
|
392
392
|
#: depth. Only meaningful when cfg.target_phase is on.
|
|
393
393
|
self._target = bytearray()
|
|
394
394
|
self._target_size = 0
|
|
395
|
+
#: how much of the current trail the target already records: for every
|
|
396
|
+
#: i < _target_synced, _target holds trail[i]'s value. The trail only
|
|
397
|
+
#: grows by appending and only shrinks in _cancel_until, so a prefix
|
|
398
|
+
#: that has been copied stays copied until a backtrack cuts into it.
|
|
399
|
+
self._target_synced = 0
|
|
395
400
|
self._walk_best = 1 << 30
|
|
396
401
|
self._walk_stale = 0
|
|
397
402
|
self.frozen = bytearray() # variables excluded from decisions
|
|
@@ -605,6 +610,8 @@ class Solver:
|
|
|
605
610
|
del trail[bound:]
|
|
606
611
|
del self.trail_lim[level:]
|
|
607
612
|
self.qhead = len(trail)
|
|
613
|
+
if self._target_synced > bound:
|
|
614
|
+
self._target_synced = bound
|
|
608
615
|
|
|
609
616
|
# ------------------------------------------------------------ propagation
|
|
610
617
|
|
|
@@ -1154,6 +1161,7 @@ class Solver:
|
|
|
1154
1161
|
self._target[:] = best
|
|
1155
1162
|
self.polarity[:] = best
|
|
1156
1163
|
self._target_size = 0 # the target now describes the walk, not a trail
|
|
1164
|
+
self._target_synced = 0
|
|
1157
1165
|
|
|
1158
1166
|
# -------------------------------------------------------------- restarts
|
|
1159
1167
|
|
|
@@ -1229,13 +1237,21 @@ class Solver:
|
|
|
1229
1237
|
self._reduce_db()
|
|
1230
1238
|
else:
|
|
1231
1239
|
# A new deepest conflict-free trail: remember the assignment
|
|
1232
|
-
# that reached it
|
|
1233
|
-
#
|
|
1240
|
+
# that reached it -- copying only what the target does not
|
|
1241
|
+
# already hold. This used to copy the whole trail on every
|
|
1242
|
+
# improvement, on the theory that improvements become rare
|
|
1243
|
+
# quickly. They do once conflicts start; a search that makes
|
|
1244
|
+
# many decisions without one improves on every decision, and
|
|
1245
|
+
# n of them cost O(n^2) (tests/test_target_scaling.py). The
|
|
1246
|
+
# target ends up byte-for-byte what the full copy produced.
|
|
1234
1247
|
if self.cfg.target_phase and len(self.trail) > self._target_size:
|
|
1235
|
-
|
|
1248
|
+
trail = self.trail
|
|
1249
|
+
self._target_size = len(trail)
|
|
1236
1250
|
target = self._target
|
|
1237
|
-
for
|
|
1251
|
+
for i in range(self._target_synced, len(trail)):
|
|
1252
|
+
t = trail[i]
|
|
1238
1253
|
target[t >> 1] = 0 if (t & 1) else 1
|
|
1254
|
+
self._target_synced = len(trail)
|
|
1239
1255
|
|
|
1240
1256
|
if max_conflicts is not None and conflicts >= max_conflicts:
|
|
1241
1257
|
self._cancel_until(len(self.assumptions_applied))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cdclkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
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
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
21
|
Classifier: Programming Language :: Rust
|
|
21
22
|
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
22
23
|
Requires-Python: >=3.10
|
|
@@ -24,7 +25,7 @@ Description-Content-Type: text/markdown
|
|
|
24
25
|
License-File: LICENSE
|
|
25
26
|
Requires-Dist: dratify>=0.1.7
|
|
26
27
|
Provides-Extra: native
|
|
27
|
-
Requires-Dist: cdclkit-native==0.1.
|
|
28
|
+
Requires-Dist: cdclkit-native==0.1.6; extra == "native"
|
|
28
29
|
Dynamic: license-file
|
|
29
30
|
|
|
30
31
|
# cdclkit
|
|
@@ -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.6"
|
|
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"
|
|
@@ -22,6 +22,7 @@ classifiers = [
|
|
|
22
22
|
"Programming Language :: Python :: 3.12",
|
|
23
23
|
"Programming Language :: Python :: 3.13",
|
|
24
24
|
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Programming Language :: Python :: 3.15",
|
|
25
26
|
"Programming Language :: Rust",
|
|
26
27
|
"Topic :: Scientific/Engineering :: Mathematics",
|
|
27
28
|
]
|
|
@@ -35,7 +36,7 @@ dependencies = ["dratify>=0.1.7"]
|
|
|
35
36
|
# The Rust accelerator, as abi3 wheels for Linux and macOS. Pinned exactly:
|
|
36
37
|
# the two are bit-exact by construction and a mismatched pair is a divergence
|
|
37
38
|
# nobody would look for. tests/test_packaging.py enforces the pin.
|
|
38
|
-
native = ["cdclkit-native==0.1.
|
|
39
|
+
native = ["cdclkit-native==0.1.6"]
|
|
39
40
|
|
|
40
41
|
[project.scripts]
|
|
41
42
|
cdclkit = "cdclkit.__main__:main"
|
|
@@ -113,11 +113,14 @@ class TestAdaptiveEmitsACheckableProof(unittest.TestCase):
|
|
|
113
113
|
class TestAdaptiveCLI(unittest.TestCase):
|
|
114
114
|
"""End to end, because the bug was in the wiring rather than the pipeline."""
|
|
115
115
|
|
|
116
|
-
def _cli(self, *args):
|
|
116
|
+
def _cli(self, *args, engine=None):
|
|
117
117
|
root = pathlib.Path(__file__).resolve().parent.parent
|
|
118
118
|
import os
|
|
119
119
|
|
|
120
120
|
env = dict(os.environ)
|
|
121
|
+
env.pop("CDCLKIT_ENGINE", None)
|
|
122
|
+
if engine is not None:
|
|
123
|
+
env["CDCLKIT_ENGINE"] = engine
|
|
121
124
|
# prepend, do not replace: a pure-Python checkout finds dratify on the
|
|
122
125
|
# inherited path, and clobbering it made these fail for the wrong reason
|
|
123
126
|
env["PYTHONPATH"] = os.pathsep.join(
|
|
@@ -156,6 +159,47 @@ class TestAdaptiveCLI(unittest.TestCase):
|
|
|
156
159
|
self.assertEqual(r.returncode, 1)
|
|
157
160
|
self.assertIn("interleave", r.stdout)
|
|
158
161
|
|
|
162
|
+
# AGENTS.md said CDCLKIT_ENGINE selected the engine. Nothing read it, and
|
|
163
|
+
# `--adaptive` picked native on its own whenever it was installed.
|
|
164
|
+
|
|
165
|
+
def _php(self, engine=None):
|
|
166
|
+
with tempfile.TemporaryDirectory() as d:
|
|
167
|
+
cnf = pathlib.Path(d) / "php.cnf"
|
|
168
|
+
cnf.write_text(PHP)
|
|
169
|
+
return self._cli("solve", str(cnf), "--adaptive", engine=engine)
|
|
170
|
+
|
|
171
|
+
def test_adaptive_uses_native_when_installed(self):
|
|
172
|
+
r = self._php()
|
|
173
|
+
self.assertEqual(r.returncode, 20, r.stdout + r.stderr)
|
|
174
|
+
want = "native" if native.available() else "python"
|
|
175
|
+
self.assertIn(f"c engine: {want}", r.stdout)
|
|
176
|
+
|
|
177
|
+
def test_cdclkit_engine_python_overrides_adaptive(self):
|
|
178
|
+
r = self._php(engine="python")
|
|
179
|
+
self.assertEqual(r.returncode, 20, r.stdout + r.stderr)
|
|
180
|
+
self.assertIn("c engine: python", r.stdout)
|
|
181
|
+
|
|
182
|
+
def test_cdclkit_engine_native_without_the_build_is_an_error(self):
|
|
183
|
+
if native.available():
|
|
184
|
+
self.skipTest("native engine is built here")
|
|
185
|
+
r = self._php(engine="native")
|
|
186
|
+
self.assertEqual(r.returncode, 1, r.stdout + r.stderr)
|
|
187
|
+
self.assertIn("CDCLKIT_ENGINE=native", r.stderr)
|
|
188
|
+
self.assertNotIn("Traceback", r.stderr)
|
|
189
|
+
|
|
190
|
+
def test_portfolio_honours_cdclkit_engine_too(self):
|
|
191
|
+
with tempfile.TemporaryDirectory() as d:
|
|
192
|
+
cnf = pathlib.Path(d) / "php.cnf"
|
|
193
|
+
cnf.write_text(PHP)
|
|
194
|
+
r = self._cli("solve", str(cnf), "-j", "2", engine="python")
|
|
195
|
+
self.assertEqual(r.returncode, 20, r.stdout + r.stderr)
|
|
196
|
+
self.assertIn("c engine: python", r.stdout)
|
|
197
|
+
|
|
198
|
+
def test_unknown_cdclkit_engine_is_an_error(self):
|
|
199
|
+
r = self._php(engine="quantum")
|
|
200
|
+
self.assertEqual(r.returncode, 1, r.stdout + r.stderr)
|
|
201
|
+
self.assertIn("unknown CDCLKIT_ENGINE", r.stderr)
|
|
202
|
+
|
|
159
203
|
|
|
160
204
|
if __name__ == "__main__":
|
|
161
205
|
unittest.main()
|
|
@@ -151,6 +151,12 @@ class TestEngineSelection(unittest.TestCase):
|
|
|
151
151
|
"""The portfolio can run native workers; correctness must not depend on it."""
|
|
152
152
|
|
|
153
153
|
def test_python_and_native_engines_agree(self):
|
|
154
|
+
from cdclkit import native
|
|
155
|
+
|
|
156
|
+
if not native.available():
|
|
157
|
+
# engine="native" falls back to Python, so this would compare
|
|
158
|
+
# Python against Python and pass
|
|
159
|
+
self.skipTest("native engine not built")
|
|
154
160
|
rng = random.Random(fuzz_seed(13))
|
|
155
161
|
for _ in range(15):
|
|
156
162
|
f = random_cnf(rng, max_vars=9, ratio=4.8)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2026 Carlo Perassi. Licensed under the Apache License 2.0.
|
|
3
|
+
"""Target phases must not make the search quadratic in the number of variables.
|
|
4
|
+
|
|
5
|
+
With target phases on (the default), each time the trail reached a new depth
|
|
6
|
+
the solver copied the whole trail into the target. The comment beside it said
|
|
7
|
+
improvements "become rare quickly". That holds only once conflicts happen. A
|
|
8
|
+
search that makes many decisions without a conflict improves on every one, so
|
|
9
|
+
n decisions copied 1, 2, ..., n entries.
|
|
10
|
+
|
|
11
|
+
Measured on the Python engine, solving one unit clause in a formula that
|
|
12
|
+
declares n variables: 4,096 took 0.8 s, 16,384 took 10.6 s and 65,536 took
|
|
13
|
+
144 s -- each 4x costing about 13x. Turning target phases off made 16,384
|
|
14
|
+
take 0.11 s. Since dratify 0.1.7 a header may declare up to 2^20 variables
|
|
15
|
+
from a file of any length, so a 22-byte file cost hours of CPU.
|
|
16
|
+
|
|
17
|
+
The test compares scaling rather than absolute time, which shared CI runners
|
|
18
|
+
cannot be trusted with: quadrupling the variables should cost about 4x when
|
|
19
|
+
the search is linear and about 16x when it is quadratic. The threshold sits
|
|
20
|
+
between them, and each size takes the best of three runs to damp noise.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import time
|
|
26
|
+
import unittest
|
|
27
|
+
|
|
28
|
+
from cdclkit import native
|
|
29
|
+
from cdclkit.solver import Solver
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _solve(nvars: int, engine: str) -> bool:
|
|
33
|
+
"""One unit clause, `nvars` declared: every other variable is a decision."""
|
|
34
|
+
if engine == "native":
|
|
35
|
+
s = native.require().Solver(nvars) # defaults match Config
|
|
36
|
+
else:
|
|
37
|
+
s = Solver(nvars)
|
|
38
|
+
s.add_clause([0])
|
|
39
|
+
return s.solve()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _seconds(nvars: int, engine: str) -> float:
|
|
43
|
+
best = float("inf")
|
|
44
|
+
for _ in range(3):
|
|
45
|
+
t0 = time.perf_counter()
|
|
46
|
+
sat = _solve(nvars, engine)
|
|
47
|
+
best = min(best, time.perf_counter() - t0)
|
|
48
|
+
assert sat
|
|
49
|
+
return best
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TestUnusedVariablesCostLinearTime(unittest.TestCase):
|
|
53
|
+
#: quadrupling: ~4x if linear, ~16x if quadratic
|
|
54
|
+
LIMIT = 8.0
|
|
55
|
+
|
|
56
|
+
def _check(self, engine: str, small: int):
|
|
57
|
+
t_small = _seconds(small, engine)
|
|
58
|
+
t_large = _seconds(4 * small, engine)
|
|
59
|
+
ratio = t_large / max(t_small, 1e-4)
|
|
60
|
+
self.assertLess(
|
|
61
|
+
ratio, self.LIMIT,
|
|
62
|
+
f"{engine}: {small:,} vars took {t_small:.3f}s and "
|
|
63
|
+
f"{4 * small:,} took {t_large:.3f}s, a {ratio:.1f}x increase for "
|
|
64
|
+
f"4x the variables. Linear is ~4x; this is closer to quadratic.")
|
|
65
|
+
|
|
66
|
+
def test_python_engine(self):
|
|
67
|
+
self._check("python", 4096)
|
|
68
|
+
|
|
69
|
+
@unittest.skipUnless(native.available(),
|
|
70
|
+
"native engine not built for this interpreter")
|
|
71
|
+
def test_native_engine(self):
|
|
72
|
+
# Rust is fast enough that the quadratic term only dominates at
|
|
73
|
+
# larger sizes.
|
|
74
|
+
self._check("native", 32768)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
unittest.main()
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|