packages feed

dataframe-3.7.0.0: tests/Operations/InterpreterKernels.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

-- | Kernels agree with plain Haskell row by row; floats compare bitwise.
module Operations.InterpreterKernels where

import Control.Exception (throw)
import qualified Data.Text as T
import qualified Data.Vector as V

import qualified DataFrame as D
import DataFrame.Expression.Operators (
    (./=.),
    (.<.),
    (.<=.),
    (.==.),
    (.>.),
    (.>=.),
 )
import qualified DataFrame.Functions as F
import DataFrame.Internal.Column (
    Column,
    Columnable,
    TypedColumn (..),
    toVector,
 )
import DataFrame.Internal.Expression (Expr (..), prettyPrint)
import DataFrame.Internal.Interpreter (interpret)

import Assertions (BitEq, assertBitEq)
import Test.HUnit

-- | One side of an expression: a column with known rows, or a literal.
data Operand e = Column T.Text [e] | Literal e

operandExpr :: (Columnable e) => Operand e -> Expr e
operandExpr (Column name _) = Col name
operandExpr (Literal v) = Lit v

-- | The operand's value in each of @n@ rows.
operandRows :: Int -> Operand e -> [e]
operandRows _ (Column _ rows) = rows
operandRows n (Literal v) = replicate n v

operandColumns :: (Columnable e) => Operand e -> [(T.Text, Column)]
operandColumns (Column name rows) = [name D.=: rows]
operandColumns (Literal _) = []

-- | An operator paired with what it computes for one row.
data BinOp e r = BinOp (Expr e -> Expr e -> Expr r) (e -> e -> r)

comparisons :: (Columnable e, Ord e) => [BinOp e Bool]
comparisons =
    [ BinOp (.<.) (<)
    , BinOp (.<=.) (<=)
    , BinOp (.>.) (>)
    , BinOp (.>=.) (>=)
    , BinOp (.==.) (==)
    , BinOp (./=.) (/=)
    ]

arithmetic :: (Columnable e, Num e) => [BinOp e e]
arithmetic = [BinOp (+) (+), BinOp (-) (-), BinOp (*) (*)]

doubles :: [Double]
doubles = [0 / 0, -(1 / 0), -1.5, -0.0, 0.0, 5.0e-324, 0.5, 1, 2.5, 1.0e308, 1 / 0]

ints :: [Int]
ints = [minBound, -3, -1, 0, 1, 7, maxBound]

-- | The second size is above the 200k threshold where loops run in parallel.
sizes :: [Int]
sizes = [length doubles * length doubles, 200003]

-- | Two columns holding every pair of the given values, cycled to @n@ rows.
columnPair :: Int -> [e] -> (Operand e, Operand e)
columnPair n vals = (Column "x" xs, Column "y" ys)
  where
    (xs, ys) = unzip (take n (cycle [(a, b) | a <- vals, b <- vals]))

-- | Column-column, column-literal and literal-column operand pairs.
operandPairs :: Int -> [e] -> [(Operand e, Operand e)]
operandPairs n vals = (x, y) : concat [[(x, Literal t), (Literal t, x)] | t <- lits]
  where
    (x, y) = columnPair n vals
    lits = if n > 1000 then take 3 vals else vals

run :: forall a. (Columnable a) => [(T.Text, Column)] -> Expr a -> [a]
run columns e = case interpret @a (D.fromColumns columns) e of
    Left err -> throw err
    Right (TColumn col) -> V.toList (either throw id (toVector @a @V.Vector col))

binaryCases ::
    (Columnable e, Columnable r, BitEq r, Show r) => [e] -> [BinOp e r] -> [Test]
binaryCases vals ops =
    [ TestCase
        (assertBitEq (prettyPrint e ++ " n=" ++ show n) expected (run columns e))
    | n <- sizes
    , (l, r) <- operandPairs n vals
    , let columns = operandColumns l ++ operandColumns r
    , BinOp op f <- ops
    , let e = op (operandExpr l) (operandExpr r)
          expected = zipWith f (operandRows n l) (operandRows n r)
    ]

