packages feed

smcdel-1.3.0: bench/muddychildren.hs

{-# LANGUAGE CPP #-}

module Main (main) where

{- $
This benchmark compares different model checking methods using the Muddy Children example.
See "SMCDEL.Examples.MuddyChildren" for the example itself.

We use the /Criterion/ library to benchmark different solution methods, roughly sorted by speed:

- The number triangle approach from Gierasimczuk and Szymanik (2011)
- SMCDEL with CacBDD for S5
- SMCDEL with CUDD for S5 (only when the build flag `with-cudd` is on)
- SMCDEL with CUDD using ZDDs for S5 (only when the build flag `with-cudd` is on)
- SMCDEL with decision-diagrams for S5
- SMCDEL with CacBDD for K
- DEMO-S5 by van Eijck (2014)
- SMCDEL via translation from Kripke models for S5
- SMCDEL via translation from Kripke models for K

The benchmark compares how long it takes to answer the following question:
"For \(n\) children, when \(m\) of them are muddy, how many announcements of
`Nobody knows their own state.' are needed to let at least one child know their own state?".
For this purpose we recursively define the formula to be checked and a loop function which uses the given model checker to find the answer.

To run this benchmark and create a PDF with the results plot, run @make bench/muddychildren.pdf@ in the SMCDEL folder.
-}

import Control.Monad (when)
import Criterion.Main
import qualified Criterion.Types
import qualified Data.ByteString.Lazy as BL
import Data.Char (isSpace)
import Data.Csv
import Data.Function
import Data.List
import Data.List.Split
import Data.Maybe
import Data.Scientific
import qualified Data.Vector as V
import Numeric
import System.Directory

import SMCDEL.Language
import SMCDEL.Examples.MuddyChildren
import SMCDEL.Internal.Help (apply)
import qualified SMCDEL.Explicit.DEMO_S5 as DEMO_S5
import qualified SMCDEL.Explicit.S5
import qualified SMCDEL.Symbolic.S5
import qualified SMCDEL.Symbolic.S5_DD
import qualified SMCDEL.Translations.S5
import qualified SMCDEL.Translations.K
import qualified SMCDEL.Other.MCTRIANGLE
import qualified SMCDEL.Symbolic.K

#ifdef WITH_CUDD
import SMCDEL.Internal.MyHaskCUDD
import qualified SMCDEL.Symbolic.S5_CUDD
#endif

-- | The formula to be checked.
checkForm :: Int -> Int -> Form
checkForm n 0 = nobodyknows n
checkForm n k = PubAnnounce (nobodyknows n) (checkForm n (k-1))

-- | Generic function to solve the puzzle.
-- This will be instantiated with different `evalViaBdd` functions for different BDD packages.
findNumberWith :: (Int -> Int -> a, a -> Form -> Bool) -> Int -> Int -> Int
findNumberWith (start,evalfunction) n m = k where
  k | loop 0 == (m-1) = m-1
    | otherwise       = error $ "wrong Muddy Children result: " ++ show (loop 0)
  loop count = if evalfunction (start n m) (PubAnnounce (father n) (checkForm n count))
    then loop (count+1)
    else count

mudPs :: Int -> [Prp]
mudPs n = [P 1 .. P n]

findNumberCacBDD :: Int -> Int -> Int
findNumberCacBDD = findNumberWith (cacMudScnInit,SMCDEL.Symbolic.S5.evalViaBdd) where
  cacMudScnInit n m = ( SMCDEL.Symbolic.S5.KnS (mudPs n) (SMCDEL.Symbolic.S5.boolBddOf Top) [ (show i,delete (P i) (mudPs n)) | i <- [1..n] ], mudPs m )

findNumberDD :: Int -> Int -> Int
findNumberDD = findNumberWith (ddMudScnInit,SMCDEL.Symbolic.S5_DD.evalViaBdd) where
  ddMudScnInit n m = ( SMCDEL.Symbolic.S5_DD.KnS (mudPs n) (SMCDEL.Symbolic.S5_DD.boolBddOf Top) [ (show i,delete (P i) (mudPs n)) | i <- [1..n] ], mudPs m )

#ifdef WITH_CUDD
findNumberCUDD :: Manager -> Int -> Int -> Int
findNumberCUDD mgr n m =
  let cuddMudScnInit = ( SMCDEL.Symbolic.S5_CUDD.KnS mgr (mudPs n) (SMCDEL.Symbolic.S5_CUDD.boolDdOf mgr Top :: Dd B O1 I1) [ (show i, delete (P i) (mudPs n)) | i <- [1..n] ], mudPs m )
  in findNumberWith (const $ const cuddMudScnInit, SMCDEL.Symbolic.S5_CUDD.evalViaDd) n m

findNumberCUDDz :: Manager -> Int -> Int -> Int
findNumberCUDDz mgr n m =
  let cuddMudScnInit = ( SMCDEL.Symbolic.S5_CUDD.KnS mgr (mudPs n) (SMCDEL.Symbolic.S5_CUDD.boolDdOf mgr Top :: Dd Z O1 I1) [ (show i, delete (P i) (mudPs n)) | i <- [1..n] ], mudPs m )
  in findNumberWith (const $ const cuddMudScnInit, SMCDEL.Symbolic.S5_CUDD.evalViaDd) n m
#endif

findNumberTrans :: Int -> Int -> Int
findNumberTrans = findNumberWith (start,SMCDEL.Symbolic.S5.evalViaBdd) where
  start n m = SMCDEL.Translations.S5.kripkeToKns $ mudKrpInit n m

mudKrpInit :: Int -> Int -> SMCDEL.Explicit.S5.PointedModelS5
mudKrpInit n m = (SMCDEL.Explicit.S5.KrMS5 ws rel val, cur) where
  ws    = [0..(2^n-1)]
  rel   = [ (show i, erelFor i) | i <- [1..n] ] where
    erelFor i = sort $ map sort $
      groupBy ((==) `on` setForAt i) $
      sortOn (setForAt i) ws
    setForAt i s = delete (P i) $ setAt s
    setAt s = map fst $ filter snd (apply val s)
  val         = zip ws table
  cur =
    case filter (\(_,ass)-> sort (map fst $ filter snd ass) == [P 1..P m]) val of
      ((thisCur,_):_) -> thisCur
      _ -> error "No current state found."
  table = foldl buildTable [[]] [P k | k<- [1..n]]
  buildTable partrows p = [ (p,v):pr | v <-[True,False], pr<-partrows ]

findNumberK :: Int -> Int -> Int
findNumberK = findNumberWith (mudBelScnInit, SMCDEL.Symbolic.K.evalViaBdd)

findNumberTransK :: Int -> Int -> Int
findNumberTransK = findNumberWith (start,SMCDEL.Symbolic.K.evalViaBdd) where
  start n m = SMCDEL.Translations.K.kripkeToBls $ mudGenKrpInit n m

-- | Find the Number using DEMO-S5.
-- Here for an explicit state model checker like DEMO-S5 we can not use the
-- same loop function because we want to hand on the current model to the next step
-- instead of computing it again and again.
findNumberDemoS5 :: Int -> Int -> Int
findNumberDemoS5 n m = findNumberDemoLoop 0 start where
  start = DEMO_S5.updPa mudDemoKrpInit (DEMO_S5.fatherN n)
  mudDemoKrpInit = DEMO_S5.Mo states agents [] rels points where
    states = DEMO_S5.bTables n
    agents = map DEMO_S5.Ag [1..n]
    rels = [(DEMO_S5.Ag i, [[tab1++[True]++tab2,tab1++[False]++tab2] |
                     tab1 <- DEMO_S5.bTables (i-1),
                     tab2 <- DEMO_S5.bTables (n-i) ]) | i <- [1..n] ]
    points = [replicate (n-m) False ++ replicate m True]
  findNumberDemoLoop :: Int -> DEMO_S5.EpistM [Bool] -> Int
  findNumberDemoLoop count curMod =
    if DEMO_S5.isTrue curMod (DEMO_S5.dont n)
      then findNumberDemoLoop (count+1) (DEMO_S5.updPa curMod (DEMO_S5.dont n))
      else count

-- | Solve the puzzle with the number triangle approach.
-- See "SMCDEL.Other.MCTRIANGLE" for the details.
-- Note that here the formula \texttt{nobodyknows} does not depend on the number of agents.
-- Therefore the loop function does not have to pass on any variables.
findNumberTriangle :: Int -> Int -> Int
findNumberTriangle n m = findNumberTriangleLoop 0 start where
  start = SMCDEL.Other.MCTRIANGLE.mcUpdate
            (SMCDEL.Other.MCTRIANGLE.mcModel (n-m,m))
            (SMCDEL.Other.MCTRIANGLE.Qf SMCDEL.Other.MCTRIANGLE.some)
  findNumberTriangleLoop count curMod =
    if SMCDEL.Other.MCTRIANGLE.eval curMod SMCDEL.Other.MCTRIANGLE.nobodyknows
      then findNumberTriangleLoop
             (count+1)
             (SMCDEL.Other.MCTRIANGLE.mcUpdate curMod SMCDEL.Other.MCTRIANGLE.nobodyknows)
      else count

main :: IO ()
main = prepareMain >> benchMain >> convertMain

benchMain :: IO ()
benchMain = do
#ifdef WITH_CUDD
  mgr <- makeManagerZ 40 -- one CUDD manager for BDDs and ZDDs
#endif
  defaultMainWith myConfig (map mybench
    [ ("Triangle"  , findNumberTriangle  , [7..40] )
    , ("CacBDD"    , findNumberCacBDD    , [3..40] )
#ifdef WITH_CUDD
    , ("CUDD"      , findNumberCUDD mgr  , [3..40] )
    , ("CUDDz"     , findNumberCUDDz mgr , [3..40] )
#endif
    , ("DD"        , findNumberDD        , [3..30] )
    , ("K"         , findNumberK         , [3..12] )
    , ("DEMOS5"    , findNumberDemoS5    , [3..12] )
    , ("Trans"     , findNumberTrans     , [3..12] )
    , ("TransK"    , findNumberTransK    , [3..10] ) ])
  where
    mybench (name,f,range) = bgroup name $ map (run f) range
    run f k = bench (show k) $ whnf (\n -> f n n) k
    myConfig = defaultConfig { Criterion.Types.csvFile = Just theCSVname }

-- * CSV to pgfplots

-- | The filename to which the benchmark results will be written in CSV.
theCSVname :: String
theCSVname = "bench/muddychildren-results.csv"

prepareMain :: IO ()
prepareMain = do
  oldResults <- doesFileExist theCSVname
  when oldResults $ do
    putStrLn "moving away old results!"
    renameFile theCSVname (theCSVname ++ ".OLD")
    oldDATfile <- doesFileExist (theCSVname ++ ".dat")
    when oldDATfile $ removeFile (theCSVname ++ ".dat")

-- | Convert the .csv file to a .dat file to be use with pgfplots.
convertMain :: IO ()
convertMain = do
  putStrLn "Reading muddychildren-results.csv and converting to .dat for pgfplots."
  c <- BL.readFile theCSVname
  case decode NoHeader c of
    Left e -> error $ "could not parse the csv file:" ++ show e
    Right csv -> do
      let results = map (parseLine . take 2) $ tail $ V.toList (csv :: V.Vector [String])
      let columns = nub.sort $ map (fst.fst) results
      let firstLine = longifyTo 5 "n" ++ dropWhileEnd isSpace (concatMap longify columns)
      let resAt n col = longify $ fromMaybe "nan" $ Data.List.lookup (col,n) results
      let resultrow n = concatMap (resAt n) columns
      let firstcol = nub.sort $ map (snd.fst) results
      let resultrows = map (\n -> longifyTo 5 (show n) ++ dropWhileEnd isSpace (resultrow n)) firstcol
      writeFile (theCSVname ++ ".dat") (intercalate "\n" (firstLine:resultrows) ++ "\n")
  where
    parseLine [namestr,numberstr] = case splitOn "/" namestr of
      [name,nstr] -> ((name,n),valuestr) where
        n = read nstr :: Integer
        value = toRealFloat (read numberstr :: Scientific) :: Double
        valuestr = Numeric.showFFloat (Just 7) value ""
      _ -> error $ "could not parse this case: " ++ namestr
    parseLine l = error $ "could not parse this line:\n  " ++ show l
    longify = longifyTo 14
    longifyTo n s = s ++ replicate (n - length s) ' '