packages feed

sat-micro-hs-0.1.1: SatMicro.hs

{-# LANGUAGE DeriveDataTypeable
           , PatternSignatures #-}

{-
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    DPLLSat is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with DPLLSat.  If not, see <http://www.gnu.org/licenses/>.

    Copyright 2008 Denis Bueno
-}


-- | A Haskell implementation of the basic algorithm, including
-- non-chronological backtracking, from ''SAT-MICRO: petit mais costaud!'' by
-- Sylvain Conchon, Johannes Kanig, and Stephane Lescuyer.
--
-- One interesting thing about this implementation is its use of CPS where the
-- OCaml implementation uses exceptions, to handle control flow.
--
-- Optimisations:
-- non-chronological backtracking;
--
-- Backtracking uses the control stack, so, you may want to invoke with
-- something like @
--    sat-micro cnf-file +RTS -K768M -RTS@,
-- depending on the size of the SAT instance.

module SatMicro where

import Control.Monad.Cont hiding (mapM_)
import Control.Monad.State.Strict hiding ((>=>), mapM_)
import Data.Foldable hiding (sequence_)
import Data.List hiding (elem, concat, foldl', foldl, any, all, foldr, maximumBy)
import Data.Map (Map)
import Data.Ord (comparing)
import Data.Set (Set)
import Debug.Trace()
import Prelude hiding (or, and, all, any, elem, minimum, foldr, splitAt
                      , concatMap, foldl, catch, mapM_)
import Text.PrettyPrint.HughesPJ
import qualified Data.Foldable as Foldable
import qualified Data.List as L
import qualified Data.Map as Map
import qualified Data.Set as Set

type CNF = [[Lit]]

data Result = Sat [Lit] | Unsat
instance Show Result where
   show (Sat lits) = "satisfiable: " ++ intercalate " " (map show lits)
   show Unsat      = "unsatisfiable"


newtype Lit = L {unLit :: Int} deriving (Eq, Ord)
{-# INLINE inLit #-}
inLit f = L . f . unLit

instance Show Lit where
    show = show . unLit
instance Read Lit where
    readsPrec i s = map (\(i,s) -> (L i, s)) (readsPrec i s :: [(Int, String)])

instance Num Lit where
    _ + _ = error "+ doesn't make sense for literals"
    _ - _ = error "- doesn't make sense for literals"
    _ * _ = error "* doesn't make sense for literals"
    signum _ = error "signum doesn't make sense for literals"
    negate   = inLit negate
    abs      = inLit abs
    fromInteger l | l == 0    = error "0 is not a literal"
                  | otherwise = L $ fromInteger l


-- | The state of the search process.
data StateContents = S {
      gamma :: Map Lit (Set Lit), -- ^ annotated assignment literals
      delta :: [([Lit], Set Lit)] -- ^ annotated CNF
    }

getGamma l e = Map.findWithDefault (error $ show l ++ ": annotation not found")
               l (gamma e)
instance Show StateContents where
    show = render . stateDoc
      where
stateDoc (S {gamma=gamma, delta=delta}) =
    brackets (hcat . intersperse space . map (text . show) $ Map.keys gamma)
    <+> braces (hcat
                . intersperse (comma <> space)
                . map (\(c, a) -> braces (hcat
                                          . intersperse space
                                          . map (text . show) $ c)
                                  <> tups (hcat
                                           . intersperse comma
                                           . map (text . show)
                                           $ Set.toList a))
                $ delta)
        where tups p = char '<' <> p <> char '>'


-- | The entry point to the solver.  Searches for a solution to the given
-- satisfiability problem.
dpll :: CNF -> Result
dpll f = (`runCont` id) $ do
    r <- callCC $ \bj -> do
           (Right env) <- bcp bj (initialState f)
           unsat env return
    either (const $ return Unsat) (return . Sat) r

dispatch d = map (\l -> (l, d))
initialState f = S {gamma = Map.empty,
                    delta = (dispatch Set.empty f)}

-- bcp either:
--  1. finds a conflict and returns annotation literals (Left)
--  2. computes a new environment (Right)

-- | Given an annotated literal, assume it and propagate this information.
-- This may cause other assignments to take place.
assume :: (Monad m) =>
          (Either (Set Lit) b -> m StateContents)
          -> StateContents
          -> (Lit, Set Lit)
          -> m (Either a StateContents)
{-# INLINE assume #-}
assume bj env (l, s) = -- update only if not present
    if l `Map.member` gamma env
    then return (Right env)
    else bcp bj env{gamma = Map.insert l s (gamma env)}

-- | Boolean constraint propagation.  Under the current assignment, finds any
-- conflicting or unit clauses, and then back jumps or assigns, respectively.
-- If there is no conflict, computes a new environment (@Right@). If this
-- function finds a conflict, calls @bj@ with set of literals annotating the
-- conflicting clause (@Left@).
bcp :: (Monad m) =>
       (Either (Set Lit) b -> m StateContents) -- ^ for backjumping
       -> StateContents
       -> m (Either a StateContents)
bcp bj env = do
    env' <-
        foldM (\env' (cl, a) -> do
                 let (cl_neg, cl') =
                         partition (\l -> negate l `Map.member` gamma env') cl
                 if any (`Map.member` gamma env') cl'
                  then return env'
                  else do
                    -- update clause annotation
                    let a' = foldl'
                             (\set l -> set `Set.union` getGamma (negate l) env')
                             a cl_neg
                    case cl' of
                         []  -> bj (Left a')
                         [f] -> assume bj env' (f, a') >>= return . fromRight
                         _   -> return $ env'{delta = (cl', a'):(delta env')})
        (env{delta = []})
        (delta env)
    return $ Right env'

-- | @unsat@ either:
--
--    1. returns annotation literals (@Left@)
--
--    2. finds satisfying assignment (@Right@)
unsat :: (MonadCont m) =>
         StateContents
         -> (Either (Set Lit) [Lit] -> m (Either (Set Lit) [Lit]))
            -- ^ the back jump function, allowing conflicts to backtrack to
            -- the point where the last involved literal was decided.
         -> m (Either (Set Lit) [Lit])
unsat env bj =
    case delta env of
      [] -> return $ Right $ Map.keys (gamma env)
      ([_],_):_ -> error "unpropagated unit literal"
      ([],_):_  -> error "conflict unresolved"
      _ -> do
        let a = maxSatLit (delta env)
        r <- callCC $ \innerBj -> do
               (Right env') <- assume innerBj env (a, Set.singleton a)
               -- done propagating, no conflicts: continue
               unsat env' return
        case r of
          Left d ->
              if not $ a `elem` d
              then bj (Left d)
              else (callCC $ \innerBj -> do
                      (Right env') <-
                          assume innerBj env (negate a, Set.delete a d)
                      unsat env' bj)
                   >>= either (bj . Left) (return . Right)
          Right _ -> bj r


-- | Returns a literals satisfying a maximal number of clauses.
maxSatLit cs = (`evalState` Map.empty) $ do
  mapM_ (\(c, _) -> mapM_ incr c) cs
  map <- get
  return $ maximumBy (comparing (valueIn map)) lits
    where
      valueIn m l = Map.findWithDefault (error $ "key not found: " ++ show l) l m
      lits = foldl (\cs' (c, _) -> cs' `L.union` c) [] cs

-- * Helpers


fromRight (Right a) = a
fromRight (Left _)  = error "fromRight: Left"

modSlot slot f = modify $ \s -> f s (slot s)

incr :: (Num a) => Lit -> State (Map Lit a) ()
{-# INLINE incr #-}
incr l = modify $! Map.insertWith (\_ i -> 1+i) l 1

paper1 :: CNF =
  [[-1, -3, -4]
  ,[-1, -3, 4]
  ,[2, 3, 5]
  ,[3, 5]
  ,[3, -5]]



-- | Verify a satisfying assignment.
verifyResult :: Result -> CNF -> Bool
verifyResult (Sat m) cnf =
   -- m is well-formed
   all (\l -> not $ negate l `elem` m) m
   && all (\cl -> any (`elem` cl) m) cnf
verifyResult Unsat _ = True