packages feed

moonlight-linalg-0.1.0.0: src-carrier/Moonlight/LinAlg/Internal/GF2/SparseColumn.hs

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE StandaloneKindSignatures #-}

module Moonlight.LinAlg.Internal.GF2.SparseColumn
  ( GF2SparseColumn
  , gf2SparseColumnIndex
  , gf2SparseColumnRows
  , mkGF2SparseColumn
  , GF2SparseReducerConfig
  , gf2SparseDensifyThreshold
  , mkGF2SparseReducerConfig
  , defaultGF2SparseReducerConfig
  , GF2SparseColumnReduction (..)
  , reduceGF2SparseColumns
  , rankGF2SparseColumns
  , independentGF2SparseColumns
  , kernelBasisGF2SparseColumns
  ) where

import Control.Monad (foldM, unless)
import Data.Foldable (traverse_)
import Data.IntMap.Strict (IntMap)
import Data.IntMap.Strict qualified as IntMap
import Data.Kind (Type)
import Data.List (sortOn)
import Data.Vector (Vector)
import Data.Vector qualified as V
import Moonlight.Core (MoonlightError (..))
import Moonlight.LinAlg.Internal.GF2.Xor
  ( PackedRow
  , packedRowFromIndices
  , packedRowIndices
  , packedRowIsZero
  , packedRowNonZeroCount
  , packedRowXor
  , unitPackedRow
  )

-- | Sparse low-pivot GF2 column reduction with optional packed fallback.
type GF2SparseColumn :: Type
data GF2SparseColumn = GF2SparseColumn
  { gf2SparseColumnIndex :: !Int
  , gf2SparseColumnRows :: ![Int]
  }
  deriving stock (Eq, Show)

type GF2SparseReducerConfig :: Type
data GF2SparseReducerConfig = GF2SparseReducerConfig
  { gf2SparseDensifyThreshold :: !Int
  }
  deriving stock (Eq, Show)

type GF2SparseColumnReduction :: Type
data GF2SparseColumnReduction = GF2SparseColumnReduction
  { gf2SparseReductionRank :: !Int
  , gf2SparseIndependentColumns :: !(Vector Int)
  , gf2SparseKernelBasis :: !(Vector PackedRow)
  }
  deriving stock (Eq, Show)

type SparseColumnBody :: Type
data SparseColumnBody
  = SparseRows ![Int]
  | PackedRows !PackedRow
  deriving stock (Eq, Show)

type TrackedSparseBasisColumn :: Type
data TrackedSparseBasisColumn = TrackedSparseBasisColumn
  { tsbcData :: !SparseColumnBody
  , tsbcWitness :: !PackedRow
  }
  deriving stock (Eq, Show)

mkGF2SparseColumn :: String -> Int -> Int -> [Int] -> Either MoonlightError GF2SparseColumn
mkGF2SparseColumn context rowCount columnIndex rowsValue = do
  unless (rowCount >= 0)
    (Left (InvariantViolation (context <> ": negative sparse GF2 row count " <> show rowCount)))
  unless (columnIndex >= 0)
    (Left (InvariantViolation (context <> ": negative sparse GF2 column index " <> show columnIndex)))
  traverse_ validateRow rowsValue
  Right
    GF2SparseColumn
      { gf2SparseColumnIndex = columnIndex
      , gf2SparseColumnRows = canonicalGF2Support rowsValue
      }
  where
    validateRow rowIndex
      | rowIndex < 0 || rowIndex >= rowCount =
          Left
            ( InvariantViolation
                ( context
                    <> ": sparse GF2 row "
                    <> show rowIndex
                    <> " is outside row count "
                    <> show rowCount
                )
            )
      | otherwise = Right ()

mkGF2SparseReducerConfig :: String -> Int -> Either MoonlightError GF2SparseReducerConfig
mkGF2SparseReducerConfig context thresholdValue
  | thresholdValue < 0 =
      Left (InvariantViolation (context <> ": negative sparse GF2 densify threshold " <> show thresholdValue))
  | otherwise =
      Right GF2SparseReducerConfig {gf2SparseDensifyThreshold = thresholdValue}

defaultGF2SparseReducerConfig :: GF2SparseReducerConfig
defaultGF2SparseReducerConfig =
  GF2SparseReducerConfig {gf2SparseDensifyThreshold = 64}

