{-# LANGUAGE CPP #-} ----------------------------------------------------------------------------- -- | -- Module : toysolver -- Copyright : (c) Masahiro Sakai 2011-2014 -- License : BSD-style -- -- Maintainer : masahiro.sakai@gmail.com -- Stability : experimental -- Portability : portable -- ----------------------------------------------------------------------------- module Main where import Control.Monad import Control.Concurrent import Data.Array.IArray import Data.Char import Data.Default.Class import Data.List import Data.Maybe import Data.Monoid import Data.Ratio import Data.Scientific (Scientific) import qualified Data.Scientific as Scientific import Data.String import qualified Data.Version as V import qualified Data.Set as Set import Data.Map (Map) import qualified Data.Map as Map import qualified Data.IntMap as IntMap import qualified Data.IntSet as IntSet import qualified Data.Traversable as T import Options.Applicative hiding (Const) import System.Exit import System.Environment import System.FilePath import System.Console.GetOpt import System.IO import Text.Printf import GHC.Conc (getNumProcessors, setNumCapabilities) import Data.OptDir import ToySolver.Data.OrdRel import ToySolver.Data.FOL.Arith as FOL import qualified ToySolver.Data.LA as LA import qualified ToySolver.Data.LA.FOL as LAFOL import qualified ToySolver.Data.Polynomial as P import qualified ToySolver.Data.AlgebraicNumber.Real as AReal import qualified ToySolver.Data.MIP as MIP import qualified ToySolver.Data.MIP.Solution.Gurobi as GurobiSol import qualified ToySolver.Arith.OmegaTest as OmegaTest import qualified ToySolver.Arith.Cooper as Cooper import qualified ToySolver.Arith.Simplex.Textbook.MIPSolver.Simple as TextbookMIP import qualified ToySolver.Arith.Simplex as Simplex import qualified ToySolver.Arith.MIP as MIPSolver import qualified ToySolver.Arith.CAD as CAD import qualified ToySolver.Arith.ContiTraverso as ContiTraverso import qualified ToySolver.FileFormat as FF import ToySolver.Converter import ToySolver.SAT.Printer import qualified ToySolver.SAT.Types as SAT import ToySolver.Version import ToySolver.Internal.Util -- --------------------------------------------------------------------------- data Mode = ModeSAT | ModePB | ModeWBO | ModeMaxSAT | ModeMIP deriving (Eq, Ord, Show) data Options = Options { optInput :: FilePath , optMode :: Maybe Mode , optSolver :: String , optPrintRational :: Bool , optWriteFile :: Maybe FilePath , optNoMIP :: Bool , optPivotStrategy :: Simplex.PivotStrategy -- String , optBoundTightening :: Bool , optNThread :: Int , optOmegaReal :: String , optFileEncoding :: Maybe String } deriving (Eq, Show) optionsParser :: Parser Options optionsParser = Options <$> fileInput <*> modeOption <*> solverOption <*> printRationalOption <*> writeFileOption <*> noMIPOption <*> pivotStrategyOption <*> boundTighteningOption <*> nThreadOption <*> omegaRealOption <*> fileEncodingOption where fileInput :: Parser FilePath fileInput = argument str (metavar "FILE") modeOption :: Parser (Maybe Mode) modeOption = optional $ flag' ModeSAT (long "sat" <> help "solve boolean satisfiability problem in .cnf file") <|> flag' ModePB (long "pb" <> help "solve pseudo boolean problem in .opb file") <|> flag' ModeWBO (long "wbo" <> help "solve weighted boolean optimization problem in .wbo file") <|> flag' ModeMaxSAT (long "maxsat" <> help "solve MaxSAT problem in .cnf or .wcnf file") <|> flag' ModeMIP (long "lp" <> help "solve LP/MIP problem in .lp or .mps file") solverOption :: Parser String solverOption = strOption $ long "solver" <> metavar "SOLVER" <> help "Solver algorithm: mip, omega-test, cooper, cad, old-mip, ct" <> value "mip" <> showDefaultWith id printRationalOption :: Parser Bool printRationalOption = switch $ long "print-rational" <> help "print rational numbers instead of decimals" writeFileOption :: Parser (Maybe FilePath) writeFileOption = optional $ strOption $ short 'w' <> metavar "FILE" <> help "write solution to filename in Gurobi .sol format" noMIPOption :: Parser Bool noMIPOption = switch $ long "nomip" <> help "consider all integer variables as continuous" pivotStrategyOption :: Parser Simplex.PivotStrategy pivotStrategyOption = option (maybeReader Simplex.parsePivotStrategy) $ long "pivot-strategy" <> metavar "NAME" <> help ("pivot strategy for simplex: " ++ intercalate ", " [Simplex.showPivotStrategy ps | ps <- [minBound..maxBound]]) <> value (Simplex.configPivotStrategy def) <> showDefaultWith Simplex.showPivotStrategy boundTighteningOption :: Parser Bool boundTighteningOption = switch $ long "bound-tightening" <> help "enable bound tightening in simplex algorithm" nThreadOption :: Parser Int nThreadOption = option auto $ long "threads" <> metavar "INT" <> help "number of threads to use (0: auto)" <> value 0 <> showDefault omegaRealOption :: Parser String omegaRealOption = strOption $ long "omega-real" <> metavar "SOLVER" <> help "fourier-motzkin, virtual-substitution (or vs), cad, simplex, none" <> value "fourier-motzkin" <> showDefaultWith id fileEncodingOption :: Parser (Maybe String) fileEncodingOption = optional $ strOption $ long "encoding" <> metavar "ENCODING" <> help "file encoding for LP/MPS files" parserInfo :: ParserInfo Options parserInfo = info (helper <*> versionOption <*> optionsParser) $ fullDesc <> header "toysolver - a solver for arithmetic problems" where versionOption :: Parser (a -> a) versionOption = infoOption (V.showVersion version) $ hidden <> long "version" <> help "Show version" #if !MIN_VERSION_optparse_applicative(0,13,0) -- | Convert a function producing a 'Maybe' into a reader. maybeReader :: (String -> Maybe a) -> ReadM a maybeReader f = eitherReader $ \arg -> case f arg of Nothing -> Left $ "cannot parse value `" ++ arg ++ "'" Just a -> Right a #endif -- --------------------------------------------------------------------------- run :: String -> Options -> MIP.Problem Rational -> (Map MIP.Var Rational -> IO ()) -> IO () run solver opt mip printModel = do unless (Set.null (MIP.semiContinuousVariables mip)) $ do hPutStrLn stderr "semi-continuous variables are not supported." exitFailure case map toLower solver of s | s `elem` ["omega", "omega-test", "cooper"] -> solveByQE s | s `elem` ["old-mip"] -> solveByMIP s | s `elem` ["cad"] -> solveByCAD s | s `elem` ["ct", "conti-traverso"] -> solveByContiTraverso _ -> solveByMIP2 where vs = MIP.variables mip vsAssoc = zip (Set.toList vs) [0..] nameToVar = Map.fromList vsAssoc varToName = IntMap.fromList [(v,name) | (name,v) <- vsAssoc] compileE :: MIP.Expr Rational -> Expr Rational compileE = foldr (+) (Const 0) . map compileT . MIP.terms compileT :: MIP.Term Rational -> Expr Rational compileT (MIP.Term c vs) = foldr (*) (Const c) [Var (nameToVar Map.! v) | v <- vs] obj = compileE $ MIP.objExpr $ MIP.objectiveFunction mip cs1 = do v <- Set.toList vs let v2 = Var (nameToVar Map.! v) let (l,u) = MIP.getBounds mip v [Const x .<=. v2 | MIP.Finite x <- return l] ++ [v2 .<=. Const x | MIP.Finite x <- return u] cs2 = do MIP.Constraint { MIP.constrIndicator = ind , MIP.constrExpr = e , MIP.constrLB = lb , MIP.constrUB = ub } <- MIP.constraints mip case ind of Nothing -> do let e2 = compileE e msum [ case lb of MIP.NegInf -> [] MIP.PosInf -> [OrdRel 1 Le 0] -- False MIP.Finite x -> [OrdRel e2 Ge (Const x)] , case ub of MIP.NegInf -> [OrdRel 1 Le 0] -- False MIP.PosInf -> [] MIP.Finite x -> [OrdRel e2 Le (Const x)] ] Just _ -> error "indicator constraint is not supported yet" ivs | optNoMIP opt = Set.empty | otherwise = MIP.integerVariables mip vs2 = IntMap.keysSet varToName ivs2 = IntSet.fromList . map (nameToVar Map.!) . Set.toList $ ivs solveByQE = case mapM LAFOL.fromFOLAtom (cs1 ++ cs2) of Nothing -> do putSLine "UNKNOWN" exitFailure Just cs -> case f vs2 cs ivs2 of Nothing -> do putSLine "UNSATISFIABLE" exitFailure Just m -> do putOLine $ showValue (FOL.evalExpr m obj) putSLine "SATISFIABLE" let m2 = Map.fromAscList [(v, m IntMap.! (nameToVar Map.! v)) | v <- Set.toList vs] printModel m2 where f = case solver of "omega" -> OmegaTest.solveQFLIRAConj omegaOpt "omega-test" -> OmegaTest.solveQFLIRAConj omegaOpt "cooper" -> Cooper.solveQFLIRAConj _ -> error "unknown solver" omegaOpt = def { OmegaTest.optCheckReal = realSolver } where realSolver = case optOmegaReal opt of "fourier-motzkin" -> OmegaTest.checkRealByFM "virtual-substitution" -> OmegaTest.checkRealByVS "vs" -> OmegaTest.checkRealByVS "cad" -> OmegaTest.checkRealByCAD "simplex" -> OmegaTest.checkRealBySimplex "none" -> OmegaTest.checkRealNoCheck s -> error ("unknown solver: " ++ s) solveByMIP = do let m = do cs' <- mapM LAFOL.fromFOLAtom (cs1 ++ cs2) obj' <- LAFOL.fromFOLExpr obj return (cs',obj') case m of Nothing -> do putSLine "UNKNOWN" exitFailure Just (cs',obj') -> case TextbookMIP.optimize (MIP.objDir $ MIP.objectiveFunction mip) obj' cs' ivs2 of TextbookMIP.OptUnsat -> do putSLine "UNSATISFIABLE" exitFailure TextbookMIP.Unbounded -> do putSLine "UNBOUNDED" exitFailure TextbookMIP.Optimum r m -> do putOLine $ showValue r putSLine "OPTIMUM FOUND" let m2 = Map.fromAscList [(v, m IntMap.! (nameToVar Map.! v)) | v <- Set.toList vs] printModel m2 solveByMIP2 = do solver <- Simplex.newSolver let config = def { Simplex.configPivotStrategy = optPivotStrategy opt , Simplex.configEnableBoundTightening = optBoundTightening opt } nthreads = optNThread opt Simplex.setConfig solver config Simplex.setLogger solver putCommentLine Simplex.enableTimeRecording solver replicateM (length vsAssoc) (Simplex.newVar solver) -- XXX Simplex.setOptDir solver $ MIP.objDir $ MIP.objectiveFunction mip Simplex.setObj solver $ fromJust (LAFOL.fromFOLExpr obj) putCommentLine "Loading constraints... " forM_ (cs1 ++ cs2) $ \c -> do Simplex.assertAtom solver $ fromJust (LAFOL.fromFOLAtom c) putCommentLine "Loading constraints finished" mip <- MIPSolver.newSolver solver ivs2 MIPSolver.setShowRational mip printRat MIPSolver.setLogger mip putCommentLine MIPSolver.setOnUpdateBestSolution mip $ \m val -> putOLine (showValue val) procs <- if nthreads >= 1 then return nthreads else do ncap <- getNumCapabilities procs <- getNumProcessors return $ max (procs - 1) ncap setNumCapabilities procs MIPSolver.setNThread mip procs ret <- MIPSolver.optimize mip case ret of Simplex.Unsat -> do putSLine "UNSATISFIABLE" exitFailure Simplex.Unbounded -> do putSLine "UNBOUNDED" Just m <- MIPSolver.getBestModel mip let m2 = Map.fromAscList [(v, m IntMap.! (nameToVar Map.! v)) | v <- Set.toList vs] printModel m2 exitFailure Simplex.Optimum -> do Just m <- MIPSolver.getBestModel mip putSLine "OPTIMUM FOUND" let m2 = Map.fromAscList [(v, m IntMap.! (nameToVar Map.! v)) | v <- Set.toList vs] printModel m2 solveByCAD | not (IntSet.null ivs2) = do putSLine "UNKNOWN" putCommentLine "integer variables are not supported by CAD" exitFailure | otherwise = do let cs = map (fmap f) $ cs1 ++ cs2 vs3 = Set.fromAscList $ IntSet.toAscList vs2 case CAD.solve vs3 cs of Nothing -> do putSLine "UNSATISFIABLE" exitFailure Just m -> do let m2 = IntMap.map (\x -> AReal.approx x (2^^(-64::Int))) $ IntMap.fromAscList $ Map.toAscList $ m putOLine $ showValue (FOL.evalExpr m2 obj) putSLine "SATISFIABLE" let m3 = Map.fromAscList [(v, m2 IntMap.! (nameToVar Map.! v)) | v <- Set.toList vs] printModel m3 where f (Const r) = P.constant r f (Var v) = P.var v f (e1 :+: e2) = f e1 + f e2 f (e1 :*: e2) = f e1 * f e2 f (e1 :/: e2) | P.deg p > 0 = error "can't handle rational expression" | otherwise = P.mapCoeff (/ c) $ f e1 where p = f e2 c = P.coeff P.mone p solveByContiTraverso | not (vs `Set.isSubsetOf` ivs) = do putSLine "UNKNOWN" putCommentLine "continuous variables are not supported by Conti-Traverso algorithm" exitFailure | otherwise = do let tmp = do linObj <- LAFOL.fromFOLExpr obj linCon <- mapM LAFOL.fromFOLAtom (cs1 ++ cs2) return (linObj, linCon) case tmp of Nothing -> do putSLine "UNKNOWN" putCommentLine "non-linear expressions are not supported by Conti-Traverso algorithm" exitFailure Just (linObj, linCon) -> do case ContiTraverso.solve P.grlex vs2 (MIP.objDir $ MIP.objectiveFunction mip) linObj linCon of Nothing -> do putSLine "UNSATISFIABLE" exitFailure Just m -> do let m2 = IntMap.map fromInteger m putOLine $ showValue (FOL.evalExpr m2 obj) putSLine "OPTIMUM FOUND" let m3 = Map.fromAscList [(v, m2 IntMap.! (nameToVar Map.! v)) | v <- Set.toList vs] printModel m3 printRat :: Bool printRat = optPrintRational opt showValue :: Rational -> String showValue = showRational printRat mipPrintModel :: Handle -> Bool -> Map MIP.Var Rational -> IO () mipPrintModel h asRat m = do forM_ (Map.toList m) $ \(v, val) -> do printf "v %s = %s\n" (MIP.fromVar v) (showRational asRat val) putCommentLine :: String -> IO () putCommentLine s = do putStr "c " putStrLn s hFlush stdout putSLine :: String -> IO () putSLine s = do putStr "s " putStrLn s hFlush stdout putOLine :: String -> IO () putOLine s = do putStr "o " putStrLn s hFlush stdout -- --------------------------------------------------------------------------- main :: IO () main = do #ifdef FORCE_CHAR8 setEncodingChar8 #endif o <- execParser parserInfo case fromMaybe ModeMIP (optMode o) of ModeSAT -> do cnf <- FF.readFile (optInput o) let (mip,info) = sat2ip cnf run (optSolver o) o (fmap fromInteger mip) $ \m -> do let m2 = transformBackward info m satPrintModel stdout m2 0 writeSOLFileSAT o m2 ModePB -> do pb <- FF.readFile (optInput o) let (mip,info) = pb2ip pb run (optSolver o) o (fmap fromInteger mip) $ \m -> do let m2 = transformBackward info m pbPrintModel stdout m2 0 writeSOLFileSAT o m2 ModeWBO -> do wbo <- FF.readFile (optInput o) let (mip,info) = wbo2ip False wbo run (optSolver o) o (fmap fromInteger mip) $ \m -> do let m2 = transformBackward info m pbPrintModel stdout m2 0 writeSOLFileSAT o m2 ModeMaxSAT -> do wcnf <- FF.readFile (optInput o) let (mip,info) = maxsat2ip False wcnf run (optSolver o) o (fmap fromInteger mip) $ \m -> do let m2 = transformBackward info m maxsatPrintModel stdout m2 0 writeSOLFileSAT o m2 ModeMIP -> do enc <- T.mapM mkTextEncoding $ optFileEncoding o mip <- MIP.readFile def{ MIP.optFileEncoding = enc } (optInput o) run (optSolver o) o (fmap toRational mip) $ \m -> do mipPrintModel stdout (optPrintRational o) m writeSOLFileMIP o m -- FIXME: 目的関数値を表示するように writeSOLFileMIP :: Options -> Map MIP.Var Rational -> IO () writeSOLFileMIP opt m = do let sol = MIP.Solution { MIP.solStatus = MIP.StatusUnknown , MIP.solObjectiveValue = Nothing , MIP.solVariables = Map.fromList [(v, Scientific.fromFloatDigits (fromRational val :: Double)) | (v,val) <- Map.toList m] } writeSOLFileRaw opt sol -- FIXME: 目的関数値を表示するように writeSOLFileSAT :: Options -> SAT.Model -> IO () writeSOLFileSAT opt m = do let sol = MIP.Solution { MIP.solStatus = MIP.StatusUnknown , MIP.solObjectiveValue = Nothing , MIP.solVariables = Map.fromList [(fromString ("x" ++ show x), if b then 1 else 0) | (x,b) <- assocs m] } writeSOLFileRaw opt sol writeSOLFileRaw :: Options -> MIP.Solution Scientific -> IO () writeSOLFileRaw opt sol = do case optWriteFile opt of Just fname -> GurobiSol.writeFile fname sol Nothing -> return ()