packages feed

srtree-3.0.0.0: src/Algorithm/EqSat/Simplify.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Algorithm.EqSat.Simplify
-- Copyright   :  (c) Fabricio Olivetti 2021 - 2024
-- License     :  BSD3
-- Maintainer  :  fabricio.olivetti@gmail.com
-- Stability   :  experimental
-- Portability :
--
-- Module containing the algebraic rules and simplification function.
--
-----------------------------------------------------------------------------
module Algorithm.EqSat.Simplify ( Rule(..), simplifyEqSatDefault, applyMergeOnlyDftl, rewrites, rewritesParams, rewriteBasic, rewritesFun, rewritesSimple, rewritesWithConstant, myCost ) where

import Algorithm.EqSat (eqSat, applySingleMergeOnlyEqSat)
import Algorithm.EqSat.Egraph
import Algorithm.EqSat.DB
  ( ClassOrVar,
    Condition (Condition),
    NChild (Ch, MapP, Rest),
    Pattern (Fixed, Hole, NAry, VarPat),
    Rule (..),
    Subst,
    SubVal (SVMap, SVOne),
    getInt,
  )
import Control.Monad.State.Strict (evalState)
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IM
import Data.Map (Map)
import qualified Data.Map as Map
import Data.SRTree

-- | A constraint over a match's substitution: when applied to a substitution it
-- runs in the e-graph monad and fetches e-class data through 'ClassStore', so it
-- works on a paged (out-of-core) graph whose resident cache is bounded/empty.
type ConstrFun = Pattern -> Condition

constrainOnVal :: (Consts -> Bool) -> Pattern -> Condition
constrainOnVal f (VarPat c) = Condition $ \subst -> do
    let cid = getInt $ case Map.lookup (Right (fromEnum c)) subst of
                        Nothing -> error $ "CONSTRAINVAL_MISSING var=" <> show (fromEnum c) <> " substSize=" <> show (Map.size subst)
                        Just (SVOne v) -> v
                        Just (SVMap _) -> error $ "CONSTRAINVAL_REST_AS_SINGLE var=" <> show (fromEnum c)
    ec <- getEClass cid
    pure (f (_consts . _info $ ec))
constrainOnVal _ _ = Condition $ \_ -> pure False

-- TODO: aux functions to avoid repeated pattern in constraint creation 
--
-- check if a matched pattern contains constant 
isConstPt :: ConstrFun
isConstPt = constrainOnVal $ 
    \case
       ConstVal _ -> True 
       _          -> False

-- check if the matched pattern is a positive constant 
isConstPos :: ConstrFun
isConstPos = constrainOnVal $
    \case
      ConstVal x -> x > 0 
      _          -> False

isNotParam :: ConstrFun
isNotParam = constrainOnVal $
   \case
      ParamIx _ -> False
      _         -> True

-- check if the matched pattern is nonzero
isNotZero :: ConstrFun
isNotZero = constrainOnVal $
    \case
       ConstVal x -> abs x > 1e-9
       _          -> True

-- check if the matched pattern is even 
isEven :: ConstrFun
isEven = constrainOnVal $
    \case
       ConstVal x -> ceiling x == floor x && even (round x) 
       _          -> True

-- check if the matched pattern is integer
isInteger :: ConstrFun
isInteger = constrainOnVal $
    \case
       ConstVal x -> ceiling x == floor x
       _          -> True

-- check if the matched pattern is positive
isPositive :: ConstrFun
isPositive = constrainOnVal $
    \case
       ConstVal x -> x > 0
       _          -> True

-- check if the matched pattern is valid
isValid :: ConstrFun
isValid = constrainOnVal $
    \case
       ConstVal x -> not (isNaN x || isInfinite x)
       _          -> True

-- | e-class ids bound to a rest variable
restEidsOf :: Char -> Subst -> [EClassId]
restEidsOf c subst = case Map.lookup (Right (fromEnum c)) subst of
                       Just (SVMap m) -> expandedList m
                       _              -> []

-- | every e-class bound to a rest variable holds a valid value
allValidRest :: Char -> Condition
allValidRest c = Condition $ \subst -> do
    let eids = restEidsOf c subst
        validEid eid = getEClass eid >>= \ec ->
            pure $ case _consts . _info $ ec of
                     ConstVal x -> not (isNaN x || isInfinite x)
                     _          -> True
    and <$> mapM validEid eids