reduceGF2SparseColumns ::
  GF2SparseReducerConfig ->
  Int ->
  Int ->
  Vector GF2SparseColumn ->
  Either MoonlightError GF2SparseColumnReduction
reduceGF2SparseColumns configValue rowCount columnCount columnsValue = do
  unless (rowCount >= 0 && columnCount >= 0)
    (Left (InvariantViolation ("reduceGF2SparseColumns: negative sparse GF2 shape " <> show (rowCount, columnCount))))
  orderedColumns <- validateColumnCover columnCount columnsValue
  (_, independentReversed, kernelReversed) <-
    foldM
      (reduceColumn configValue rowCount columnCount)
      (IntMap.empty, [], [])
      (V.toList orderedColumns)
  let independentColumns = V.fromList (reverse independentReversed)
  Right
    GF2SparseColumnReduction
      { gf2SparseReductionRank = V.length independentColumns
      , gf2SparseIndependentColumns = independentColumns
      , gf2SparseKernelBasis = V.fromList (reverse kernelReversed)
      }

rankGF2SparseColumns ::
  GF2SparseReducerConfig ->
  Int ->
  Int ->
  Vector GF2SparseColumn ->
  Either MoonlightError Int
rankGF2SparseColumns configValue rowCount columnCount columnsValue =
  gf2SparseReductionRank <$> reduceGF2SparseColumns configValue rowCount columnCount columnsValue

independentGF2SparseColumns ::
  GF2SparseReducerConfig ->
  Int ->
  Int ->
  Vector GF2SparseColumn ->
  Either MoonlightError (Vector Int)
independentGF2SparseColumns configValue rowCount columnCount columnsValue =
  gf2SparseIndependentColumns <$> reduceGF2SparseColumns configValue rowCount columnCount columnsValue

kernelBasisGF2SparseColumns ::
  GF2SparseReducerConfig ->
  Int ->
  Int ->
  Vector GF2SparseColumn ->
  Either MoonlightError (Vector PackedRow)
kernelBasisGF2SparseColumns configValue rowCount columnCount columnsValue =
  gf2SparseKernelBasis <$> reduceGF2SparseColumns configValue rowCount columnCount columnsValue

reduceColumn ::
  GF2SparseReducerConfig ->
  Int ->
  Int ->
  (IntMap TrackedSparseBasisColumn, [Int], [PackedRow]) ->
  GF2SparseColumn ->
  Either MoonlightError (IntMap TrackedSparseBasisColumn, [Int], [PackedRow])
reduceColumn configValue rowCount columnCount (basisColumns, independentReversed, kernelReversed) columnValue = do
  witnessValue <- unitPackedRow "reduceGF2SparseColumns: witness" columnCount (gf2SparseColumnIndex columnValue)
  initialBody <- normalizeRows configValue rowCount (gf2SparseColumnRows columnValue)
  (reducedData, reducedWitness) <- reduceSparseTracked configValue rowCount basisColumns initialBody witnessValue
  case sparseBodyLowPivot reducedData of
    Nothing -> Right (basisColumns, independentReversed, reducedWitness : kernelReversed)
    Just pivotIndex ->
      Right
        ( IntMap.insert
            pivotIndex
            TrackedSparseBasisColumn
              { tsbcData = reducedData
              , tsbcWitness = reducedWitness
              }
            basisColumns
        , gf2SparseColumnIndex columnValue : independentReversed
        , kernelReversed
        )

reduceSparseTracked ::
  GF2SparseReducerConfig ->
  Int ->
  IntMap TrackedSparseBasisColumn ->
  SparseColumnBody ->
  PackedRow ->
  Either MoonlightError (SparseColumnBody, PackedRow)
reduceSparseTracked configValue rowCount basisColumns dataValue witnessValue =
  case sparseBodyLowPivot dataValue of
    Nothing -> Right (dataValue, witnessValue)
    Just pivotIndex ->
      case IntMap.lookup pivotIndex basisColumns of
        Nothing -> Right (dataValue, witnessValue)
        Just TrackedSparseBasisColumn {tsbcData, tsbcWitness} -> do
          reducedData <- sparseBodyXor configValue rowCount dataValue tsbcData
          reducedWitness <- packedRowXor "reduceGF2SparseColumns: witness xor" witnessValue tsbcWitness
          reduceSparseTracked configValue rowCount basisColumns reducedData reducedWitness

