packages feed

moonlight-linalg-0.1.0.0: src-carrier/Moonlight/LinAlg/Pure/Dense/Rows.hs

{-# LANGUAGE DerivingStrategies #-}

-- | Validated rectangular row authoring surface.
--
-- `DenseRows` exists to seal rectangular nested-list input and return precise
-- shape errors. It is deliberately not the hot dense-storage owner; use vector,
-- sparse, tridiagonal, or native kernels for benchmark-sensitive work.
module Moonlight.LinAlg.Pure.Dense.Rows
  ( DenseRows,
    mkDenseRows,
    mkDenseRowsWithShape,
    mkDenseRowsFromFlat,
    denseRowsShape,
    denseRowsToLists,
    transposeRowsExact,
    zipRowsExactWith,
    matrixVectorProductRowsWith,
    matrixProductRowsWith,
    hcatRowsExact,
    vcatRowsExact,
  )
where

import Data.Bifunctor (first)
import Data.Kind (Type)
import Moonlight.Core
  ( MoonlightError (..),
    checkedNonNegativeProduct,
  )
import Prelude

type DenseRows :: Type -> Type
data DenseRows a = DenseRows
  { denseRowCount :: !Int,
    denseColumnCount :: !Int,
    denseRowsData :: ![[a]]
  }
  deriving stock (Eq, Show)

mkDenseRows :: [[a]] -> Either MoonlightError (DenseRows a)
mkDenseRows rowValues =
  case rowValues of
    [] ->
      Right (DenseRows 0 0 [])
    firstRow : _ ->
      mkDenseRowsWithShape
        (length rowValues)
        (length firstRow)
        rowValues

mkDenseRowsWithShape :: Int -> Int -> [[a]] -> Either MoonlightError (DenseRows a)
mkDenseRowsWithShape expectedRowCount expectedColumnCount rowValues
  | expectedRowCount < 0 =
      Left
        ( InvariantViolation
            ( "dense row matrix row count must be non-negative, received "
                <> show expectedRowCount
            )
        )
  | expectedColumnCount < 0 =
      Left
        ( InvariantViolation
            ( "dense row matrix column count must be non-negative, received "
                <> show expectedColumnCount
            )
        )
  | actualRowCount /= expectedRowCount =
      Left
        ( InvariantViolation
            ( "dense row matrix row count mismatch: expected "
                <> show expectedRowCount
                <> " rows but received "
                <> show actualRowCount
            )
        )
  | otherwise =
      case firstMismatchedRowWidth expectedColumnCount rowValues of
        Nothing ->
          Right
            DenseRows
              { denseRowCount = expectedRowCount,
                denseColumnCount = expectedColumnCount,
                denseRowsData = rowValues
              }
        Just (rowIndex, actualColumnCount) ->
          Left
            ( InvariantViolation
                ( "dense row matrix is ragged at row "
                    <> show rowIndex
                    <> " (expected "
                    <> show expectedColumnCount
                    <> " columns, got "
                    <> show actualColumnCount
                    <> ")"
                )
            )
  where
    actualRowCount = length rowValues

firstMismatchedRowWidth :: Int -> [[a]] -> Maybe (Int, Int)
firstMismatchedRowWidth expectedColumnCount =
  foldr firstMismatch Nothing . zip [0 :: Int ..]
  where
    firstMismatch (rowIndex, rowValues) remainingMismatch =
      let actualColumnCount = length rowValues
       in if actualColumnCount == expectedColumnCount
            then remainingMismatch
            else Just (rowIndex, actualColumnCount)

mkDenseRowsFromFlat :: Int -> Int -> [a] -> Either MoonlightError (DenseRows a)
mkDenseRowsFromFlat rowCount columnCount values
  | rowCount < 0 || columnCount < 0 =
      Left (InvariantViolation "dense row matrix dimensions must be non-negative")
  | otherwise = do
      expectedLength <-
        first
          (const (InvariantViolation "dense row matrix dimensions exceed Int cardinality"))
          (checkedNonNegativeProduct rowCount columnCount)
      if expectedLength /= length values
        then
          Left
            ( InvariantViolation
                ( "dense row flat payload length mismatch: expected "
                    <> show expectedLength
                    <> " values but received "
                    <> show (length values)
                )
            )
        else
          if columnCount == 0
            then mkDenseRowsWithShape rowCount columnCount (replicate rowCount [])
            else mkDenseRowsWithShape rowCount columnCount (flatRows rowCount columnCount values)

flatRows :: Int -> Int -> [a] -> [[a]]
flatRows remainingRows columnCount values
  | remainingRows <= 0 = []
  | otherwise =
      let (rowValues, restValues) = splitAt columnCount values
       in rowValues : flatRows (remainingRows - 1) columnCount restValues

denseRowsShape :: DenseRows a -> (Int, Int)
denseRowsShape denseRowsValue =
  (denseRowCount denseRowsValue, denseColumnCount denseRowsValue)

denseRowsToLists :: DenseRows a -> [[a]]
denseRowsToLists = denseRowsData

transposeRowsExact :: [[a]] -> Either MoonlightError [[a]]
transposeRowsExact =
  fmap (denseRowsToLists . transposeDenseRows) . mkDenseRows

zipRowsExactWith :: (left -> right -> result) -> [[left]] -> [[right]] -> Either MoonlightError [[result]]
zipRowsExactWith combine leftRows rightRows = do
  leftDenseRows <- mkDenseRows leftRows
  rightDenseRows <- mkDenseRows rightRows
  denseRowsToLists <$> zipDenseRowsWith combine leftDenseRows rightDenseRows

matrixVectorProductRowsWith ::
  (entry -> value -> product) ->
  (product -> accumulator -> accumulator) ->
  accumulator ->
  [[entry]] ->
  [value] ->
  Either MoonlightError [accumulator]
matrixVectorProductRowsWith multiply append zeroValue rowValues vectorValue =
  do
    denseRowsValue <- mkDenseRows rowValues
    if length vectorValue /= denseColumnCount denseRowsValue
      then
        Left
          ( InvariantViolation
              ( "dense row matrix/vector shape mismatch (matrix="
                  <> show (denseRowsShape denseRowsValue)
                  <> ", vector="
                  <> show (length vectorValue)
                  <> ")"
              )
          )
      else
        Right
          ( (\rowValue -> trustedDotProductWith multiply append zeroValue rowValue vectorValue)
              <$> denseRowsData denseRowsValue
          )

matrixProductRowsWith ::
  (left -> right -> product) ->
  (product -> accumulator -> accumulator) ->
  accumulator ->
  [[left]] ->
  [[right]] ->
  Either MoonlightError [[accumulator]]
matrixProductRowsWith multiply append zeroValue leftRows rightRows = do
  leftDenseRows <- mkDenseRows leftRows
  rightDenseRows <- mkDenseRows rightRows
  if denseColumnCount leftDenseRows /= denseRowCount rightDenseRows
    then
      Left
        ( InvariantViolation
            ( "dense row matrix product shape mismatch (left="
                <> show (denseRowsShape leftDenseRows)
                <> ", right="
                <> show (denseRowsShape rightDenseRows)
                <> ")"
            )
        )
    else
      let rightColumns = denseRowsData (transposeDenseRows rightDenseRows)
       in Right
            ( (\leftRow -> trustedDotProductWith multiply append zeroValue leftRow <$> rightColumns)
                <$> denseRowsData leftDenseRows
            )

trustedDotProductWith ::
  (left -> right -> product) ->
  (product -> accumulator -> accumulator) ->
  accumulator ->
  [left] ->
  [right] ->
  accumulator
trustedDotProductWith multiply append zeroValue left right =
  foldr append zeroValue (zipWith multiply left right)
{-# INLINE trustedDotProductWith #-}

hcatRowsExact :: [[[a]]] -> Either MoonlightError [[a]]
hcatRowsExact rowMatrices = do
  denseRowMatrices <- traverse mkDenseRows rowMatrices
  case denseRowMatrices of
    [] ->
      Right []
    firstDenseRows : remainingDenseRows ->
      if all (\denseRowsValue -> denseRowCount denseRowsValue == denseRowCount firstDenseRows) remainingDenseRows
        then
          pure
            ( foldr
                (zipWith (++))
                (replicate (denseRowCount firstDenseRows) [])
                (map denseRowsData denseRowMatrices)
            )
        else
          Left
            ( InvariantViolation
                ( "dense horizontal concatenation requires equal row counts, got "
                    <> show (map denseRowsShape denseRowMatrices)
                )
            )

vcatRowsExact :: [[[a]]] -> Either MoonlightError [[a]]
vcatRowsExact rowMatrices = do
  denseRowMatrices <- traverse mkDenseRows rowMatrices
  case denseRowMatrices of
    [] ->
      Right []
    firstDenseRows : remainingDenseRows ->
      if all (\denseRowsValue -> denseColumnCount denseRowsValue == denseColumnCount firstDenseRows) remainingDenseRows
        then pure (denseRowMatrices >>= denseRowsData)
        else
          Left
            ( InvariantViolation
                ( "dense vertical concatenation requires equal column counts, got "
                    <> show (map denseRowsShape denseRowMatrices)
                )
            )

transposeDenseRows :: DenseRows a -> DenseRows a
transposeDenseRows denseRowsValue =
  DenseRows
    { denseRowCount = denseColumnCount denseRowsValue,
      denseColumnCount = denseRowCount denseRowsValue,
      denseRowsData = foldr (zipWith (:)) (replicate (denseColumnCount denseRowsValue) []) (denseRowsData denseRowsValue)
    }

zipDenseRowsWith :: (left -> right -> result) -> DenseRows left -> DenseRows right -> Either MoonlightError (DenseRows result)
zipDenseRowsWith combine leftDenseRows rightDenseRows
  | denseRowsShape leftDenseRows /= denseRowsShape rightDenseRows =
      Left
        ( InvariantViolation
            ( "dense row matrix zip shape mismatch (left="
                <> show (denseRowsShape leftDenseRows)
                <> ", right="
                <> show (denseRowsShape rightDenseRows)
                <> ")"
            )
        )
  | otherwise =
      Right
        ( DenseRows
          { denseRowCount = denseRowCount leftDenseRows,
            denseColumnCount = denseColumnCount leftDenseRows,
            denseRowsData =
              zipWith
                (zipWith combine)
                (denseRowsData leftDenseRows)
                (denseRowsData rightDenseRows)
          }
        )