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.
Files changed (45) hide show
  1. {cdclkit-0.1.4 → cdclkit-0.1.6}/PKG-INFO +3 -2
  2. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/__init__.py +1 -1
  3. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/cli.py +29 -2
  4. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/native.py +8 -6
  5. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/pyeq.py +10 -5
  6. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/solver.py +20 -4
  7. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit.egg-info/PKG-INFO +3 -2
  8. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit.egg-info/SOURCES.txt +2 -1
  9. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit.egg-info/requires.txt +1 -1
  10. {cdclkit-0.1.4 → cdclkit-0.1.6}/pyproject.toml +3 -2
  11. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_adaptive_proof.py +45 -1
  12. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_portfolio.py +6 -0
  13. cdclkit-0.1.6/tests/test_target_scaling.py +78 -0
  14. {cdclkit-0.1.4 → cdclkit-0.1.6}/LICENSE +0 -0
  15. {cdclkit-0.1.4 → cdclkit-0.1.6}/README.md +0 -0
  16. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/__main__.py +0 -0
  17. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/brute.py +0 -0
  18. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/encodings.py +0 -0
  19. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/heap.py +0 -0
  20. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/model.py +0 -0
  21. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/mus.py +0 -0
  22. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/pipeline.py +0 -0
  23. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/portfolio.py +0 -0
  24. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/preprocess.py +0 -0
  25. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit/py.typed +0 -0
  26. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit.egg-info/dependency_links.txt +0 -0
  27. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit.egg-info/entry_points.txt +0 -0
  28. {cdclkit-0.1.4 → cdclkit-0.1.6}/cdclkit.egg-info/top_level.txt +0 -0
  29. {cdclkit-0.1.4 → cdclkit-0.1.6}/setup.cfg +0 -0
  30. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_agent_docs.py +0 -0
  31. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_brute.py +0 -0
  32. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_cli_defaults.py +0 -0
  33. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_core.py +0 -0
  34. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_encodings.py +0 -0
  35. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_integration.py +0 -0
  36. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_mus.py +0 -0
  37. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_native.py +0 -0
  38. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_native_solver.py +0 -0
  39. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_packaging.py +0 -0
  40. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_pipeline.py +0 -0
  41. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_preprocess.py +0 -0
  42. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_proof.py +0 -0
  43. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_pyeq.py +0 -0
  44. {cdclkit-0.1.4 → cdclkit-0.1.6}/tests/test_readme.py +0 -0
  45. {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.4
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.4; extra == "native"
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.4"
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="native" if _native_ok() else "python",
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
- pr = solve_portfolio(f, jobs=jobs, want_proof=proof_sink is not None)
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, it is opt-in.** Nothing selects the native path on its
17
- own. A caller asks for it explicitly (`--engine native`, or
18
- `CDCLKIT_ENGINE=native`), because a silent switch between two implementations
19
- is how differential bugs hide.
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. The solver is
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, via the sign of the difference with overflow
202
- correction: a < b iff (a-b) is negative, XOR the signed-overflow flag."""
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
- """Assign under the current path condition.
457
+ """Bind `name` to `value` in the current environment, unconditionally.
455
458
 
456
- Inside a branch the assignment is conditional, so the variable becomes
457
- a select between the new value and whatever it held before.
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. Copying costs O(|trail|), but only on a
1233
- # strict improvement, and improvements become rare quickly.
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
- self._target_size = len(self.trail)
1248
+ trail = self.trail
1249
+ self._target_size = len(trail)
1236
1250
  target = self._target
1237
- for t in self.trail:
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.4
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.4; extra == "native"
28
+ Requires-Dist: cdclkit-native==0.1.6; extra == "native"
28
29
  Dynamic: license-file
29
30
 
30
31
  # cdclkit
@@ -39,4 +39,5 @@ tests/test_preprocess.py
39
39
  tests/test_proof.py
40
40
  tests/test_pyeq.py
41
41
  tests/test_readme.py
42
- tests/test_solver.py
42
+ tests/test_solver.py
43
+ tests/test_target_scaling.py
@@ -1,4 +1,4 @@
1
1
  dratify>=0.1.7
2
2
 
3
3
  [native]
4
- cdclkit-native==0.1.4
4
+ cdclkit-native==0.1.6
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cdclkit"
7
- version = "0.1.4"
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.4"]
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