{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
-----------------------------------------------------------------------------
-- |
-- Module : ToySolver.FileFormat.CNF
-- Copyright : (c) Masahiro Sakai 2016-2018
-- License : BSD-style
--
-- Maintainer : masahiro.sakai@gmail.com
-- Stability : provisional
-- Portability : non-portable
--
-- Reader and Writer for DIMACS CNF and family of similar formats.
--
-----------------------------------------------------------------------------
module ToySolver.FileFormat.CNF
(
-- * FileFormat class
module ToySolver.FileFormat.Base
-- * CNF format
, CNF (..)
-- * WCNF format
, WCNF (..)
, WeightedClause
, Weight
-- * GCNF format
, GCNF (..)
, GroupIndex
, GClause
-- * QDIMACS format
, QDimacs (..)
, Quantifier (..)
, QuantSet
, Atom
-- * Re-exports
, Lit
, Clause
, PackedClause
, packClause
, unpackClause
) where
import Prelude hiding (readFile, writeFile)
import Control.DeepSeq
import qualified Data.ByteString.Lazy.Char8 as BS
import Data.ByteString.Builder
import Data.Char
#if !MIN_VERSION_base(4,11,0)
import Data.Monoid
#endif
import ToySolver.FileFormat.Base
import qualified ToySolver.SAT.Types as SAT
import ToySolver.SAT.Types (Lit, Clause, PackedClause, packClause, unpackClause)
-- -------------------------------------------------------------------
-- | DIMACS CNF format
data CNF
= CNF
{ cnfNumVars :: !Int
-- ^ Number of variables
, cnfNumClauses :: !Int
-- ^ Number of clauses
, cnfClauses :: [SAT.PackedClause]
-- ^ Clauses
}
deriving (Eq, Ord, Show, Read)
instance FileFormat CNF where
parse s =
case BS.words l of
(["p","cnf", nvar, nclause]) ->
Right $
CNF
{ cnfNumVars = read $ BS.unpack nvar
, cnfNumClauses = read $ BS.unpack nclause
, cnfClauses = map parseClauseBS ls
}
_ ->
Left "cannot find cnf header"
where
l :: BS.ByteString
ls :: [BS.ByteString]
(l:ls) = filter (not . isCommentBS) (BS.lines s)
render cnf = header <> mconcat (map f (cnfClauses cnf))
where
header = mconcat
[ byteString "p cnf "
, intDec (cnfNumVars cnf), char7 ' '
, intDec (cnfNumClauses cnf), char7 '\n'
]
f c = mconcat [intDec lit <> char7 ' '| lit <- SAT.unpackClause c] <> byteString "0\n"
readInts :: BS.ByteString -> [Int]
readInts s =
case BS.readInt (BS.dropWhile isSpace s) of
Just (0,_) -> []
Just (z,s') -> z : readInts s'
Nothing -> error "ToySolver.FileFormat.CNF.readInts: 0 is missing"
parseClauseBS :: BS.ByteString -> SAT.PackedClause
parseClauseBS = SAT.packClause . readInts
isCommentBS :: BS.ByteString -> Bool
isCommentBS s =
case BS.uncons s of
Just ('c',_) -> True
_ -> False
-- -------------------------------------------------------------------
-- | WCNF format for representing partial weighted Max-SAT problems.
--
-- This format is used for for MAX-SAT evaluations.
--
-- References:
--
-- *
data WCNF
= WCNF
{ wcnfNumVars :: !Int
-- ^ Number of variables
, wcnfNumClauses :: !Int
-- ^ Number of (weighted) clauses
, wcnfTopCost :: !Weight
-- ^ Hard clauses have weight equal or greater than "top".
-- We assure that "top" is a weight always greater than the sum of the weights of violated soft clauses in the solution.
, wcnfClauses :: [WeightedClause]
-- ^ Weighted clauses
}
deriving (Eq, Ord, Show, Read)
-- | Weighted clauses
type WeightedClause = (Weight, SAT.PackedClause)
-- | Weigths must be greater than or equal to 1, and smaller than 2^63.
type Weight = Integer
instance FileFormat WCNF where
parse s =
case BS.words l of
(["p","wcnf", nvar, nclause, top]) ->
Right $
WCNF
{ wcnfNumVars = read $ BS.unpack nvar
, wcnfNumClauses = read $ BS.unpack nclause
, wcnfTopCost = read $ BS.unpack top
, wcnfClauses = map parseWCNFLineBS ls
}
(["p","wcnf", nvar, nclause]) ->
Right $
WCNF
{ wcnfNumVars = read $ BS.unpack nvar
, wcnfNumClauses = read $ BS.unpack nclause
-- top must be greater than the sum of the weights of violated soft clauses.
, wcnfTopCost = fromInteger $ 2^(63::Int) - 1
, wcnfClauses = map parseWCNFLineBS ls
}
(["p","cnf", nvar, nclause]) ->
Right $
WCNF
{ wcnfNumVars = read $ BS.unpack nvar
, wcnfNumClauses = read $ BS.unpack nclause
-- top must be greater than the sum of the weights of violated soft clauses.
, wcnfTopCost = fromInteger $ 2^(63::Int) - 1
, wcnfClauses = map ((\c -> seq c (1,c)) . parseClauseBS) ls
}
_ ->
Left "cannot find wcnf/cnf header"
where
l :: BS.ByteString
ls :: [BS.ByteString]
(l:ls) = filter (not . isCommentBS) (BS.lines s)
render wcnf = header <> mconcat (map f (wcnfClauses wcnf))
where
header = mconcat
[ byteString "p wcnf "
, intDec (wcnfNumVars wcnf), char7 ' '
, intDec (wcnfNumClauses wcnf), char7 ' '
, integerDec (wcnfTopCost wcnf), char7 '\n'
]
f (w,c) = integerDec w <> mconcat [char7 ' ' <> intDec lit | lit <- SAT.unpackClause c] <> byteString " 0\n"
parseWCNFLineBS :: BS.ByteString -> WeightedClause
parseWCNFLineBS s =
case BS.readInteger (BS.dropWhile isSpace s) of
Nothing -> error "ToySolver.FileFormat.CNF: no weight"
Just (w, s') -> seq w $ seq xs $ (w, xs)
where
xs = parseClauseBS s'
-- -------------------------------------------------------------------
-- | Group oriented CNF Input Format
--
-- This format is used in Group oriented MUS track of the SAT Competition 2011.
--
-- References:
--
-- *
data GCNF
= GCNF
{ gcnfNumVars :: !Int
-- ^ Nubmer of variables
, gcnfNumClauses :: !Int
-- ^ Number of clauses
, gcnfLastGroupIndex :: !GroupIndex
-- ^ The last index of a group in the file number of components contained in the file.
, gcnfClauses :: [GClause]
}
deriving (Eq, Ord, Show, Read)
-- | Component number between 0 and `gcnfLastGroupIndex`
type GroupIndex = Int
-- | Clause together with component number
type GClause = (GroupIndex, SAT.PackedClause)
instance FileFormat GCNF where
parse s =
case BS.words l of
(["p","gcnf", nbvar', nbclauses', lastGroupIndex']) ->
Right $
GCNF
{ gcnfNumVars = read $ BS.unpack nbvar'
, gcnfNumClauses = read $ BS.unpack nbclauses'
, gcnfLastGroupIndex = read $ BS.unpack lastGroupIndex'
, gcnfClauses = map parseGCNFLineBS ls
}
(["p","cnf", nbvar', nbclause']) ->
Right $
GCNF
{ gcnfNumVars = read $ BS.unpack nbvar'
, gcnfNumClauses = read $ BS.unpack nbclause'
, gcnfLastGroupIndex = read $ BS.unpack nbclause'
, gcnfClauses = zip [1..] $ map parseClauseBS ls
}
_ ->
Left "cannot find gcnf header"
where
l :: BS.ByteString
ls :: [BS.ByteString]
(l:ls) = filter (not . isCommentBS) (BS.lines s)
render gcnf = header <> mconcat (map f (gcnfClauses gcnf))
where
header = mconcat
[ byteString "p gcnf "
, intDec (gcnfNumVars gcnf), char7 ' '
, intDec (gcnfNumClauses gcnf), char7 ' '
, intDec (gcnfLastGroupIndex gcnf), char7 '\n'
]
f (idx,c) = char7 '{' <> intDec idx <> char7 '}' <>
mconcat [char7 ' ' <> intDec lit | lit <- SAT.unpackClause c] <>
byteString " 0\n"
parseGCNFLineBS :: BS.ByteString -> GClause
parseGCNFLineBS s
| Just ('{', s1) <- BS.uncons (BS.dropWhile isSpace s)
, Just (!idx,s2) <- BS.readInt s1
, Just ('}', s3) <- BS.uncons s2 =
let ys = parseClauseBS s3
in seq ys $ (idx, ys)
| otherwise = error "ToySolver.FileFormat.CNF: parse error"
-- -------------------------------------------------------------------
{-
http://www.qbflib.org/qdimacs.html
::= EOF
::= []
::= |
::= c EOL
::= p cnf EOL
::= []
::= |
::= 0 EOL
::= e | a
::= |
::=
::= |
::= | 0 EOL
::=
::= {A sequence of non-special ASCII characters}
::= {A 32-bit signed integer different from 0}
::= {A 32-bit signed integer greater than 0}
-}
-- | QDIMACS format
--
-- Quantified boolean expression in prenex normal form,
-- consisting of a sequence of quantifiers ('qdimacsPrefix') and
-- a quantifier-free CNF part ('qdimacsMatrix').
--
-- References:
--
-- * QDIMACS standard Ver. 1.1
--
data QDimacs
= QDimacs
{ qdimacsNumVars :: !Int
-- ^ Number of variables
, qdimacsNumClauses :: !Int
-- ^ Number of clauses
, qdimacsPrefix :: [QuantSet]
-- ^ Sequence of quantifiers
, qdimacsMatrix :: [SAT.PackedClause]
-- ^ Clauses
}
deriving (Eq, Ord, Show, Read)
-- | Quantifier
data Quantifier
= E -- ^ existential quantifier (∃)
| A -- ^ universal quantifier (∀)
deriving (Eq, Ord, Show, Read, Enum, Bounded)
-- | Quantified set of variables
type QuantSet = (Quantifier, [Atom])
-- | Synonym of 'SAT.Var'
type Atom = SAT.Var
instance FileFormat QDimacs where
parse = f . BS.lines
where
f [] = Left "ToySolver.FileFormat.CNF.parse: premature end of file"
f (l : ls) =
case BS.uncons l of
Nothing -> Left "ToySolver.FileFormat.CNF.parse: no problem line"
Just ('c', _) -> f ls
Just ('p', s) ->
case BS.words s of
["cnf", numVars', numClauses'] ->
case parsePrefix ls of
(prefix', ls') -> Right $
QDimacs
{ qdimacsNumVars = read $ BS.unpack numVars'
, qdimacsNumClauses = read $ BS.unpack numClauses'
, qdimacsPrefix = prefix'
, qdimacsMatrix = map parseClauseBS ls'
}
_ -> Left "ToySolver.FileFormat.CNF.parse: invalid problem line"
Just (c, _) -> Left ("ToySolver.FileFormat.CNF.parse: invalid prefix " ++ show c)
render qdimacs = problem_line <> prefix' <> mconcat (map f (qdimacsMatrix qdimacs))
where
problem_line = mconcat
[ byteString "p cnf "
, intDec (qdimacsNumVars qdimacs), char7 ' '
, intDec (qdimacsNumClauses qdimacs), char7 '\n'
]
prefix' = mconcat
[ char7 (if q == E then 'e' else 'a') <> mconcat [char7 ' ' <> intDec atom | atom <- atoms] <> byteString " 0\n"
| (q, atoms) <- qdimacsPrefix qdimacs
]
f c = mconcat [intDec lit <> char7 ' '| lit <- SAT.unpackClause c] <> byteString "0\n"
parsePrefix :: [BS.ByteString] -> ([QuantSet], [BS.ByteString])
parsePrefix = go []
where
go result [] = (reverse result, [])
go result lls@(l : ls) =
case BS.uncons l of
Just (c,s)
| c=='a' || c=='e' ->
let atoms = readInts s
q = if c=='a' then A else E
in seq q $ deepseq atoms $ go ((q, atoms) : result) ls
| otherwise ->
(reverse result, lls)
_ -> error "ToySolver.FileFormat.CNF.parseProblem: invalid line"
-- -------------------------------------------------------------------