-- basic algebraic rules
rewriteBasic :: [Rule]
rewriteBasic =
    [
      -- B7/B8/C5: factor a common term out of a sum of products, and the
      -- reverse (distribute), which make x*(y+z) and x*y+x*z equivalent.
      NAry EAdd [ Ch (NAry EMul [Ch "x", Rest '1'])
                , Ch (NAry EMul [Ch "x", Rest '2'])
                , Rest '3' ]
        :=>
      NAry EAdd [ Ch (NAry EMul [ Ch "x"
                                , Ch (NAry EAdd [Rest '1', Rest '2'])
                                ])
                , Rest '3' ]
    , NAry EAdd [ Ch (NAry EMul [ Ch "x"
                                , Ch (NAry EAdd [Rest '1'])
                                ])
                , Rest '2' ]
        :=>
      NAry EAdd [ MapP (NAry EMul [Ch "x", Ch Hole]) '1'
                , Rest '2' ]
    -- C5: x*y - z*x = x*(y - z)
    , NAry EAdd [ Ch (NAry EMul [Ch "x", Rest '1'])
                , Ch (NAry EMul [Ch (Fixed (Const (-1))), Ch "x", Ch "z"])
                , Rest '3' ]
        :=>
      NAry EAdd [ Ch (NAry EMul [ Ch "x"
                                , Ch (NAry EAdd [Rest '1', Ch (negate (VarPat 'z'))])
                                ])
                , Rest '3' ]
    -- B1: group duplicate factors into a power (x*x = x^2)
    , NAry EMul [Ch "x", Ch "x"] :=> "x" ** 2
    -- C9: binomial expansion of a closed 2-ary square
    , ("x" + "y") ** 2 :=> "x" ** 2 + 2 * "x" * "y" + "y" ** 2
    -- C10: x^2 + x*y + ... = x*(x + y) + ...
    , NAry EAdd [ Ch (Fixed (Bin Power (VarPat 'x') (Fixed (Const 2))))
                , Ch (NAry EMul [Ch "x", Rest '1'])
                , Rest '2' ]
        :=>
      NAry EAdd [ Ch (NAry EMul [ Ch "x"
                                , Ch (NAry EAdd [Ch "x", Rest '1'])
                                ])
                , Rest '2' ]
    ]

-- rules for nonlinear functions 
rewritesFun :: [Rule]
rewritesFun =
    [
      log (exp "x")  :=> "x"
    -- C11: log(x*y*z*...) = log x + log y + ...
    , log (NAry EMul [Rest '1']) :=> NAry EAdd [MapP (Fixed (Uni Log Hole)) '1']
    , log ("x" ** "y") :=> "y" * log "x"
    , log (powabs "x" "y") :=> "y" * log (abs "x")
    -- C12: abs(x*y*z*...) = abs x * abs y * ...
    , abs (NAry EMul [Rest '1']) :=> NAry EMul [MapP (Fixed (Uni Abs Hole)) '1']
    , abs ("x" ** "y") :=> abs "x" ** "y"
    , recip (recip "x") :=> "x" :| isNotZero "x"
    -- C13: (x*y*z*...)^w = x^w * y^w * ...   [was disabled: combinatorial blowup on (x*x)^t; the multiset matcher + matchCap bound that]
    , (NAry EMul [Rest '1']) ** "z" :=> NAry EMul [MapP (Hole ** VarPat 'z') '1']
    , abs "x" ** "y" :=> "x" ** "y" :| isEven "y"
    -- C14: sqrt(x*x) = abs x
    , sqrt (NAry EMul [Ch "x", Ch "x"]) :=> abs "x"
    ]

