packages feed

keel-0.1.0.0: src/Keel/Bridge.hs

-- | The explicit frame ↔ buffer copy: dataframe 'Column's to
-- 'VS.Vector' buffers (ready for keel-linalg \/ keel-abi) and back.
--
-- This module is keel's ONLY point of contact with the dataframe stack,
-- and it is honest about the cost: every conversion here is an O(n)
-- copy — dataframe's unboxed columns and Storable vectors are different
-- memory representations, exactly the trade numpy↔pandas makes. No API
-- here pretends a zero-copy path exists where the representation cannot
-- provide one.
--
-- Nulls are refused, not imputed: a column with missing values raises
-- 'ColumnHasNulls' — fill or drop them with dataframe's own operations
-- first, where that decision belongs.
module Keel.Bridge
  ( BridgeError (..)
  , columnToStorable
  , columnsToMatrix
  , storableToColumn
  ) where

import Data.Text qualified as T
import Data.Vector qualified as V
import Data.Vector.Generic qualified as VG
import Data.Vector.Storable qualified as VS

import DataFrame.Internal.Column
  ( Column
  , columnLength
  , fromUnboxedVector
  , numElements
  , toUnboxedVector
  )
import DataFrame.Internal.DataFrame (DataFrame, getColumn)

-- | Why a frame ↔ buffer conversion was refused.
data BridgeError
  = ColumnNotFound T.Text
    -- ^ No column of that name in the frame.
  | ColumnHasNulls T.Text Int
    -- ^ The column has this many missing values; handle them in
    -- dataframe first (fill\/drop) — the bridge never imputes.
  | ColumnTypeMismatch T.Text String
    -- ^ The column does not hold @Double@s (upstream's own type error
    -- is carried verbatim).
  | ColumnLengthMismatch T.Text Int Int
    -- ^ (column, expected rows, actual rows) while assembling a matrix.
    -- Defensive: dataframe pads short columns with nulls on insert, so
    -- columns from one frame are always equal-length (a padded column
    -- trips 'ColumnHasNulls' first); this guards the invariant anyway.
  deriving (Eq, Show)

-- | O(n) copy of a @Double@ column out of a frame into a Storable
-- buffer.
columnToStorable :: DataFrame -> T.Text -> Either BridgeError (VS.Vector Double)
columnToStorable df name = do
  col <- maybe (Left (ColumnNotFound name)) Right (getColumn name df)
  let nulls = columnLength col - numElements col
  if nulls > 0
    then Left (ColumnHasNulls name nulls)
    else case toUnboxedVector col of
      Left e -> Left (ColumnTypeMismatch name (show e))
      Right vu -> Right (VG.convert vu)

-- | O(n·k) copy of @k@ same-length @Double@ columns into one row-major
-- @rows × k@ matrix (the layout every keel-linalg driver takes).
-- Returns the row count alongside the buffer.
columnsToMatrix :: DataFrame -> [T.Text] -> Either BridgeError (Int, VS.Vector Double)
columnsToMatrix df names = do
  cols <- traverse (\n -> (,) n <$> columnToStorable df n) names
  case cols of
    [] -> Right (0, VS.empty)
    (_, c0) : rest -> do
      let m = VS.length c0
      mapM_
        ( \(n, c) ->
            if VS.length c == m
              then Right ()
              else Left (ColumnLengthMismatch n m (VS.length c))
        )
        rest
      let k = length cols
          -- boxed vector for O(1) column lookup per cell (a list here
          -- makes the copy O(n·k²), belying the O(n·k) contract above)
          vecs = V.fromList (map snd cols)
      Right (m, VS.generate (m * k) (\i -> (vecs V.! (i `mod` k)) VS.! (i `div` k)))

-- | O(n) copy of a Storable buffer back into a (null-free) dataframe
-- column, e.g. to insert a keel-linalg result as a new column.
storableToColumn :: VS.Vector Double -> Column
storableToColumn = fromUnboxedVector . VG.convert