cdclkit 0.1.0__py3-none-any.whl
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/__init__.py +152 -0
- cdclkit/__main__.py +10 -0
- cdclkit/brute.py +210 -0
- cdclkit/cli.py +513 -0
- cdclkit/encodings.py +842 -0
- cdclkit/heap.py +180 -0
- cdclkit/model.py +420 -0
- cdclkit/mus.py +159 -0
- cdclkit/native.py +111 -0
- cdclkit/pipeline.py +212 -0
- cdclkit/portfolio.py +683 -0
- cdclkit/preprocess.py +500 -0
- cdclkit/pyeq.py +824 -0
- cdclkit/solver.py +1377 -0
- cdclkit-0.1.0.dist-info/METADATA +136 -0
- cdclkit-0.1.0.dist-info/RECORD +20 -0
- cdclkit-0.1.0.dist-info/WHEEL +5 -0
- cdclkit-0.1.0.dist-info/entry_points.txt +2 -0
- cdclkit-0.1.0.dist-info/licenses/LICENSE +202 -0
- cdclkit-0.1.0.dist-info/top_level.txt +1 -0
cdclkit/cli.py
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2026 Carlo Perassi. Licensed under the Apache License 2.0.
|
|
3
|
+
"""Command line interface: ``python -m cdclkit <command> ...``.
|
|
4
|
+
|
|
5
|
+
Follows SAT competition conventions where they exist, because that is what
|
|
6
|
+
scripts around a solver expect:
|
|
7
|
+
|
|
8
|
+
* solution lines ``s SATISFIABLE`` / ``s UNSATISFIABLE`` / ``s UNKNOWN``;
|
|
9
|
+
* the model on ``v`` lines terminated by ``0``;
|
|
10
|
+
* comments on ``c`` lines;
|
|
11
|
+
* exit status **10** for SAT, **20** for UNSAT, **0** for unknown, **1** for
|
|
12
|
+
an error, and **30** when a proof check fails (a solver that emits a bad
|
|
13
|
+
proof must be loudly distinguishable from one that merely times out).
|
|
14
|
+
|
|
15
|
+
Commands::
|
|
16
|
+
|
|
17
|
+
solve FILE solve a DIMACS CNF, optionally emitting and self-checking a proof
|
|
18
|
+
(--jobs N runs a parallel portfolio)
|
|
19
|
+
check FILE PROOF verify a DRAT proof against a formula
|
|
20
|
+
prep FILE preprocess only, write the reduced formula
|
|
21
|
+
count FILE enumerate models (with optional projection)
|
|
22
|
+
opt FILE minimise the number of true literals among given variables
|
|
23
|
+
gen KIND ... generate benchmark families
|
|
24
|
+
mus FILE extract a minimal unsatisfiable subset (why is it UNSAT?)
|
|
25
|
+
stats FILE report formula statistics
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import argparse
|
|
31
|
+
import sys
|
|
32
|
+
import time
|
|
33
|
+
from typing import Sequence
|
|
34
|
+
|
|
35
|
+
from dratify.cnf import CNF, parse_dimacs_file, write_dimacs
|
|
36
|
+
from .encodings import Encoder, optimise
|
|
37
|
+
from dratify.lits import from_dimacs, mk_lit, to_dimacs
|
|
38
|
+
from .mus import MUSExtractor
|
|
39
|
+
from .portfolio import performance_cores, solve_portfolio
|
|
40
|
+
from .preprocess import Preprocessor
|
|
41
|
+
from dratify.proof import MemoryProof, ProofWriter, check_proof, parse_proof
|
|
42
|
+
from .solver import Config, Solver
|
|
43
|
+
|
|
44
|
+
def _native_ok() -> bool:
|
|
45
|
+
from . import native
|
|
46
|
+
|
|
47
|
+
return native.available()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
EXIT_SAT = 10
|
|
51
|
+
EXIT_UNSAT = 20
|
|
52
|
+
EXIT_UNKNOWN = 0
|
|
53
|
+
EXIT_ERROR = 1
|
|
54
|
+
EXIT_BAD_PROOF = 30
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _emit_model(model: Sequence[bool], out=None, per_line: int = 20) -> None:
|
|
58
|
+
# resolved at call time, not at import time: a default of `sys.stdout` binds
|
|
59
|
+
# the stream object once and then ignores any later redirection
|
|
60
|
+
out = sys.stdout if out is None else out
|
|
61
|
+
lits = [(i + 1) if b else -(i + 1) for i, b in enumerate(model)]
|
|
62
|
+
for i in range(0, len(lits), per_line):
|
|
63
|
+
out.write("v " + " ".join(str(x) for x in lits[i : i + per_line]) + "\n")
|
|
64
|
+
out.write("v 0\n")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# --------------------------------------------------------------------------
|
|
68
|
+
# commands
|
|
69
|
+
# --------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def cmd_solve(args) -> int:
|
|
73
|
+
f = parse_dimacs_file(args.file)
|
|
74
|
+
print(f"c cdclkit :: {args.file}")
|
|
75
|
+
print(f"c formula: {f.nvars} vars, {f.nclauses} clauses")
|
|
76
|
+
|
|
77
|
+
proof_sink = None
|
|
78
|
+
mem_proof = None
|
|
79
|
+
if args.self_check:
|
|
80
|
+
mem_proof = MemoryProof()
|
|
81
|
+
proof_sink = mem_proof
|
|
82
|
+
elif args.proof:
|
|
83
|
+
proof_sink = ProofWriter(args.proof)
|
|
84
|
+
|
|
85
|
+
original = f.copy() if (args.self_check or args.check_model) else None
|
|
86
|
+
|
|
87
|
+
pre = None
|
|
88
|
+
if args.preprocess:
|
|
89
|
+
t0 = time.perf_counter()
|
|
90
|
+
pre = Preprocessor(f, proof=proof_sink)
|
|
91
|
+
f = pre.run(rounds=args.prep_rounds)
|
|
92
|
+
print(f"c preprocessing took {time.perf_counter()-t0:.3f}s")
|
|
93
|
+
for line in pre.stats.report().splitlines():
|
|
94
|
+
print(line)
|
|
95
|
+
print(f"c reduced: {f.nvars} vars, {f.nclauses} clauses")
|
|
96
|
+
|
|
97
|
+
cfg = Config(
|
|
98
|
+
restart=args.restart,
|
|
99
|
+
var_decay=args.var_decay,
|
|
100
|
+
ccmin=args.ccmin,
|
|
101
|
+
phase_saving=not args.no_phase_saving,
|
|
102
|
+
rnd_freq=args.rnd_freq,
|
|
103
|
+
rnd_seed=args.seed,
|
|
104
|
+
)
|
|
105
|
+
if getattr(args, "adaptive", False):
|
|
106
|
+
from .pipeline import solve_adaptive
|
|
107
|
+
|
|
108
|
+
pr = solve_adaptive(f, engine="native" if _native_ok() else "python")
|
|
109
|
+
print(pr.report())
|
|
110
|
+
print(f"c {pr.conflicts} conflicts in {pr.seconds:.3f}s")
|
|
111
|
+
if pr.sat:
|
|
112
|
+
if args.check_model:
|
|
113
|
+
base = original if original is not None else f
|
|
114
|
+
if base.falsified_clauses(pr.model):
|
|
115
|
+
print("c MODEL CHECK FAILED")
|
|
116
|
+
return EXIT_ERROR
|
|
117
|
+
print("c model verified against the input formula")
|
|
118
|
+
print("s SATISFIABLE")
|
|
119
|
+
if not args.no_model:
|
|
120
|
+
_emit_model(pr.model)
|
|
121
|
+
return EXIT_SAT
|
|
122
|
+
print("s UNSATISFIABLE")
|
|
123
|
+
return EXIT_UNSAT
|
|
124
|
+
|
|
125
|
+
jobs = args.jobs
|
|
126
|
+
if jobs is not None and jobs > 1:
|
|
127
|
+
# Parallel portfolio. Only the winning worker's proof exists, and
|
|
128
|
+
# because workers share no clauses it is a complete refutation on its
|
|
129
|
+
# own -- see cdclkit/portfolio.py.
|
|
130
|
+
pr = solve_portfolio(f, jobs=jobs, want_proof=proof_sink is not None)
|
|
131
|
+
if not pr.finished:
|
|
132
|
+
print("s UNKNOWN")
|
|
133
|
+
return EXIT_UNKNOWN
|
|
134
|
+
print(pr.report())
|
|
135
|
+
result = pr.sat
|
|
136
|
+
s = Solver(0) # placeholder so the reporting below has a stats object
|
|
137
|
+
s.stats.conflicts = pr.stats.get("conflicts", 0)
|
|
138
|
+
s.stats.propagations = pr.stats.get("propagations", 0)
|
|
139
|
+
s.stats.decisions = pr.stats.get("decisions", 0)
|
|
140
|
+
s.model = pr.model or []
|
|
141
|
+
if proof_sink is not None and pr.proof_steps:
|
|
142
|
+
for kind, lits in pr.proof_steps:
|
|
143
|
+
(proof_sink.delete if kind == "d" else proof_sink.add)(lits)
|
|
144
|
+
else:
|
|
145
|
+
s = Solver(f.nvars, proof=proof_sink, config=cfg)
|
|
146
|
+
ok = s.add_cnf(f)
|
|
147
|
+
result = s.solve(max_conflicts=args.conflicts) if ok else False
|
|
148
|
+
print(s.stats.report())
|
|
149
|
+
|
|
150
|
+
if result is None:
|
|
151
|
+
print("s UNKNOWN")
|
|
152
|
+
return EXIT_UNKNOWN
|
|
153
|
+
if result:
|
|
154
|
+
model = s.model
|
|
155
|
+
if pre is not None:
|
|
156
|
+
model = pre.reconstruct(model)
|
|
157
|
+
if args.check_model:
|
|
158
|
+
base = original if original is not None else f
|
|
159
|
+
bad = base.falsified_clauses(model)
|
|
160
|
+
if bad:
|
|
161
|
+
print(f"c MODEL CHECK FAILED: {len(bad)} clauses unsatisfied")
|
|
162
|
+
return EXIT_ERROR
|
|
163
|
+
print("c model verified against the input formula")
|
|
164
|
+
print("s SATISFIABLE")
|
|
165
|
+
if not args.no_model:
|
|
166
|
+
_emit_model(model)
|
|
167
|
+
return EXIT_SAT
|
|
168
|
+
|
|
169
|
+
print("s UNSATISFIABLE")
|
|
170
|
+
if mem_proof is not None:
|
|
171
|
+
t0 = time.perf_counter()
|
|
172
|
+
res = check_proof(original, mem_proof)
|
|
173
|
+
print(f"c proof self-check took {time.perf_counter()-t0:.3f}s "
|
|
174
|
+
f"({len(mem_proof.steps)} steps)")
|
|
175
|
+
for line in res.report().splitlines():
|
|
176
|
+
print(line)
|
|
177
|
+
if not res.ok:
|
|
178
|
+
return EXIT_BAD_PROOF
|
|
179
|
+
elif isinstance(proof_sink, ProofWriter):
|
|
180
|
+
proof_sink.close()
|
|
181
|
+
print(f"c proof written to {args.proof} "
|
|
182
|
+
f"({proof_sink.n_add} additions, {proof_sink.n_del} deletions)")
|
|
183
|
+
return EXIT_UNSAT
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def cmd_check(args) -> int:
|
|
187
|
+
f = parse_dimacs_file(args.file)
|
|
188
|
+
with open(args.proof, "r", encoding="ascii") as fh:
|
|
189
|
+
steps = parse_proof(fh.read())
|
|
190
|
+
print(f"c checking {len(steps)} proof steps against {args.file}")
|
|
191
|
+
t0 = time.perf_counter()
|
|
192
|
+
res = check_proof(f, steps, check_rat=not args.no_rat, apply_deletions=not args.keep_deleted)
|
|
193
|
+
print(f"c took {time.perf_counter()-t0:.3f}s")
|
|
194
|
+
print(res.report())
|
|
195
|
+
return EXIT_UNSAT if res.ok else EXIT_BAD_PROOF
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def cmd_prep(args) -> int:
|
|
199
|
+
f = parse_dimacs_file(args.file)
|
|
200
|
+
pre = Preprocessor(f, do_bve=not args.no_bve, do_bce=not args.no_bce)
|
|
201
|
+
red = pre.run(rounds=args.prep_rounds)
|
|
202
|
+
print(pre.summary(red))
|
|
203
|
+
if args.out:
|
|
204
|
+
red.save(args.out)
|
|
205
|
+
print(f"c reduced formula written to {args.out}")
|
|
206
|
+
else:
|
|
207
|
+
write_dimacs(red, sys.stdout)
|
|
208
|
+
return EXIT_UNKNOWN
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def cmd_count(args) -> int:
|
|
212
|
+
f = parse_dimacs_file(args.file)
|
|
213
|
+
s = Solver(f.nvars)
|
|
214
|
+
if not s.add_cnf(f):
|
|
215
|
+
print("c 0 models")
|
|
216
|
+
print("s UNSATISFIABLE")
|
|
217
|
+
return EXIT_UNSAT
|
|
218
|
+
proj = None
|
|
219
|
+
if args.project:
|
|
220
|
+
proj = [abs(int(x)) - 1 for x in args.project.split(",")]
|
|
221
|
+
n = 0
|
|
222
|
+
for model in s.enumerate_models(projection=proj, limit=args.limit):
|
|
223
|
+
n += 1
|
|
224
|
+
if args.show:
|
|
225
|
+
print("v " + " ".join(str(to_dimacs(mk_lit(v, not b))) for v, b in enumerate(model)))
|
|
226
|
+
print(f"c models found: {n}" + (" (limit reached)" if args.limit and n >= args.limit else ""))
|
|
227
|
+
return EXIT_SAT if n else EXIT_UNSAT
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def cmd_opt(args) -> int:
|
|
231
|
+
f = parse_dimacs_file(args.file)
|
|
232
|
+
s = Solver(f.nvars)
|
|
233
|
+
if not s.add_cnf(f):
|
|
234
|
+
print("s UNSATISFIABLE")
|
|
235
|
+
return EXIT_UNSAT
|
|
236
|
+
if args.soft:
|
|
237
|
+
soft = [from_dimacs(int(x)) for x in args.soft.split(",")]
|
|
238
|
+
else:
|
|
239
|
+
soft = [mk_lit(v) for v in range(f.nvars)]
|
|
240
|
+
res = optimise(s, soft, minimise=not args.maximise,
|
|
241
|
+
on_improve=lambda c, m: print(f"c improved to {c}"))
|
|
242
|
+
if res is None:
|
|
243
|
+
print("s UNSATISFIABLE")
|
|
244
|
+
return EXIT_UNSAT
|
|
245
|
+
count, model = res
|
|
246
|
+
print(f"c optimum: {count}")
|
|
247
|
+
print("s SATISFIABLE")
|
|
248
|
+
_emit_model(model[: f.nvars])
|
|
249
|
+
return EXIT_SAT
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def cmd_mus(args) -> int:
|
|
253
|
+
"""Explain an UNSAT answer: which clauses are actually to blame."""
|
|
254
|
+
f = parse_dimacs_file(args.file)
|
|
255
|
+
ex = MUSExtractor(f)
|
|
256
|
+
t0 = time.perf_counter()
|
|
257
|
+
if args.method == "core":
|
|
258
|
+
result = ex.core()
|
|
259
|
+
elif args.method == "quickxplain":
|
|
260
|
+
result = ex.quickxplain()
|
|
261
|
+
else:
|
|
262
|
+
result = ex.deletion()
|
|
263
|
+
dt = time.perf_counter() - t0
|
|
264
|
+
if not result:
|
|
265
|
+
print("c the formula is satisfiable: there is nothing to explain")
|
|
266
|
+
print("s SATISFIABLE")
|
|
267
|
+
return EXIT_SAT
|
|
268
|
+
print(f"c {args.method}: {len(result)} of {f.nclauses} clauses, "
|
|
269
|
+
f"{ex.calls} solver calls, {dt:.3f}s")
|
|
270
|
+
if args.verify:
|
|
271
|
+
ok, msg = ex.verify(result)
|
|
272
|
+
print(f"c {msg}")
|
|
273
|
+
if not ok:
|
|
274
|
+
return EXIT_ERROR
|
|
275
|
+
for i in result:
|
|
276
|
+
body = " ".join(str(to_dimacs(l)) for l in f.clauses[i])
|
|
277
|
+
print(f"c clause {i}: {body} 0")
|
|
278
|
+
print("s UNSATISFIABLE")
|
|
279
|
+
return EXIT_UNSAT
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def cmd_stats(args) -> int:
|
|
283
|
+
f = parse_dimacs_file(args.file)
|
|
284
|
+
st = f.stats()
|
|
285
|
+
for k, v in st.items():
|
|
286
|
+
print(f"c {k:<10}: {v}")
|
|
287
|
+
return EXIT_UNKNOWN
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# --------------------------------------------------------------------------
|
|
291
|
+
# benchmark generators
|
|
292
|
+
# --------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def gen_php(pigeons: int, holes: int) -> CNF:
|
|
296
|
+
"""Pigeonhole: `pigeons` items into `holes` slots, at most one per slot."""
|
|
297
|
+
f = CNF()
|
|
298
|
+
f.comments.append(f"pigeonhole {pigeons} pigeons into {holes} holes")
|
|
299
|
+
x = [[f.new_var(f"p{i}h{j}") for j in range(holes)] for i in range(pigeons)]
|
|
300
|
+
for i in range(pigeons):
|
|
301
|
+
f.add([mk_lit(x[i][j]) for j in range(holes)])
|
|
302
|
+
for j in range(holes):
|
|
303
|
+
for i in range(pigeons):
|
|
304
|
+
for k in range(i + 1, pigeons):
|
|
305
|
+
f.add([mk_lit(x[i][j], True), mk_lit(x[k][j], True)])
|
|
306
|
+
return f
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def gen_random_ksat(n: int, m: int, k: int, seed: int) -> CNF:
|
|
310
|
+
import random
|
|
311
|
+
|
|
312
|
+
rng = random.Random(seed)
|
|
313
|
+
f = CNF(n)
|
|
314
|
+
f.comments.append(f"uniform random {k}-SAT n={n} m={m} seed={seed}")
|
|
315
|
+
for _ in range(m):
|
|
316
|
+
vs = rng.sample(range(n), k)
|
|
317
|
+
f.add([mk_lit(v, rng.random() < 0.5) for v in vs])
|
|
318
|
+
return f
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def gen_queens(n: int) -> CNF:
|
|
322
|
+
f = CNF()
|
|
323
|
+
f.comments.append(f"{n}-queens")
|
|
324
|
+
enc = Encoder(f)
|
|
325
|
+
q = [[f.new_var(f"q{r}c{c}") for c in range(n)] for r in range(n)]
|
|
326
|
+
for r in range(n):
|
|
327
|
+
enc.exactly_one([mk_lit(q[r][c]) for c in range(n)])
|
|
328
|
+
for c in range(n):
|
|
329
|
+
enc.exactly_one([mk_lit(q[r][c]) for r in range(n)])
|
|
330
|
+
for d in range(-n + 1, n):
|
|
331
|
+
diag = [mk_lit(q[r][r - d]) for r in range(n) if 0 <= r - d < n]
|
|
332
|
+
if len(diag) > 1:
|
|
333
|
+
enc.at_most_one(diag)
|
|
334
|
+
anti = [mk_lit(q[r][d - r]) for r in range(n) if 0 <= d - r < n]
|
|
335
|
+
if len(anti) > 1:
|
|
336
|
+
enc.at_most_one(anti)
|
|
337
|
+
return f
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def gen_parity(n: int, seed: int) -> CNF:
|
|
341
|
+
"""A random XOR (parity) system -- exponentially hard for pure resolution."""
|
|
342
|
+
import random
|
|
343
|
+
|
|
344
|
+
rng = random.Random(seed)
|
|
345
|
+
f = CNF(n)
|
|
346
|
+
enc = Encoder(f)
|
|
347
|
+
f.comments.append(f"random parity system n={n} seed={seed}")
|
|
348
|
+
for _ in range(n):
|
|
349
|
+
vs = rng.sample(range(n), 3)
|
|
350
|
+
enc.xor_chain([mk_lit(v) for v in vs], value=rng.random() < 0.5)
|
|
351
|
+
return f
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def cmd_gen(args) -> int:
|
|
355
|
+
if args.kind == "php":
|
|
356
|
+
f = gen_php(args.n + 1, args.n)
|
|
357
|
+
elif args.kind == "random":
|
|
358
|
+
f = gen_random_ksat(args.n, args.m or int(4.26 * args.n), args.k, args.seed)
|
|
359
|
+
elif args.kind == "queens":
|
|
360
|
+
f = gen_queens(args.n)
|
|
361
|
+
elif args.kind == "parity":
|
|
362
|
+
f = gen_parity(args.n, args.seed)
|
|
363
|
+
else:
|
|
364
|
+
print(f"unknown family {args.kind}", file=sys.stderr)
|
|
365
|
+
return EXIT_ERROR
|
|
366
|
+
if args.out:
|
|
367
|
+
f.save(args.out)
|
|
368
|
+
print(f"c wrote {args.out}: {f.nvars} vars, {f.nclauses} clauses")
|
|
369
|
+
else:
|
|
370
|
+
write_dimacs(f, sys.stdout)
|
|
371
|
+
return EXIT_UNKNOWN
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# --------------------------------------------------------------------------
|
|
375
|
+
# argument parsing
|
|
376
|
+
# --------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
380
|
+
p = argparse.ArgumentParser(
|
|
381
|
+
prog="cdclkit", description="CDCL SAT solving with checkable proofs"
|
|
382
|
+
)
|
|
383
|
+
# Reports the engine too. "Which cdclkit is this" and "was the accelerator
|
|
384
|
+
# actually loaded" are the same question when a benchmark number looks
|
|
385
|
+
# wrong, and the second is the one people forget to ask.
|
|
386
|
+
from . import __version__, native
|
|
387
|
+
|
|
388
|
+
p.add_argument(
|
|
389
|
+
"--version", action="version",
|
|
390
|
+
version=(f"cdclkit {__version__} "
|
|
391
|
+
f"(native engine: {'yes' if native.available() else 'no'})"),
|
|
392
|
+
)
|
|
393
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
394
|
+
|
|
395
|
+
s = sub.add_parser("solve", help="solve a DIMACS CNF file")
|
|
396
|
+
s.add_argument("file")
|
|
397
|
+
s.add_argument("--proof", help="write a DRAT proof here (UNSAT only)")
|
|
398
|
+
s.add_argument("--self-check", action="store_true",
|
|
399
|
+
help="keep the proof in memory and verify it before reporting UNSAT")
|
|
400
|
+
s.add_argument("--check-model", action="store_true",
|
|
401
|
+
help="verify the model against the input before reporting SAT")
|
|
402
|
+
s.add_argument("--preprocess", action="store_true")
|
|
403
|
+
s.add_argument("--adaptive", action="store_true",
|
|
404
|
+
help="probe briefly, then preprocess only if the instance "
|
|
405
|
+
"turns out to be hard enough to repay it")
|
|
406
|
+
s.add_argument("--prep-rounds", type=int, default=3)
|
|
407
|
+
s.add_argument("--conflicts", type=int, default=None, help="conflict budget")
|
|
408
|
+
s.add_argument("--restart", default="glucose", choices=["glucose", "luby", "none"])
|
|
409
|
+
s.add_argument("--var-decay", type=float, default=0.8)
|
|
410
|
+
s.add_argument("--ccmin", default="deep", choices=["deep", "basic", "none"])
|
|
411
|
+
s.add_argument("--no-phase-saving", action="store_true")
|
|
412
|
+
s.add_argument("--rnd-freq", type=float, default=0.0)
|
|
413
|
+
s.add_argument("--seed", type=int, default=91648253)
|
|
414
|
+
s.add_argument("--no-model", action="store_true", help="suppress the v lines")
|
|
415
|
+
s.add_argument("--jobs", "-j", type=int, default=None,
|
|
416
|
+
metavar="N",
|
|
417
|
+
help=f"run a parallel portfolio of N differently-configured "
|
|
418
|
+
f"solvers and take the first answer (this machine has "
|
|
419
|
+
f"{performance_cores()} performance cores; more workers "
|
|
420
|
+
f"than that is usually slower)")
|
|
421
|
+
s.set_defaults(func=cmd_solve)
|
|
422
|
+
|
|
423
|
+
c = sub.add_parser("check", help="verify a DRAT proof")
|
|
424
|
+
c.add_argument("file")
|
|
425
|
+
c.add_argument("proof")
|
|
426
|
+
c.add_argument("--no-rat", action="store_true", help="RUP only")
|
|
427
|
+
c.add_argument("--keep-deleted", action="store_true", help="ignore deletion lines")
|
|
428
|
+
c.set_defaults(func=cmd_check)
|
|
429
|
+
|
|
430
|
+
pr = sub.add_parser("prep", help="preprocess a formula")
|
|
431
|
+
pr.add_argument("file")
|
|
432
|
+
pr.add_argument("--out")
|
|
433
|
+
pr.add_argument("--prep-rounds", type=int, default=3)
|
|
434
|
+
pr.add_argument("--no-bve", action="store_true")
|
|
435
|
+
pr.add_argument("--no-bce", action="store_true")
|
|
436
|
+
pr.set_defaults(func=cmd_prep)
|
|
437
|
+
|
|
438
|
+
ct = sub.add_parser("count", help="enumerate models")
|
|
439
|
+
ct.add_argument("file")
|
|
440
|
+
ct.add_argument("--limit", type=int, default=0)
|
|
441
|
+
ct.add_argument("--project", help="comma-separated 1-based variables")
|
|
442
|
+
ct.add_argument("--show", action="store_true")
|
|
443
|
+
ct.set_defaults(func=cmd_count)
|
|
444
|
+
|
|
445
|
+
op = sub.add_parser("opt", help="minimise true literals among a soft set")
|
|
446
|
+
op.add_argument("file")
|
|
447
|
+
op.add_argument("--soft", help="comma-separated DIMACS literals")
|
|
448
|
+
op.add_argument("--maximise", action="store_true")
|
|
449
|
+
op.set_defaults(func=cmd_opt)
|
|
450
|
+
|
|
451
|
+
mu = sub.add_parser("mus", help="extract a minimal unsatisfiable subset")
|
|
452
|
+
mu.add_argument("file")
|
|
453
|
+
mu.add_argument("--method", default="deletion",
|
|
454
|
+
choices=["deletion", "quickxplain", "core"])
|
|
455
|
+
mu.add_argument("--verify", action="store_true",
|
|
456
|
+
help="check that the result is unsatisfiable and minimal")
|
|
457
|
+
mu.set_defaults(func=cmd_mus)
|
|
458
|
+
|
|
459
|
+
st = sub.add_parser("stats", help="formula statistics")
|
|
460
|
+
st.add_argument("file")
|
|
461
|
+
st.set_defaults(func=cmd_stats)
|
|
462
|
+
|
|
463
|
+
g = sub.add_parser("gen", help="generate benchmark instances")
|
|
464
|
+
g.add_argument("kind", choices=["php", "random", "queens", "parity"])
|
|
465
|
+
g.add_argument("-n", type=int, default=8)
|
|
466
|
+
g.add_argument("-m", type=int, default=0)
|
|
467
|
+
g.add_argument("-k", type=int, default=3)
|
|
468
|
+
g.add_argument("--seed", type=int, default=1)
|
|
469
|
+
g.add_argument("--out")
|
|
470
|
+
g.set_defaults(func=cmd_gen)
|
|
471
|
+
|
|
472
|
+
return p
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
476
|
+
args = build_parser().parse_args(argv)
|
|
477
|
+
try:
|
|
478
|
+
return args.func(args)
|
|
479
|
+
except FileNotFoundError as e:
|
|
480
|
+
print(f"c error: {e}", file=sys.stderr)
|
|
481
|
+
return EXIT_ERROR
|
|
482
|
+
except IsADirectoryError as e:
|
|
483
|
+
print(f"c error: {e}", file=sys.stderr)
|
|
484
|
+
return EXIT_ERROR
|
|
485
|
+
except PermissionError as e:
|
|
486
|
+
print(f"c error: {e}", file=sys.stderr)
|
|
487
|
+
return EXIT_ERROR
|
|
488
|
+
except ValueError as e:
|
|
489
|
+
# Malformed DIMACS. The parser raises with a line number and the
|
|
490
|
+
# offending token, which is the useful half of a traceback; the other
|
|
491
|
+
# half is our call stack, which tells the user nothing about their
|
|
492
|
+
# file and reads like a crash.
|
|
493
|
+
print(f"c error: {e}", file=sys.stderr)
|
|
494
|
+
return EXIT_ERROR
|
|
495
|
+
except UnicodeDecodeError as e:
|
|
496
|
+
print(f"c error: not a text file ({e})", file=sys.stderr)
|
|
497
|
+
return EXIT_ERROR
|
|
498
|
+
except BrokenPipeError: # pragma: no cover - `cdclkit ... | head`
|
|
499
|
+
# Python prints its own noisy warning at shutdown unless stdout is
|
|
500
|
+
# closed first, and `| head` is a normal thing to do to a solver that
|
|
501
|
+
# prints a model with 100k literals in it.
|
|
502
|
+
try:
|
|
503
|
+
sys.stdout.close()
|
|
504
|
+
except Exception:
|
|
505
|
+
pass
|
|
506
|
+
return EXIT_ERROR
|
|
507
|
+
except KeyboardInterrupt: # pragma: no cover - interactive
|
|
508
|
+
print("c interrupted", file=sys.stderr)
|
|
509
|
+
return EXIT_UNKNOWN
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
if __name__ == "__main__": # pragma: no cover
|
|
513
|
+
sys.exit(main())
|