-- Rules that reduces redundant parameters
constReduction :: [Rule]
constReduction =
    [
      -- B3: 0 + rest = rest
      NAry EAdd [Ch (Fixed (Const 0)), Rest '1'] :=> NAry EAdd [Rest '1']
    , "x" ** 1 :=> "x"
    , powabs "x" 1 :=> abs "x"

    -- B9: x^y * x^z = x^(y+z)
    , NAry EMul [Ch (Fixed (Bin Power (VarPat 'x') (VarPat 'y'))), Ch (Fixed (Bin Power (VarPat 'x') (VarPat 'z')))]
        :==:
      Fixed (Bin Power (VarPat 'x') (NAry EAdd [Ch (VarPat 'y'), Ch (VarPat 'z')]))
        :| isPositive "x"
    -- B10: |x|^y * |x|^z = |x|^(y+z)  (fixed: target used "y+x" instead of "y+z")
    , NAry EMul [Ch (Fixed (Bin PowerAbs (VarPat 'x') (VarPat 'y'))), Ch (Fixed (Bin PowerAbs (VarPat 'x') (VarPat 'z')))]
        :=>
      Fixed (Bin PowerAbs (VarPat 'x') (NAry EAdd [Ch (VarPat 'y'), Ch (VarPat 'z')]))
    -- B11: (x^y)^z = x^(y*z)
    , Fixed (Bin Power (Fixed (Bin Power (VarPat 'x') (VarPat 'y'))) (VarPat 'z'))
        :==:
      Fixed (Bin Power (VarPat 'x') (NAry EMul [Ch (VarPat 'y'), Ch (VarPat 'z')]))
        :| isPositive "x"
    , powabs (powabs "x" "y") "z" :=> powabs "x" ("y" * "z")
    ]

rewritesWithConstant :: [Rule]
rewritesWithConstant =
    [
      "x" - "x" :=> 0
    , "x" / "x" :=> 1 :| isNotZero "x"
    , "x" ** "y" * "x" :=> "x" ** ("y" + 1) :| isPositive "x"
    , 1 ** "x" :=> 1
    , powabs 1 "x" :=> 1
    , log (sqrt "x") :=> 0.5 * log "x" :| isNotParam "x"
    , "x" ** (1/2)   :==: sqrt "x"
    , powabs "x" (1/2) :=> sqrt (abs "x")
    , "x" ** (1/3) :==: Fixed (Uni Cbrt "x")
    -- B4: 0 * rest = 0 (provided every factor is valid)
    , NAry EMul [Ch (Fixed (Const 0)), Rest '1'] :=> 0 :| allValidRest '1'
    , 0 ** "x" :=> 0 :| isPositive "x"
    , powabs 0 "x" :=> 0
    -- n-ary cancellation: x + y - x = y
    , NAry EAdd [ Ch "a"
                , Ch (NAry EMul [ Ch (Fixed (Const (-1.0))), Ch "a" ])
                , Rest 'r' ]
        :=> NAry EAdd [Rest 'r']
    -- combining like terms: x + x = 2*x
    , NAry EAdd [ Ch "a", Ch "a", Rest 'r' ]
        :=> NAry EAdd [ Ch (2 * "a"), Rest 'r' ]
    ]
rewritesWithParam :: [Rule]
rewritesWithParam =
    [
      "x" - "x" :=> Fixed (Param 0)
    , "x" / "x" :=> Fixed (Param 0) :| isNotZero "x"
    , 1 ** "x" :=> Fixed (Param 0)
    , powabs 1 "x" :=> Fixed (Param 0)
    ]

rewritesSimple :: [Rule]
rewritesSimple = rewriteBasic <> constReduction <> rewritesFun
powabs l r = Fixed (Bin PowerAbs l r)

-- | default cost function for simplification
-- TODO:
-- num_params:
--   length:
--      terminal < nonterminal:
--        symbol comparison (constants, parameters, variables x0, x10, x2)
--          op priorities (+, -, *, inv_div, pow, abs, exp, log, log10, sqrt)
--            univariates
myCost :: SRTree Int -> Int
myCost (Var _)      = 1
myCost (Const _)    = 3
myCost (Param _)    = 3
myCost (Bin op l r) = 2 + l + r
myCost (Uni _ t)    = 3 + t

-- all rewrite rules
rewrites :: [Rule]
rewrites = rewriteBasic <> constReduction <> rewritesFun <> rewritesWithConstant
rewritesParams :: [Rule]
rewritesParams = rewriteBasic <> constReduction <> rewritesFun <> rewritesWithParam

-- | simplify using the default parameters
simplifyEqSatDefault :: Fix SRTree -> Fix SRTree
simplifyEqSatDefault t = eqSat t rewrites myCost 30 `evalState` emptyGraphNoTrack

-- | simplifies with custom parameters
simplifyEqSat :: [Rule] -> CostFun -> Int -> Fix SRTree -> Fix SRTree
simplifyEqSat rwrts costFun it t = eqSat t rwrts costFun it `evalState` emptyGraph

-- | apply a single step of merge-only using default rules
applyMergeOnlyDftl :: ClassStore m => CostFun -> EGraphST m ()
applyMergeOnlyDftl costFun = applySingleMergeOnlyEqSat costFun rewrites