sparseBodyXor ::
  GF2SparseReducerConfig ->
  Int ->
  SparseColumnBody ->
  SparseColumnBody ->
  Either MoonlightError SparseColumnBody
sparseBodyXor configValue rowCount leftBody rightBody =
  case (leftBody, rightBody) of
    (PackedRows leftPacked, PackedRows rightPacked) ->
      packedRowXor "reduceGF2SparseColumns: packed sparse body xor" leftPacked rightPacked
        >>= normalizePacked configValue
    _ ->
      normalizeRows
        configValue
        rowCount
        (xorSortedSupports (sparseBodyRows leftBody) (sparseBodyRows rightBody))

normalizeRows :: GF2SparseReducerConfig -> Int -> [Int] -> Either MoonlightError SparseColumnBody
normalizeRows configValue rowCount rowsValue
  | supportPastThreshold configValue rowsValue =
      PackedRows <$> packedRowFromIndices "reduceGF2SparseColumns: densified sparse body" rowCount rowsValue
  | otherwise = Right (SparseRows rowsValue)

normalizePacked :: GF2SparseReducerConfig -> PackedRow -> Either MoonlightError SparseColumnBody
normalizePacked configValue packedValue
  | packedRowIsZero packedValue = Right (SparseRows [])
  | packedRowNonZeroCount packedValue >= gf2SparseDensifyThreshold configValue =
      Right (PackedRows packedValue)
  | otherwise = Right (SparseRows (packedRowIndices packedValue))

supportPastThreshold :: GF2SparseReducerConfig -> [Int] -> Bool
supportPastThreshold configValue rowsValue =
  not (null rowsValue) && length rowsValue >= gf2SparseDensifyThreshold configValue

sparseBodyLowPivot :: SparseColumnBody -> Maybe Int
sparseBodyLowPivot bodyValue =
  foldl' (\_ rowIndex -> Just rowIndex) Nothing (sparseBodyRows bodyValue)

sparseBodyRows :: SparseColumnBody -> [Int]
sparseBodyRows bodyValue =
  case bodyValue of
    SparseRows rowsValue -> rowsValue
    PackedRows packedValue -> packedRowIndices packedValue

validateColumnCover :: Int -> Vector GF2SparseColumn -> Either MoonlightError (Vector GF2SparseColumn)
validateColumnCover columnCount columnsValue = do
  unless (V.length columnsValue == columnCount)
    ( Left
        ( InvariantViolation
            ( "reduceGF2SparseColumns: received "
                <> show (V.length columnsValue)
                <> " sparse GF2 columns for column count "
                <> show columnCount
            )
        )
    )
  traverse_ validateIndexedColumn (zip [0 .. columnCount - 1] orderedColumns)
  Right (V.fromList orderedColumns)
  where
    orderedColumns =
      sortOn gf2SparseColumnIndex (V.toList columnsValue)

    validateIndexedColumn (expectedIndex, columnValue)
      | gf2SparseColumnIndex columnValue == expectedIndex = Right ()
      | otherwise =
          Left
            ( InvariantViolation
                ( "reduceGF2SparseColumns: sparse GF2 column cover expected index "
                    <> show expectedIndex
                    <> " but found "
                    <> show (gf2SparseColumnIndex columnValue)
                )
            )

canonicalGF2Support :: [Int] -> [Int]
canonicalGF2Support =
  IntMap.keys . foldl' toggleRow IntMap.empty
  where
    toggleRow supportMap rowIndex =
      case IntMap.lookup rowIndex supportMap of
        Nothing -> IntMap.insert rowIndex () supportMap
        Just () -> IntMap.delete rowIndex supportMap

xorSortedSupports :: [Int] -> [Int] -> [Int]
xorSortedSupports leftRows rightRows =
  case (leftRows, rightRows) of
    ([], _) -> rightRows
    (_, []) -> leftRows
    (leftRow : remainingLeft, rightRow : remainingRight) ->
      case compare leftRow rightRow of
        LT -> leftRow : xorSortedSupports remainingLeft rightRows
        EQ -> xorSortedSupports remainingLeft remainingRight
        GT -> rightRow : xorSortedSupports leftRows remainingRight