cdclkit 0.1.3__tar.gz → 0.1.5__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.3 → cdclkit-0.1.5}/PKG-INFO +9 -6
- {cdclkit-0.1.3 → cdclkit-0.1.5}/README.md +5 -3
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit/__init__.py +1 -1
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit/solver.py +20 -4
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit.egg-info/PKG-INFO +9 -6
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit.egg-info/SOURCES.txt +3 -1
- cdclkit-0.1.5/cdclkit.egg-info/requires.txt +4 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/pyproject.toml +4 -3
- {cdclkit-0.1.3 → cdclkit-0.1.5}/tests/test_packaging.py +25 -0
- cdclkit-0.1.5/tests/test_readme.py +91 -0
- cdclkit-0.1.5/tests/test_target_scaling.py +78 -0
- cdclkit-0.1.3/cdclkit.egg-info/requires.txt +0 -4
- {cdclkit-0.1.3 → cdclkit-0.1.5}/LICENSE +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit/__main__.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit/brute.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit/cli.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit/encodings.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit/heap.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit/model.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit/mus.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit/native.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit/pipeline.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit/portfolio.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit/preprocess.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit/py.typed +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit/pyeq.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit.egg-info/dependency_links.txt +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit.egg-info/entry_points.txt +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/cdclkit.egg-info/top_level.txt +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/setup.cfg +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/tests/test_adaptive_proof.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/tests/test_agent_docs.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/tests/test_brute.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/tests/test_cli_defaults.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/tests/test_core.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/tests/test_encodings.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/tests/test_integration.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/tests/test_mus.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/tests/test_native.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/tests/test_native_solver.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/tests/test_pipeline.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/tests/test_portfolio.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/tests/test_preprocess.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/tests/test_proof.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/tests/test_pyeq.py +0 -0
- {cdclkit-0.1.3 → cdclkit-0.1.5}/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.5
|
|
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,14 +17,15 @@ 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
|
|
23
24
|
Description-Content-Type: text/markdown
|
|
24
25
|
License-File: LICENSE
|
|
25
|
-
Requires-Dist: dratify>=0.1.
|
|
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.5; extra == "native"
|
|
28
29
|
Dynamic: license-file
|
|
29
30
|
|
|
30
31
|
# cdclkit
|
|
@@ -142,9 +143,11 @@ An optional Rust engine ([`cdclkit-native`](https://pypi.org/project/cdclkit-nat
|
|
|
142
143
|
is **bit-exact** with the Python one — identical conflicts, decisions and
|
|
143
144
|
propagations on every instance — and about **20x faster**: the geometric mean
|
|
144
145
|
of per-instance ratios over the 17 benchmark instances is 20.1x, ranging from
|
|
145
|
-
11x to 51x.
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
11x to 51x. That comes from two checkpoints recorded 34 seconds apart at the
|
|
147
|
+
same commit, one sample per instance — good enough for the mean, not for any
|
|
148
|
+
single row. Regenerate it with `make history` (the `python` and `native`
|
|
149
|
+
entries in `bench/history.jsonl`); see [BENCHMARKS.md](BENCHMARKS.md) for the
|
|
150
|
+
method. The pure-Python path has zero third-party
|
|
148
151
|
dependencies and is the one that must never break.
|
|
149
152
|
|
|
150
153
|
## Relationship to dratify
|
|
@@ -113,9 +113,11 @@ An optional Rust engine ([`cdclkit-native`](https://pypi.org/project/cdclkit-nat
|
|
|
113
113
|
is **bit-exact** with the Python one — identical conflicts, decisions and
|
|
114
114
|
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
|
-
11x to 51x.
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
11x to 51x. That comes from two checkpoints recorded 34 seconds apart at the
|
|
117
|
+
same commit, one sample per instance — good enough for the mean, not for any
|
|
118
|
+
single row. Regenerate it with `make history` (the `python` and `native`
|
|
119
|
+
entries in `bench/history.jsonl`); see [BENCHMARKS.md](BENCHMARKS.md) for the
|
|
120
|
+
method. The pure-Python path has zero third-party
|
|
119
121
|
dependencies and is the one that must never break.
|
|
120
122
|
|
|
121
123
|
## Relationship to dratify
|
|
@@ -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.5"
|
|
56
56
|
|
|
57
57
|
__all__ = [
|
|
58
58
|
"CNF",
|
|
@@ -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.5
|
|
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,14 +17,15 @@ 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
|
|
23
24
|
Description-Content-Type: text/markdown
|
|
24
25
|
License-File: LICENSE
|
|
25
|
-
Requires-Dist: dratify>=0.1.
|
|
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.5; extra == "native"
|
|
28
29
|
Dynamic: license-file
|
|
29
30
|
|
|
30
31
|
# cdclkit
|
|
@@ -142,9 +143,11 @@ An optional Rust engine ([`cdclkit-native`](https://pypi.org/project/cdclkit-nat
|
|
|
142
143
|
is **bit-exact** with the Python one — identical conflicts, decisions and
|
|
143
144
|
propagations on every instance — and about **20x faster**: the geometric mean
|
|
144
145
|
of per-instance ratios over the 17 benchmark instances is 20.1x, ranging from
|
|
145
|
-
11x to 51x.
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
11x to 51x. That comes from two checkpoints recorded 34 seconds apart at the
|
|
147
|
+
same commit, one sample per instance — good enough for the mean, not for any
|
|
148
|
+
single row. Regenerate it with `make history` (the `python` and `native`
|
|
149
|
+
entries in `bench/history.jsonl`); see [BENCHMARKS.md](BENCHMARKS.md) for the
|
|
150
|
+
method. The pure-Python path has zero third-party
|
|
148
151
|
dependencies and is the one that must never break.
|
|
149
152
|
|
|
150
153
|
## Relationship to dratify
|
|
@@ -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.5"
|
|
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
|
]
|
|
@@ -29,13 +30,13 @@ classifiers = [
|
|
|
29
30
|
# The only dependency is the proof checker, which itself has none. The solver
|
|
30
31
|
# uses it to check its own refutations, so the checker is exercised by every
|
|
31
32
|
# test run here as well as by its own suite.
|
|
32
|
-
dependencies = ["dratify>=0.1.
|
|
33
|
+
dependencies = ["dratify>=0.1.7"]
|
|
33
34
|
|
|
34
35
|
[project.optional-dependencies]
|
|
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.5"]
|
|
39
40
|
|
|
40
41
|
[project.scripts]
|
|
41
42
|
cdclkit = "cdclkit.__main__:main"
|
|
@@ -103,6 +103,31 @@ class TestVersions(unittest.TestCase):
|
|
|
103
103
|
self.assertRegex(cdclkit.__version__, r"^\d+\.\d+\.\d+([.-]\w+)?$")
|
|
104
104
|
|
|
105
105
|
|
|
106
|
+
class TestSmokeTestIsCurrent(unittest.TestCase):
|
|
107
|
+
"""The post-install smoke test must ask for the modules that exist.
|
|
108
|
+
|
|
109
|
+
Its list named `cnf`, `lits` and `proof` long after they moved to
|
|
110
|
+
`dratify`, so `make smoke` failed on every run and nobody saw, because
|
|
111
|
+
nothing ran it automatically. CI now runs it; this keeps its list honest.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def test_the_smoke_module_list_matches_the_package(self):
|
|
115
|
+
import importlib.util
|
|
116
|
+
|
|
117
|
+
spec = importlib.util.spec_from_file_location(
|
|
118
|
+
"smoke_installed", ROOT / "tests" / "smoke_installed.py")
|
|
119
|
+
smoke = importlib.util.module_from_spec(spec)
|
|
120
|
+
spec.loader.exec_module(smoke)
|
|
121
|
+
|
|
122
|
+
on_disk = {p.stem for p in (ROOT / "cdclkit").glob("*.py")
|
|
123
|
+
if p.stem not in ("__init__", "__main__")}
|
|
124
|
+
self.assertEqual(
|
|
125
|
+
set(smoke.MODULES), on_disk,
|
|
126
|
+
"tests/smoke_installed.py MODULES does not match cdclkit/*.py. "
|
|
127
|
+
"A module added here and not there ships untested; one listed "
|
|
128
|
+
"there and not here makes the smoke test fail on every run.")
|
|
129
|
+
|
|
130
|
+
|
|
106
131
|
class TestEntryPoints(unittest.TestCase):
|
|
107
132
|
def test_console_script_target_exists_and_is_callable(self):
|
|
108
133
|
"""`cdclkit = "cdclkit.cli:main"` has to still resolve after a refactor."""
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2026 Carlo Perassi. Licensed under the Apache License 2.0.
|
|
3
|
+
"""Every Python example in the README has to run, and print what it says.
|
|
4
|
+
|
|
5
|
+
The README is the first code a visitor copies, and until this test existed
|
|
6
|
+
nothing executed it. The sibling project found its own example broken this
|
|
7
|
+
way: fine on the input its author tried, and failing on the first line for
|
|
8
|
+
the most common input there is.
|
|
9
|
+
|
|
10
|
+
Each ```python block runs in a fresh namespace from a temporary directory. A
|
|
11
|
+
`print(x) # value` line is an assertion: the printed value must match the
|
|
12
|
+
first word of the comment, so an example that runs but answers wrongly fails
|
|
13
|
+
too. A block preceded by `<!-- readme-test: skip ... -->` is not executed.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import contextlib
|
|
19
|
+
import io
|
|
20
|
+
import os
|
|
21
|
+
import pathlib
|
|
22
|
+
import re
|
|
23
|
+
import shutil
|
|
24
|
+
import tempfile
|
|
25
|
+
import unittest
|
|
26
|
+
|
|
27
|
+
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
28
|
+
|
|
29
|
+
SKIP = re.compile(r"<!--\s*readme-test:\s*skip")
|
|
30
|
+
EXPECT = re.compile(r"^\s*print\(.*\)\s*#\s*(\S+)")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def blocks(markdown: str) -> list[tuple[int, str, bool]]:
|
|
34
|
+
"""(line number, source, skipped) for every ```python block."""
|
|
35
|
+
lines = markdown.splitlines()
|
|
36
|
+
out, i = [], 0
|
|
37
|
+
while i < len(lines):
|
|
38
|
+
if lines[i].strip() == "```python":
|
|
39
|
+
start = i + 1
|
|
40
|
+
skipped = i > 0 and bool(SKIP.search(lines[i - 1]))
|
|
41
|
+
j = start
|
|
42
|
+
while lines[j].strip() != "```":
|
|
43
|
+
j += 1
|
|
44
|
+
out.append((start + 1, "\n".join(lines[start:j]) + "\n", skipped))
|
|
45
|
+
i = j
|
|
46
|
+
i += 1
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def expected_output(source: str) -> list[str]:
|
|
51
|
+
return [m.group(1) for ln in source.splitlines()
|
|
52
|
+
if (m := EXPECT.match(ln))]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestReadmeExamples(unittest.TestCase):
|
|
56
|
+
def setUp(self):
|
|
57
|
+
self.dir = pathlib.Path(tempfile.mkdtemp())
|
|
58
|
+
self.addCleanup(shutil.rmtree, self.dir)
|
|
59
|
+
self.all = blocks((ROOT / "README.md").read_text(encoding="utf-8"))
|
|
60
|
+
|
|
61
|
+
def test_the_readme_has_examples_to_test(self):
|
|
62
|
+
"""Guards the extractor: a regex that matches nothing passes everything."""
|
|
63
|
+
self.assertGreaterEqual(len(self.all), 1)
|
|
64
|
+
|
|
65
|
+
def test_every_example_runs_and_prints_what_it_promises(self):
|
|
66
|
+
ran = 0
|
|
67
|
+
for lineno, source, skipped in self.all:
|
|
68
|
+
if skipped:
|
|
69
|
+
continue
|
|
70
|
+
with self.subTest(line=lineno):
|
|
71
|
+
want = expected_output(source)
|
|
72
|
+
out = io.StringIO()
|
|
73
|
+
here = os.getcwd()
|
|
74
|
+
os.chdir(self.dir)
|
|
75
|
+
try:
|
|
76
|
+
with contextlib.redirect_stdout(out):
|
|
77
|
+
exec(compile(source, "README.md", "exec"),
|
|
78
|
+
{"__name__": "__readme__"})
|
|
79
|
+
finally:
|
|
80
|
+
os.chdir(here)
|
|
81
|
+
got = out.getvalue().split()
|
|
82
|
+
self.assertEqual(
|
|
83
|
+
got, want,
|
|
84
|
+
f"README.md:{lineno} printed {got}, but its comments "
|
|
85
|
+
f"promise {want}")
|
|
86
|
+
ran += 1
|
|
87
|
+
self.assertGreater(ran, 0, "no README example was run")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
unittest.main()
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|