1 ;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10; -*-
3 ;;; satwrap.lisp --- SAT solvers wrapped for CL
5 ;; Copyright (C) 2010 Utz-Uwe Haus <lisp@uuhaus.de>
7 ;; This code is free software; you can redistribute it and/or modify
8 ;; it under the terms of the version 3 of the GNU General
9 ;; Public License as published by the Free Software Foundation, as
10 ;; clarified by the prequel found in LICENSE.Lisp-GPL-Preface.
12 ;; This code is distributed in the hope that it will be useful, but
13 ;; without any warranty; without even the implied warranty of
14 ;; merchantability or fitness for a particular purpose. See the GNU
15 ;; Lesser General Public License for more details.
17 ;; Version 3 of the GNU General Public License is in the file
18 ;; LICENSE.GPL that was distributed with this file. If it is not
19 ;; present, you can access it from
20 ;; http://www.gnu.org/copyleft/gpl.txt (until superseded by a
21 ;; newer version) or write to the Free Software Foundation, Inc., 59
22 ;; Temple Place, Suite 330, Boston, MA 02111-1307 USA
30 (eval-when (:compile-toplevel
:load-toplevel
)
31 (declaim (optimize (speed 3) (debug 1) (safety 1))))
34 (in-package #:satwrap
)
36 (defvar *default-sat-backend
* :minisat
37 "Name of the default backend to be used by make-sat-solver.")
39 (defparameter *satwrap-backends
*
40 `((:precosat .
,(find-class 'satwrap.precosat
:precosat-backend
))
41 (:minisat .
,(find-class 'satwrap.minisat
:minisat-backend
)))
42 "Alist of symbolic name to backend class name for all supported backends.")
44 (defun describe-supported-backends ()
45 "Return a list of (designator . description) pairs of the supported backends"
46 (loop :for
(key . class
) :in
*satwrap-backends
*
47 :as descr
:= (let ((instance (make-instance class
)))
48 (format nil
"~A version ~A"
49 (satwrap.backend
:sat-backend-name instance
)
50 (satwrap.backend
:sat-backend-version instance
)))
51 :collect
`(,key .
,descr
)))
55 (defclass sat-solver
()
56 ((backend :initarg
:backend
:accessor sat-solver-backend
)
57 (numvars :initform
0 :accessor sat-solver-numvars
)
58 ;; CNF; split into 'old' and 'new' to allow incremental solving
59 (new-clauses :initform
'() :accessor sat-solver-new-clauses
)
60 (old-clauses :initform
'() :accessor sat-solver-old-clauses
)
62 (:default-initargs
:backend
(make-instance (cdr (assoc *default-sat-backend
*
63 *satwrap-backends
*))))
64 (:documentation
"A Sat solver abstraction"))
66 (defmethod sat-solver-numclauses ((solver sat-solver
))
67 (+ (length (sat-solver-new-clauses solver
))
68 (length (sat-solver-old-clauses solver
))
71 (defmethod print-object ((solver sat-solver
) stream
)
72 (print-unreadable-object (solver stream
:type T
:identity T
)
73 (format stream
"~D vars, ~D clauses, backend ~A"
74 (sat-solver-numvars solver
)
75 (sat-solver-numclauses solver
)
76 (sat-solver-backend solver
))))
78 (defmethod clause-valid ((solver sat-solver
) (clause list
))
79 "Check that CLAUSE is a valid clause for SOLVER."
81 :with max
:= (sat-solver-numvars solver
)
84 :always
(and (not (zerop v
))
87 (defmacro iota
(n &optional
(start 1))
88 "Return a list of the N sequential integers starting at START (default: 1)."
89 (let ((i (gensym "i")))
94 (define-condition satwrap-condition
(error)
96 (:documentation
"Superclass of all conditions raised by satwrap code."))
98 (define-condition invalid-clause
(satwrap-condition)
99 ((solver :initarg
:solver
:reader invalid-clause-solver
)
100 (clause :initarg
:clause
:reader invalid-clause-clause
))
101 (:report
(lambda (condition stream
)
102 (format stream
"Invalid clause ~A for solver ~A"
103 (invalid-clause-clause condition
)
104 (invalid-clause-solver condition
)))))
106 (define-condition invalid-backend
(satwrap-condition)
107 ((name :initarg
:name
:reader invalid-backend-name
))
108 (:report
(lambda (condition stream
)
109 (declare (special *satwrap-backends
* *default-sat-backend
*))
110 (format stream
"Invalid backend ~S specified. Supported: ~{~S~^, ~}, default ~S."
111 (invalid-backend-name condition
)
112 (mapcar #'car
*satwrap-backends
*)
113 *default-sat-backend
*))))
115 (defmethod flush-to-backend ((solver sat-solver
))
116 "Populate backend, possibly flushing old backend contents."
117 (satwrap.backend
:synchronize-backend
(sat-solver-backend solver
)
118 (sat-solver-numvars solver
)
119 (sat-solver-new-clauses solver
)
120 '() ;; deleted clauses, not yet supported
121 (sat-solver-old-clauses solver
))
122 (loop :with deleted
:= '()
123 :for c
:in
(sat-solver-old-clauses solver
)
124 :unless
(find c deleted
:test
#'equal
)
125 :do
(push c
(sat-solver-new-clauses solver
)))
126 (setf (sat-solver-old-clauses solver
) (sat-solver-new-clauses solver
)
127 (sat-solver-new-clauses solver
) '()))
130 (defun make-sat-solver (&optional
(backend *default-sat-backend
*))
131 "Return a new sat solver instance. Optional argument BACKEND can be used to
132 specify which backend should be used. It defaults to *default-sat-backend*."
133 (let ((be (assoc backend
*satwrap-backends
* :test
#'eq
)))
135 (make-instance 'sat-solver
:backend
(make-instance (cdr be
)))
136 (error 'invalid-backend
:name backend
))))
138 (defgeneric add-variable
(solver)
139 (:documentation
"Add another variable to SOLVER. Returns new variable index.")
140 (:method
((solver sat-solver
))
141 (incf (sat-solver-numvars solver
))))
143 (defgeneric add-clause
(solver clause
)
144 (:documentation
"Add CLAUSE to SOLVER's cnf formula. Clause is consumed.")
145 (:method
((solver sat-solver
) (clause list
))
146 (if (clause-valid solver clause
)
147 (push clause
(sat-solver-new-clauses solver
))
148 (error 'invalid-clause
:clause clause
:solver solver
)))
149 (:method
((solver sat-solver
) (clause vector
))
150 (add-clause solver
(coerce clause
'list
))))
152 (defgeneric add-clauses
(solver clauses
)
153 (:documentation
"Add CLAUSES to SOLVER's cnf formula. Clauses are consumed.")
154 (:method
((solver sat-solver
) (clauses list
))
156 (add-clause solver c
))))
158 (defgeneric satisfiablep
(solver &key assume
)
159 (:documentation
"Check whether current CNF in SOLVER is satisfiable.
160 Keyword argument :ASSUME can provide a sequence of literals assumed TRUE or FALSE.
162 (:method
((solver sat-solver
) &key
(assume '()))
163 (if (clause-valid solver assume
) ;; misusing 'clause' concept here
165 (flush-to-backend solver
)
166 (satwrap.backend
:satisfiablep
(sat-solver-backend solver
) assume
))
167 (error 'invalid-clause
:clause assume
:solver solver
))))
169 (defgeneric solution
(solver &key interesting-vars
)
170 (:documentation
"Return solution for SOLVER. If unsat, return '(). If sat return
171 sequence of 0/1 values for variables [1...N]. Keyword argument INTERESTING-VARS can be used to restrict the variables whose values are reported. Solution components will be in the same order that INTERESTING-VARS lists the variables in.")
172 (:method
((solver sat-solver
)
173 &key
(interesting-vars (iota (sat-solver-numvars solver
))))
174 (satwrap.backend
:solution
(sat-solver-backend solver
) interesting-vars
)))
176 (defgeneric get-essential-variables
(solver)
177 (:documentation
"Compute the variables that are essential, i.e. fixed in all solutions of SOLVER.
178 Returns a list of literals fixed, sign according to phase.")
179 (:method
((solver sat-solver
))
180 ;; The backend may define a specialized method, but then it has to
181 ;; work on the one copy of data flushed to the solver. Precosat for example
182 ;; does not allow that, so we define the universal implementation here instead of
183 ;; a default implementation in the backend.
184 (let ((have-specialized-method
185 (find-method #'satwrap.backend
:get-essential-variables
187 `(,(class-of (sat-solver-backend solver
)))
189 (if have-specialized-method
191 (flush-to-backend solver
)
192 (satwrap.backend
:get-essential-variables
(sat-solver-backend solver
)))
194 (loop :for var
:of-type
(integer 1) :in
(iota (sat-solver-numvars solver
))
195 :as sat-for-
+ := (satisfiablep solver
:assume
`(,var
,@fixed
))
196 :as sat-for--
:= (satisfiablep solver
:assume
`(,(- var
) ,@fixed
))
198 ((and sat-for-
+ (not sat-for--
))
200 ((and (not sat-for-
+) sat-for--
)
201 (push (- var
) fixed
))))
205 (defmacro with-sat-solver
((name numatoms
&key
(backend *default-sat-backend
* backend-given
))
207 "Bind NAME to a fresh sat-solver with backend :BACKEND (default: *default-sat-backend*) and declare NUMATOMS variables numbered 1..NUMATOMS for the duration of BODY."
208 `(let ((,name
(make-sat-solver ,(if backend-given
210 '*default-sat-backend
*))))
211 (loop :repeat
,numatoms
:do
(add-variable ,name
))
215 (defmacro with-index-hash
((mapping &key
(test 'cl
:eq
)) objects
&body body
)
216 "Execute BODY while binding MAPPING to a hash-table (with predicate
217 TEST, defaults to #'cl:eq) mapping the elements of the sequence OBJECTS
218 to integer 1..[length objects].
219 Typically used to map objects to variables inside WITH-SAT-SOLVER."
220 (let ((o (gensym "objects")))
221 `(let* ((,o
,objects
)
222 (,mapping
(etypecase ,o
223 (list (loop :with ht
:= (make-hash-table :test
,test
)
225 :for num
:of-type fixnum
:from
1
226 :do
(setf (gethash x ht
) num
)
227 :finally
(return ht
)))
228 (vector (loop :with ht
:= (make-hash-table :test
,test
)
230 :for num
:of-type fixnum
:from
1
231 :do
(setf (gethash x ht
) num
)
232 :finally
(return ht
))))))
235 ;; convenience syntax for logical operations:
236 (defun standardize (tree)
237 "Convert to NOT, AND, OR (preserving :ATOM)"
240 (destructuring-bind (op . expr
)
244 (if (= 2 (length expr
))
245 `(:OR
(:NOT
,(standardize (car expr
)))
246 ,(standardize (cadr expr
)))
249 (if (= 2 (length expr
))
250 `(:AND
,(standardize `(:IMPLIES
,(car expr
) ,(cadr expr
)))
251 ,(standardize `(:IMPLIES
,(cadr expr
) ,(car expr
))))
253 (T `(,op
,@(standardize expr
))))))
256 (defun move-nots (tree)
257 "Move NOTs inward. Input must be AND/OR/NOT-only; returns NNF tree."
260 (destructuring-bind (op . expr
)
263 (:NOT
(if (= 1 (length expr
))
264 (let ((subexpr (first expr
)))
266 (case (first subexpr
)
267 ;; double-not: drop 2 levels
268 (:NOT
(move-nots (cdr subexpr
)))
270 (:AND
`(:OR
,@(mapcar #'(lambda (c)
271 (move-nots `(:NOT
,c
)))
274 (:OR
`(:AND
,@(mapcar #'(lambda (c)
275 (move-nots `(:NOT
,c
)))
277 ;; `quoted' atom: keep
279 (otherwise `(:NOT
,subexpr
)))
282 (error "non-unary :NOT expression: ~W" tree
)))
283 ;; other operators: AND, OR, ATOM: descend
285 `(,op
,@(mapcar #'move-nots expr
))))))
289 (defun crossprod (list-of-lists)
290 (reduce #'(lambda (collected term
)
291 (loop :for x
:in term
:appending
292 (loop :for tail
:in collected
293 :collect
`(,x
,@tail
))))
295 :initial-value
'(())))
298 "Check that TREE is in CNF."
307 (and (not (eq :OR
(car f
)))
308 (not (eq :AND
(car f
))))
316 (every #'atomic
(cdr c
)))))
319 (defun distribute2 (tree)
322 (:AND
(let ((subtrees (mapcar #'distribute2
(cdr tree
))))
323 (assert (every #'is-cnf subtrees
))
324 `(:AND
,@(apply #'append subtrees
))))
325 (:OR
(let ((subtrees (mapcar #'distribute2
(cdr tree
))))
326 (assert (every #'is-cnf subtree
))
331 (defun distribute (tree)
332 "Pull out ANDs and unite sequential OR/OR or AND/AND. Input must be in NNF. Returns
335 (destructuring-bind (op . expr
)
338 (:OR
(let (ands ors others
)
339 (dolist (term (mapcar #'distribute expr
))
342 (:OR
(push term ors
))
343 (:AND
(push term ands
))
344 (otherwise (push term others
)))
346 ; (break "~A ~A ~A" ands ors others)
349 (let ((terms (mapcar #'(lambda (or-clause)
350 `(:OR
,@(mapcar #'cdr ors
)
353 (crossprod (mapcar #'cdr ands
)))))
354 (break "~A" `(:AND
,@terms
))
356 `(:OR
,@others
,@(mapcar #'cdr ors
)))))
357 (:AND
(let ((subexpr '()))
361 (:AND
(dolist (f (cdr e
))
362 (push (distribute f
) subexpr
)))
363 (:OR
(push (mapcar #'distribute
(cdr e
)) subexpr
))
364 (otherwise (push e subexpr
)))
368 ;; :NOT, :ATOM or implicit leaf: keep
372 (defun tree->cnf
(tree)
378 (standardize tree
))))
381 ;; (destructuring-bind (op . expr)
384 ;; ((:IMPLY :IMPILES)
385 ;; (if (= 2 (length expr))
386 ;; (tree->cnf `(:OR (:NOT ,(car expr)) ,(cadr expr)))
389 ;; (if (= 2 (length expr))
390 ;; (tree->cnf `(:AND (:IMPLIES ,(car expr) ,(cadr expr))
391 ;; (:IMPLIES ,(cadr expr) ,(car expr))))
395 ;; (:AND (mapcar #'tree->cnf expr))
396 ;; (:OR (tree->cnf `(:NOT (:AND (:NOT ,expr)))))
397 ;; (:NOT (if (= 1 (length expr))
399 ;; (error)) (mapcar ))))
404 (defun tree->cnf
(tree)
407 (cons (case (car tree
)
410 `(,(apply #'append
(mapcar #'tree-
>cnf
(cdr tree
)))))
412 (if (= 2 (length tree
))
413 (let ((expr (cdr tree
)))
415 ;; we prefer AND by swapping NOT AND to AND NOT
416 ((eq :AND
(car expr
))
417 (tree->cnf
`(:AND
(mapcar #'-
(tree->cnf
(cdr expr
))))))
419 (tree->cnf
`(:AND
)))
420 ((numberp expr
) (- expr
))
421 (T (error "unsupported"))))
422 `(,(mapcar #'(lambda (clause) (mapcar #'- clause
))
423 (tree->cnf
(cadr tree
))))
424 (error "non-unary :NOT expression: ~A" tree
)))
428 (:OR
(tree->cnf
`(:NOT
(:AND
,(cdr tree
)))))
430 (if (= 3 (length tree
))
431 (let ((a (cadr tree
))
433 (tree->cnf
`(:AND
(:OR
,a
,b
)
434 (:OR
(:NOT
,a
) (:NOT
,b
)))))
435 (error "non-binary :XOR expression: ~A" tree
)))
437 (if (= 3 (length tree
))
438 (tree->cnf
`(:OR
(:NOT
,(cadr tree
))
440 (error "non-binary :IMPLIES expression: ~A" tree
)))
442 (if (= 3 (length tree
))
443 (let ((a (cadr tree
))
445 (tree->cnf
`(:AND
(:IMPLIES
,a
,b
)
447 (error "non-binary :IFF expression: ~A" tree
)))
449 (error "unsupported expression: ~A" tree
))))
450 (T (error "Formula contains unsupported expression: ~A" tree
))))
452 (defmacro build-cnf
(tree)
455 (defun make-and-cnf (literals)
456 (mapcar #'list literals
))
457 (defun make-or-cnf (literals)
459 (defun negate-cnf (list-of-clauses)
460 (mapcar #'(lambda (c) (mapcar #'- c
)) list-of-clauses
))
461 (defun make-xor-cnf (a b
)
462 (make-or-cnf)`(,a
,(- b
))
465 (defgeneric add-and
(solver literals
)
466 (:documentation
"Add clauses for AND(literals) to SOLVER."))
467 (defgeneric add-or
(solver literals
)
468 (:documentation
"Add clauses for OR(literals) to SOLVER."))
469 (defgeneric add-xor
(solver literals
)
470 (:documentation
"Add clauses for XOR(literals) to SOLVER."))
471 (defgeneric add-if
(solver lhs rhs
)
472 (:documentation
"Add clauses for lhs -> rhs to SOLVER."))
473 (defgeneric add-iff
(solver lhs rhs
)
474 (:documentation
"Add clauses for lhs <-> rhs to SOLVER."))