packages feed

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