packages feed

haal-0.4.0.1: src/Haal/Learning/LMstar.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
#ifdef LIQUID
{-# OPTIONS_GHC -fplugin=LiquidHaskell
                -fplugin-opt=LiquidHaskell:--prune-unsorted
                -fplugin-opt=LiquidHaskell:--no-termination #-}
#endif

-- | This module implements the LM* algorithm for learning Mealy automata.
module Haal.Learning.LMstar (
    lmstar,
    LMstar,
    LMstarState (..),
    LMstarConfig (..),
    mkLMstar,
)
where

import Control.Monad (foldM)
import Control.Monad.Reader (MonadReader (ask), MonadTrans (lift))
import Data.Foldable (find)
import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import qualified Data.Set as Set
import Haal.Automaton.MealyAutomaton
import Haal.BlackBox
import Haal.Experiment

{-@ ignore otRefinePlus @-}
{-@ ignore otRefineAngluin @-}
{-@ ignore lmstar @-}

{-@ die :: {v:String | false} -> a @-}
die :: String -> a
die = error

{-@ headLH :: {v:[a] | len v > 0} -> a @-}
headLH :: [a] -> a
headLH [] = die "impossible: headLH called with empty list"
headLH (x : _) = x

{-@ dropLH :: xs:[a] -> {v:Int | 0 <= v && v < len xs} -> {r:[a] | len r = len xs - v} @-}
dropLH :: [a] -> Int -> [a]
dropLH list number
    | number >= length list = die "impossible: dropLH called with number larger than list length"
    | otherwise = drop number list

-- | The 'ObservationTable' type is a data type for storing the observation table of the LM* algorithm.

{-@ data ObservationTable i o = ObservationTable
    { prefixSetS  :: Set.Set [i]
    , suffixSetE  :: Set.Set {v:[i] | len v > 0}
    , mappingT    :: Map.Map ([i], [i]) {v:[o] | len v > 0}
    , prefixSetSI :: Set.Set {v:[i] | len v > 0}
    } @-}
data ObservationTable i o = ObservationTable
    { prefixSetS :: Set.Set [i]
    -- ^ sm = prefix closed set over @i@
    , suffixSetE :: Set.Set [i]
    -- ^ em = suffix closed set over @i@, excluding the empty word
    , mappingT :: Map.Map ([i], [i]) [o]
    -- ^ tm = finite mapping from (sm U (sm * I)) X em -> @o@+
    , prefixSetSI :: Set.Set [i]
    -- ^ sm * I = one-symbol extension of sm
    }
    deriving (Show)

{-@ assume Set.difference :: forall <p :: a -> Bool>.
     Ord a => Set.Set (a<p>) -> Set.Set a -> Set.Set (a<p>)
@-}
{-@ assume Set.cartesianProduct :: forall <p1 :: a -> Bool, p2 :: b -> Bool>.
     (Ord a, Ord b) => Set.Set (a<p1>) -> Set.Set (b<p2>) -> Set.Set (a<p1>, b<p2>)
@-}

{- | The 'LMstarConfig' type is a configuration type for the LM* algorithm.
It allows the user to choose between the original LM* algorithm and the LM+ algorithm.
-}
data LMstarConfig = Star | Plus

-- | The 'LMstarState' type tracks whether the observation table has been initialized.
data LMstarState i o = Uninit | Init (ObservationTable i o)
    deriving (Show)

{-@ measure _isUninit @-}
_isUninit :: LMstarState i o -> Bool
_isUninit Uninit = True
_isUninit _ = False

{-@ measure _lmState @-}
_lmState :: LMstar i o -> LMstarState i o
_lmState (LMstar s) = s
_lmState (LMplus s) = s

-- | The 'LMstar' type wraps an 'LMstarState' and represents the LM* algorithm.
data LMstar i o = LMstar (LMstarState i o) | LMplus (LMstarState i o)

-- | The 'mkLMstar' function creates a new uninitialized instance of the 'LMstar' type.

{-@ mkLMstar :: LMstarConfig -> {v:LMstar i o | _isUninit (_lmState v)} @-}
mkLMstar :: LMstarConfig -> LMstar i o
mkLMstar Star = LMstar Uninit
mkLMstar Plus = LMplus Uninit

-- | The 'equivalentRows' function checks if two rows in the observation table are equivalent.
equivalentRows :: forall i o. (Ord i, Eq o) => ObservationTable i o -> [i] -> [i] -> Bool
equivalentRows ot r1 r2 = and $ Set.map (\e -> mapping (r1, e) == mapping (r2, e)) em
  where
    mapping = flip Map.lookup (mappingT ot)
    em = suffixSetE ot

{- | The 'initializeOT' function initializes the observation table for the LM* algorithm.
It must be in the 'Experiment' monad to allow queries to the SUL.
-}
initializeOT ::
    forall i o sul m.
    (FiniteOrd i, SUL sul m) =>
    ExperimentT (sul i o) m (ObservationTable i o)
initializeOT = do
    sul <- ask
    let
        alph = List.map (: []) $ Set.toList $ inputs sul
        sm = Set.singleton []
        {-@ sm_I :: Set.Set {v:[i] | len v = 1} @-}
        sm_I = Set.fromList alph
        {-@ em :: Set.Set {v:[i] | len v = 1} @-}
        em = Set.fromList alph
        {-@ domain :: Set.Set ([i], {v:[i] | len v = 1}) @-}
        domain = (sm `Set.union` sm_I) `Set.cartesianProduct` em
    sulR <- lift $ reset sul
    tm <- lift $ updateMap Map.empty domain sulR

    return
        ( ObservationTable
            { prefixSetS = sm
            , suffixSetE = em
            , mappingT = tm
            , prefixSetSI = sm_I
            }
        )

-- | The 'equivalenceClasses' function computes the equivalence classes of the observation table.

{-@ equivalenceClasses :: (FiniteOrd i, Eq o) => ObservationTable i o -> Map.Map [i] [[i]] @-}
equivalenceClasses ::
    forall i o.
    (FiniteOrd i, Eq o) =>
    ObservationTable i o ->
    Map.Map [i] [[i]]
equivalenceClasses ot = go Map.empty (sm `Set.union` sm_I)
  where
    sm = prefixSetS ot
    sm_I = prefixSetSI ot
    go acc s
        | Set.null s = acc
        | otherwise =
            let (x, rest) = Set.deleteFindMin s
                (equivClass, remainder) = Set.partition (equivalentRows ot x) rest
                classMembers = x : Set.toList equivClass
             in go (Map.insert x classMembers acc) remainder

-- | The 'lmstar' function implements one iteration of the LM* algorithm.
lmstar ::
    forall sul i o m.
    (SUL sul m, FiniteOrd i, Eq o, Monad m) =>
    LMstar i o ->
    ExperimentT (sul i o) m (LMstar i o, MealyAutomaton StateID i o)
lmstar (LMstar (Init ot)) = case otIsClosed ot of
    [] -> case otIsConsistent ot of
        ([], []) -> case makeHypothesis ot of
            Just hyp -> return (LMstar (Init ot), hyp)
            Nothing -> die "LM*: invariant violation — makeHypothesis failed on closed consistent table"
        inc' -> do
            ot' <- makeConsistent ot inc'
            lmstar (LMstar (Init ot'))
    inc -> do
        ot' <- makeClosed ot inc
        lmstar (LMstar (Init ot'))
lmstar (LMplus (Init ot)) = case otIsClosed ot of
    [] -> case makeHypothesis ot of
        Just hyp -> return (LMplus (Init ot), hyp)
        Nothing -> die "LM+: invariant violation — makeHypothesis failed on closed table"
    inc -> do
        ot' <- makeClosed ot inc
        lmstar (LMplus (Init ot'))
lmstar (LMstar Uninit) = die "lmstar called before initialize"
lmstar (LMplus Uninit) = die "lmstar called before initialize"

{- | The 'otIsClosed' function checks if the observation table is closed.
The observation table is closed if every prefix of `prefixSetSI` belongs
to the same equivalence class as some prefix of `prefixSetS`. If the observation
table is closed, it returns an empty list, whereas if it is not, it returns the
problematic prefix from `prefixSetSI` that does not have the same equivalence class
as any prefix from `prefixSetS`.
-}
otIsClosed :: forall i o. (FiniteOrd i, Eq o) => ObservationTable i o -> [i]
otIsClosed ot = Maybe.fromMaybe [] exists
  where
    sm = prefixSetS ot
    sm_I = prefixSetSI ot

    exists = find (\x -> not $ any (equivalentRows ot x) sm) sm_I

{- | The 'otIsConsistent' function checks if the observation table is consistent.
Returns @([], [])@ if consistent, otherwise returns @([a], e)@ where @a@ is the
distinguishing letter and @e@ is an existing suffix witnessing the inconsistency,
so that @[a] ++ e@ can be added to E.
-}
otIsConsistent :: forall i o. (FiniteOrd i, Eq o) => ObservationTable i o -> ([i], [i])
otIsConsistent ot = Maybe.fromMaybe ([], []) condition
  where
    alph = [minBound .. maxBound] :: [i]
    sm = Set.toList $ prefixSetS ot
    em = Set.toList $ suffixSetE ot

    equivalentPairs = [(r1, r2) | r1 <- sm, r2 <- sm, r1 <= r2, equivalentRows ot r1 r2]

    condition = do
        (s1, s2) <-
            find
                ( \(s1, s2) ->
                    any (\x -> not (equivalentRows ot (s1 ++ [x]) (s2 ++ [x]))) alph
                )
                equivalentPairs
        x <-
            find
                (\x -> not (equivalentRows ot (s1 ++ [x]) (s2 ++ [x])))
                alph
        e <-
            find
                (\e -> Map.lookup (s1 ++ [x], e) (mappingT ot) /= Map.lookup (s2 ++ [x], e) (mappingT ot))
                em
        return ([x], e)

-- | The 'otRefineAngluin' function refines the observation table based on a counterexample, according to Angluin's algorithm.
otRefineAngluin ::
    forall sul i o m.
    (FiniteOrd i, SUL sul m) =>
    ObservationTable i o ->
    [i] ->
    ExperimentT (sul i o) m (ObservationTable i o)
otRefineAngluin ot [] = return ot
otRefineAngluin ot cex = do
    sul <- ask
    let
        sm = prefixSetS ot
        em = suffixSetE ot
        tm = mappingT ot
        -- insert all prefixes of the counterexample
        sm' = List.foldr Set.insert sm [take n cex | n <- [1 .. length cex]]
        sm_I' = Set.fromList [w ++ [a] | w <- Set.toList sm', a <- Set.toList $ inputs sul]
        missing = (sm' `Set.union` sm_I') `Set.cartesianProduct` em

    tm' <- lift $ updateMap tm missing sul
    let ot' = ObservationTable{prefixSetS = sm', suffixSetE = em, mappingT = tm', prefixSetSI = sm_I'}
    return ot'

{- | The 'makeHypothesis' function constructs a Mealy automaton from the observation table. It uses
the default 'StateID' type defined in the 'Experiment' module for representing the automaton states.
Returns 'Nothing' if the observation table is malformed (invariant violated).
-}

{-@ makeHypothesis :: (FiniteOrd i, Eq o) => ObservationTable i o -> Maybe (MealyAutomaton StateID i o) @-}
makeHypothesis :: forall i o. (FiniteOrd i, Eq o) => ObservationTable i o -> Maybe (MealyAutomaton StateID i o)
makeHypothesis ot = do
    startId <- getStateId []
    let stateInputPairs = [(sid, i) | sid <- [0 .. numStates - 1], i <- alphaList]
    deltaEntries <- mapM buildDeltaEntry stateInputPairs
    lambdaEntries <- mapM buildLambdaEntry stateInputPairs
    let deltaMap = Map.fromList deltaEntries
        lambdaMap = Map.fromList lambdaEntries
        delta' sid i = deltaMap Map.! (sid, i)
        lambda' sid i = lambdaMap Map.! (sid, i)
    return $ mkMealyAutomaton delta' lambda' (Set.fromList [0 .. numStates - 1]) startId
  where
    equivMap = equivalenceClasses ot
    repList = Map.keys equivMap
    numStates = length repList
    repToId = Map.fromList (zip repList [0 ..])
    idToRep = Map.fromList (zip [0 ..] repList)
    alphaList = [minBound .. maxBound] :: [i]

    getStateId :: [i] -> Maybe StateID
    getStateId s = List.find (equivalentRows ot s) repList >>= flip Map.lookup repToId

    repAt :: StateID -> Maybe [i]
    repAt sid = Map.lookup sid idToRep

    buildDeltaEntry :: (StateID, i) -> Maybe ((StateID, i), StateID)
    buildDeltaEntry (sid, i) = do
        rep <- repAt sid
        target <- getStateId (rep ++ [i])
        return ((sid, i), target)

    buildLambdaEntry :: (StateID, i) -> Maybe ((StateID, i), o)
    buildLambdaEntry (sid, i) = do
        rep <- repAt sid
        out <- Map.lookup (rep, [i]) (mappingT ot)
        return ((sid, i), headLH out)

-- | The 'makeConsistent' function makes the observation table consistent by adding missing prefixes.

{-@ makeConsistent :: (FiniteOrd i, SUL sul m) =>
      ObservationTable i o ->
      ({v:[i] | len v = 1}, {v:[i] | len v >= 1}) ->
      ExperimentT (sul i o) m (ObservationTable i o) @-}
makeConsistent ::
    forall i o sul m.
    (FiniteOrd i, SUL sul m) =>
    ObservationTable i o ->
    ([i], [i]) ->
    ExperimentT (sul i o) m (ObservationTable i o)
makeConsistent ot ([], []) = return ot
makeConsistent ot (symbol, column) = do
    sul <- ask
    let
        query = symbol ++ column
        em = suffixSetE ot
        em' = query `Set.insert` em
        sm = prefixSetS ot
        sm_I = prefixSetSI ot
        tm = mappingT ot
        missing = (sm `Set.union` sm_I) `Set.cartesianProduct` Set.singleton query
    tm' <- lift $ updateMap tm missing sul
    return (ObservationTable{prefixSetS = sm, suffixSetE = em', mappingT = tm', prefixSetSI = sm_I})

-- | The 'makeClosed' function makes the observation table closed by adding missing suffixes.

{-@ makeClosed :: (FiniteOrd i, SUL sul m) =>
      ObservationTable i o ->
      {v:[i] | len v > 0} ->
      ExperimentT (sul i o) m (ObservationTable i o) @-}
makeClosed ::
    forall sul i o m.
    (FiniteOrd i, SUL sul m) =>
    ObservationTable i o ->
    [i] ->
    ExperimentT (sul i o) m (ObservationTable i o)
makeClosed ot [] = return ot
makeClosed ot inc = do
    sul <- ask
    let alph = inputs sul
        sm = prefixSetS ot
        em = suffixSetE ot
        tm = mappingT ot
        sm' = inc `Set.insert` sm
        sm_I' = Set.map (\(w, a) -> w ++ [a]) (sm' `Set.cartesianProduct` alph)
        newPrefixes = Set.map (\a -> inc ++ [a]) alph
        missing = newPrefixes `Set.cartesianProduct` em
    tm' <- lift $ updateMap tm missing sul
    return (ObservationTable{prefixSetS = sm', suffixSetE = em, mappingT = tm', prefixSetSI = sm_I'})

instance Learner LMstar MealyAutomaton StateID where
    initialize (LMstar _) = do
        LMstar . Init <$> initializeOT
    initialize (LMplus _) = do
        LMplus . Init <$> initializeOT

    refine (LMstar (Init ot)) cex = do
        ot' <- otRefineAngluin ot cex
        return (LMstar (Init ot'))
    refine (LMplus (Init ot)) cex = do
        ot' <- otRefinePlus ot cex
        return (LMplus (Init ot'))
    refine (LMstar Uninit) _ = initialize (LMstar Uninit)
    refine (LMplus Uninit) _ = initialize (LMplus Uninit)

    learn = lmstar

{- | The 'otRefinePlus' function refines the observation table based on a counterexample, according to the LM+ algorithm,
which is an improvement over Angluin's algorithm.
-}

{-@ otRefinePlus :: (FiniteOrd i, SUL sul m) =>
      ObservationTable i o ->
      [i] ->
      ExperimentT (sul i o) m (ObservationTable i o) @-}
otRefinePlus ::
    forall sul i o m.
    (FiniteOrd i, SUL sul m) =>
    ObservationTable i o ->
    [i] ->
    ExperimentT (sul i o) m (ObservationTable i o)
otRefinePlus ot [] = return ot
otRefinePlus ot cex = do
    sul <- ask
    let sm = prefixSetS ot
        em = suffixSetE ot
        tm = mappingT ot
        sm_I = prefixSetSI ot
        -- look for the longest prefix of the counterexample
        -- that is in sm U sm_I
        prefixes = List.inits cex
        suffixes = List.tails cex
        pairs = List.reverse $ List.zip prefixes suffixes
        wrapped = List.find (\x -> Set.member (fst x) sm || Set.member (fst x) sm_I) pairs
    case wrapped of
        Nothing -> return ot
        -- TODO: suffix triggers liquid haskell false
        Just (_, suffix) -> do
            let
                -- the suffix is the distinguishing suffix. insert all non-empty tails not already in E
                newSuffixes = Set.fromList (init $ List.tails suffix) `Set.difference` em
                em' = List.foldr Set.insert em newSuffixes
                missing = (sm `Set.union` sm_I) `Set.cartesianProduct` newSuffixes
            tm' <- lift $ updateMap tm missing sul
            return (ObservationTable{prefixSetS = sm, suffixSetE = em', mappingT = tm', prefixSetSI = sm_I})

{-@ insertStep
      :: (Ord i, SUL sul m, Monad m)
      => sul i o
      -> Map.Map ([i],[i]) {v:[o] | len v > 0}
      -> ([i], {b:[i] | len b > 0})
      -> m (Map.Map ([i],[i]) {v:[o] | len v > 0}) @-}
insertStep ::
    (Ord i, SUL sul m, Monad m) =>
    sul i o ->
    Map.Map ([i], [i]) [o] ->
    ([i], [i]) ->
    m (Map.Map ([i], [i]) [o])
insertStep thesul acc (a, b) = do
    (_, outs) <- walk thesul (a ++ b)
    -- the table is prefix closed, so no need to store
    -- the whole length of outs, just the output that corresponds
    -- to the suffix
    pure (Map.insert (a, b) (dropLH outs (length a)) acc)

{-@ updateMap
      :: (Ord i, SUL sul m, Monad m)
      => Map.Map ([i],[i]) {v:[o] | len v > 0}
      -> Set.Set ([i], {v:[i] | len v > 0})
      -> sul i o
      -> m (Map.Map ([i],[i]) {v:[o] | len v > 0}) @-}
updateMap ::
    (Ord i, SUL sul m, Monad m) =>
    Map.Map ([i], [i]) [o] ->
    Set.Set ([i], [i]) ->
    sul i o ->
    m (Map.Map ([i], [i]) [o])
updateMap themap thestuff thesul =
    foldM (insertStep thesul) themap thestuff