packages feed

toysolver-0.9.0: src/ToySolver/SAT/Encoder/PB/Internal/BCCNF.hs

{-# LANGUAGE BangPatterns #-}
{-# OPTIONS_GHC -Wall #-}
{-# OPTIONS_HADDOCK show-extensions #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  ToySolver.SAT.Encoder.PB.Internal.BCCNF
-- Copyright   :  (c) Masahiro Sakai 2022
-- License     :  BSD-style
--
-- Maintainer  :  masahiro.sakai@gmail.com
-- Stability   :  provisional
-- Portability :  non-portable
--
-- References
--
-- * 南 雄之 (Yushi Minami), 宋 剛秀 (Takehide Soh), 番原 睦則
--   (Mutsunori Banbara), 田村 直之 (Naoyuki Tamura). ブール基数制約を
--   経由した擬似ブール制約のSAT符号化手法 (A SAT Encoding of
--   Pseudo-Boolean Constraints via Boolean Cardinality Constraints).
--   Computer Software, 2018, volume 35, issue 3, pages 65-78,
--   <https://doi.org/10.11309/jssst.35.3_65>
--
-----------------------------------------------------------------------------
module ToySolver.SAT.Encoder.PB.Internal.BCCNF
  (
  -- * Monadic interface
    addPBLinAtLeastBCCNF
  , encodePBLinAtLeastWithPolarityBCCNF

  -- * High-level pure encoder
  , encode

  -- * Low-level implementation
  , preprocess

  -- ** Prefix sum
  , PrefixSum
  , toPrefixSum
  , encodePrefixSum
  , encodePrefixSumBuggy
  , encodePrefixSumNaive

  -- ** Boolean cardinality constraints
  , BCLit
  , toAtLeast
  , implyBCLit

  -- ** Clause over boolean cardinality constraints
  , BCClause
  , reduceBCClause
  , implyBCClause

  -- ** CNF over boolean cardinality constraints
  , BCCNF
  , reduceBCCNF
  ) where

import Control.Exception (assert)
import Control.Monad
import Control.Monad.Primitive
import Data.Function (on)
import Data.List (sortBy)
import Data.Maybe (listToMaybe)
import Data.Ord (comparing)

import ToySolver.SAT.Types
import qualified ToySolver.SAT.Encoder.Cardinality as Card
import qualified ToySolver.SAT.Encoder.Tseitin as Tseitin

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

-- | \(\sum_{j=1}^i b_j s_j = \sum_{j=1}^i b_j (x_1, \ldots, x_j)\) is represented
-- as a list of tuples consisting of \(b_j, j, [x_1, \ldots, x_j]\).
type PrefixSum = [(Integer, Int, [Lit])]

-- | Convert 'PBLinSum' to 'PrefixSum'.
-- The 'PBLinSum' must be 'preprocess'ed before calling the function.
toPrefixSum :: PBLinSum -> PrefixSum
toPrefixSum s =
  assert (and [c > 0 | (c, _) <- s] && and (zipWith ((>=) `on` fst) s (tail s))) $
    go 0 [] s
  where
    go :: Int -> [Lit] -> PBLinSum -> PrefixSum
    go _ _ [] = []
    go !i prefix ((c, l) : ts)
      | c > c1 = (c - c1, i + 1, reverse (l : prefix)) : go (i + 1) (l : prefix) ts
      | otherwise = go (i + 1) (l : prefix) ts
      where
        c1 = maybe 0 fst (listToMaybe ts)

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

-- | A constraint \(s_i \ge c\) where \(s_i = x_1 + \ldots + x_i\) is represnted as
-- a tuple of \(i\), \([x_1, \ldots, x_i]\), and \(c\).
type BCLit = (Int, [Lit], Int)

-- | Disjunction of 'BCLit'
type BCClause = [BCLit]

-- | Conjunction of 'BCClause'
type BCCNF = [BCClause]

-- | Forget \(s_i\) and returns \(x_1 + \ldots + x_i \ge c\).
toAtLeast :: BCLit -> AtLeast
toAtLeast (_, lhs, rhs) = (lhs, rhs)

-- | \((s_i \ge a) \Rightarrow (s_j \ge b)\) is defined as
-- \((i \le j \wedge a \ge b) \vee (i \ge b \wedge i - a \le j - b)\).
implyBCLit :: BCLit -> BCLit -> Bool
implyBCLit (i,_,a) (j,_,b)
  | i <= j = a >= b
  | otherwise = i - a <= j - b

-- | Remove redundant literals based on 'implyBCLit'.
reduceBCClause :: BCClause -> BCClause
reduceBCClause lits = assert (isAsc lits2) $ lits2
  where
    isAsc  ls = and [i1 <= i2 | let is = [i | (i,_,_) <- ls], (i1,i2) <- zip is (tail is)]
    isDesc ls = and [i1 >= i2 | let is = [i | (i,_,_) <- ls], (i1,i2) <- zip is (tail is)]

    lits1 = assert (isAsc lits) $ f1 [] minBound lits
    lits2 = assert (isDesc lits1) $ f2 [] maxBound lits1

    f1 r !_ [] = r
    f1 r !jb (l@(i,_,a) : ls)
      | ia > jb = f1 (l : r) ia ls
      | otherwise = f1 r jb ls
      where
        ia = i - a

    f2 r !_ [] = r
    f2 r !b (l@(_,_,a) : ls)
      | a < b = f2 (l : r) a ls
      | otherwise = f2 r b ls

-- | \(C \Rightarrow C'\) is defined as \(\forall l\in C \exists l' \in C' (l \Rightarrow l')\).
implyBCClause :: BCClause -> BCClause -> Bool
implyBCClause lits1 lits2 = all (\lit1 -> any (implyBCLit lit1) lits2) lits1

-- | Reduce 'BCCNF' by reducing each clause using 'reduceBCClause' and then
-- remove redundant clauses based on 'implyBCClause'.
reduceBCCNF :: BCCNF -> BCCNF
reduceBCCNF = reduceBCCNF' . map reduceBCClause

reduceBCCNF' :: BCCNF -> BCCNF
reduceBCCNF' = go []
  where
    go r [] = reverse r
    go r (c : cs)
      | any (\c' -> implyBCClause c' c) (r ++ cs) = go r cs
      | otherwise = go (c : r) cs

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

-- | Encode a given pseudo boolean constraint \(\sum_i a_i x_i \ge c\)
-- into an equilavent formula in the form of
-- \(\bigwedge_j \bigvee_k \sum_{l \in L_{j,k}} l \ge d_{j,k}\).
encode :: PBLinAtLeast -> [[AtLeast]]
encode constr = map (map toAtLeast) $ reduceBCCNF $ encodePrefixSum (toPrefixSum lhs) rhs
  where
    (lhs, rhs) = preprocess constr

-- | Perform 'normalizePBLinAtLeast' and sort the terms in descending order of coefficients
preprocess :: PBLinAtLeast -> PBLinAtLeast
preprocess constr = (lhs2, rhs1)
  where
    (lhs1, rhs1) = normalizePBLinAtLeast constr
    lhs2 = sortBy (flip (comparing fst) <> comparing (abs . snd)) lhs1

-- | Algorithm 2 in the paper but with a bug fixed
encodePrefixSum :: PrefixSum -> Integer -> [[BCLit]]
encodePrefixSum = f 0 0
  where
    f !_ !_ [] !c = if c > 0 then [[]] else []
    f i0 d0 ((0,_,_) : bss) c = f i0 d0 bss c
    f i0 d0 ((b,i,ls) : bss) c =
      [ [(i, ls, maximum ds' + 1)] | not (null ds') ]
      ++
      [ if d+1 > i then theta else (i, ls, d+1) : theta
      | d <- ds, theta <- f i d bss (fromIntegral (c - b * fromIntegral d))
      ]
      where
        bssMax d = bssMin d + sum [b' * fromIntegral (i' - i) | (b', i', _) <- bss]
        bssMin d = sum [b' | (b', _, _) <- bss]  * fromIntegral d
        ds  = [d | d <- [d0 .. d0 + i - i0], let bd = b * fromIntegral d, c - bssMax d <= bd, bd < c - bssMin d]
        ds' = [d | d <- [d0 .. d0 + i - i0], b * fromIntegral d < c - bssMax d]

-- | Algorithm 2 in the paper
encodePrefixSumBuggy :: PrefixSum -> Integer -> [[BCLit]]
encodePrefixSumBuggy = f 0 0
  where
    f !_ !_ [] !c = if c > 0 then [[]] else []
    f i0 d0 ((0,_,_) : bss) c = f i0 d0 bss c
    f i0 d0 ((b,i,ls) : bss) c =
      [ [(i, ls, max (maximum ds' + 1) d0)] | not (null ds') ]
      ++
      [ if d+1 > i then theta else (i, ls, d+1) : theta
      | d <- ds, theta <- f i d bss (fromIntegral (c - b * fromIntegral d))
      ]
      where
        bssMax d = bssMin d + sum [b' * fromIntegral (i' - i) | (b', i', _) <- bss]
        bssMin d = sum [b' | (b', _, _) <- bss]  * fromIntegral d
        ds  = [d | d <- [d0 .. d0 + i - i0], let bd = b * fromIntegral d, c - bssMax d <= bd, bd < c - bssMin d]
        ds' = [d | d <- [0..i], b * fromIntegral d < c - bssMax d]

-- | Algorithm 1 in the paper
encodePrefixSumNaive :: PrefixSum -> Integer -> [[BCLit]]
encodePrefixSumNaive = f
  where
    f [] !c = if c > 0 then [[]] else []
    f ((0,_,_) : bss) c = f bss c
    f ((b,i,ls) : bss) c =
      [ [(i, ls, maximum ds' + 1)] | not (null ds') ]
      ++
      [ if d+1 > i then theta else (i, ls, d+1) : theta
      | d <- ds, theta <- f bss (fromIntegral (c - b * fromIntegral d))
      ]
      where
        bssMax = sum [b' * fromIntegral i' | (b', i', _) <- bss]
        bssMin = 0
        ds  = [d | d <- [0..i], let bd = b * fromIntegral d, c - bssMax <= bd, bd < c - bssMin]
        ds' = [d | d <- [0..i], b * fromIntegral d < c - bssMax]

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

addPBLinAtLeastBCCNF :: PrimMonad m => Card.Encoder m -> PBLinAtLeast -> m ()
addPBLinAtLeastBCCNF enc constr = do
  forM_ (encode constr) $ \clause -> do
    addClause enc =<< mapM (Card.encodeAtLeast enc) clause

encodePBLinAtLeastWithPolarityBCCNF :: PrimMonad m => Card.Encoder m -> Tseitin.Polarity -> PBLinAtLeast -> m Lit
encodePBLinAtLeastWithPolarityBCCNF enc polarity constr = do
  let tseitin = Card.getTseitinEncoder enc
  ls <- forM (encode constr) $ \clause -> do
    Tseitin.encodeDisjWithPolarity tseitin polarity =<< mapM (Card.encodeAtLeastWithPolarity enc polarity) clause
  Tseitin.encodeConjWithPolarity tseitin polarity ls

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