srtree-2.0.1.2: src/Algorithm/EqSat/Egraph.hs
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
-----------------------------------------------------------------------------
-- |
-- Module : Algorithm.EqSat.Egraph
-- Copyright : (c) Fabricio Olivetti 2021 - 2024
-- License : BSD3
-- Maintainer : fabricio.olivetti@gmail.com
-- Stability : experimental
-- Portability :
--
-- Equality Graph data structure
-- Heavily based on hegg (https://github.com/alt-romes/hegg by alt-romes)
--
-----------------------------------------------------------------------------
module Algorithm.EqSat.Egraph where
import Control.Lens (element, makeLenses, view, over, (&), (+~), (-~), (.~), (^.))
--import Control.Monad (forM, forM_, when, foldM, void)
import Data.List ( intercalate )
import Control.Monad.State.Strict hiding ( get, put )
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IntMap
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.HashSet (HashSet)
import qualified Data.HashSet as Set
import Data.IntSet (IntSet)
import qualified Data.IntSet as IntSet
import Data.Sequence ( Seq(..), (><) )
import qualified Data.Sequence as FingerTree
import Data.Foldable ( toList )
import Data.SRTree
import Data.SRTree.Eval
import Data.Hashable
import Data.Binary
import qualified Data.Binary as Bin
import qualified Data.Massiv.Array as MA
import GHC.Generics
import Debug.Trace
type EClassId = Int -- NOTE: DO NOT CHANGE THIS, this will break the use of IntMap and IntSet
type ClassIdMap = IntMap
type ENode = SRTree EClassId
type ENodeEnc = (Int, Int, Int, Double)
type EGraphST m a = StateT EGraph m a
type Cost = Int
type CostFun = SRTree Cost -> Cost
instance Hashable ENode where
hashWithSalt n enode = hashWithSalt n (encodeEnode enode)
type RangeTree a = Seq (a, EClassId)
-- | this assumes up to 999 variables and params
encodeEnode :: ENode -> ENodeEnc
--encodeEnode = id
{--}
encodeEnode (Var ix) = (0, ix, -1, 0)
encodeEnode (Param ix) = (1, ix, -1, 0)
encodeEnode (Const x) = (2, -1, -1, x)
encodeEnode (Uni f ed) = (300 + fromEnum f, ed, -1, 0)
encodeEnode (Bin op ed1 ed2) = (400 + fromEnum op, ed1, ed2, 0)
{--}
{-# INLINE encodeEnode #-}
decodeEnode :: ENodeEnc -> ENode
--decodeEnode = id
{--}
decodeEnode (0, ix, _, _) = Var ix
decodeEnode (1, ix, _, _) = Param ix
decodeEnode (2, _, _, x) = Const x
decodeEnode (opCode, arg1, arg2, arg3)
| opCode < 400 = Uni (toEnum $ opCode-300) arg1
| otherwise = Bin (toEnum $ opCode-400) arg1 arg2
{--}
{-# INLINE decodeEnode #-}
insertRange :: (Ord a, Show a) => EClassId -> a -> RangeTree a -> RangeTree a
insertRange eid x Empty = FingerTree.singleton (x, eid)
insertRange eid x (y :<| _xs) | (x, eid) < y = (x, eid) :<| y :<| _xs
insertRange eid x (_xs :|> y) | (x, eid) > y = _xs :|> y :|> (x, eid)
insertRange eid x rt = go rt
where
entry = (x, eid)
go root = case FingerTree.splitAt (n `div` 2) root of
(Empty, Empty) -> FingerTree.singleton entry
(Empty, z :<| zs) | entry < z -> entry :<| z :<| zs
| otherwise -> z :<| (go zs)
(ys :|> y, Empty) | entry > y -> ys :|> y :|> entry
| otherwise -> (go ys) :|> y
(ys :|> y, z :<| zs)
| entry > y && entry < z -> (ys :|> y :|> entry) >< (z :<| zs)
| entry > z -> (ys :|> y) >< go (z :<| zs)
| entry < y -> go (ys :|> y) >< (z :<| zs)
| otherwise -> root
where
n = FingerTree.length root
removeRange :: (Ord a, Show a) => EClassId -> a -> RangeTree a -> RangeTree a
removeRange eid x Empty = Empty
removeRange eid x (y :<| _xs) | (x, eid) < y = (y :<| _xs)
removeRange eid x (_xs :|> y) | (x, eid) > y = (_xs :|> y)
removeRange eid x rt = go rt
where
entry = (x, eid)
go root = case FingerTree.splitAt (n `div` 2) root of
(Empty, Empty) -> root
(Empty, z :<| zs)
| entry < z -> z :<| zs
| entry == z -> zs
| otherwise -> z :<| (go zs)
(ys :|> y, Empty)
| entry > y -> ys :|> y
| entry == y -> ys
| otherwise -> (go ys) :|> y
(ys :|> y, z :<| zs)
| entry > y && entry < z -> root
| entry > z -> (ys :|> y) >< go (z :<| zs)
| entry < y -> go (ys :|> y) >< (z :<| zs)
| otherwise -> root
where
n = FingerTree.length root
-- TODO: check this \/
getWithinRange :: Ord a => a -> a -> RangeTree a -> [EClassId]
getWithinRange lb ub rt = map snd . toList $ go rt
where
go Empty = Empty
go root = case FingerTree.splitAt (n `div` 2) root of
(Empty, Empty) -> Empty
(ys :|> y, Empty)
| fst y < lb -> Empty
| otherwise -> go (ys :|> y)
(Empty, z :<| zs)
| fst z > ub -> Empty
| otherwise -> go (z :<| zs)
(ys :|> y, z :<| zs)
| fst y < lb -> go (z :<| zs)
| fst z > ub -> go (ys :|> y)
| otherwise -> go (ys :|> y) >< go (z :<| zs)
where
n = FingerTree.length root
getSmallest :: Ord a => RangeTree a -> (a, EClassId)
getSmallest rt = case rt of
Empty -> error "empty finger"
x :<| t -> x
{-# INLINE getSmallest #-}
getGreatest :: Ord a => RangeTree a -> (a, EClassId)
getGreatest rt = case rt of
Empty -> error "empty finger"
t :|> x -> x
{-# INLINE getGreatest #-}
data EGraph = EGraph { _canonicalMap :: ClassIdMap EClassId -- maps an e-class id to its canonical form
, _eNodeToEClass :: Map ENode EClassId -- maps an e-node to its e-class id
, _eClass :: ClassIdMap EClass -- maps an e-class id to its e-class data
, _eDB :: EGraphDB
} deriving (Show, Generic)
data EGraphDB = EDB { _worklist :: HashSet (EClassId, ENode) -- e-nodes and e-class schedule for analysis
, _analysis :: HashSet (EClassId, ENode) -- e-nodes and e-class that changed data
, _refits :: HashSet EClassId
, _patDB :: DB -- database of patterns
, _fitRangeDB :: RangeTree Double -- database of valid fitness
, _dlRangeDB :: RangeTree Double
, _sizeDB :: IntMap IntSet -- database of model sizes
, _sizeFitDB :: IntMap (RangeTree Double) -- hacky! Size x Fitness DB
, _sizeDLDB :: IntMap (RangeTree Double)
, _unevaluated :: IntSet -- set of not-evaluated e-classes
, _nextId :: Int -- next available id
} deriving (Show, Generic)
data EClass = EClass { _eClassId :: Int -- e-class id (maybe we don't need that here)
, _eNodes :: HashSet ENodeEnc -- set of e-nodes inside this e-class
, _parents :: HashSet (EClassId, ENode) -- parents (e-class, e-node)'s
, _height :: Int -- height
, _info :: EClassData -- data
} deriving (Show, Eq, Generic)
data Consts = NotConst | ParamIx Int | ConstVal Double deriving (Show, Eq, Generic)
data Property = Positive | Negative | NonZero | Real deriving (Show, Eq, Generic) -- TODO: incorporate properties
data EClassData = EData { _cost :: Cost
, _best :: ENode
, _consts :: Consts
, _fitness :: Maybe Double -- NOTE: this cannot be NaN
, _dl :: Maybe Double
, _theta :: [PVector]
, _size :: Int
-- , _properties :: Property
-- TODO: include evaluation of expression from this e-class
} deriving (Show, Generic)
-- * Serialization
instance Generic (EClassId, ENode)
instance Binary (SRTree EClassId) where
put (Var ix) = put (0 :: Word8) >> put ix
put (Param ix) = put (1 :: Word8) >> put ix
put (Const x) = put (2 :: Word8) >> put x
put (Uni f t) = put (3 :: Word8) >> put (fromEnum f) >> put t
put (Bin op l r) = put (4 :: Word8) >> put (fromEnum op) >> put l >> put r
get = do t <- get :: Get Word8
case t of
0 -> Var <$> get
1 -> Param <$> get
2 -> Const <$> get
3 -> Uni <$> (toEnum <$> get) <*> get
4 -> Bin <$> (toEnum <$> get) <*> get <*> get
instance Binary (SRTree ()) where
put (Var ix) = put (0 :: Word8) >> put ix
put (Param ix) = put (1 :: Word8) >> put ix
put (Const x) = put (2 :: Word8) >> put x
put (Uni f t) = put (3 :: Word8) >> put (fromEnum f)
put (Bin op l r) = put (4 :: Word8) >> put (fromEnum op)
get = do t <- get :: Get Word8
case t of
0 -> Var <$> get
1 -> Param <$> get
2 -> Const <$> get
3 -> Uni <$> (toEnum <$> get) <*> pure ()
4 -> Bin <$> (toEnum <$> get) <*> pure () <*> pure ()
instance (Binary a, Hashable a) => Binary (HashSet a) where
put hs = put (Set.toList hs)
get = Set.fromList <$> get
instance Binary PVector where
put xs = put (MA.toList xs)
get = MA.fromList compMode <$> get
instance Binary IntTrie
instance Binary EClass
instance Binary Consts
instance Binary Property
instance Binary EClassData
instance Binary EGraphDB
instance Binary EGraph
instance Eq EClassData where
EData c1 b1 cs1 ft1 dl1 _ s1 == EData c2 b2 cs2 ft2 dl2 _ s2 = c1==c2 && b1==b2 && cs1==cs2 && ft1==ft2 && dl1==dl2 && s1==s2
-- The database maps a symbol to an IntTrie
-- The IntTrie stores the possible paths from a certain e-class
-- that matches a pattern
type DB = Map (SRTree ()) IntTrie
-- The IntTrie is composed of the set of available keys (for convenience)
-- and an IntMap that maps one e-class id to the first child IntTrie,
-- the first child IntTrie will point to the next child and so on
data IntTrie = IntTrie { _keys :: HashSet EClassId, _trie :: IntMap IntTrie } deriving (Generic)
-- Shows the IntTrie as {keys} -> {show IntTries}
instance Show IntTrie where
show (IntTrie k t) = let keys = intercalate "," (map show $ Set.toList k)
tries = intercalate "," (map (\(k,v) -> show k <> " -> " <> show v) $ IntMap.toList t)
in "{" <> keys <> "} - {" <> tries <> "}"
makeLenses ''EGraph
makeLenses ''EClass
makeLenses ''EClassData
makeLenses ''EGraphDB
-- * E-Graph basic supporting functions
-- | returns an empty e-graph
emptyGraph :: EGraph
emptyGraph = EGraph IntMap.empty Map.empty IntMap.empty emptyDB
{-# INLINE emptyGraph #-}
-- | returns an empty e-graph DB
emptyDB :: EGraphDB
emptyDB = EDB Set.empty Set.empty Set.empty Map.empty FingerTree.empty FingerTree.empty IntMap.empty IntMap.empty IntMap.empty IntSet.empty 0
{-# INLINE emptyDB #-}
-- | Creates a new e-class from an e-class id, a new e-node,
-- and the info of this e-class
createEClass :: EClassId -> ENode -> EClassData -> Int -> EClass
createEClass cId enode' info h = EClass cId (Set.singleton $ encodeEnode enode') Set.empty h info
{-# INLINE createEClass #-}
-- | gets the canonical id of an e-class
canonical :: Monad m => EClassId -> EGraphST m EClassId
canonical eclassId =
do m <- gets _canonicalMap
let oneStep = m IntMap.! eclassId
if oneStep == eclassId
then pure eclassId
else go m oneStep
where
go :: Monad m => IntMap EClassId -> EClassId -> EGraphST m EClassId
go m ecId
| m IntMap.! ecId == ecId = do modify' $ over canonicalMap (IntMap.insert eclassId ecId) -- creates a shortcut for next time
pure ecId -- if the e-class id is mapped to itself, it's canonical
| otherwise = go m (m IntMap.! ecId) -- otherwise, check the next id in the sequence
{-# INLINE canonical #-}
-- | canonize the e-node children
canonize :: Monad m => ENode -> EGraphST m ENode
canonize = mapM canonical -- applies canonical to the children
{-# INLINE canonize #-}
-- | gets an e-class with id `c`
getEClass :: Monad m => EClassId -> EGraphST m EClass
getEClass c = gets ((IntMap.! c) . _eClass)
{-# INLINE getEClass #-}
-- | Creates a singleton trie from an e-class id
trie :: EClassId -> IntMap IntTrie -> IntTrie
trie eid = IntTrie (Set.singleton eid)
{-# INLINE trie #-}
-- | Check whether an e-class is a constant value
isConst :: Monad m => EClassId -> EGraphST m Bool
isConst eid = do ec <- gets ((IntMap.! eid) . _eClass)
case (_consts . _info) ec of
ConstVal _ -> pure True
_ -> pure False
{-# INLINE isConst #-}