sat-simple-0.1.0.0: src/Control/Monad/SAT.hs
-- | A monadic interface to the SAT (@minisat@) solver.
--
-- The interface is inspired by ST monad. 'SAT' and 'Lit' are indexed by a "state" token,
-- so you cannot mixup literals from different SAT computations.
module Control.Monad.SAT (
-- * SAT Monad
SAT,
runSAT,
runSATMaybe,
UnsatException (..),
-- * Literals
Lit,
newLit,
-- ** Negation
Neg (..),
-- * Clauses
addClause,
assertAtLeastOne,
assertAtMostOne,
assertAtMostOnePairwise,
assertAtMostOneSequential,
assertEqual,
assertAllEqual,
-- ** Propositional formulas
Prop,
true, false,
lit, (\/), (/\), (<->), (-->), xor, ite,
addDefinition,
addProp,
-- ** Clause definitions
trueLit,
falseLit,
addConjDefinition,
addDisjDefinition,
-- * Solving
solve,
solve_,
-- * Simplification
simplify,
-- * Statistics
numberOfVariables,
numberOfClauses,
numberOfLearnts,
numberOfConflicts,
) where
import Control.Exception (Exception, catch, throwIO)
import Control.Monad (forM_, unless)
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.IO.Unlift (MonadUnliftIO (..))
import Data.Bits (shiftR, testBit)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.List (tails)
import Data.Map.Strict (Map)
import Data.Set (Set)
import GHC.Exts (oneShot)
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified MiniSat
-------------------------------------------------------------------------------
-- SAT Monad
-------------------------------------------------------------------------------
-- | Satisfiability monad.
newtype SAT s a = SAT_ { unSAT :: MiniSat.Solver -> Lit s -> IORef (Definitions s) -> IO a }
deriving Functor
-- The SAT monad environment consists of
-- * A solver instance
-- * A literal constraint to be true.
-- * A map of asserted definitions, to dedup these for 'addDefinitions' and 'addProp' calls.
-- (we don't dedup clauses though - it's up to the solver).
type role SAT nominal representational
pattern SAT :: forall s a. (MiniSat.Solver -> Lit s -> IORef (Definitions s) -> IO a) -> SAT s a
pattern SAT m <- SAT_ m
where SAT m = SAT_ (oneShot m)
{-# COMPLETE SAT #-}
type Definitions s = Map (Set (Lit s)) (Lit s)
-- | Unsatisfiable exception.
--
-- It may be thrown by various functions: in particular 'solve' and 'solve_', but also 'addClause', 'simplify'.
--
-- The reason to use an exception is because after unsatisfiable state is reached the underlying solver instance is unusable.
-- You may use 'runSATMaybe' to catch it.
data UnsatException = UnsatException
deriving (Show)
instance Exception UnsatException
data SATPanic = SATPanic
deriving (Show)
instance Exception SATPanic
instance Applicative (SAT s) where
pure x = SAT (\_ _ _ -> pure x)
SAT f <*> SAT x = SAT (\s t r -> f s t r <*> x s t r)
instance Monad (SAT s) where
m >>= k = SAT $ \s t r -> do
x <- unSAT m s t r
unSAT (k x) s t r
instance MonadIO (SAT s) where
liftIO m = SAT (\_ _ _ -> m)
instance MonadUnliftIO (SAT s) where
withRunInIO kont = SAT $ \s t r -> kont (\(SAT m) -> m s t r)
-- | Run 'SAT' computation.
runSAT :: (forall s. SAT s a) -> IO a
runSAT (SAT f) = MiniSat.withNewSolverAsync $ \s -> do
t <- MiniSat.newLit s
add_clause s [L t]
r <- newIORef Map.empty
f s (L t) r
-- | Run 'SAT' computation. Return 'Nothing' if 'UnsatException' is thrown.
runSATMaybe :: (forall s. SAT s a) -> IO (Maybe a)
runSATMaybe m = fmap Just (runSAT m) `catch` \UnsatException -> return Nothing
-------------------------------------------------------------------------------
-- Literals
-------------------------------------------------------------------------------
-- | Literal.
--
-- To negate literate use 'neg'.
newtype Lit s = L { unL :: MiniSat.Lit }
deriving (Eq, Ord)
type role Lit nominal
instance Show (Lit s) where
showsPrec d (L (MiniSat.MkLit l))
| p = showParen (d > 6) (showChar '-' . shows v)
| otherwise = shows v
where
i :: Int
i = fromIntegral l
-- minisat encodes polarity of literal in 0th bit.
-- (this way normal order groups same variable literals).
p :: Bool
p = testBit i 0
v :: Int
v = shiftR i 1
class Neg a where
neg :: a -> a
-- | Negate literal.
instance Neg (Lit s) where
neg (L l) = L (MiniSat.neg l)
-- | Create fresh literal.
newLit :: SAT s (Lit s)
newLit = SAT $ \s _t _r -> do
l <- MiniSat.newLit s
return (L l)
-------------------------------------------------------------------------------
-- Prop
-------------------------------------------------------------------------------
-- | Propositional formula.
data Prop s
= PTrue
| PFalse
| P (Prop1 s)
deriving (Eq, Ord)
infixr 5 \/
infixr 6 /\
instance Show (Prop s) where
showsPrec _ PTrue = showString "true"
showsPrec _ PFalse = showString "false"
showsPrec d (P p) = showsPrec d p
-- | True 'Prop'.
true :: Prop s
true = PTrue
-- | False 'Prop'.
false :: Prop s
false = PFalse
-- | Make 'Prop' from a literal.
lit :: Lit s -> Prop s
lit l = P (P1Lit l)
-- | Disjunction of propositional formulas, or.
(\/) :: Prop s -> Prop s -> Prop s
x \/ y = neg (neg x /\ neg y)
-- | Conjunction of propositional formulas, and.
(/\) :: Prop s -> Prop s -> Prop s
PFalse /\ _ = PFalse
_ /\ PFalse = PFalse
PTrue /\ y = y
x /\ PTrue = x
P x /\ P y = P (p1and x y)
-- | Implication of propositional formulas.
(-->) :: Prop s -> Prop s -> Prop s
x --> y = neg x \/ y
-- | Equivalence of propositional formulas.
(<->) :: Prop s -> Prop s -> Prop s
x <-> y = (x --> y) /\ (y --> x)
-- | Exclusive or, not equal of propositional formulas.
xor :: Prop s -> Prop s -> Prop s
xor x y = x <-> neg y
-- | If-then-else.
--
-- Semantics of @'ite' c t f@ are @ (c '/\' t) '\/' ('neg' c '/\' f)@.
--
ite :: Prop s -> Prop s -> Prop s -> Prop s
-- ite c t f = (c /\ t) \/ (neg c /\ f)
ite c t f = (c \/ f) /\ (neg c \/ t) /\ (t \/ f) -- this encoding makes (t == f) case propagate even when c is yet undecided.
-- | Negation of propositional formulas.
instance Neg (Prop s) where
neg PTrue = PFalse
neg PFalse = PTrue
neg (P p) = P (p1neg p)
-------------------------------------------------------------------------------
-- Prop1
-------------------------------------------------------------------------------
data Prop1 s
= P1Lit !(Lit s)
| P1Nnd !(Set (PropA s))
| P1And !(Set (PropA s))
deriving (Eq, Ord)
data PropA s
= PALit !(Lit s)
| PANnd !(Set (PropA s))
deriving (Eq, Ord)
instance Show (Prop1 s) where
showsPrec d (P1Lit l) = showsPrec d l
showsPrec _ (P1And xs) = showNoCommaListWith shows (Set.toList xs)
showsPrec _ (P1Nnd xs) = showChar '-' . showNoCommaListWith shows (Set.toList xs)
instance Show (PropA s) where
showsPrec d (PALit l) = showsPrec d l
showsPrec _ (PANnd xs) = showChar '-' . showNoCommaListWith shows (Set.toList xs)
showNoCommaListWith :: (a -> ShowS) -> [a] -> ShowS
showNoCommaListWith _ [] s = "[]" ++ s
showNoCommaListWith showx (x:xs) s = '[' : showx x (showl xs)
where
showl [] = ']' : s
showl (y:ys) = ' ' : showx y (showl ys)
p1and :: Prop1 s -> Prop1 s -> Prop1 s
p1and p@(P1Lit x) (P1Lit y)
| x == y = p
| otherwise = P1And (double (PALit x) (PALit y))
p1and p@(P1Nnd x) (P1Nnd y)
| x == y = p
| otherwise = P1And (double (PANnd x) (PANnd y))
p1and (P1Lit x) (P1Nnd y) = P1And (double (PALit x) (PANnd y))
p1and (P1Nnd x) (P1Lit y) = P1And (double (PANnd x) (PALit y))
p1and (P1Lit x) (P1And ys) = P1And (Set.insert (PALit x) ys)
p1and (P1Nnd x) (P1And ys) = P1And (Set.insert (PANnd x) ys)
p1and (P1And xs) (P1Lit y) = P1And (Set.insert (PALit y) xs)
p1and (P1And xs) (P1Nnd y) = P1And (Set.insert (PANnd y) xs)
p1and (P1And xs) (P1And ys) = P1And (Set.union xs ys)
p1neg :: Prop1 s -> Prop1 s
p1neg (P1Lit l) = P1Lit (neg l)
p1neg (P1Nnd xs) = P1And xs
p1neg (P1And xs) = P1Nnd xs
double :: Ord a => a -> a -> Set a
double x y = Set.insert x (Set.singleton y)
-------------------------------------------------------------------------------
-- Clause definitions
-------------------------------------------------------------------------------
-- | Add conjunction definition.
--
-- @addConjDefinition x ys@ asserts that @x ↔ ⋀ yᵢ@
addConjDefinition :: Lit s -> [Lit s] -> SAT s ()
addConjDefinition x zs = do
y <- add_definition (Set.fromList zs)
if x == y
then return ()
else assertEqual x y
-- | Add disjunction definition.
--
-- @addDisjDefinition x ys@ asserts that @x ↔ ⋁ yᵢ@
--
addDisjDefinition :: Lit s -> [Lit s] -> SAT s ()
addDisjDefinition x ys = addConjDefinition (neg x) (fmap neg ys)
-- Implementation: @(x ↔ ⋁ yᵢ) ↔ (¬x ↔ ⋀ ¬xyᵢ)@
-------------------------------------------------------------------------------
-- Methods
-------------------------------------------------------------------------------
-- | Assert that given 'Prop' is true.
--
-- This is equivalent to
--
-- @
-- addProp p = do
-- l <- addDefinition p
-- addClause l
-- @
--
-- but avoid creating the definition, asserting less clauses.
--
addProp :: Prop s -> SAT s ()
addProp PTrue = return ()
addProp PFalse = SAT $ \s t _ -> add_clause s [neg t]
addProp (P p) = add_prop p
-- | Add definition of 'Prop'. The resulting literal is equivalent to the argument 'Prop'.
--
addDefinition :: Prop s -> SAT s (Lit s)
addDefinition PTrue = trueLit
addDefinition PFalse = falseLit
addDefinition (P p) = addDefinition1 p
-- | True literal.
trueLit :: SAT s (Lit s)
trueLit = SAT $ \_s t _ -> return t
-- | False literal
falseLit :: SAT s (Lit s)
falseLit = SAT $ \_s t _ -> return (neg t)
addDefinition1 :: Prop1 s -> SAT s (Lit s)
addDefinition1 = tseitin1
-- | Add conjuctive definition.
add_definition :: Set (Lit s) -> SAT s (Lit s)
add_definition ps
| Set.null ps
= trueLit
add_definition ps = SAT $ \s _ defsRef -> do
defs <- readIORef defsRef
case Map.lookup ps defs of
Just d -> return d
Nothing -> do
d' <- MiniSat.newLit s
let d = L d'
-- putStrLn $ "add_definition " ++ show (Set.toList ps) ++ " = " ++ show d
-- d ∨ ¬x₁ ∨ ¬x₂ ∨ ... ∨ ¬xₙ
add_clause s $ d : map neg (Set.toList ps)
-- ¬d ∨ x₁
-- ¬d ∨ x₂
-- ...
-- ¬d ∨ xₙ
forM_ ps $ \p -> do
add_clause s [neg d, p]
-- save the definition.
writeIORef defsRef $! Map.insert ps d defs
return d
-- top-level add prop: CNF
add_prop :: Prop1 s -> SAT s ()
add_prop (P1Lit l) = addClause [l]
add_prop (P1And xs) = forM_ xs add_prop'
add_prop (P1Nnd xs) = do
ls <- traverse tseitinA (Set.toList xs)
addClause (map neg ls)
-- first-level: Clauses
add_prop' :: PropA s -> SAT s ()
add_prop' (PALit l) = addClause [l]
add_prop' (PANnd xs) = do
ls <- traverse tseitinA (Set.toList xs)
addClause (map neg ls)
tseitin1 :: Prop1 s -> SAT s (Lit s)
tseitin1 (P1Lit l) = return l
tseitin1 (P1And xs) = do
xs' <- traverse tseitinA (Set.toList xs)
add_definition (Set.fromList xs')
tseitin1 (P1Nnd xs) = do
xs' <- traverse tseitinA (Set.toList xs)
neg <$> add_definition (Set.fromList xs')
tseitinA :: PropA s -> SAT s (Lit s)
tseitinA (PALit l) = return l
tseitinA (PANnd xs) = do
xs' <- traverse tseitinA (Set.toList xs)
neg <$> add_definition (Set.fromList xs')
-------------------------------------------------------------------------------
-- Constraints
-------------------------------------------------------------------------------
-- | Add a clause to the solver.
addClause :: [Lit s] -> SAT s ()
addClause ls = SAT $ \s _t _r -> add_clause s ls
add_clause :: MiniSat.Solver -> [Lit s] -> IO ()
add_clause s ls = do
-- putStrLn $ "add_clause " ++ show ls
ok <- MiniSat.addClause s (map unL ls)
unless ok $ throwIO UnsatException
-- | At least one -constraint.
--
-- Alias to 'addClause'.
assertAtLeastOne :: [Lit s] -> SAT s ()
assertAtLeastOne = addClause
-- | At most one -constraint.
--
-- Uses 'atMostOnePairwise' for lists of length 2 to 5
-- and 'atMostOneSequential' for longer lists.
--
-- The cutoff is chosen by picking encoding with least clauses:
-- For 5 literals, 'atMostOnePairwise' needs 10 clauses and 'assertAtMostOneSequential' needs 11 (and 4 new variables).
-- For 6 literals, 'atMostOnePairwise' needs 15 clauses and 'assertAtMostOneSequential' needs 14.
--
assertAtMostOne :: [Lit s] -> SAT s ()
assertAtMostOne ls = case ls of
[] -> return ()
[_] -> return ()
[_,_] -> assertAtMostOnePairwise ls
[_,_,_] -> assertAtMostOnePairwise ls
[_,_,_,_] -> assertAtMostOnePairwise ls
[_,_,_,_,_] -> assertAtMostOnePairwise ls
_ -> assertAtMostOneSequential ls
-- | At most one -constraint using pairwise encoding.
--
-- \[
-- \mathrm{AMO}(x_1, \ldots, x_n) = \bigwedge_{1 \le i < j \le n} \neg x_i \lor \neg x_j
-- \]
--
-- \(n(n-1)/2\) clauses, zero auxiliary variables.
--
assertAtMostOnePairwise :: [Lit s] -> SAT s ()
assertAtMostOnePairwise literals = mapM_ f (tails literals) where
f :: [Lit s] -> SAT s ()
f [] = return ()
f (l:ls) = mapM_ (g l) ls
g :: Lit s -> Lit s -> SAT s ()
g l1 l2 = addClause [neg l1, neg l2]
-- | At most one -constraint using sequential counter encoding.
--
-- \[
-- \mathrm{AMO}(x_1, \ldots, x_n) =
-- (\neg x_1 \lor s_1) \land
-- (\neg x_n \lor \neg s_{n-1}) \land
-- \bigwedge_{1 < i < n} (\neg x_i \lor a_i) \land (\neg a_{i-1} \lor a_i) \land (\neg x_i \lor \neg a_{i-1})
-- \]
--
-- Sinz, C.: Towards an optimal CNF encoding of Boolean cardinality constraints, Proceedings of Principles and Practice of Constraint Programming (CP), 827–831 (2005)
--
-- \(3n-4\) clauses, \(n-1\) auxiliary variables.
--
-- We optimize the two literal case immediately ([resolution](https://en.wikipedia.org/wiki/Resolution_(logic)) on \(s_1\).
--
-- \[
-- (\neg x_1 \lor s_1) \land (\neg x_2 \lor \neg s_1) \Longrightarrow \neg x_1 \lor \neg x_2
-- \]
--
assertAtMostOneSequential :: [Lit s] -> SAT s ()
assertAtMostOneSequential [] = return ()
assertAtMostOneSequential [_] = return ()
assertAtMostOneSequential [x1,x2] = addClause [neg x1, neg x2]
assertAtMostOneSequential (xn:x1:xs) = do
a1 <- newLit
addClause [neg x1, a1]
go a1 xs
where
go an1 [] = addClause [neg xn, neg an1]
go ai1 (xi:xis) = do
ai <- newLit
addClause [neg xi, ai]
addClause [neg ai1, ai]
addClause [neg xi, neg ai1]
go ai xis
-- | Assert that two literals are equal.
assertEqual :: Lit s -> Lit s -> SAT s ()
assertEqual l l'
| l == l' = return ()
| otherwise = do
addClause [l, neg l']
addClause [neg l, l']
-- | Assert that all literals in the list are equal.
assertAllEqual :: [Lit s] -> SAT s ()
assertAllEqual [] = return ()
assertAllEqual (l:ls) = forM_ (Set.fromList ls) $ \l' -> assertEqual l l'
-------------------------------------------------------------------------------
-- Solving
-------------------------------------------------------------------------------
-- | Search without returning a model.
solve_ :: SAT s ()
solve_ = SAT $ \s _t _r -> do
ok <- MiniSat.solve s []
unless ok $ throwIO UnsatException
-- | Search and return a model.
solve :: Traversable model => model (Lit s) -> SAT s (model Bool)
solve model = SAT $ \s _t _r -> do
ok <- MiniSat.solve s []
unless ok $ throwIO UnsatException
traverse (getSym s) model
where
getSym :: MiniSat.Solver -> Lit s -> IO Bool
getSym s (L l) = do
b <- MiniSat.modelValue s l
case b of
Nothing -> throwIO SATPanic
Just b' -> return b'
-------------------------------------------------------------------------------
-- Simplification
-------------------------------------------------------------------------------
-- | Removes already satisfied clauses.
simplify :: SAT s ()
simplify = SAT $ \s _t _r -> do
ok <- MiniSat.simplify s
unless ok $ throwIO UnsatException
-------------------------------------------------------------------------------
-- Statistics
-------------------------------------------------------------------------------
-- | The current number of variables.
numberOfVariables :: SAT s Int
numberOfVariables = SAT $ \s _t _r -> MiniSat.minisat_num_vars s
-- | The current number of original clauses.
numberOfClauses :: SAT s Int
numberOfClauses = SAT $ \s _t _r -> MiniSat.minisat_num_clauses s
-- | The current number of learnt clauses.
numberOfLearnts :: SAT s Int
numberOfLearnts = SAT $ \s _t _r -> MiniSat.minisat_num_learnts s
-- | The current number of conflicts.
numberOfConflicts :: SAT s Int
numberOfConflicts = SAT $ \s _t _r -> MiniSat.minisat_num_conflicts s