-- | If-then-else over a Bool column, with branches of every operand mix.
selectCases :: (Columnable e, BitEq e, Show e) => [e] -> [Test]
selectCases vals =
    [ TestCase
        (assertBitEq (prettyPrint e ++ " n=" ++ show n) expected (run columns e))
    | n <- sizes
    , let cond = Column "c" (take n (cycle [True, False, False]))
          lits = take 3 vals
    , (l, r) <-
        operandPairs n vals ++ zip (map Literal lits) (map Literal (drop 1 lits))
    , let columns = operandColumns cond ++ operandColumns l ++ operandColumns r
          e = If (operandExpr cond) (operandExpr l) (operandExpr r)
          expected =
            zipWith3
                (\c a b -> if c then a else b)
                (operandRows n cond)
                (operandRows n l)
                (operandRows n r)
    ]

caseWhenCases :: [Test]
caseWhenCases =
    [ TestCase
        (assertBitEq (prettyPrint e ++ " n=" ++ show n) expected (run columns e))
    | n <- sizes
    , let x = fst (columnPair n ints)
          columns = operandColumns x
          xe = operandExpr x
          e =
            F.caseWhen (xe .>. Lit 5) (Lit 1) $
                F.caseWhen (xe .>. Lit 0) (Lit 2) $
                    F.orElse (Lit (3 :: Int))
          expected = [if v > 5 then 1 else if v > 0 then 2 else 3 | v <- operandRows n x]
    ]

toDoubleCases :: [Test]
toDoubleCases =
    [ TestCase
        ( assertBitEq
            (prettyPrint e ++ " n=" ++ show n)
            (map fromIntegral (operandRows n x))
            (run (operandColumns x) e)
        )
    | n <- sizes
    , let x = fst (columnPair n ints)
          e = F.toDouble (operandExpr x)
    ]

mergeAggCases :: [Test]
mergeAggCases = concatMap sizeCases sizes
  where
    sizeCases n =
        [ TestCase $
            assertEqual
                ("count over " ++ show n ++ " rows")
                [n]
                (take 1 (run (operandColumns xsI) (F.count (F.col @Int "x"))))
        , TestCase $
            assertEqual
                ("countDistinct of an expression over " ++ show n ++ " rows")
                [length (takeWhile (< n) [0 .. 6])]
                (take 1 (run (operandColumns xsI) (F.countDistinct (F.col @Int "x" + 1))))
        , TestCase $
            assertBool
                ("mean of ints over " ++ show n ++ " rows")
                ( all
                    (approx (sumD / fromIntegral n))
                    (run (operandColumns xsI) (F.mean (F.col @Int "x")))
                )
        , TestCase $
            assertEqual
                ("mean of doubles over " ++ show n ++ " rows")
                [sum (take n smallDoubles) / fromIntegral n]
                (take 1 (run (operandColumns xsD) (F.mean (F.col @Double "x"))))
        ]
      where
        -- Seven distinct ints with a large spread; the sum fits in a Double.
        xs = take n (cycle [3, -1, 0, 1, 7, 1000000007, -999999989 :: Int])
        xsI = Column "x" xs
        xsD = Column "x" (take n smallDoubles)
        sumD = sum (map fromIntegral xs) :: Double
    smallDoubles = cycle [0.5, 1, 2.5, -3, 0.25]
    approx a b = abs (a - b) <= 1.0e-9 * max 1 (abs a)

tests :: [Test]
tests =
    mergeAggCases
        ++ binaryCases doubles comparisons
        ++ binaryCases ints comparisons
        ++ binaryCases doubles arithmetic
        ++ binaryCases ints arithmetic
        ++ selectCases doubles
        ++ selectCases ints
        ++ caseWhenCases
        ++ toDoubleCases