srtree-3.0.0.0: src/Data/SRTree/Eval.hs
{-# LANGUAGE LambdaCase, BangPatterns #-}
-----------------------------------------------------------------------------
-- |
-- Module : Data.SRTree.Eval
-- Copyright : (c) Fabricio Olivetti 2021 - 2024
-- License : BSD3
-- Maintainer : fabricio.olivetti@gmail.com
-- Stability : experimental
-- Portability : FlexibleInstances, DeriveFunctor, ScopedTypeVariables
--
-- Evaluation of SRTree expressions
--
-----------------------------------------------------------------------------
{-# LANGUAGE FlexibleInstances #-}
module Data.SRTree.Eval
( evalOp
, evalFun
, cbrt
, inverseFunc
, invertibles
, evalInverse
, invright
, invleft
, replicateAs
, Target, Theta, Columns
, compile
, compileLoss
)
where
import Data.SRTree.Internal
import Data.SRTree.Recursion (Fix (..), cata)
import Data.Vector.Unboxed (Vector)
import qualified Data.Vector.Unboxed as V
import Control.Monad.ST (runST)
import qualified Data.Vector as VB -- Boxed vector for instructions
import qualified Data.Vector.Unboxed.Mutable as VM
import Control.Concurrent.Async (forConcurrently_)
import System.IO.Unsafe (unsafePerformIO)
import Control.Concurrent (getNumCapabilities)
import Data.Maybe (fromJust)
-- | Vector of target values
type Target = Vector Double
-- | Vector of parameter values. Needs to be strict to be readily accesible.
type Theta = Vector Double
-- | Matrix of features values
type Columns = [Vector Double]
-- A multi-threaded replacement for V.sum
sumParallel :: Int -> (Int -> Double) -> Double
sumParallel n f = unsafePerformIO $ do
numThreads <- getNumCapabilities
let chunkSize = n `quot` numThreads
-- 1. Allocate a single block of unboxed memory EXACTLY ONCE
out <- VM.unsafeNew numThreads
-- 2. Spawn threads. Each thread gets a unique ID and a slice of memory.
forConcurrently_ [0 .. numThreads - 1] $ \tId -> do
let !start = tId * chunkSize
-- The last thread cleans up the remainder
!end = if tId == numThreads - 1 then n else start + chunkSize
-- 3. The inner thread loop. Strict, unboxed, and bounds-check free.
let loop !i !acc
| i >= end = return acc
| otherwise = loop (i + 1) (acc + f i)
total <- loop start 0.0
VM.unsafeWrite out tId total
-- 4. Instantly cast the mutable memory to an immutable Vector (O(1) cost)
totals <- V.unsafeFreeze out
return (V.sum totals)
{-# NOINLINE sumParallel #-}
-- Improve quality of life with Num and Floating instances for our matrices
instance Num Target where
(+) = V.zipWith (+)
(-) = V.zipWith (-)
(*) = V.zipWith (*)
abs = V.map abs
signum = V.map signum
fromInteger = V.singleton . fromInteger
negate = V.map negate
instance Floating Target where
pi = V.singleton pi
exp = V.map exp
log = V.map log
sqrt = V.map sqrt
sin = V.map sin
cos = V.map cos
tan = V.map tan
asin = V.map asin
acos = V.map acos
atan = V.map atan
sinh = V.map sinh
cosh = V.map cosh
tanh = V.map tanh
asinh = V.map asinh
acosh = V.map acosh
atanh = V.map atanh
(**) = V.zipWith (**)
instance Fractional Target where
fromRational = V.singleton . fromRational
(/) = V.zipWith (/)
recip = V.map recip
-- We change the Dynamic type to evaluate a single scalar at a specific row index (Int)
data Staged =
Scl Double
| Static (Vector Double)
| Dynamic (Vector Double -> Int -> Double) -- (Theta -> RowIndex -> Result)
-- A multi-threaded replacement for V.generate
generateParallel :: Int -> (Int -> Double) -> V.Vector Double
generateParallel n f = unsafePerformIO $ do
numThreads <- getNumCapabilities
let chunkSize = n `quot` numThreads
-- 1. Allocate a single block of unboxed memory EXACTLY ONCE
out <- VM.unsafeNew n
-- 2. Spawn threads. Each thread gets a unique ID and a slice of memory.
forConcurrently_ [0 .. numThreads - 1] $ \tId -> do
let !start = tId * chunkSize
-- The last thread cleans up the remainder
!end = if tId == numThreads - 1 then n else start + chunkSize
-- 3. The inner thread loop. Strict, unboxed, and bounds-check free.
let loop !i
| i >= end = return ()
| otherwise = do
-- Write directly to the shared memory pointer
VM.unsafeWrite out i (f i)
loop (i + 1)
loop start
-- 4. Instantly cast the mutable memory to an immutable Vector (O(1) cost)
V.unsafeFreeze out
{-# NOINLINE generateParallel #-}
compileLoss :: [Vector Double] -> Fix SRTree -> Target -> Maybe Target -> (Vector Double -> Double)
compileLoss dataset tree y mYerr =
case cata alg tree of
Scl c -> \_ -> V.sum $ V.replicate n c
Static v -> \_ -> V.sum v
-- We only allocate memory EXACTLY ONCE here at the top level
--Dynamic f -> \th -> V.generate n (f th)
Dynamic f -> \th -> V.sum (V.generate n (f th))
where
n = V.length (head dataset)
yErr = fromJust mYerr
alg :: SRTree Staged -> Staged
-- 1. Base Cases
alg (Const c) = Scl c
alg (Var (-1)) = Static y
alg (Var (-2)) = Static yErr
alg (Var i) = Static (dataset !! i)
alg (Param i) = Dynamic (\th !idx -> th `V.unsafeIndex` i)
-- 2. Univariate Functions
alg (Uni f (Scl c)) = Scl (evalFun f c)
alg (Uni f (Static v)) = Static (V.map (evalFun f) v)
-- We map the function over the scalar result of the inner closure
alg (Uni f (Dynamic g)) = let !rawFun = evalFun f in Dynamic (\th !i -> rawFun (g th i))
-- 3. Binary Functions
alg (Bin op (Scl c1) (Scl c2)) = Scl (evalOp op c1 c2)
alg (Bin op (Scl c) (Static v)) = Static (V.map (evalOp op c) v)
alg (Bin op (Static v) (Scl c)) = Static (V.map (\c2 -> evalOp op c2 c) v)
alg (Bin op (Static v1) (Static v2)) = Static (V.zipWith (evalOp op) v1 v2)
-- 4. Dynamic Combinations (The Core Optimization)
alg (Bin op (Scl c) (Dynamic g)) =
let !rawOp = evalOp op in Dynamic (\th !i -> rawOp c (g th i))
alg (Bin op (Dynamic g) (Scl c)) =
let !rawOp = evalOp op in Dynamic (\th !i -> rawOp (g th i) c)
-- When combining a Static array with a Dynamic closure,
-- we use unsafeIndex to fetch the static value at row 'i' directly.
alg (Bin op (Static v) (Dynamic g)) =
let !rawOp = evalOp op in Dynamic (\th !i -> rawOp (v `V.unsafeIndex` i) (g th i))
alg (Bin op (Dynamic g) (Static v)) =
let !rawOp = evalOp op in Dynamic (\th !i -> rawOp (g th i) (v `V.unsafeIndex` i))
alg (Bin op (Dynamic g1) (Dynamic g2)) =
let !rawOp = evalOp op in Dynamic (\th !i -> rawOp (g1 th i) (g2 th i))
compile :: [Vector Double] -> Fix SRTree -> (Vector Double -> Vector Double)
compile dataset tree =
case cata alg tree of
Scl c -> \_ -> V.replicate n c
Static v -> \_ -> v
-- We only allocate memory EXACTLY ONCE here at the top level
--Dynamic f -> \th -> V.generate n (f th)
Dynamic f -> \th -> V.generate n (f th)
where
n = V.length (head dataset)
alg :: SRTree Staged -> Staged
-- 1. Base Cases
alg (Const c) = Scl c
alg (Var i) = Static (dataset !! i)
-- Look at this! No more V.replicate. It just fetches the scalar directly.
alg (Param i) = Dynamic (\th !idx -> th `V.unsafeIndex` i)
alg (Y i) = undefined -- this shouldn't be called
-- 2. Univariate Functions
alg (Uni f (Scl c)) = Scl (evalFun f c)
alg (Uni f (Static v)) = Static (V.map (evalFun f) v)
-- We map the function over the scalar result of the inner closure
alg (Uni f (Dynamic g)) = let !rawFun = evalFun f in Dynamic (\th !i -> rawFun (g th i))
-- 3. Binary Functions
alg (Bin op (Scl c1) (Scl c2)) = Scl (evalOp op c1 c2)
alg (Bin op (Scl c) (Static v)) = Static (V.map (evalOp op c) v)
alg (Bin op (Static v) (Scl c)) = Static (V.map (\c2 -> evalOp op c2 c) v)
alg (Bin op (Static v1) (Static v2)) = Static (V.zipWith (evalOp op) v1 v2)
-- 4. Dynamic Combinations (The Core Optimization)
alg (Bin op (Scl c) (Dynamic g)) =
let !rawOp = evalOp op in Dynamic (\th !i -> rawOp c (g th i))
alg (Bin op (Dynamic g) (Scl c)) =
let !rawOp = evalOp op in Dynamic (\th !i -> rawOp (g th i) c)
-- When combining a Static array with a Dynamic closure,
-- we use unsafeIndex to fetch the static value at row 'i' directly.
alg (Bin op (Static v) (Dynamic g)) =
let !rawOp = evalOp op in Dynamic (\th !i -> rawOp (v `V.unsafeIndex` i) (g th i))
alg (Bin op (Dynamic g) (Static v)) =
let !rawOp = evalOp op in Dynamic (\th !i -> rawOp (g th i) (v `V.unsafeIndex` i))
alg (Bin op (Dynamic g1) (Dynamic g2)) =
let !rawOp = evalOp op in Dynamic (\th !i -> rawOp (g1 th i) (g2 th i))
-- returns a vector with the same number of rows as xss and containing a single repeated value.
replicateAs :: Columns -> Double -> Target
replicateAs xss c = let m = V.length (head xss) in V.replicate m c
{-# INLINE replicateAs #-}
-- | Evaluates the tree given a vector of variable values, a vector of parameter values and a function that takes a Double and change to whatever type the variables have. This is useful when working with datasets of many values per variables.
evalTree :: Columns -> Theta -> Fix SRTree -> Target
evalTree xss params = cata $
\case
Var ix -> xss !! ix
Param ix -> replicateAs xss $ params V.! ix
Const c -> replicateAs xss c
Y _ -> undefined
Uni g t -> evalFun g t
Bin op l r -> evalOp op l r
{-# INLINE evalTree #-}
-- evaluates an operator
evalOp :: Floating a => Op -> a -> a -> a
evalOp Add = (+)
evalOp Sub = (-)
evalOp Mul = (*)
evalOp Div = (/)
evalOp Power = (**)
evalOp PowerAbs = \l r -> abs l ** r
evalOp AQ = \l r -> l / sqrt(1 + r*r)
{-# INLINE evalOp #-}
-- evaluates a function
evalFun :: Floating a => Function -> a -> a
evalFun Id = id
evalFun Abs = abs
evalFun Sin = sin
evalFun Cos = cos
evalFun Tan = tan
evalFun Sinh = sinh
evalFun Cosh = cosh
evalFun Tanh = tanh
evalFun ASin = asin
evalFun ACos = acos
evalFun ATan = atan
evalFun ASinh = asinh
evalFun ACosh = acosh
evalFun ATanh = atanh
evalFun Sqrt = sqrt
evalFun SqrtAbs = sqrt . abs
evalFun Cbrt = cbrt
evalFun Square = (^2)
evalFun Log = log
evalFun LogAbs = log . abs
evalFun Exp = exp
evalFun Recip = recip
evalFun Cube = (^3)
{-# INLINE evalFun #-}
-- Cubic root
cbrt :: Floating a => a -> a
cbrt x = signum x * abs x ** (1/3)
{-# INLINE cbrt #-}
-- | Returns the inverse of a function. This is a partial function.
inverseFunc :: Function -> Function
inverseFunc Id = Id
inverseFunc Sin = ASin
inverseFunc Cos = ACos
inverseFunc Tan = ATan
inverseFunc Sinh = ASinh
inverseFunc Cosh = ACosh
inverseFunc Tanh = ATanh
inverseFunc ASin = Sin
inverseFunc ACos = Cos
inverseFunc ATan = Tan
inverseFunc ASinh = Sinh
inverseFunc ACosh = Cosh
inverseFunc ATanh = Tanh
inverseFunc Sqrt = Square
inverseFunc Square = Sqrt
-- inverseFunc Cbrt = (^3)
inverseFunc Log = Exp
inverseFunc Exp = Log
inverseFunc Recip = Recip
-- inverseFunc Abs = Abs -- we assume abs(x) = sqrt(x^2) so y = sqrt(x^2) => x^2 = y^2 => x = sqrt(y^2) = x = abs(y)
inverseFunc x = error $ show x ++ " has no support for inverse function"
{-# INLINE inverseFunc #-}
-- | evals the inverse of a function
evalInverse :: Floating a => Function -> a -> a
evalInverse Id = id
evalInverse Sin = asin
evalInverse Cos = acos
evalInverse Tan = atan
evalInverse Sinh = asinh
evalInverse Cosh = acosh
evalInverse Tanh = atanh
evalInverse ASin = sin
evalInverse ACos = cos
evalInverse ATan = tan
evalInverse ASinh = sinh
evalInverse ACosh = cosh
evalInverse ATanh = tanh
evalInverse Sqrt = (^2)
evalInverse SqrtAbs = (^2)
evalInverse Square = sqrt
evalInverse Cbrt = (^3)
evalInverse Log = exp
evalInverse LogAbs = exp
evalInverse Exp = log
evalInverse Abs = abs -- we assume abs(x) = sqrt(x^2) so y = sqrt(x^2) => x^2 = y^2 => x = sqrt(y^2) = x = abs(y)
evalInverse Recip = recip
evalInverse Cube = cbrt
{-# INLINE evalInverse #-}
-- | evals the right inverse of an operator
invright :: Floating a => Op -> a -> (a -> a)
invright Add v = subtract v
invright Sub v = (+v)
invright Mul v = (/v)
invright Div v = (*v)
invright Power v = (**(1/v))
invright PowerAbs v = (**(1/v))
invright AQ v = (* sqrt (1 + v*v))
{-# INLINE invright #-}
-- | evals the left inverse of an operator
invleft :: Floating a => Op -> a -> (a -> a)
invleft Add v = subtract v
invleft Sub v = (+v) . negate -- y = v - r => r = v - y
invleft Mul v = (/v)
invleft Div v = (v/) -- y = v / r => r = v/y
invleft Power v = logBase v -- (/(log v)) . log -- y = v ^ r log y = r log v r = log y / log v
invleft PowerAbs v = logBase v . abs
invleft AQ v = (v/)
{-# INLINE invleft #-}
-- | List of invertible functions
invertibles :: [Function]
invertibles = [Id, Sin, Cos, Tan, Tanh, ASin, ACos, ATan, ATanh, Sqrt, Square, Log, Exp, Recip]
{-# INLINE invertibles #-}