mirror of
https://git.cs.ou.nl/joshua.moerman/mealy-decompose.git
synced 2025-04-30 02:07:44 +02:00
Algorithm to compute splitting trees. (TODO: adapt to uncertain outputs)
This commit is contained in:
parent
74f547ed38
commit
fe21bc794c
4 changed files with 381 additions and 2 deletions
30
app/Playground.hs
Normal file
30
app/Playground.hs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import DotParser ( convertToMealy, parseTransFull )
|
||||||
|
import Mealy ( MealyMachine(..) )
|
||||||
|
import SplittingTree ( PRState(..), refine, initialPRState )
|
||||||
|
|
||||||
|
import Control.Monad.Trans.State ( execStateT )
|
||||||
|
import Data.Map.Strict qualified as Map
|
||||||
|
import Data.Maybe ( mapMaybe )
|
||||||
|
import System.Environment ( getArgs )
|
||||||
|
import Text.Megaparsec ( parseMaybe )
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
[dotFile] <- getArgs
|
||||||
|
print dotFile
|
||||||
|
transitions <- mapMaybe (parseMaybe parseTransFull) . lines <$> readFile dotFile
|
||||||
|
|
||||||
|
-- convert to mealy
|
||||||
|
let
|
||||||
|
MealyMachine{..} = convertToMealy transitions
|
||||||
|
outputFuns = [(i, fun) | i <- inputs, let fun s = fst (behaviour s i)]
|
||||||
|
reverseTransitionMaps i = Map.fromListWith (++) [ (t, [s]) | s <- states, let t = snd (behaviour s i)]
|
||||||
|
reverseFuns = [(i, fun) | i <- inputs, let m = reverseTransitionMaps i, let fun s = Map.findWithDefault [] s m]
|
||||||
|
|
||||||
|
tree <- execStateT (refine print outputFuns reverseFuns) (initialPRState states)
|
||||||
|
|
||||||
|
print (partition tree)
|
||||||
|
print (splittingTree tree)
|
||||||
|
|
|
@ -31,7 +31,8 @@ library
|
||||||
MealyRefine,
|
MealyRefine,
|
||||||
Merger,
|
Merger,
|
||||||
Partition,
|
Partition,
|
||||||
Preorder
|
Preorder,
|
||||||
|
SplittingTree
|
||||||
build-depends:
|
build-depends:
|
||||||
vector
|
vector
|
||||||
|
|
||||||
|
@ -42,6 +43,13 @@ executable mealy-decompose
|
||||||
build-depends:
|
build-depends:
|
||||||
mealy-decompose
|
mealy-decompose
|
||||||
|
|
||||||
|
executable mealy-decompose-playground
|
||||||
|
import: stuff
|
||||||
|
hs-source-dirs: app
|
||||||
|
main-is: Playground.hs
|
||||||
|
build-depends:
|
||||||
|
mealy-decompose
|
||||||
|
|
||||||
test-suite mealy-decompose-test
|
test-suite mealy-decompose-test
|
||||||
import: stuff
|
import: stuff
|
||||||
hs-source-dirs: test
|
hs-source-dirs: test
|
||||||
|
|
|
@ -8,7 +8,7 @@ import Preorder
|
||||||
import Control.Monad.Trans.State.Strict (runState, get, put)
|
import Control.Monad.Trans.State.Strict (runState, get, put)
|
||||||
import Data.Coerce (coerce)
|
import Data.Coerce (coerce)
|
||||||
import Data.Map.Strict qualified as Map
|
import Data.Map.Strict qualified as Map
|
||||||
import Data.Partition (Partition(..), numStates, blockOfState)
|
import Data.Partition (Partition(..), numStates, blockOfState, toBlocks)
|
||||||
import Data.Partition.Common (Block(..))
|
import Data.Partition.Common (Block(..))
|
||||||
import Data.Vector qualified as V
|
import Data.Vector qualified as V
|
||||||
|
|
||||||
|
|
341
src/SplittingTree.hs
Normal file
341
src/SplittingTree.hs
Normal file
|
@ -0,0 +1,341 @@
|
||||||
|
{-# language PartialTypeSignatures #-}
|
||||||
|
{-# OPTIONS_GHC -Wno-partial-type-signatures #-}
|
||||||
|
|
||||||
|
module SplittingTree where
|
||||||
|
|
||||||
|
import Control.Monad.Trans.Class
|
||||||
|
import Control.Monad.Trans.State
|
||||||
|
import Data.Map.Strict qualified as Map
|
||||||
|
import Data.Coerce (coerce)
|
||||||
|
import Data.List (sortOn)
|
||||||
|
|
||||||
|
newtype Block = Block Int
|
||||||
|
deriving (Eq, Ord, Read, Show, Enum)
|
||||||
|
|
||||||
|
-- A partition is represented by a map s -> Block. Two elements mapped to the
|
||||||
|
-- same block are equivalent. Note that a permutation of the blocks will
|
||||||
|
-- not change the partition, but it does change the underlying representation.
|
||||||
|
-- (That is why I haven't given it an Eq instance yet.)
|
||||||
|
newtype Partition s = Partition { getPartition :: Map.Map s Block }
|
||||||
|
deriving (Read, Show)
|
||||||
|
|
||||||
|
-- Determines whether two elements are equivalent in the partition.
|
||||||
|
sameBlock :: Ord s => Partition s -> s -> s -> Bool
|
||||||
|
sameBlock (Partition m) s t = m Map.! s == m Map.! t
|
||||||
|
|
||||||
|
-- In the splitting tree, we record the splits we have made during partition
|
||||||
|
-- refinement. The leafs correspond to the blocks in the partition, and the
|
||||||
|
-- other nodes will be inner nodes. An inner node is just a number, the
|
||||||
|
-- associated information is kept in the SplittingTree type.
|
||||||
|
newtype InnerNode = Node Int
|
||||||
|
deriving (Eq, Ord, Read, Show, Enum)
|
||||||
|
|
||||||
|
-- Note that this type of tree is given by the "parent relation", so it is not
|
||||||
|
-- inductively defined. (And we don't need that for partition refinement.)
|
||||||
|
data SplittingTree s i o = SplittingTree
|
||||||
|
{ label :: Map.Map InnerNode [i]
|
||||||
|
-- ^ The separating word for this node. We use a lazy list for this, because
|
||||||
|
-- sharing is important here.
|
||||||
|
, innerParent :: Map.Map InnerNode (InnerNode, o)
|
||||||
|
-- ^ Tree structure. The node without parent is the root.
|
||||||
|
, blockParent :: Map.Map Block (InnerNode, o)
|
||||||
|
-- ^ Tree structure of the leaves.
|
||||||
|
, size :: Map.Map Block Int
|
||||||
|
-- ^ Size of each block of the partition
|
||||||
|
}
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
-- Add size, and perhaps some info about the missing subblock
|
||||||
|
data Splitter s i o = Splitter
|
||||||
|
{ split :: Map.Map o [s]
|
||||||
|
, leftOut :: o
|
||||||
|
, witness :: [i]
|
||||||
|
}
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
-- The data structure used during partition refinement.
|
||||||
|
data PRState s i o = PRState
|
||||||
|
{ partition :: Partition s
|
||||||
|
, nextBlockId :: Block
|
||||||
|
, splittingTree :: SplittingTree s i o
|
||||||
|
, nextNodeId :: InnerNode
|
||||||
|
}
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
updatePartition :: (Monad m, Ord s) => s -> Block -> StateT (PRState s i o) m ()
|
||||||
|
updatePartition s b = modify foo where
|
||||||
|
foo prs@PRState{..} = prs { partition = coerce (Map.insert s b) partition }
|
||||||
|
|
||||||
|
updateSize :: Monad m => Block -> Int -> StateT (PRState s i o) m Int
|
||||||
|
updateSize b n =
|
||||||
|
modify (\prs@PRState{..} -> prs { splittingTree = splittingTree { size = Map.insert b n (size splittingTree) }})
|
||||||
|
>> return n
|
||||||
|
|
||||||
|
genNextBlockId :: Monad m => StateT (PRState s i o) m Block
|
||||||
|
genNextBlockId = do
|
||||||
|
idx <- gets nextBlockId
|
||||||
|
modify (\prs@PRState{..} -> prs { nextBlockId = succ nextBlockId })
|
||||||
|
return idx
|
||||||
|
|
||||||
|
updateParent :: Monad m => Either Block InnerNode -> InnerNode -> o -> StateT (PRState s i o) m ()
|
||||||
|
updateParent (Left block) target output = modify foo where
|
||||||
|
foo prs@PRState{..} = prs { splittingTree = splittingTree { blockParent = Map.insert block (target, output) (blockParent splittingTree) }}
|
||||||
|
updateParent (Right node) target output = modify foo where
|
||||||
|
foo prs@PRState{..} = prs { splittingTree = splittingTree { innerParent = Map.insert node (target, output) (innerParent splittingTree) }}
|
||||||
|
|
||||||
|
updateLabel :: Monad m => InnerNode -> [i] -> StateT (PRState s i o) m ()
|
||||||
|
updateLabel node witness = modify (\prs@PRState{..} -> prs { splittingTree = splittingTree { label = Map.insert node witness (label splittingTree) }})
|
||||||
|
|
||||||
|
genNextNodeId :: Monad m => StateT (PRState s i o) m InnerNode
|
||||||
|
genNextNodeId = do
|
||||||
|
idx <- gets nextNodeId
|
||||||
|
modify (\prs@PRState{..} -> prs { nextNodeId = succ nextNodeId })
|
||||||
|
return idx
|
||||||
|
|
||||||
|
refineWithSplitter :: (Monad m, Ord o, Ord s) => i -> (s -> [s]) -> Splitter s i o -> StateT (PRState s i o) m [Splitter s i o]
|
||||||
|
refineWithSplitter action rev Splitter{..} = do
|
||||||
|
currentPartition <- getPartition <$> gets partition
|
||||||
|
currentSplittingTree <- gets splittingTree
|
||||||
|
|
||||||
|
let
|
||||||
|
-- For each block in the splitter, we get its predecessors.
|
||||||
|
predecessors = Map.map (concatMap rev) split
|
||||||
|
|
||||||
|
-- For each block that we found, we are going to create temporary children.
|
||||||
|
-- Only when the splitter actually splits the subblock, we change the
|
||||||
|
-- partition. We work on list here, because we are going to re-order
|
||||||
|
-- the data anyways.
|
||||||
|
tempChildsList = [(b, [(o, [s])]) | (o, ls) <- Map.toList predecessors, s <- ls, let b = currentPartition Map.! s]
|
||||||
|
|
||||||
|
-- We need it sorted on the block and the output
|
||||||
|
tempChildsMaps = Map.map (Map.fromListWith (++)) . Map.fromListWith (++) $ tempChildsList
|
||||||
|
|
||||||
|
-- Now we need to check the 3-way split:
|
||||||
|
-- * Some blocks have no states which occured, these don't appear.
|
||||||
|
-- * Some blocks have all states move to a single subblock, this is not a
|
||||||
|
-- proper split and should be removed.
|
||||||
|
-- * Some blocks have different outputs (a proper split) or states which
|
||||||
|
-- moved and states which didn't.
|
||||||
|
properSplit b os
|
||||||
|
| Map.null os = error "Should not happen"
|
||||||
|
| Map.size os >= 2 = True
|
||||||
|
| length (head (Map.elems os)) == size currentSplittingTree Map.! b = False
|
||||||
|
| otherwise = True
|
||||||
|
|
||||||
|
-- We keep the proper splits only
|
||||||
|
tempChildsMaps2 = Map.filterWithKey properSplit tempChildsMaps
|
||||||
|
|
||||||
|
-- Now we can assign new blocks to the newly split states.
|
||||||
|
updateSubBlock nNIdx o ls = do
|
||||||
|
-- Create a new sub-block
|
||||||
|
nBIdx <- genNextBlockId
|
||||||
|
-- Set all states to that id
|
||||||
|
mapM_ (\s -> updatePartition s nBIdx) ls
|
||||||
|
-- And update the tree
|
||||||
|
updateParent (Left nBIdx) nNIdx o
|
||||||
|
n <- updateSize nBIdx (length ls)
|
||||||
|
return (n, ls)
|
||||||
|
|
||||||
|
updateBlock b children = do
|
||||||
|
-- Create a new inner node
|
||||||
|
nNIdx <- genNextNodeId
|
||||||
|
-- Update all sub-blocks
|
||||||
|
sizesAndSubblocks <- Map.traverseWithKey (updateSubBlock nNIdx) children
|
||||||
|
|
||||||
|
-- There may be states remaining in b (because we process the smaller
|
||||||
|
-- halves). So we need to update its size.
|
||||||
|
-- TODO: Do we need to remove b if it is empty?
|
||||||
|
let oldSize = size currentSplittingTree Map.! b
|
||||||
|
missingSize = oldSize - sum (Map.map fst sizesAndSubblocks)
|
||||||
|
_ <- updateSize b missingSize
|
||||||
|
|
||||||
|
-- Also update the "missing block". This means b becomes a child of nNIdx
|
||||||
|
-- and nNIdx a child of the current parent of b.
|
||||||
|
-- And we update the witness by prepending the action
|
||||||
|
let (currentParent, op) = blockParent currentSplittingTree Map.! b
|
||||||
|
newWitness = action:witness
|
||||||
|
updateParent (Right nNIdx) currentParent op
|
||||||
|
updateParent (Left b) nNIdx leftOut
|
||||||
|
updateLabel nNIdx newWitness
|
||||||
|
|
||||||
|
-- Lastly, we make the new splitter. We cannot properly remove the
|
||||||
|
-- largest subblock, as we would have to look up its states. I'm not
|
||||||
|
-- sure it's worth the effort, so we only do it when we can remove a
|
||||||
|
-- subblock.
|
||||||
|
if missingSize == 0
|
||||||
|
then let ls = Map.toList sizesAndSubblocks
|
||||||
|
-- TODO: sort(On) is unnecessarily expensive, we only need to
|
||||||
|
-- know the biggest...
|
||||||
|
((o1, _) : smallerBlocks) = sortOn (\(_, (n, _)) -> -n) ls
|
||||||
|
in
|
||||||
|
return Splitter
|
||||||
|
{ split = Map.fromList (fmap (\(o, (_, lss)) -> (o, lss)) smallerBlocks)
|
||||||
|
, leftOut = o1
|
||||||
|
, witness = newWitness
|
||||||
|
}
|
||||||
|
else return Splitter
|
||||||
|
{ split = Map.map snd sizesAndSubblocks
|
||||||
|
, leftOut = leftOut
|
||||||
|
, witness = newWitness
|
||||||
|
}
|
||||||
|
|
||||||
|
Map.elems <$> Map.traverseWithKey updateBlock tempChildsMaps2
|
||||||
|
|
||||||
|
refineWithOutput :: (Monad m, Ord o, Ord s) => i -> (s -> o) -> StateT (PRState s i o) m [Splitter s i o]
|
||||||
|
refineWithOutput action out = do
|
||||||
|
currentPartition <- getPartition <$> gets partition
|
||||||
|
currentSplittingTree <- gets splittingTree
|
||||||
|
|
||||||
|
let
|
||||||
|
-- Compute all outputs and (existing blocks)
|
||||||
|
tempChildsList = [(b, [(o, [s])]) | (s, b) <- Map.toList currentPartition, let o = out s]
|
||||||
|
|
||||||
|
-- Then sort them on blocks and outputs
|
||||||
|
tempChildsMaps = Map.map (Map.fromListWith (++)) . Map.fromListWith (++) $ tempChildsList
|
||||||
|
|
||||||
|
-- Only consider actual splits
|
||||||
|
tempChildsMaps2 = Map.filter (\children -> Map.size children >= 2) tempChildsMaps
|
||||||
|
|
||||||
|
updateStates nNIdx o ss = do
|
||||||
|
-- Create a new sub-block
|
||||||
|
nBIdx <- genNextBlockId
|
||||||
|
-- Set all states to that id
|
||||||
|
mapM_ (\s -> updatePartition s nBIdx) ss
|
||||||
|
-- And update the tree
|
||||||
|
updateParent (Left nBIdx) nNIdx o
|
||||||
|
_ <- updateSize nBIdx (length ss)
|
||||||
|
return ()
|
||||||
|
|
||||||
|
updateBlock b children = do
|
||||||
|
-- We skip the biggest part, and don't update the blocks.
|
||||||
|
let ((o1, biggest) : smaller) = sortOn (\(_, ss) -> negate (length ss)) . Map.toList $ children
|
||||||
|
witness = [action]
|
||||||
|
|
||||||
|
-- For the remaining blocks, we update the partition
|
||||||
|
nNIdx <- genNextNodeId
|
||||||
|
_ <- traverse (\(o, ss) -> updateStates nNIdx o ss) smaller
|
||||||
|
|
||||||
|
-- If we are doing the very first split, the nNIdx node does not have a
|
||||||
|
-- parent. So we don't have to do updates. Now nNIdx will be the root.
|
||||||
|
case Map.lookup b (blockParent currentSplittingTree) of
|
||||||
|
Nothing -> return ()
|
||||||
|
Just (currentParent, op) -> updateParent (Right nNIdx) currentParent op
|
||||||
|
|
||||||
|
updateLabel nNIdx witness
|
||||||
|
|
||||||
|
-- Remember to update the tree structure for the biggest (skipped) block
|
||||||
|
updateParent (Left b) nNIdx o1
|
||||||
|
_ <- updateSize b (length biggest)
|
||||||
|
|
||||||
|
-- Return the splitter, not that we already skipped the larger part.
|
||||||
|
return Splitter
|
||||||
|
{ split = Map.fromList smaller
|
||||||
|
, leftOut = o1
|
||||||
|
, witness = witness
|
||||||
|
}
|
||||||
|
|
||||||
|
Map.elems <$> Map.traverseWithKey updateBlock tempChildsMaps2
|
||||||
|
|
||||||
|
initialPRState :: Ord s => [s] -> PRState s i o
|
||||||
|
initialPRState ls = PRState
|
||||||
|
{ partition = Partition . Map.fromList $ [(s, Block 0) | s <- ls]
|
||||||
|
, nextBlockId = Block 1
|
||||||
|
, splittingTree = SplittingTree
|
||||||
|
{ label = Map.empty
|
||||||
|
, innerParent = Map.empty
|
||||||
|
, blockParent = Map.empty
|
||||||
|
, size = Map.singleton (Block 0) (length ls)
|
||||||
|
}
|
||||||
|
, nextNodeId = Node 0
|
||||||
|
}
|
||||||
|
|
||||||
|
refineWithAllOutputs :: (Monad m, Ord o, Ord s) => [(i, (s -> o))] -> StateT (PRState s i o) m [Splitter s i o]
|
||||||
|
refineWithAllOutputs ls = concat <$> traverse (uncurry refineWithOutput) ls
|
||||||
|
|
||||||
|
refineWithSplitterAllInputs :: (Monad m, Ord o, Ord s) => [(i, (s -> [s]))] -> Splitter s i o -> StateT (PRState s i o) m [Splitter s i o]
|
||||||
|
refineWithSplitterAllInputs ls splitter = concat <$> traverse (\(i, rev) -> refineWithSplitter i rev splitter) ls
|
||||||
|
|
||||||
|
refine :: (Monad m, Ord o, Ord s) => ([i] -> m ()) -> [(i, (s -> o))] -> [(i, (s -> [s]))] -> StateT (PRState s i o) m ()
|
||||||
|
refine ping outputs transitionsReverse = do
|
||||||
|
initialQueue <- refineWithAllOutputs outputs
|
||||||
|
|
||||||
|
let
|
||||||
|
loop [] = return ()
|
||||||
|
loop (splitter : splitters) = do
|
||||||
|
_ <- lift (ping (witness splitter))
|
||||||
|
newQueue <- refineWithSplitterAllInputs transitionsReverse splitter
|
||||||
|
loop (splitters <> newQueue)
|
||||||
|
|
||||||
|
loop initialQueue
|
||||||
|
|
||||||
|
-- Below is an example
|
||||||
|
|
||||||
|
outputA :: String -> Bool
|
||||||
|
outputA "s1" = False
|
||||||
|
outputA "s2" = True
|
||||||
|
outputA "s3" = False
|
||||||
|
outputA "s4" = True
|
||||||
|
outputA "s5" = False
|
||||||
|
outputA "s6" = True
|
||||||
|
outputA str = error $ "outputA '" ++ str ++ "'"
|
||||||
|
|
||||||
|
outputB :: String -> Bool
|
||||||
|
outputB = const False
|
||||||
|
|
||||||
|
reverseA :: String -> [String]
|
||||||
|
reverseA "s1" = ["s6"]
|
||||||
|
reverseA "s2" = ["s1"]
|
||||||
|
reverseA "s3" = ["s2"]
|
||||||
|
reverseA "s4" = ["s3"]
|
||||||
|
reverseA "s5" = ["s4"]
|
||||||
|
reverseA "s6" = ["s5"]
|
||||||
|
reverseA str = error $ "reverseA '" ++ str ++ "'"
|
||||||
|
|
||||||
|
reverseB :: String -> [String]
|
||||||
|
reverseB "s1" = ["s1", "s6"]
|
||||||
|
reverseB "s2" = []
|
||||||
|
reverseB "s3" = ["s2"]
|
||||||
|
reverseB "s4" = ["s3"]
|
||||||
|
reverseB "s5" = ["s4"]
|
||||||
|
reverseB "s6" = ["s5"]
|
||||||
|
reverseB str = error $ "reverseB '" ++ str ++ "'"
|
||||||
|
|
||||||
|
splitter1 :: Splitter String Char Bool
|
||||||
|
splitter1 = Splitter
|
||||||
|
{ split = Map.fromList [(False, ["s1", "s3", "s5"])]
|
||||||
|
, leftOut = True
|
||||||
|
, witness = "a"
|
||||||
|
}
|
||||||
|
|
||||||
|
splitter2 :: Splitter String Char Bool
|
||||||
|
splitter2 = Splitter
|
||||||
|
{ split = Map.fromList [(True, ["s2", "s4", "s6"])]
|
||||||
|
, leftOut = False
|
||||||
|
, witness = "a"
|
||||||
|
}
|
||||||
|
|
||||||
|
prState0 :: PRState String Char Bool
|
||||||
|
prState0 = PRState
|
||||||
|
{ partition = Partition $ Map.fromList [("s1", Block 0), ("s2", Block 0), ("s3", Block 0), ("s4", Block 0), ("s5", Block 0), ("s6", Block 0)]
|
||||||
|
, nextBlockId = Block 1
|
||||||
|
, splittingTree = SplittingTree
|
||||||
|
{ label = Map.empty
|
||||||
|
, innerParent = Map.empty
|
||||||
|
, blockParent = Map.empty
|
||||||
|
, size = Map.fromList [(Block 0, 6)]
|
||||||
|
}
|
||||||
|
, nextNodeId = Node 0
|
||||||
|
}
|
||||||
|
|
||||||
|
prState :: PRState String Char Bool
|
||||||
|
prState = PRState
|
||||||
|
{ partition = Partition $ Map.fromList [("s1", Block 0), ("s2", Block 1), ("s3", Block 0), ("s4", Block 1), ("s5", Block 0), ("s6", Block 1)]
|
||||||
|
, nextBlockId = Block 2
|
||||||
|
, splittingTree = SplittingTree
|
||||||
|
{ label = Map.singleton (Node 0) "a"
|
||||||
|
, innerParent = Map.empty
|
||||||
|
, blockParent = Map.fromList [(Block 0, (Node 0, False)), (Block 1, (Node 0, True))]
|
||||||
|
, size = Map.fromList [(Block 0, 3), (Block 1, 3)]
|
||||||
|
}
|
||||||
|
, nextNodeId = Node 1
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue