(* Title: HOL/Tools/sat_solver.ML
ID: $Id$
Author: Tjark Weber
Copyright 2004
Interface to external SAT solvers, and (simple) built-in SAT solvers.
*)
signature SAT_SOLVER =
sig
type solver (* PropLogic.prop_formula -> (int -> bool) option option *)
(* external SAT solvers *)
val write_dimacs_cnf_file : Path.T -> PropLogic.prop_formula -> unit
val write_dimacs_sat_file : Path.T -> PropLogic.prop_formula -> unit
val parse_std_result_file : Path.T -> string -> (int -> bool) option
val make_external_solver : string -> (PropLogic.prop_formula -> unit) -> (unit -> (int -> bool) option)
-> PropLogic.prop_formula -> (int -> bool) option
(* generic interface *)
val solvers : (string * solver) list ref
val add_solver : string * solver -> unit
val invoke_solver : string -> solver (* exception OPTION *)
end;
structure SatSolver : SAT_SOLVER =
struct
open PropLogic;
(* ------------------------------------------------------------------------- *)
(* Type of SAT solvers: given a propositional formula, a satisfying *)
(* assignment may be returned *)
(* - 'Some None' means that no satisfying assignment was found *)
(* - 'None' means that the solver was not configured/installed properly *)
(* ------------------------------------------------------------------------- *)
type solver = prop_formula -> (int -> bool) option option;
(* ------------------------------------------------------------------------- *)
(* write_dimacs_cnf_file: serializes a formula 'fm' of propositional logic *)
(* to a file in DIMACS CNF format (see "Satisfiability Suggested *)
(* Format", May 8 1993, Section 2.1) *)
(* Note: 'fm' must not contain a variable index less than 1. *)
(* Note: 'fm' is converted into (definitional) CNF. *)
(* ------------------------------------------------------------------------- *)
(* Path.T -> prop_formula -> unit *)
fun write_dimacs_cnf_file path fm =
let
(* prop_formula -> prop_formula *)
fun cnf_True_False_elim True =
Or (BoolVar 1, Not (BoolVar 1))
| cnf_True_False_elim False =
And (BoolVar 1, Not (BoolVar 1))
| cnf_True_False_elim fm =
fm (* since 'fm' is in CNF, either 'fm'='True'/'False', or 'fm' does not contain 'True'/'False' at all *)
(* prop_formula -> int *)
fun cnf_number_of_clauses (And (fm1,fm2)) =
(cnf_number_of_clauses fm1) + (cnf_number_of_clauses fm2)
| cnf_number_of_clauses _ =
1
(* prop_formula -> string *)
fun cnf_string True =
error "formula is not in CNF"
| cnf_string False =
error "formula is not in CNF"
| cnf_string (BoolVar i) =
(assert (i>=1) "formula contains a variable index less than 1";
string_of_int i)
| cnf_string (Not fm) =
"-" ^ (cnf_string fm)
| cnf_string (Or (fm1,fm2)) =
(cnf_string fm1) ^ " " ^ (cnf_string fm2)
| cnf_string (And (fm1,fm2)) =
(cnf_string fm1) ^ " 0\n" ^ (cnf_string fm2)
in
File.write path
(let
val cnf = (cnf_True_False_elim o defcnf) fm (* conversion into def. CNF *)
val number_of_vars = maxidx cnf
val number_of_clauses = cnf_number_of_clauses cnf
in
"c This file was generated by SatSolver.write_dimacs_cnf_file\n" ^
"c (c) Tjark Weber\n" ^
"p cnf " ^ (string_of_int number_of_vars) ^ " " ^ (string_of_int number_of_clauses) ^ "\n" ^
(cnf_string cnf) ^ "\n"
end)
end;
(* ------------------------------------------------------------------------- *)
(* write_dimacs_sat_file: serializes a formula 'fm' of propositional logic *)
(* to a file in DIMACS SAT format (see "Satisfiability Suggested *)
(* Format", May 8 1993, Section 2.2) *)
(* Note: 'fm' must not contain a variable index less than 1. *)
(* ------------------------------------------------------------------------- *)
(* Path.T -> prop_formula -> unit *)
fun write_dimacs_sat_file path fm =
let
(* prop_formula -> string *)
fun sat_string True =
"*()"
| sat_string False =
"+()"
| sat_string (BoolVar i) =
(assert (i>=1) "formula contains a variable index less than 1";
string_of_int i)
| sat_string (Not fm) =
"-(" ^ (sat_string fm) ^ ")"
| sat_string (Or (fm1,fm2)) =
"+(" ^ (sat_string fm1) ^ " " ^ (sat_string fm2) ^ ")"
| sat_string (And (fm1,fm2)) =
"*(" ^ (sat_string fm1) ^ " " ^ (sat_string fm2) ^ ")"
in
File.write path
(let
val number_of_vars = Int.max (maxidx fm, 1)
in
"c This file was generated by SatSolver.write_dimacs_sat_file\n" ^
"c (c) Tjark Weber\n" ^
"p sat " ^ (string_of_int number_of_vars) ^ "\n" ^
"(" ^ (sat_string fm) ^ ")\n"
end)
end;
(* ------------------------------------------------------------------------- *)
(* parse_std_result_file: scans a SAT solver's output file for a satisfying *)
(* variable assignment. Returns the assignment, or 'None' if the SAT *)
(* solver did not find one. The file format must be as follows: *)
(* ,----- *)
(* | 0 or more lines not containing 'success' *)
(* | A line containing 'success' as a substring *)
(* | A line ASSIGNMENT containing integers, separated by " " *)
(* | 0 or more lines *)
(* `----- *)
(* If variable i is contained in ASSIGNMENT, then i is interpreted as *)
(* 'true'. If ~i is contained in ASSIGNMENT, then i is interpreted as *)
(* 'false'. *)
(* ------------------------------------------------------------------------- *)
(* Path.T -> string -> (int -> bool) option *)
fun parse_std_result_file path success =
let
(* 'a option -> 'a Library.option *)
fun option (SOME a) =
Some a
| option NONE =
None
(* string -> int list *)
fun int_list_from_string s =
mapfilter (option o Int.fromString) (space_explode " " s)
(* int list -> int -> bool *)
fun assignment_from_list [] i =
false (* could be 'true' just as well; the SAT solver didn't provide a value for this variable *)
| assignment_from_list (x::xs) i =
if x=i then true
else if x=(~i) then false
else assignment_from_list xs i
(* string -> string -> bool *)
fun is_substring needle haystack =
let
val length1 = String.size needle
val length2 = String.size haystack
in
if length2 < length1 then
false
else if needle = String.substring (haystack, 0, length1) then
true
else is_substring needle (String.substring (haystack, 1, length2-1))
end
(* string list -> int list option *)
fun parse_lines [] =
None
| parse_lines (line::lines) =
if is_substring success line then
(* the next line must be a list of integers *)
(Some o assignment_from_list o int_list_from_string o hd) lines
else
parse_lines lines
in
(parse_lines o split_lines o File.read) path
end;
(* ------------------------------------------------------------------------- *)
(* make_external_solver: call 'writefn', execute 'cmd', call 'readfn' *)
(* ------------------------------------------------------------------------- *)
(* string -> (prop_formula -> unit) -> (unit -> (int -> bool) option) -> prop_formula -> (int -> bool) option *)
fun make_external_solver cmd writefn readfn fm =
(writefn fm;
assert ((system cmd)=0) ("system command " ^ quote cmd ^ " failed (make sure the SAT solver is installed)");
readfn ());
(* ------------------------------------------------------------------------- *)
(* solvers: a (reference to a) table of all registered SAT solvers *)
(* ------------------------------------------------------------------------- *)
val solvers = ref ([] : (string * solver) list);
(* ------------------------------------------------------------------------- *)
(* add_solver: updates 'solvers' by adding a new solver *)
(* ------------------------------------------------------------------------- *)
(* string * solver -> unit *)
fun add_solver (name, new_solver) =
(solvers := overwrite_warn (!solvers, (name, new_solver)) ("SAT solver " ^ quote name ^ " was defined before"));
(* ------------------------------------------------------------------------- *)
(* invoke_solver: returns the solver associated with the given 'name' *)
(* Note: If no solver is associated with 'name', exception 'OPTION' will be *)
(* raised. *)
(* ------------------------------------------------------------------------- *)
(* string -> solver *)
fun invoke_solver name =
(the o assoc) (!solvers, name);
end; (* SatSolver *)
(* ------------------------------------------------------------------------- *)
(* Predefined SAT solvers *)
(* ------------------------------------------------------------------------- *)
(* ------------------------------------------------------------------------- *)
(* Internal SAT solver, available as 'SatSolver.solve "enumerate"' -- simply *)
(* enumerates assignments until a satisfying assignment is found *)
(* ------------------------------------------------------------------------- *)
let
fun enum_solver fm =
let
(* int list *)
val indices = PropLogic.indices fm
(* int list -> int list -> int list option *)
(* binary increment: list 'xs' of current bits, list 'ys' of all bits (lower bits first) *)
fun next_list _ ([]:int list) =
None
| next_list [] (y::ys) =
Some [y]
| next_list (x::xs) (y::ys) =
if x=y then
(* reset the bit, continue *)
next_list xs ys
else
(* set the lowest bit that wasn't set before, keep the higher bits *)
Some (y::x::xs)
(* int list -> int -> bool *)
fun assignment_from_list xs i =
i mem xs
(* int list -> (int -> bool) option *)
fun solver_loop xs =
if PropLogic.eval (assignment_from_list xs) fm then
Some (assignment_from_list xs)
else
(case next_list xs indices of
Some xs' => solver_loop xs'
| None => None)
in
(* start with the "first" assignment (all variables are interpreted as 'False') *)
solver_loop []
end
in
SatSolver.add_solver ("enumerate", Some o enum_solver)
end;
(* ------------------------------------------------------------------------- *)
(* Internal SAT solver, available as 'SatSolver.solve "dpll"' -- a simple *)
(* implementation of the DPLL algorithm (cf. L. Zhang, S. Malik: "The Quest *)
(* for Efficient Boolean Satisfiability Solvers", July 2002, Fig. 1). *)
(* ------------------------------------------------------------------------- *)
let
local
open PropLogic
in
fun dpll_solver fm =
let
(* We could use 'PropLogic.defcnf fm' instead of 'nnf fm', but that *)
(* sometimes introduces more boolean variables than we can handle *)
(* efficiently. *)
val fm' = PropLogic.nnf fm
(* int list *)
val indices = PropLogic.indices fm'
(* int list -> int -> prop_formula *)
fun partial_var_eval [] i = BoolVar i
| partial_var_eval (x::xs) i = if x=i then True else if x=(~i) then False else partial_var_eval xs i
(* int list -> prop_formula -> prop_formula *)
fun partial_eval xs True = True
| partial_eval xs False = False
| partial_eval xs (BoolVar i) = partial_var_eval xs i
| partial_eval xs (Not fm) = SNot (partial_eval xs fm)
| partial_eval xs (Or (fm1, fm2)) = SOr (partial_eval xs fm1, partial_eval xs fm2)
| partial_eval xs (And (fm1, fm2)) = SAnd (partial_eval xs fm1, partial_eval xs fm2)
| partial_eval xs _ = raise ERROR (* formula is not in negation normal form *)
(* prop_formula -> int list *)
fun forced_vars True = []
| forced_vars False = []
| forced_vars (BoolVar i) = [i]
| forced_vars (Not (BoolVar i)) = [~i]
| forced_vars (Or (fm1, fm2)) = (forced_vars fm1) inter_int (forced_vars fm2)
| forced_vars (And (fm1, fm2)) = (forced_vars fm1) union_int (forced_vars fm2)
(* Above, i *and* ~i may be forced. In this case the first occurrence takes *)
(* precedence, and the next partial evaluation of the formula returns 'False'. *)
| forced_vars _ = raise ERROR (* formula is not in negation normal form *)
(* int list -> prop_formula -> (int list * prop_formula) *)
fun eval_and_force xs fm =
let
val fm' = partial_eval xs fm
val xs' = forced_vars fm'
in
if null xs' then
(xs, fm')
else
eval_and_force (xs@xs') fm' (* xs and xs' should be distinct, so '@' here should have *)
(* the same effect as 'union_int' *)
end
(* int list -> int option *)
fun fresh_var xs =
Library.find_first (fn i => not (i mem_int xs) andalso not ((~i) mem_int xs)) indices
(* int list -> prop_formula -> int list option *)
(* partial assignment 'xs' *)
fun dpll xs fm =
let
val (xs', fm') = eval_and_force xs fm
in
case fm' of
True => Some xs'
| False => None
| _ =>
let
val x = the (fresh_var xs') (* a fresh variable must exist since 'fm' did not evaluate to 'True'/'False' *)
in
case dpll (x::xs') fm' of (* passing fm' rather than fm should save some simplification work *)
Some xs'' => Some xs''
| None => dpll ((~x)::xs') fm' (* now try interpreting 'x' as 'False' *)
end
end
(* int list -> int -> bool *)
fun assignment_from_list [] i =
false (* could be 'true' just as well; the DPLL procedure didn't provide a value for this variable *)
| assignment_from_list (x::xs) i =
if x=i then true
else if x=(~i) then false
else assignment_from_list xs i
in
(* initially, no variable is interpreted yet *)
apsome assignment_from_list (dpll [] fm')
end
end (* local *)
in
SatSolver.add_solver ("dpll", Some o dpll_solver)
end;
(* ------------------------------------------------------------------------- *)
(* Internal SAT solver, available as 'SatSolver.solve "auto"': uses the *)
(* first installed solver (other than "auto" itself) *)
(* ------------------------------------------------------------------------- *)
let
fun auto_solver fm =
get_first (fn (name,solver) =>
if name="auto" then
None
else
((if name="dpll" then
warning ("Using SAT solver \"dpll\"; for better performance, consider using an external solver.")
else
());
solver fm)) (rev (!SatSolver.solvers))
in
SatSolver.add_solver ("auto", auto_solver)
end;
(* ------------------------------------------------------------------------- *)
(* ZChaff, Version 2003.12.04 *)
(* ------------------------------------------------------------------------- *)
let
fun zchaff fm =
let
val inname = "isabelle.cnf"
val outname = "result"
val inpath = File.tmp_path (Path.unpack inname)
val outpath = File.tmp_path (Path.unpack outname)
val cmd = (getenv "ZCHAFF_HOME") ^ "/zchaff " ^ (Path.pack inpath) ^ " > " ^ (Path.pack outpath)
fun writefn fm = SatSolver.write_dimacs_cnf_file inpath fm
fun readfn () = SatSolver.parse_std_result_file outpath "Verify Solution successful. Instance satisfiable"
val _ = assert (not (File.exists inpath)) ("file " ^ quote (Path.pack inpath) ^ " exists, please delete (will not overwrite)")
val _ = assert (not (File.exists outpath)) ("file " ^ quote (Path.pack outpath) ^ " exists, please delete (will not overwrite)")
val assignment = SatSolver.make_external_solver cmd writefn readfn fm
val _ = (File.rm inpath handle _ => ())
val _ = (File.rm outpath handle _ => ())
in
assignment
end
in
SatSolver.add_solver ("zchaff", (fn fm => if getenv "ZCHAFF_HOME" <> "" then Some (zchaff fm) else None))
end;
(* ------------------------------------------------------------------------- *)
(* Add code for other SAT solvers below. *)
(* ------------------------------------------------------------------------- *)
(*
let
fun mysolver fm = ...
in
SatSolver.add_solver ("myname", (fn fm => if mysolver_is_installed then Some (mysolver fm) else None)); -- register the solver
end;
-- the solver is now available as SatSolver.invoke_solver "myname"
*)