dataframe-core-1.0.2.0: src/DataFrame/Internal/Simplify.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module DataFrame.Internal.Simplify (
simplify,
simplifyPredicatePair,
-- * Path-condition entailment (for fitted-tree pruning)
PredFact,
factTrue,
factFalse,
entails,
) where
import Control.Monad (guard)
import Data.Maybe (fromMaybe)
import Data.Type.Equality (testEquality, (:~:) (Refl))
import Type.Reflection (eqTypeRep, typeRep, (:~~:) (HRefl), pattern App)
import DataFrame.Internal.Column (Columnable)
import DataFrame.Internal.Expression (
BinaryOp,
Expr (..),
UnaryOp (unaryName),
eqExpr,
normalize,
)
import DataFrame.Operators (
NullAnd,
NullEq,
NullGeq,
NullGt,
NullLeq,
NullLt,
NullNeq,
NullOr,
(.==.),
)
simplify :: forall a. (Columnable a) => Expr a -> Expr a
simplify e
| isBoolish @a = fixpoint (10 :: Int) e
| otherwise = e
where
fixpoint 0 x = x
fixpoint n x = let x' = simplifyB x in if eqExpr x x' then x else fixpoint (n - 1) x'
isBoolish :: forall a. (Columnable a) => Bool
isBoolish =
case ( testEquality (typeRep @a) (typeRep @Bool)
, testEquality (typeRep @a) (typeRep @(Maybe Bool))
) of
(Just Refl, _) -> True
(_, Just Refl) -> True
_ -> False
data Conn = ConnAnd | ConnOr
connOf :: forall op c b r. (BinaryOp op) => op c b r -> Maybe Conn
connOf _
| Just HRefl <- eqTypeRep (typeRep @op) (typeRep @NullAnd) = Just ConnAnd
| Just HRefl <- eqTypeRep (typeRep @op) (typeRep @NullOr) = Just ConnOr
| otherwise = Nothing
simplifyB :: forall a. (Columnable a) => Expr a -> Expr a
simplifyB expr = case expr of
Binary (op :: op c b a) l r
| Just conn <- connOf op
, Just Refl <- testEquality (typeRep @c) (typeRep @a)
, Just Refl <- testEquality (typeRep @b) (typeRep @a) ->
let l' = simplifyB l; r' = simplifyB r
in fromMaybe (Binary op l' r') (combine conn l' r')
| otherwise -> expr
Unary (op :: op b a) inner
| Just Refl <- testEquality (typeRep @a) (typeRep @Bool)
, Just Refl <- testEquality (typeRep @b) (typeRep @Bool)
, unaryName op == "not" ->
simplifyNot op (simplifyB inner)
| otherwise -> expr
If c t f ->
let c' = simplify c
t' = simplifyB t
f' = simplifyB f
in case asBoolLit c' of
Just True -> t'
Just False -> f'
Nothing
| eqExpr t' f' -> t'
| Just Refl <- testEquality (typeRep @a) (typeRep @Bool)
, asBoolLit t' == Just True
, asBoolLit f' == Just False ->
c'
| otherwise -> If c' t' f'
_ -> expr
simplifyNot :: (UnaryOp op) => op Bool Bool -> Expr Bool -> Expr Bool
simplifyNot op inner = case asBoolLit inner of
Just b -> Lit (not b)
Nothing -> case inner of
Unary (op2 :: op2 b2 Bool) inner2
| unaryName op2 == "not"
, Just Refl <- testEquality (typeRep @b2) (typeRep @Bool) ->
inner2
_ -> Unary op inner
combine :: (Columnable a) => Conn -> Expr a -> Expr a -> Maybe (Expr a)
combine ConnAnd = combineAnd
combine ConnOr = combineOr
asBoolLit :: forall a. (Columnable a) => Expr a -> Maybe Bool
asBoolLit (Lit v) =
case testEquality (typeRep @a) (typeRep @Bool) of
Just Refl -> Just v
Nothing -> case testEquality (typeRep @a) (typeRep @(Maybe Bool)) of
Just Refl -> v
Nothing -> Nothing
asBoolLit _ = Nothing
{- | Polymorphic boolean literal: @Lit b@ for @Expr Bool@, @Lit (Just b)@ for
@Expr (Maybe Bool)@.
-}
litBoolish :: forall a. (Columnable a) => Bool -> Maybe (Expr a)
litBoolish v =
case testEquality (typeRep @a) (typeRep @Bool) of
Just Refl -> Just (Lit v)
Nothing -> case testEquality (typeRep @a) (typeRep @(Maybe Bool)) of
Just Refl -> Just (Lit (Just v))
Nothing -> Nothing
combineAnd :: (Columnable a) => Expr a -> Expr a -> Maybe (Expr a)
combineAnd l r
| eqExpr l r = Just l
| asBoolLit l == Just False = litBoolish False
| asBoolLit r == Just False = litBoolish False
| asBoolLit l == Just True = Just r
| asBoolLit r == Just True = Just l
| absorbs ConnOr l r = Just l
| absorbs ConnOr r l = Just r
| otherwise = simplifyPredicatePair True l r
combineOr :: (Columnable a) => Expr a -> Expr a -> Maybe (Expr a)
combineOr l r
| eqExpr l r = Just l
| asBoolLit l == Just True = litBoolish True
| asBoolLit r == Just True = litBoolish True
| asBoolLit l == Just False = Just r
| asBoolLit r == Just False = Just l
| absorbs ConnAnd l r = Just l
| absorbs ConnAnd r l = Just r
| otherwise = simplifyPredicatePair False l r
absorbs :: (Columnable a) => Conn -> Expr a -> Expr a -> Bool
absorbs conn x (Binary (op :: op c b a) ya yb)
| Just c' <- connOf op
, sameConn conn c'
, Just Refl <- testEquality (typeRep @c) (typeRep @a)
, Just Refl <- testEquality (typeRep @b) (typeRep @a) =
eqExpr x ya || eqExpr x yb
absorbs _ _ _ = False
sameConn :: Conn -> Conn -> Bool
sameConn ConnAnd ConnAnd = True
sameConn ConnOr ConnOr = True
sameConn _ _ = False
data Cmp = CLt | CLeq | CGt | CGeq | CEq | CNeq deriving (Eq)
data NullK = Total | FalseOnNull | UnknownOnNull deriving (Eq)
data Atom = Atom
{ aCmp :: Cmp
, aThr :: !Double
, aKey :: String
, aNull :: NullK
, aIntegral :: Bool
}
cmpOf :: forall op c b r. (BinaryOp op) => op c b r -> Maybe Cmp
cmpOf _
| Just HRefl <- eqTypeRep (typeRep @op) (typeRep @NullLt) = Just CLt
| Just HRefl <- eqTypeRep (typeRep @op) (typeRep @NullLeq) = Just CLeq
| Just HRefl <- eqTypeRep (typeRep @op) (typeRep @NullGt) = Just CGt
| Just HRefl <- eqTypeRep (typeRep @op) (typeRep @NullGeq) = Just CGeq
| Just HRefl <- eqTypeRep (typeRep @op) (typeRep @NullEq) = Just CEq
| Just HRefl <- eqTypeRep (typeRep @op) (typeRep @NullNeq) = Just CNeq
| otherwise = Nothing
isLower, isUpper :: Cmp -> Bool
isLower c = c == CGt || c == CGeq
isUpper c = c == CLt || c == CLeq
-- | True if @x@ is a @Maybe _@ type.
isMaybeTy :: forall x. (Columnable x) => Bool
isMaybeTy = case typeRep @x of
App con _ -> case eqTypeRep con (typeRep @Maybe) of Just HRefl -> True; _ -> False
_ -> False
litDouble :: forall b. (Columnable b) => Expr b -> Maybe Double
litDouble (Lit v) =
case testEquality (typeRep @b) (typeRep @Double) of
Just Refl -> Just v
Nothing -> case testEquality (typeRep @b) (typeRep @Int) of
Just Refl -> Just (fromIntegral v)
Nothing -> case testEquality (typeRep @b) (typeRep @(Maybe Double)) of
Just Refl -> v
Nothing -> case testEquality (typeRep @b) (typeRep @(Maybe Int)) of
Just Refl -> fromIntegral <$> v
Nothing -> Nothing
litDouble _ = Nothing
{- | True for a column lifted from an integral type (never NaN): @toDouble (col …)@
or a column whose type is itself integral.
-}
integralColE :: forall c. (Columnable c) => Expr c -> Bool
integralColE (Unary op _) = unaryName op == "toDouble"
integralColE _ =
or
[ matches @Int
, matches @(Maybe Int)
]
where
matches :: forall t. (Columnable t) => Bool
matches = case testEquality (typeRep @c) (typeRep @t) of Just Refl -> True; _ -> False
atomOf :: forall a. (Columnable a) => Expr a -> Maybe Atom
atomOf (Unary fm (Binary (op :: op c b r) (colE :: Expr c) litE))
| unaryName fm == "fromMaybe"
, Just cmp <- cmpOf op
, Just t <- litDouble litE =
Just (Atom cmp t (show (normalize colE)) FalseOnNull (integralColE colE))
atomOf (Binary (op :: op c b a) (colE :: Expr c) litE)
| Just cmp <- cmpOf op
, Just t <- litDouble litE =
let nk = if isMaybeTy @c then UnknownOnNull else Total
in Just (Atom cmp t (show (normalize colE)) nk (integralColE colE))
atomOf _ = Nothing
simplifyPredicatePair ::
forall a. (Columnable a) => Bool -> Expr a -> Expr a -> Maybe (Expr a)
simplifyPredicatePair isAnd a b = do
atomA <- atomOf a
atomB <- atomOf b
guard (aKey atomA == aKey atomB)
let nk = aNull atomA
integral = aIntegral atomA
if isAnd
then andAtoms a atomA b atomB nk integral
else orAtoms a atomA b atomB nk integral
-- | Contradiction folds to a literal False unless null-rows make it unknown.
litFalseGated :: (Columnable a) => NullK -> Maybe (Expr a)
litFalseGated UnknownOnNull = Nothing
litFalseGated _ = litBoolish False
{- | Tautology to literal True is sound only for total (never-null) atoms; the
exhaustive-cover form additionally needs a non-NaN (integral) column.
-}
litTrueTotal :: (Columnable a) => NullK -> Maybe (Expr a)
litTrueTotal Total = litBoolish True
litTrueTotal _ = Nothing
andAtoms ::
(Columnable a) =>
Expr a -> Atom -> Expr a -> Atom -> NullK -> Bool -> Maybe (Expr a)
andAtoms a atomA b atomB nk _ =
let cA = aCmp atomA; tA = aThr atomA; cB = aCmp atomB; tB = aThr atomB
in if
| isLower cA, isLower cB, cA == cB -> Just (if tA >= tB then a else b)
| isUpper cA, isUpper cB, cA == cB -> Just (if tA <= tB then a else b)
| isLower cA, isUpper cB -> lu cA tA cB tB
| isUpper cA, isLower cB -> lu cB tB cA tA
| cA == CEq, cB == CEq -> if tA == tB then Just a else litFalseGated nk
| cA == CEq, cB == CNeq -> if tA == tB then litFalseGated nk else Just a
| cA == CNeq, cB == CEq -> if tA == tB then litFalseGated nk else Just b
| cA == CEq -> if satisfies tA cB tB then Just a else litFalseGated nk
| cB == CEq -> if satisfies tB cA tA then Just b else litFalseGated nk
| cA == CNeq, cB == CNeq -> Nothing
| cA == CNeq -> if outside tA cB tB then Just b else Nothing
| cB == CNeq -> if outside tB cA tA then Just a else Nothing
| otherwise -> Nothing
where
lu lc lo uc hi
| lo > hi = litFalseGated nk
| lo == hi, lc == CGeq, uc == CLeq = pointEq a lo
| lo == hi = litFalseGated nk
| otherwise = Nothing
orAtoms ::
(Columnable a) =>
Expr a -> Atom -> Expr a -> Atom -> NullK -> Bool -> Maybe (Expr a)
orAtoms a atomA b atomB nk integral =
let cA = aCmp atomA; tA = aThr atomA; cB = aCmp atomB; tB = aThr atomB
in if
| isLower cA, isLower cB, cA == cB -> Just (if tA <= tB then a else b)
| isUpper cA, isUpper cB, cA == cB -> Just (if tA >= tB then a else b)
| isUpper cA
, isLower cB
, nk == Total
, integral
, covers cB tB cA tA ->
litTrueTotal nk
| isLower cA
, isUpper cB
, nk == Total
, integral
, covers cA tA cB tB ->
litTrueTotal nk
| cA == CNeq, cB == CNeq -> if tA == tB then Just a else litTrueTotal nk
| cA == CEq, cB == CNeq -> if tA == tB then litTrueTotal nk else Just b
| cA == CNeq, cB == CEq -> if tA == tB then litTrueTotal nk else Just a
| cA == CEq, cB == CEq -> if tA == tB then Just a else Nothing
| otherwise -> Nothing
{- | Build @col == t@ for the point-collapse rule; only strict @Expr Bool@ over a
@Double@ column (otherwise bail).
-}
pointEq :: forall a. (Columnable a) => Expr a -> Double -> Maybe (Expr a)
pointEq atom lo = case testEquality (typeRep @a) (typeRep @Bool) of
Just Refl -> (\colE -> colE .==. Lit lo) <$> recoverColD atom
Nothing -> Nothing
recoverColD :: Expr x -> Maybe (Expr Double)
recoverColD (Binary _ (colE :: Expr c) _) =
case testEquality (typeRep @c) (typeRep @Double) of
Just Refl -> Just colE
_ -> Nothing
recoverColD (Unary _ inner) = recoverColD inner
recoverColD _ = Nothing
covers :: Cmp -> Double -> Cmp -> Double -> Bool
covers lowerCmp lo upperCmp hi =
lo < hi || (lo == hi && (lowerCmp == CGeq || upperCmp == CLeq))
satisfies :: Double -> Cmp -> Double -> Bool
satisfies t CGt tb = t > tb
satisfies t CGeq tb = t >= tb
satisfies t CLt tb = t < tb
satisfies t CLeq tb = t <= tb
satisfies _ _ _ = False
outside :: Double -> Cmp -> Double -> Bool
outside t CGt tb = t <= tb
outside t CGeq tb = t < tb
outside t CLt tb = t >= tb
outside t CLeq tb = t > tb
outside _ _ _ = False
-- ---------------------------------------------------------------------------
-- Path-condition entailment for fitted-tree pruning.
-- ---------------------------------------------------------------------------
-- | A known same-column threshold fact accumulated along a tree path.
data PredFact = PredFact !String !Cmp !Double
-- | The fact a branch's true edge establishes (the condition holds).
factTrue :: Expr Bool -> Maybe PredFact
factTrue e = (\a -> PredFact (aKey a) (aCmp a) (aThr a)) <$> atomOf e
{- | The fact a branch's false edge establishes (the negated condition). Only
sound for non-NaN (integral) columns — a NaN row takes the false edge too,
so @¬(x>t)@ is not a clean @x<=t@ bound for floats.
-}
factFalse :: Expr Bool -> Maybe PredFact
factFalse e = do
a <- atomOf e
guard (aIntegral a && aNull a == Total)
nc <- negCmp (aCmp a)
pure (PredFact (aKey a) nc (aThr a))
negCmp :: Cmp -> Maybe Cmp
negCmp CLt = Just CGeq
negCmp CLeq = Just CGt
negCmp CGt = Just CLeq
negCmp CGeq = Just CLt
negCmp _ = Nothing
{- | @entails facts cond@: 'Just' 'True' when the path facts force @cond@ true,
'Just' 'False' when they force it false, 'Nothing' when undecided.
-}
entails :: [PredFact] -> Expr Bool -> Maybe Bool
entails facts cond = do
a <- atomOf cond
let decisions =
[ d
| PredFact fk fc ft <- facts
, fk == aKey a
, Just d <- [factImplies (fc, ft) (aCmp a, aThr a)]
]
case decisions of
(d : _) -> Just d
[] -> Nothing
{- | Does the fact's solution set sit inside @cond@ ('Just' 'True'), disjoint
from it ('Just' 'False'), or neither ('Nothing')? Boundary strictness is
honoured: e.g. @x<=t@ does NOT entail @x<t@, and @x>=t ∧ x<=t@ is not empty.
-}
factImplies :: (Cmp, Double) -> (Cmp, Double) -> Maybe Bool
factImplies (fc, ft) (cc, tc)
| isLower fc, isLower cc, subset = Just True
| isUpper fc, isUpper cc, subset = Just True
| isLower fc, isUpper cc, disjointAtEq = Just False
| isUpper fc, isLower cc, disjointBelow = Just False
| otherwise = Nothing
where
fIncl = fc == CGeq || fc == CLeq
cIncl = cc == CGeq || cc == CLeq
-- same-direction containment: strictly tighter, or equal threshold where the
-- fact's boundary inclusivity is no stronger than the condition's.
subset =
(if isLower fc then ft > tc else ft < tc)
|| (ft == tc && (not fIncl || cIncl))
-- lower fact ∩ upper cond empty: fact starts above cond's top, or they meet
-- at a point that is not in both.
disjointAtEq = ft > tc || (ft == tc && not (fIncl && cIncl))
-- upper fact ∩ lower cond empty (mirror).
disjointBelow = ft < tc || (ft == tc && not (fIncl && cIncl))