packages feed

alms-0.4.9: src/PDNF.hs

{-# LANGUAGE DeriveDataTypeable #-}
-- | Positive Disjunctive Normal Form
module PDNF (
  -- * Abstract representation
  PDNF,
  -- * Construction
  variable, conjunct, disjunct, disjoinClause, conjoinClause,
  -- * Queries
  isUnsat, isValid, support,
  -- ** Assignments
  Assignment, satisfies, findUnsat,
  -- * Resolution and substitution
  assume, replace, mapVars, mapVarsM, mapReplace, mapReplaceM,
  -- * To and from lists
  fromLists, fromListsUnsafe, toLists,
  -- * Tests
  tests
) where

import Syntax.POClass
import Util

import Data.Generics (Typeable, Data)
import Data.List (intersperse, nub, sort)
import qualified Data.Set as S
import qualified Test.QuickCheck as QC

-- | The type of a Positive DNF over some type 'a'
newtype PDNF a = PDNF { unPDNF :: [S.Set a] }
  deriving (Typeable, Data)

-- | Is the formula unsatisfiable?
-- O(1)
isUnsat :: PDNF a -> Bool
isUnsat  = null . unPDNF

-- | Is the formula valid?
isValid :: Eq a => PDNF a -> Bool
isValid  = (== [S.empty]) . unPDNF

-- | To update the formula to reflect an assumption about the
--   assignment for a particular variable.
assume  :: Ord a => Bool -> a -> PDNF a -> PDNF a
assume True  v formula = PDNF . normalize' $
  map (S.delete v) (unPDNF formula)
assume False v formula = PDNF $
  filter (S.notMember v) (unPDNF formula)

-- | To substitute a PDNF formula for a given variable in another
--   formula.
replace :: Ord a => a -> PDNF a -> PDNF a -> PDNF a
replace v (PDNF f1) (PDNF f2) = PDNF $
  normalize' $ concatMap eachClause f2
  where
  eachClause clause
    | v `S.member` clause = conjoinClause' (S.delete v clause) f1
    | otherwise           = [clause]

-- | To map every variable in a formula
mapVars :: (Ord a, Ord b) => (a -> b) -> PDNF a -> PDNF b
mapVars f  = PDNF . normalize' . map (S.map f) . unPDNF

-- | To map every variable in a formula, in an arbitrary monad
mapVarsM :: (Ord a, Ord b, Monad m) =>
            (a -> m b) -> PDNF a -> m (PDNF b)
mapVarsM f = liftM fromLists . mapM (mapM f) . toLists'

-- | To map every variable in a formula to a formula, possibly over
--   a different type
mapReplace :: (Ord a, Ord b) =>
              PDNF a -> (a -> PDNF b) -> PDNF b
mapReplace m k = bigVee [ bigWedge [ k var | var <- clause ]
                        | clause <- toLists' m ]

-- | To map every variable in a formula to a formula, possibly over
--   a different type, in an arbitrary monad
mapReplaceM :: (Ord a, Ord b, Monad m) =>
               PDNF a -> (a -> m (PDNF b)) -> m (PDNF b)
mapReplaceM m k = liftM bigVee (mapM (liftM bigWedge . mapM k) (toLists' m))

-- | To construct a formula of a single variable
variable :: a -> PDNF a
variable  = PDNF . return . S.singleton

-- | To find the support of a PDNF
support  :: Ord a => PDNF a -> S.Set a
support   = foldr S.union S.empty . unPDNF

-- | To construct a formula of one conjuction
conjunct :: Ord a => [a] -> PDNF a
conjunct  = PDNF . return . S.fromList

disjunct :: Ord a => [a] -> PDNF a
disjunct  = PDNF . map S.singleton . nub

instance Ord a => PO (PDNF a) where
  f1 \/ f2 = PDNF $ foldr disjoinClause' (unPDNF f1) (unPDNF f2)
  f1 /\ f2 = PDNF $
    normalize' [ clause1 `S.union` clause2
               | clause1 <- unPDNF f1
               , clause2 <- unPDNF f2 ]
  PDNF ant <: PDNF con
    = all (\clause -> any (`S.isSubsetOf` clause) con) ant

instance Bounded (PDNF a) where
  minBound = PDNF []
  maxBound = PDNF [S.empty]

instance Ord a => Eq (PDNF a) where
  f1 == f2 = compare f1 f2 == EQ

instance Ord a => Ord (PDNF a) where
  f1 `compare` f2 = toLists f1 `compare` toLists f2

-- | To add a clause to a formula
disjoinClause :: Ord a => [a] -> PDNF a -> PDNF a
disjoinClause c' = PDNF . disjoinClause' (S.fromList c') . unPDNF

-- | To distribute a clause over a formula
conjoinClause :: Ord a => [a] -> PDNF a -> PDNF a
conjoinClause c' = PDNF . conjoinClause' (S.fromList c') . unPDNF

disjoinClause' :: Ord a => S.Set a -> [S.Set a] -> [S.Set a]
disjoinClause' c' []     = [c']
disjoinClause' c' (c:cs) =
  if c' `S.isSubsetOf` c
    then disjoinClause' c' cs
  else if c `S.isSubsetOf` c'
    then c:cs
    else c:disjoinClause' c' cs

conjoinClause' :: Ord a => S.Set a -> [S.Set a] -> [S.Set a]
conjoinClause' c' cs = map (S.union c') cs

normalize' :: Ord a => [S.Set a] -> [S.Set a]
normalize'  = foldr disjoinClause' []

-- | To construct a PDNF.
fromLists :: Ord a => [[a]] -> PDNF a
fromLists  = foldr (\/) minBound . map conjunct

-- | To construct a PDNF quickly, assuming that no list is a superset
--   of an other list.
fromListsUnsafe :: Ord a => [[a]] -> PDNF a
fromListsUnsafe  = PDNF . map S.fromList

-- | To construct a canonical list of lists of variables.
toLists :: Ord a => PDNF a -> [[a]]
toLists  = sort . map S.toAscList . unPDNF

toLists' :: PDNF a -> [[a]]
toLists'  = map S.toList . unPDNF

instance (Eq a, Show a) => Show (PDNF a) where
  showsPrec _ pdnf
    | isValid pdnf = showString "#t"
    | isUnsat pdnf = showString "#f"
  showsPrec p (PDNF formula) =
    showParen (p > 5) $
      foldr (.) id $
        intersperse (showString " | ")
          [ foldr (.) id $
              intersperse (showString " & ") $
                [ showsPrec 6 lit
                | lit <- S.toList clause ]
          | clause <- formula ]

---
--- Assignments
---

-- | An assignment is a map from variables to booleans, represented
--   as a list of variables to map to true, with all others mapped
--   to false.
type Assignment a = [a]

-- | Does the given assignment satisfy the PDNF?
satisfies :: Ord a => PDNF a -> Assignment a -> Bool
satisfies pdnf vs = isValid (foldr (assume True) pdnf vs)

-- | Find an assignment that satisfies the first PDNF but not
--   the second.
findUnsat :: Ord a => PDNF a -> PDNF a -> [Assignment a]
findUnsat (PDNF f1) (PDNF f2) =
  [ S.toList clause
  | clause <- f1
  , not (any (`S.isSubsetOf` clause) f2) ]

---
--- Tests
---

assignFor :: Ord a => PDNF a -> QC.Gen (Assignment a)
assignFor pdnf =
  genSublist (S.toList (support pdnf))
    where
      genSublist :: [a] -> QC.Gen [a]
      genSublist lst = do
        let den = length lst
        num <- QC.choose (0, den `div` 2)
        let each rest elt = do
              pick <- QC.choose (1, den)
              return $ if pick > num
                then elt:rest
                else rest
        foldM each [] lst

instance (Ord a, QC.Arbitrary a) => QC.Arbitrary (PDNF a) where
  arbitrary = fromLists `fmap` QC.arbitrary
  shrink    = map fromLists . QC.shrink . toLists

prop_Impl :: PDNF Int -> PDNF Int -> QC.Property
prop_Impl f1 f2 =
  if f1 <: f2 then
    impl f1 f2
  else if f2 <: f1 then
    impl f2 f1
  else
    QC.classify True "counterexample" $
      not (null (findUnsat f1 f2))
  where impl f1' f2' =
          QC.classify True "implication" $
            QC.forAll (assignFor (f1' \/ f2')) $ \s ->
              satisfies f1' s QC.==> satisfies f2' s

prop_Disj :: PDNF Int -> PDNF Int -> Bool
prop_Disj f1 f2 = f1 <: f1 \/ f2

prop_Conj :: PDNF Int -> PDNF Int -> Bool
prop_Conj f1 f2 = f1 /\ f2 <: f1

prop_Replace :: PDNF Int -> Bool -> QC.Property
prop_Replace pdnf b =
  QC.forAll (QC.elements (S.toList (support pdnf))) $ \v ->
    replace v (if b then maxBound else minBound) pdnf == assume b v pdnf

tests :: IO ()
tests  = do
  QC.quickCheck prop_Replace
  QC.quickCheck prop_Impl
  QC.quickCheck prop_Disj
  QC.quickCheck prop_Conj