dataframe-arrow-1.0.2.0: ffi-export/DataFrame/FFI.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module DataFrame.FFI where
import Control.Exception (SomeException, try)
import qualified Data.Aeson as Aeson
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import Data.Int (Int16, Int32, Int64, Int8)
import qualified Data.Maybe
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Type.Equality (
type (:~~:) (HRefl),
)
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as VU
import Data.Word (Word16, Word32, Word64, Word8)
import Foreign (Ptr, castPtr, copyBytes, mallocBytes, poke, ptrToWordPtr)
import Foreign.C.String (CString)
import Foreign.C.Types (CChar, CInt (..))
import Foreign.Marshal.Alloc (free)
import System.IO (hPrint, stderr)
import Type.Reflection (SomeTypeRep (..), eqTypeRep, typeRep)
import DataFrame.DecisionTree (
SynthConfig (..),
TreeConfig (..),
defaultColumnOrdering,
defaultSynthConfig,
defaultTreeConfig,
fit,
predict,
)
import DataFrame.IO.Arrow (dataframeToArrow)
import DataFrame.IO.CSV.Fast (fastReadCsvWithOpts)
import DataFrame.IR (PlanNode, executePlan)
import DataFrame.IR.ExprJson (encodeExprToBytes)
import DataFrame.Internal.Column (
Column (..),
Columnable,
materializePacked,
mergedHead,
)
import DataFrame.Internal.DataFrame (DataFrame, unsafeGetColumn)
import DataFrame.Internal.Expression (Expr (Col))
foreign export ccall "dfExecutePlan"
dfExecutePlan :: CString -> Ptr Word64 -> Ptr Word64 -> IO CInt
{- | Execute a JSON-encoded query plan, returning Arrow C Data Interface
pointers to Python via the @schemaOut@/@arrayOut@ output parameters (each an
address as a 'Word64'). Returns 0 on success, -1 on error.
-}
dfExecutePlan :: CString -> Ptr Word64 -> Ptr Word64 -> IO CInt
dfExecutePlan planCS schemaOut arrayOut = do
planBytes <- BS.packCString planCS
result <- try @SomeException $ do
node <-
either
fail
return
(Aeson.eitherDecode (BL.fromStrict planBytes))
df <- executePlan fastReadCsvWithOpts node
(sPtr, aPtr) <- dataframeToArrow df
poke schemaOut (fromIntegral (ptrToWordPtr (castPtr sPtr)))
poke arrayOut (fromIntegral (ptrToWordPtr (castPtr aPtr)))
case result of
Left ex -> hPrint stderr ex >> return (CInt (-1))
Right _ -> return (CInt 0)
foreign export ccall "dfFitDecisionTree"
dfFitDecisionTree ::
CString ->
CString ->
CString ->
CString ->
Ptr (Ptr CChar) ->
Ptr Word64 ->
IO CInt
foreign export ccall "dfFreeModel"
dfFreeModel :: Ptr CChar -> IO ()
{- | Fit a TAO decision tree on a plan's materialized result, predicting the
target column from the rest (@target_type@: @"auto"@ or a wire type tag;
@config_json@: 'TreeConfig' fields). Caller frees via 'dfFreeModel'; 0 ok, -1 err.
-}
dfFitDecisionTree ::
CString ->
CString ->
CString ->
CString ->
Ptr (Ptr CChar) ->
Ptr Word64 ->
IO CInt
dfFitDecisionTree planCS targetCS targetTypeCS configCS modelOut lenOut = do
result <- try @SomeException $ do
planBytes <- BS.packCString planCS
targetBytes <- BS.packCString targetCS
targetTypeBytes <- BS.packCString targetTypeCS
configBytes <- BS.packCString configCS
let target = TE.decodeUtf8 targetBytes
targetTypeStr = TE.decodeUtf8 targetTypeBytes
cfgJson <-
either
fail
return
(Aeson.eitherDecodeStrict configBytes :: Either String TreeConfigJson)
let treeCfg = treeConfigFromJson cfgJson
node <-
either
fail
return
(Aeson.eitherDecodeStrict planBytes :: Either String PlanNode)
df <- executePlan fastReadCsvWithOpts node
let typeTag =
if targetTypeStr == "auto"
then inferTargetType target df
else targetTypeStr
treeBytes <- fitTreeWithType typeTag treeCfg target df
let len = BS.length treeBytes
bufPtr <- mallocBytes (max 1 len) :: IO (Ptr CChar)
BS.useAsCStringLen treeBytes $ \(srcPtr, srcLen) ->
copyBytes bufPtr (castPtr srcPtr) srcLen
poke modelOut bufPtr
poke lenOut (fromIntegral len :: Word64)
case result of
Left ex -> hPrint stderr ex >> return (CInt (-1))
Right _ -> return (CInt 0)
dfFreeModel :: Ptr CChar -> IO ()
dfFreeModel = free
data TreeConfigJson = TreeConfigJson
{ jcMaxDepth :: Maybe Int
, jcMinSamplesSplit :: Maybe Int
, jcMinLeafSize :: Maybe Int
, jcPercentiles :: Maybe [Int]
, jcExpressionPairs :: Maybe Int
, jcTaoIterations :: Maybe Int
, jcTaoConvergenceTol :: Maybe Double
, jcMaxExprDepth :: Maybe Int
, jcBoolExpansion :: Maybe Int
, jcComplexityPenalty :: Maybe Double
, jcEnableStringOps :: Maybe Bool
, jcEnableCrossCols :: Maybe Bool
, jcEnableArithOps :: Maybe Bool
}
instance Aeson.FromJSON TreeConfigJson where
parseJSON = Aeson.withObject "TreeConfig" $ \o ->
TreeConfigJson
<$> o Aeson..:? "max_depth"
<*> o Aeson..:? "min_samples_split"
<*> o Aeson..:? "min_leaf_size"
<*> o Aeson..:? "percentiles"
<*> o Aeson..:? "expression_pairs"
<*> o Aeson..:? "tao_iterations"
<*> o Aeson..:? "tao_convergence_tol"
<*> o Aeson..:? "max_expr_depth"
<*> o Aeson..:? "bool_expansion"
<*> o Aeson..:? "complexity_penalty"
<*> o Aeson..:? "enable_string_ops"
<*> o Aeson..:? "enable_cross_cols"
<*> o Aeson..:? "enable_arith_ops"
treeConfigFromJson :: TreeConfigJson -> TreeConfig
treeConfigFromJson j =
TreeConfig
{ maxTreeDepth = pick jcMaxDepth maxTreeDepth
, minSamplesSplit = pick jcMinSamplesSplit minSamplesSplit
, minLeafSize = pick jcMinLeafSize minLeafSize
, percentiles = pick jcPercentiles percentiles
, expressionPairs = pick jcExpressionPairs expressionPairs
, taoIterations = pick jcTaoIterations taoIterations
, taoConvergenceTol = pick jcTaoConvergenceTol taoConvergenceTol
, columnOrdering = defaultColumnOrdering
, synthConfig =
SynthConfig
{ maxExprDepth = pickS jcMaxExprDepth maxExprDepth
, boolExpansion = pickS jcBoolExpansion boolExpansion
, disallowedCombinations = []
, complexityPenalty = pickS jcComplexityPenalty complexityPenalty
, enableStringOps = pickS jcEnableStringOps enableStringOps
, enableCrossCols = pickS jcEnableCrossCols enableCrossCols
, enableArithOps = pickS jcEnableArithOps enableArithOps
}
}
where
pick :: (TreeConfigJson -> Maybe a) -> (TreeConfig -> a) -> a
pick getter dflt = Data.Maybe.fromMaybe (dflt defaultTreeConfig) (getter j)
pickS :: (TreeConfigJson -> Maybe a) -> (SynthConfig -> a) -> a
pickS getter dflt = Data.Maybe.fromMaybe (dflt defaultSynthConfig) (getter j)
{- | Need this so we don't specify types on the Python side
but it means out type universe is limited/falls back to string.
-}
inferTargetType :: T.Text -> DataFrame -> T.Text
inferTargetType target df = dispatchType (columnTypeRep (unsafeGetColumn target df))
where
columnTypeRep :: Column -> SomeTypeRep
columnTypeRep (UnboxedColumn _ (_ :: VU.Vector a)) = SomeTypeRep (typeRep @a)
columnTypeRep (BoxedColumn _ (_ :: V.Vector a)) = SomeTypeRep (typeRep @a)
columnTypeRep c@(PackedText _ _) = columnTypeRep (materializePacked c)
columnTypeRep c@(MergedColumn _ _) = columnTypeRep (mergedHead c)
dispatchType :: SomeTypeRep -> T.Text
dispatchType (SomeTypeRep tr)
| Just HRefl <- eqTypeRep tr (typeRep @Int) = "int"
| Just HRefl <- eqTypeRep tr (typeRep @Int8) = "int8"
| Just HRefl <- eqTypeRep tr (typeRep @Int16) = "int16"
| Just HRefl <- eqTypeRep tr (typeRep @Int32) = "int32"
| Just HRefl <- eqTypeRep tr (typeRep @Int64) = "int64"
| Just HRefl <- eqTypeRep tr (typeRep @Word) = "word"
| Just HRefl <- eqTypeRep tr (typeRep @Word8) = "word8"
| Just HRefl <- eqTypeRep tr (typeRep @Word16) = "word16"
| Just HRefl <- eqTypeRep tr (typeRep @Word32) = "word32"
| Just HRefl <- eqTypeRep tr (typeRep @Word64) = "word64"
| Just HRefl <- eqTypeRep tr (typeRep @Integer) = "integer"
| Just HRefl <- eqTypeRep tr (typeRep @Double) = "double"
| Just HRefl <- eqTypeRep tr (typeRep @Float) = "float"
| Just HRefl <- eqTypeRep tr (typeRep @Bool) = "bool"
| Just HRefl <- eqTypeRep tr (typeRep @Char) = "char"
| Just HRefl <- eqTypeRep tr (typeRep @T.Text) = "text"
| Just HRefl <- eqTypeRep tr (typeRep @String) = "string"
| otherwise =
error $
"DataFrame.FFI.inferTargetType: unsupported target column type: "
++ show tr
fitTreeWithType ::
T.Text -> TreeConfig -> T.Text -> DataFrame -> IO BS.ByteString
fitTreeWithType ttag cfg target df = case ttag of
"int" -> fitEncode @Int
"int8" -> fitEncode @Int8
"int16" -> fitEncode @Int16
"int32" -> fitEncode @Int32
"int64" -> fitEncode @Int64
"word" -> fitEncode @Word
"word8" -> fitEncode @Word8
"word16" -> fitEncode @Word16
"word32" -> fitEncode @Word32
"word64" -> fitEncode @Word64
"integer" -> fitEncode @Integer
"double" -> fitEncode @Double
"float" -> fitEncode @Float
"bool" -> fitEncode @Bool
"char" -> fitEncode @Char
"text" -> fitEncode @T.Text
"string" -> fitEncode @String
other ->
ioError . userError $
"DataFrame.FFI.fitTreeWithType: unsupported target type tag: "
++ T.unpack other
where
fitEncode :: forall a. (Columnable a, Ord a) => IO BS.ByteString
fitEncode = do
let expr = predict (fit cfg (Col @a target) df)
case encodeExprToBytes expr of
Right bs -> return bs
Left err -> ioError (userError $ "encodeExprToBytes: " ++ err)