packages feed

ecta-1.0.0.0: src/Application/SAT.hs

{-# LANGUAGE OverloadedStrings #-}

-- | A very bad SAT solver written by reduction to ECTA
--
--  Also a constructive proof of the NP-hardness of finding
--  a term represented by an ECTA

module Application.SAT (
  -- * Data types
    Var
  , mkVar
  , CNF(..)
  , Clause(..)
  , Lit(..)

  -- * Solving
  , toEcta
  , allSolutions

  -- * Examples
  , ex1
  , ex2
  , ex3
  ) where

import Data.Hashable ( Hashable )
import Data.HashMap.Strict ( HashMap )
import qualified Data.HashMap.Strict as HashMap
import Data.HashSet ( HashSet )
import qualified Data.HashSet as HashSet
import Data.List ( elemIndex, sort )
import Data.Maybe ( fromJust )
import Data.String (IsString(..) )
import Data.Text ( Text )

import GHC.Generics ( Generic )

import Data.List.Index ( imap )

import Data.ECTA
import Data.ECTA.Paths
import Data.ECTA.Term
import Data.Text.Extended.Pretty
import Utility.Fixpoint

----------------------------------------------------------------

-------------------------------------------------------------------
------------------------- SAT variables ---------------------------
-------------------------------------------------------------------


newtype Var = Var { unVar :: Text }
  deriving ( Eq, Ord, Show, Generic )

instance Hashable Var

instance IsString Var where
  fromString = Var . fromString

mkVar :: Text -> Var
mkVar = Var

_varToSymbol :: Var -> Symbol
_varToSymbol = Symbol . unVar

_varToNegSymbol :: Var -> Symbol
_varToNegSymbol v = Symbol ("~" <> unVar v)


-------------------------------------------------------------------
----------------------- CNF representation ------------------------
-------------------------------------------------------------------

-- | Our construction generalizes to arbitrary NNF formulas,
--   and possibly to arbitrary SAT,
--   but we don't need to bother; just CNF is good enough

data CNF = And [Clause]
  deriving ( Eq, Ord, Show, Generic )

instance Hashable CNF

data Clause = Or [Lit]
  deriving ( Eq, Ord, Show, Generic )

instance Hashable Clause

data Lit = PosLit Var
         | NegLit Var
  deriving ( Eq, Ord, Show, Generic )

instance Hashable Lit

instance Pretty Lit where
  pretty (PosLit v) = unVar v
  pretty (NegLit v) = "~" <> unVar v

getLitVar :: Lit -> Var
getLitVar (PosLit v) = v
getLitVar (NegLit v) = v

---------------------
-------- Traversals
---------------------

-- | This is an updatable fold algebra; see "Dealing with Large Bananas"
data CNFAlg a  = CNFAlg { runCNF    :: CNF    -> [a] -> a
                        , runClause :: Clause -> [a] -> a
                        , runLit    :: Lit           -> a
                        }

_emptyAlg :: (Monoid m) => CNFAlg m
_emptyAlg = CNFAlg (const mempty) (const mempty) (const mempty)


class FoldAlg a where
  foldAlg :: CNFAlg m -> a -> m

instance FoldAlg CNF where
  foldAlg alg c@(And clauses) = runCNF alg c (map (foldAlg alg) clauses)

instance FoldAlg Clause where
  foldAlg alg c@(Or lits) = runClause alg c (map (foldAlg alg) lits)

instance FoldAlg Lit where
  foldAlg alg l = runLit alg l

crushAlg :: (Monoid m) => (Lit -> m) -> CNFAlg m
crushAlg f = CNFAlg (const mconcat) (const mconcat) f

getVars :: CNF -> HashSet Var
getVars = foldAlg (crushAlg (HashSet.singleton  . getLitVar))

-----
-- Lit paths
-----

newtype LitPaths = LitPaths { unLitPaths :: HashMap Lit [Path] }

instance Semigroup LitPaths where
  lp1 <> lp2 = LitPaths $ HashMap.unionWith mappend (unLitPaths lp1) (unLitPaths lp2)

instance Monoid LitPaths where
  mempty  = LitPaths HashMap.empty

getLitPathsAlg :: CNFAlg LitPaths
getLitPathsAlg = CNFAlg { runCNF    = \_ lps -> mconcat $ imap (\i lp -> LitPaths $ HashMap.map (map (ConsPath i)) $ unLitPaths lp) lps
                        , runClause = \_ lps -> mconcat lps
                        , runLit    = \lit -> LitPaths $ HashMap.singleton lit [EmptyPath]
                         }

_getLitPaths :: CNF -> LitPaths
_getLitPaths = foldAlg getLitPathsAlg

-------------------------------------------------------------------
------------------------- ECTA conversion -------------------------
-------------------------------------------------------------------

aNode :: Node
aNode = Node [Edge "a" []]

bNode :: Node
bNode = Node [Edge "b" []]

falseNode :: Node
falseNode = Node [Edge "0" []]

trueNode :: Node
trueNode = Node [Edge "1" []]

falseTerm :: Term
falseTerm = head $ naiveDenotation falseNode

trueTerm :: Term
trueTerm = head $ naiveDenotation trueNode

_trueOrFalseNode :: Node
_trueOrFalseNode = Node [Edge "0" [], Edge "1" []]

posVarNode :: Node
posVarNode = Node [Edge "" [falseNode, aNode], Edge "" [trueNode, bNode]]

negVarNode :: Node
negVarNode = Node [Edge "" [falseNode, bNode], Edge "" [trueNode, aNode]]



-- | Encoding:
--   formula(assnNode, formulaNode)
--
-- assnNode:
--  * One edge, with one child per literal (2*numVars total)
--  * Each literal has two choices, true or false
--  * Use constraints to force each positive/negative pair of literals to match.
--     * E.g.: x1 node = choice of (0, a) or (1, b). ~x1 node = choice of (0, b) or (1, a)
--             If x1/~x1 have indices 0/1, then the constraint 0.1=1.1 constrains
--             x1/~x1 to be either true/false or false/true
--
-- formulaNode:
--  * One edge, having one child per clause
--
-- Clause nodes:
--  * One edge per literal in the clause, each corresponding to a choice of which variable
--    makes the clause true.
--  * Each edge has 2*numVars children containing a copy of the assnNode, followed by
--    a single child containing "1"
--  * Constrain said final child to be equal to the truth value of the corresponding literal
--    in those 2*numVars children which copy the assnNode
--
-- Top level constraints:
--  * Constrain the variable nodes in each clause node to be equal to the global variable assignments.

toEcta :: CNF -> Node
toEcta formula = Node [mkEdge "formula" [assnNode, formulaNode] litCopyingConstraints]
  where
    clauses :: [Clause]
    And clauses = formula

    numClauses :: Int
    numClauses = length clauses

    sortedVars :: [Var]
    sortedVars = sort $ HashSet.toList $ getVars formula

    numVars :: Int
    numVars = length sortedVars

    litToIndex :: Lit -> Int
    litToIndex (PosLit v) = 2 * fromJust (elemIndex v sortedVars)
    litToIndex (NegLit v) = 2 * fromJust (elemIndex v sortedVars) + 1

    assnNode :: Node
    assnNode = Node [mkEdge "assignment" (concatMap (const [posVarNode, negVarNode]) sortedVars)
                                         (mkEqConstraints $ map (\i -> [path [2*i, 1], path [2*i+1, 1]])
                                                                [0..numVars - 1])
                    ]

    formulaNode :: Node
    formulaNode = Node [Edge "clauses" (map mkClauseNode clauses)]

    mkClauseNode :: Clause -> Node
    mkClauseNode (Or lits) = Node (map mkLitChoiceEdge lits)
      where
        mkLitChoiceEdge :: Lit -> Edge
        mkLitChoiceEdge lit = mkEdge (Symbol $ "choice[" <> pretty lit <> "]")
                                      (concatMap (const [posVarNode, negVarNode]) sortedVars ++ [trueNode])
                                      (mkEqConstraints [[path [litToIndex lit, 0],  path [2 * numVars]]])


    litCopyingConstraints :: EqConstraints
    litCopyingConstraints = mkEqConstraints [path [0, i] : [path [1, c, i] | c <- [0..numClauses-1]]
                                               | i <- [0..2*numVars - 1]
                                            ]


allSolutions :: CNF -> HashSet (HashMap Var Bool)
allSolutions formula = foldMap (HashSet.singleton . termToAssignment) $ getAllTerms $ fixUnbounded reducePartially $ toEcta formula
  where
    sortedVars :: [Var]
    sortedVars = sort $ HashSet.toList $ getVars formula

    evens :: [a] -> [a]
    evens []       = []
    evens [x]      = [x]
    evens (x:_:l) = x : evens l

    termToAssignment :: Term -> HashMap Var Bool
    termToAssignment (Term _ [Term _ litVals, _]) = foldMap (\(var, Term "" [val, _]) -> HashMap.singleton var (termToBool val))
                                                            (zip sortedVars (evens litVals))
    termToAssignment x    = error $ "Unexpected " <> show x

    termToBool :: Term -> Bool
    termToBool t | t == falseTerm = False
                 | t == trueTerm  = True
                 | otherwise      = error "termToBool: Invalid argument"


-------------------------------------------------------------------
------------------------ Example formulae -------------------------
-------------------------------------------------------------------

-- Naive generation: 2^30 * 3^4 possibilities
ex1 :: CNF
ex1 = And [ Or [PosLit "x1", PosLit "x2", PosLit "x3"]
          , Or [NegLit "x1", PosLit "x2", PosLit "x3"]
          , Or [PosLit "x1", NegLit "x2", PosLit "x3"]
          , Or [PosLit "x1", PosLit "x2", NegLit "x3"]
          ]

-- Naive generation: 2^14
ex2 :: CNF
ex2 = And [ Or [PosLit "x1", PosLit "x2"]
          , Or [NegLit "x1", NegLit "x2"]
          ]


-- Partial reduction of the ECTA effectively performs unit propagation, solving this quickly.
ex3 :: CNF
ex3 = And [ Or [NegLit "x1"]
          , Or [PosLit "x1", PosLit "x2"]
          , Or [NegLit "x2", PosLit "x3"]
          ]