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
* :precosat
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 "Alist of symbolic name to backend class name for all supported backends.")
45 (defclass sat-solver
()
46 ((backend :initarg
:backend
:accessor sat-solver-backend
)
47 (numvars :initform
0 :accessor sat-solver-numvars
)
48 (clauses :initform
'() :accessor sat-solver-clauses
) ;; CNF
50 (:default-initargs
:backend
(make-instance (cdr (assoc *default-sat-backend
*
51 *satwrap-backends
*))))
52 (:documentation
"A Sat solver abstraction"))
54 (defmethod sat-solver-numclauses ((solver sat-solver
))
55 (length (sat-solver-clauses solver
)))
57 (defmethod print-object ((solver sat-solver
) stream
)
58 (print-unreadable-object (solver stream
:type T
:identity T
)
59 (format stream
"~D vars, ~D clauses, backend ~A"
60 (sat-solver-numvars solver
)
61 (sat-solver-numclauses solver
)
62 (sat-solver-backend solver
))))
64 (defmethod clause-valid ((solver sat-solver
) (clause list
))
65 "Check that CLAUSE is a valid clause for SOLVER."
67 :with max
:= (sat-solver-numvars solver
)
70 :always
(and (not (zerop v
))
73 (defmacro iota
(n &optional
(start 1))
74 "Return a list of the N sequential integers starting at START (default: 1)."
75 (let ((i (gensym "i")))
80 (define-condition satwrap-condition
(error)
82 (:documentation
"Superclass of all conditions raised by satwrap code."))
84 (define-condition invalid-clause
(satwrap-condition)
85 ((solver :initarg
:solver
:reader invalid-clause-solver
)
86 (clause :initarg
:clause
:reader invalid-clause-clause
))
87 (:report
(lambda (condition stream
)
88 (format stream
"Invalid clause ~A for solver ~A"
89 (invalid-clause-clause condition
)
90 (invalid-clause-solver condition
)))))
92 (define-condition invalid-backend
(satwrap-condition)
93 ((name :initarg
:name
:reader invalid-backend-name
))
94 (:report
(lambda (condition stream
)
95 (declare (special *satwrap-backends
* *default-sat-backend
*))
96 (format stream
"Invalid backend ~S specified. Supported: ~{~S~^, ~}, default ~S."
97 (invalid-backend-name condition
)
98 (mapcar #'car
*satwrap-backends
*)
99 *default-sat-backend
*))))
101 (defmethod flush-to-backend ((solver sat-solver
))
102 "Populate backend, possibly flushing old backend contents."
103 (reinitialize-instance (sat-solver-backend solver
))
104 ;; declare proper number of variables
105 (setf (satwrap.backend
:numvars
(sat-solver-backend solver
))
106 (sat-solver-numvars solver
))
107 (dolist (c (sat-solver-clauses solver
))
108 (satwrap.backend
:add-clause
(sat-solver-backend solver
) c
)))
111 (defun make-sat-solver (&optional
(backend *default-sat-backend
*))
112 "Return a new sat solver instance. Optional argument BACKEND can be used to
113 specify which backend should be used. It defaults to *default-sat-backend*."
114 (let ((be (assoc backend
*satwrap-backends
* :test
#'eq
)))
116 (make-instance 'sat-solver
:backend
(make-instance (cdr be
)))
117 (error 'invalid-backend
:name backend
))))
119 (defgeneric add-variable
(solver)
120 (:documentation
"Add another variable to SOLVER. Returns new variable index.")
121 (:method
((solver sat-solver
))
122 (incf (sat-solver-numvars solver
))))
124 (defgeneric add-clause
(solver clause
)
125 (:documentation
"Add CLAUSE to SOLVER's cnf formula. Clause is consumed.")
126 (:method
((solver sat-solver
) (clause list
))
127 (if (clause-valid solver clause
)
128 (push clause
(sat-solver-clauses solver
))
129 (error 'invalid-clause
:clause clause
:solver solver
)))
130 (:method
((solver sat-solver
) (clause vector
))
131 (add-clause solver
(coerce clause
'list
))))
133 (defgeneric satisfiablep
(solver &key assume
)
134 (:documentation
"Check whether current CNF in SOLVER is satisfiable.
135 Keyword argument :ASSUME can provide a sequence of literals assumed TRUE or FALSE.
137 (:method
((solver sat-solver
) &key
(assume '()))
138 (if (clause-valid solver assume
) ;; misusing 'clause' concept here
140 (flush-to-backend solver
)
141 (satwrap.backend
:satisfiablep
(sat-solver-backend solver
) assume
))
142 (error 'invalid-clause
:clause assume
:solver solver
))))
144 (defgeneric solution
(solver &key interesting-vars
)
145 (:documentation
"Return solution for SOLVER. If unsat, return '(). If sat return
146 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.")
147 (:method
((solver sat-solver
)
148 &key
(interesting-vars (iota (sat-solver-numvars solver
))))
149 (satwrap.backend
:solution
(sat-solver-backend solver
) interesting-vars
)))
151 (defgeneric get-essential-variables
(solver)
152 (:documentation
"Compute the variables that are essential, i.e. fixed in all solutions of SOLVER.
153 Returns a list of literals fixed, sign according to phase.")
154 (:method
((solver sat-solver
))
155 ;; The backend may define a specialized method, but then it has to
156 ;; work on the one copy of data flushed to the solver. Precosat for example
157 ;; does not allow that, so we define the universal implementation here instead of
158 ;; a default implementation in the backend.
159 (let ((have-specialized-method
160 (find-method #'satwrap.backend
:get-essential-variables
162 `(,(class-of (sat-solver-backend solver
)))
164 (if have-specialized-method
166 (flush-to-backend solver
)
167 (satwrap.backend
:get-essential-variables
(sat-solver-backend solver
)))
169 (loop :for var
:of-type
(integer 1) :in
(iota (sat-solver-numvars solver
))
170 :as res-for-
+ := (satisfiablep solver
:assume
`(,var
,@fixed
))
171 :as res-for--
:= (satisfiablep solver
:assume
`(,(- var
) ,@fixed
))
173 ((and res-for-
+ (not res-for--
))
175 ((and (not res-for-
+) res-for--
)
176 (push (- var
) fixed
))))
180 (defmacro with-sat-solver
((name numatoms
&key
(backend *default-sat-backend
*))
182 "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."
183 `(let ((,name
(make-sat-solver ,backend
)))
184 (loop :repeat
,numatoms
:do
(add-variable ,name
))
188 (defmacro with-index-hash
((mapping &key
(test 'cl
:eq
)) objects
&body body
)
189 "Execute BODY while binding MAPPING to a hash-table (with predicate
190 TEST, defaults to #'cl:eq) mapping the elements of the sequence OBJECTS
191 to integer 1..[length objects].
192 Typically used to map objects to variables inside WITH-SAT-SOLVER."
193 (let ((o (gensym "objects")))
194 `(let* ((,o
,objects
)
195 (,mapping
(etypecase ,o
196 (list (loop :with ht
:= (make-hash-table :test
,test
)
198 :for num
:of-type fixnum
:from
1
199 :do
(setf (gethash x ht
) num
)
200 :finally
(return ht
)))
201 (vector (loop :with ht
:= (make-hash-table :test
,test
)
203 :for num
:of-type fixnum
:from
1
204 :do
(setf (gethash x ht
) num
)
205 :finally
(return ht
))))))