toysolver-0.10.0: src/ToySolver/Internal/SolutionChecker.hs
{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
-----------------------------------------------------------------------------
-- |
-- Module : ToySolver.Internal.SolutionChecker
-- Copyright : (c) Masahiro Sakai 2025
-- License : BSD-style
--
-- Maintainer : masahiro.sakai@gmail.com
-- Stability : unstable
-- Portability : non-portable
--
-----------------------------------------------------------------------------
module ToySolver.Internal.SolutionChecker
( checkSATResult
, checkMaxSATResult
, checkPBResult
, checkWBOResult
, checkMIPResult
) where
import Control.Monad
import Control.Monad.RWS.Lazy
import qualified Data.ByteString.Char8 as BS
import Data.List (intercalate, sortBy)
import qualified Data.Map.Lazy as Map
import Data.Maybe
import Data.Ord
import qualified Data.PseudoBoolean as PBFile
import Data.Scientific
import Data.String
import qualified Data.Text as T
import qualified Numeric.Optimization.MIP as MIP
import Text.Printf
import qualified ToySolver.FileFormat.CNF as CNF
import qualified ToySolver.SAT.Types as SAT
-- ------------------------------------------------------------------------
type M = RWS () [String] Bool
execM :: M () -> (Bool, [String])
execM m = execRWS m () True
addInfo :: String -> M ()
addInfo s = tell [s]
addError :: String -> M ()
addError s = tell [s] >> put False
-- ------------------------------------------------------------------------
checkSATResult :: CNF.CNF -> (BS.ByteString, Maybe SAT.Model) -> (Bool, [String])
checkSATResult cnf (status, m) = execM $ do
case status of
"SATISFIABLE" -> do
when (isNothing m) $ do
addError "SATISFIABLE, but a model is missing"
"UNSATISFIABLE" -> do
when (isJust m) $ do
addError "UNSATISFIABLE, but a model is provided"
"UNKNOWN" -> return ()
_ -> do
addError $ "unknown status: " ++ BS.unpack status
case m of
Nothing -> return ()
Just model -> do
forM_ (CNF.cnfClauses cnf) $ \constr ->
unless (SAT.evalClause model (SAT.unpackClause constr)) $ do
addError $ printf "violated: %s" (showClause constr :: String)
checkMaxSATResult :: CNF.WCNF -> (BS.ByteString, Maybe Integer, Maybe SAT.Model) -> (Bool, [String])
checkMaxSATResult wcnf (status, o, m) = execM $ do
case status of
"OPTIMUM FOUND" -> do
when (isNothing m) $ do
addError "OPTIMUM FOUND, but a model is missing"
"SATISFIABLE" -> do
when (isNothing m) $ do
addError "SATISFIABLE, but a model is missing"
"UNSATISFIABLE" -> do
when (isJust m) $ do
addError "UNSATISFIABLE, but a model is provided"
"UNKNOWN" -> return ()
_ -> do
addError $ "unknown status: " ++ BS.unpack status
case m of
Nothing -> return ()
Just model -> do
cost <- fmap sum $ forM (CNF.wcnfClauses wcnf) $ \(w, constr) ->
if SAT.evalClause model (SAT.unpackClause constr) then do
return 0
else if w == CNF.wcnfTopCost wcnf then do
addError $ printf "violated hard constraint: %s" (showClause constr :: String)
return 0
else do
return w
addInfo $ "total cost = " ++ show cost
case o of
Just oVal | oVal /= cost -> do
addError $ printf "o-line value (%d) is inconsistent" oVal
_ -> return ()
checkPBResult :: PBFile.Formula -> (BS.ByteString, Maybe Integer, Maybe SAT.Model) -> (Bool, [String])
checkPBResult opb (status, o, m) = execM $ do
case status of
"SATISFIABLE" -> do
when (isNothing m) $ do
addError "SATISFIABLE, but a model is missing"
"OPTIMUM FOUND" -> do
when (isNothing m) $ do
addError "OPTIMUM FOUND, but a model is missing"
"UNSATISFIABLE" -> do
when (isJust m) $ do
addError "UNSATISFIABLE, but a model is provided"
"UNSUPPORTED" -> return ()
"UNKNOWN" -> return ()
_ -> do
addError $ "unknown status: " ++ BS.unpack status
case (PBFile.pbObjectiveFunction opb, o) of
(Nothing, Just _) -> do
addInfo $ "o-line(s) exist even though it is a PBS problem. (will be ignored)"
_ -> pure ()
case m of
Nothing -> return ()
Just model -> do
case PBFile.pbObjectiveFunction opb of
Nothing -> return ()
Just objFunc -> do
let val = SAT.evalPBSum model objFunc
addInfo $ "objective function value = " ++ show val
case o of
Just oVal | val /= oVal -> do
addError $ printf "o-line value (%d) is inconsistent" oVal
_ -> return ()
forM_ (PBFile.pbConstraints opb) $ \constr -> do
unless (SAT.evalPBConstraint model constr) $ do
addError $ printf "violated: %s" (showPBConstraint constr :: String)
checkWBOResult :: PBFile.SoftFormula -> (BS.ByteString, Maybe Integer, Maybe SAT.Model) -> (Bool, [String])
checkWBOResult wbo (status, o, m) = execM $ do
case status of
"SATISFIABLE" -> do
when (isNothing m) $ do
addError "SATISFIABLE, but a model is missing"
"OPTIMUM FOUND" -> do
when (isNothing m) $ do
addError "OPTIMUM FOUND, but a model is missing"
"UNSATISFIABLE" -> do
when (isJust m) $ do
addError "UNSATISFIABLE, but a model is provided"
"UNSUPPORTED" -> return ()
"UNKNOWN" -> return ()
_ -> do
addError $ "unknown status: " ++ BS.unpack status
case m of
Nothing -> return ()
Just model -> do
cost <- fmap sum $ forM (PBFile.wboConstraints wbo) $ \(w, constr) -> do
if SAT.evalPBConstraint model constr then
return 0
else do
case w of
Nothing -> do
addError $ printf "violated hard constraint: %s" (showPBConstraint constr :: String)
return 0
Just w' -> do
return w'
addInfo $ "total cost = " ++ show cost
case PBFile.wboTopCost wbo of
Just top | top <= cost -> do
addError $ printf "total cost (%d) is greater than or equal to top cost (%d)" cost top
_ -> return ()
case o of
Just oVal | oVal /= cost -> do
addError $ printf "o-line value (%d) is inconsistent" oVal
_ -> return ()
checkMIPResult :: MIP.Tol Scientific -> MIP.Problem Scientific -> MIP.Solution Scientific -> (Bool, [String])
checkMIPResult tol mip sol = execM $ do
let m = MIP.solVariables sol
let objVal = MIP.eval tol m (MIP.objExpr (MIP.objectiveFunction mip))
addInfo $ "objective value = " ++ show objVal
case MIP.solObjectiveValue sol of
Nothing -> return ()
Just declaredObjVal -> do
unless (abs (objVal - declaredObjVal) <= MIP.feasibilityTol tol) $ do
addError $ printf "declared objective value (%s) does not match to the computed value (%s)"
(show declaredObjVal) (show objVal)
forM_ (Map.toList (MIP.varDomains mip)) $ \(v, (vt, bounds@(lb,ub))) -> do
let val = MIP.eval tol m v
flag1 =
case vt of
MIP.ContinuousVariable -> True
MIP.SemiContinuousVariable -> True
MIP.IntegerVariable -> isIntegral tol val
MIP.SemiIntegerVariable -> isIntegral tol val
flag2 =
case vt of
MIP.ContinuousVariable -> isInBounds tol bounds val
MIP.IntegerVariable -> isInBounds tol bounds val
MIP.SemiIntegerVariable -> isInBounds tol (0,0) val || isInBounds tol bounds val
MIP.SemiContinuousVariable -> isInBounds tol (0,0) val || isInBounds tol bounds val
unless flag1 $ do
addError $ printf "variable %s is not integral" (T.unpack (MIP.varName v))
unless flag2 $ do
let f MIP.NegInf = "-inf"
f MIP.PosInf = "+inf"
f (MIP.Finite x) = show x
addError $ printf "variable %s is out of bounds lb=%s ub=%s" (T.unpack (MIP.varName v)) (f lb) (f ub)
forM_ (MIP.constraints mip) $ \constr -> do
unless (MIP.eval tol m constr) $ do
addError $ case MIP.constrLabel constr of
Just name -> printf "violated: %s" (T.unpack name)
Nothing -> printf "violated: %s" (showMIPConstraint constr)
forM_ (MIP.sosConstraints mip) $ \constr -> do
unless (MIP.eval tol m constr) $ do
addError $ case MIP.sosLabel constr of
Just name -> printf "violated: %s" (T.unpack name)
Nothing -> printf "violated: %s" (showMIPSOSConstraint constr)
-- ------------------------------------------------------------------------
showClause :: (Monoid a, IsString a) => SAT.PackedClause -> a
showClause = foldr (\f g -> f <> fromString " " <> g) mempty . map (fromString . show) . SAT.unpackClause
showPBSum :: (Monoid a, IsString a) => PBFile.Sum -> a
showPBSum = mconcat . map showWeightedTerm
where
showWeightedTerm :: (Monoid a, IsString a) => PBFile.WeightedTerm -> a
showWeightedTerm (c, lits) = foldr (\f g -> f <> fromString " " <> g) mempty (x:xs)
where
x = if c >= 0 then fromString "+" <> fromString (show c) else fromString (show c)
xs = map showLit $ sortBy (comparing abs) lits
showLit :: (Monoid a, IsString a) => PBFile.Lit -> a
showLit lit = if lit > 0 then v else fromString "~" <> v
where
v = fromString "x" <> fromString (show (abs lit))
showPBConstraint :: (Monoid a, IsString a) => PBFile.Constraint -> a
showPBConstraint (lhs, op, rhs) =
showPBSum lhs <> f op <> fromString " " <> fromString (show rhs)
where
f PBFile.Eq = fromString "="
f PBFile.Ge = fromString ">="
showMIPConstraint :: MIP.Constraint Scientific -> String
showMIPConstraint constr = concat
[ case MIP.constrIndicator constr of
Nothing -> ""
Just (MIP.Var var, val) ->
let rhs =
case floatingOrInteger val of
Right (i :: Integer) -> show i
Left (_ :: Double) -> show val -- should be error?
in T.unpack var ++ " = " ++ rhs ++ " -> "
, case MIP.constrLB constr of
MIP.NegInf -> ""
MIP.PosInf -> "+inf <= "
MIP.Finite x -> show x ++ " <= "
, showMIPExpr (MIP.constrExpr constr)
, case MIP.constrUB constr of
MIP.NegInf -> "<= -inf"
MIP.PosInf -> ""
MIP.Finite x -> " <= " ++ show x
]
showMIPSOSConstraint :: MIP.SOSConstraint Scientific -> String
showMIPSOSConstraint constr = concat $
[show (MIP.sosType constr), " ::"] ++ [
" " ++ T.unpack (MIP.varName v) ++ " : " ++ show r
| (v, r) <- MIP.sosBody constr
]
showMIPExpr :: MIP.Expr Scientific -> String
showMIPExpr e = intercalate " "
[ intercalate "*" (((if c >= 0 then "+" ++ show c else show c) : map (T.unpack . MIP.varName) vs))
| MIP.Term c vs <- MIP.terms e
]
isIntegral :: RealFrac r => MIP.Tol r -> r -> Bool
isIntegral tol x = abs (x - fromIntegral (floor (x + 0.5) :: Integer)) <= MIP.integralityTol tol
isInBounds :: (Num r, Ord r) => MIP.Tol r -> MIP.Bounds r -> r -> Bool
isInBounds tol (lb, ub) x =
lb - MIP.Finite (MIP.feasibilityTol tol) <= MIP.Finite x &&
MIP.Finite x <= ub + MIP.Finite (MIP.feasibilityTol tol)
-- ------------------------------------------------------------------------