packages feed

moonlight-linalg-0.1.0.0: src-sparse/Moonlight/LinAlg/Pure/Sparse/Solver/IncompleteCholesky0.hs

{-# LANGUAGE BangPatterns #-}

module Moonlight.LinAlg.Pure.Sparse.Solver.IncompleteCholesky0
  ( IC0Factor,
    ic0FactorDimension,
    incompleteCholesky0Factor,
    applyIC0FactorMutable,
    applyIC0FactorAndDotMutable,
  )
where

import Control.Monad.ST (ST, runST)
import Data.Kind (Type)
import Data.Vector.Unboxed qualified as U
import Data.Vector.Unboxed.Mutable qualified as MU
import Moonlight.Core (fieldValueValid)
import Moonlight.LinAlg.Pure.Sparse.Solver.Common (solverEpsilon)
import Moonlight.LinAlg.Pure.Sparse.Solver.Mutable
  ( MutableDoubleVector,
    copyMutableVector,
    dotMutableVector,
  )
import Moonlight.LinAlg.Pure.Sparse.Solver.Types
  ( IC0Config (..),
    SparseIterativeFailure (..),
  )
import Moonlight.LinAlg.Pure.Sparse.Types
  ( SparseCSR,
    csrCols,
    csrColumnIndicesVector,
    csrRows,
    csrRowOffsetsVector,
    csrValuesVector,
  )
import Prelude

type IC0Factor :: Type
data IC0Factor = IC0Factor
  { ic0FactorDimension :: !Int,
    ic0FactorRowOffsets :: !(U.Vector Int),
    ic0FactorColumnIndices :: !(U.Vector Int),
    ic0FactorValues :: !(U.Vector Double),
    ic0FactorDiagonal :: !(U.Vector Double),
    ic0FactorPivots :: !(U.Vector Double)
  }
  deriving stock (Eq, Show)

type IC0SymbolicPattern :: Type
data IC0SymbolicPattern = IC0SymbolicPattern
  { ic0PatternRowOffsets :: !(U.Vector Int),
    ic0PatternColumnIndices :: !(U.Vector Int),
    ic0PatternValues :: !(U.Vector Double),
    ic0PatternDiagonalValues :: !(U.Vector Double),
    ic0PatternSuspectedNullspace :: !Bool
  }

type IC0RowPayload :: Type
data IC0RowPayload = IC0RowPayload
  { ic0RowColumns :: ![Int],
    ic0RowValues :: ![Double],
    ic0RowDiagonal :: !(Maybe Double),
    ic0RowSum :: !Double
  }

incompleteCholesky0Factor ::
  IC0Config ->
  SparseCSR Double ->
  Either SparseIterativeFailure IC0Factor
incompleteCholesky0Factor configValue sparseMatrix = do
  shiftValue <- validateIC0Shift configValue
  validateIC0Shape sparseMatrix
  symbolicPattern <- ic0SymbolicPattern sparseMatrix
  factorIC0SymbolicPattern
    shiftValue
    symbolicPattern

applyIC0FactorMutable ::
  IC0Factor ->
  MutableDoubleVector s ->
  MutableDoubleVector s ->
  MutableDoubleVector s ->
  ST s ()
applyIC0FactorMutable factorValue sourceVector scratchVector targetVector = do
  ic0ForwardSolveIntoMutable factorValue sourceVector scratchVector
  ic0BackwardSolveIntoMutable factorValue scratchVector targetVector
{-# INLINE applyIC0FactorMutable #-}

applyIC0FactorAndDotMutable ::
  IC0Factor ->
  MutableDoubleVector s ->
  MutableDoubleVector s ->
  MutableDoubleVector s ->
  ST s Double
applyIC0FactorAndDotMutable factorValue sourceVector scratchVector targetVector = do
  applyIC0FactorMutable factorValue sourceVector scratchVector targetVector
  dotMutableVector sourceVector targetVector
{-# INLINE applyIC0FactorAndDotMutable #-}

validateIC0Shift :: IC0Config -> Either SparseIterativeFailure Double
validateIC0Shift configValue =
  case ic0DiagonalShift configValue of
    Nothing -> Right 0.0
    Just shiftValue
      | fieldValueValid shiftValue && shiftValue >= 0.0 -> Right shiftValue
      | otherwise -> Left (SparseInvalidDiagonalShift shiftValue)

validateIC0Shape :: SparseCSR Double -> Either SparseIterativeFailure ()
validateIC0Shape sparseMatrix
  | csrRows sparseMatrix /= csrCols sparseMatrix =
      Left (SparseNonSquareSparsePreconditioner (csrRows sparseMatrix) (csrCols sparseMatrix))
  | otherwise = Right ()

ic0SymbolicPattern ::
  SparseCSR Double ->
  Either SparseIterativeFailure IC0SymbolicPattern
ic0SymbolicPattern sparseMatrix = do
  rows <- traverse (ic0RowPayload sparseMatrix) [0 .. dimension - 1]
  let diagonalValues = traverse ic0DiagonalFromPayload (zip [0 ..] rows)
  case diagonalValues of
    Left failureValue -> Left failureValue
    Right rowDiagonals ->
      let !rowCounts = length . ic0RowColumns <$> rows
          !rowOffsets = U.fromList (scanl (+) 0 rowCounts)
          !columnIndices = U.fromList (ic0RowColumns =<< rows)
          !lowerValues = U.fromList (ic0RowValues =<< rows)
          !diagonalVector = U.fromList rowDiagonals
          !nullspaceLike =
            all
              (\rowValue -> abs (ic0RowSum rowValue) <= solverEpsilon)
              rows
       in Right
            IC0SymbolicPattern
              { ic0PatternRowOffsets = rowOffsets,
                ic0PatternColumnIndices = columnIndices,
                ic0PatternValues = lowerValues,
                ic0PatternDiagonalValues = diagonalVector,
                ic0PatternSuspectedNullspace = nullspaceLike
              }
  where
    !dimension = csrRows sparseMatrix

ic0DiagonalFromPayload ::
  (Int, IC0RowPayload) ->
  Either SparseIterativeFailure Double
ic0DiagonalFromPayload (rowIndex, rowValue) =
  case ic0RowDiagonal rowValue of
    Nothing -> Left (SparseMissingDiagonal rowIndex)
    Just diagonalValue -> Right diagonalValue

ic0RowPayload ::
  SparseCSR Double ->
  Int ->
  Either SparseIterativeFailure IC0RowPayload
ic0RowPayload sparseMatrix rowIndex =
  collectEntries startOffset [] [] Nothing 0.0
  where
    !rowOffsets = csrRowOffsetsVector sparseMatrix
    !columnIndices = csrColumnIndicesVector sparseMatrix
    !values = csrValuesVector sparseMatrix
    !startOffset = rowOffsets `U.unsafeIndex` rowIndex
    !endOffset = rowOffsets `U.unsafeIndex` (rowIndex + 1)

    collectEntries !entryIndex !columnsRev !valuesRev !diagonalValue !rowSum
      | entryIndex >= endOffset =
          Right
            IC0RowPayload
              { ic0RowColumns = reverse columnsRev,
                ic0RowValues = reverse valuesRev,
                ic0RowDiagonal = diagonalValue,
                ic0RowSum = rowSum
              }
      | otherwise =
          let !columnIndex = columnIndices `U.unsafeIndex` entryIndex
              !entryValue = values `U.unsafeIndex` entryIndex
              !nextRowSum = rowSum + entryValue
           in if not (fieldValueValid entryValue)
                then Left (SparseNonFiniteUpdate rowIndex columnIndex entryValue)
                else
                  case compare columnIndex rowIndex of
                    LT ->
                      case findCSRValue sparseMatrix columnIndex rowIndex of
                        Nothing -> Left (SparseStructuralAsymmetry rowIndex columnIndex)
                        Just _ ->
                          collectEntries
                            (entryIndex + 1)
                            (columnIndex : columnsRev)
                            (entryValue : valuesRev)
                            diagonalValue
                            nextRowSum
                    EQ ->
                      collectEntries
                        (entryIndex + 1)
                        columnsRev
                        valuesRev
                        (Just entryValue)
                        nextRowSum
                    GT ->
                      case findCSRValue sparseMatrix columnIndex rowIndex of
                        Nothing -> Left (SparseStructuralAsymmetry rowIndex columnIndex)
                        Just _ ->
                          collectEntries
                            (entryIndex + 1)
                            columnsRev
                            valuesRev
                            diagonalValue
                            nextRowSum

factorIC0SymbolicPattern ::
  Double ->
  IC0SymbolicPattern ->
  Either SparseIterativeFailure IC0Factor
factorIC0SymbolicPattern !shiftValue symbolicPattern =
  runST $ do
    lowerValues <- U.thaw (ic0PatternValues symbolicPattern)
    diagonalValues <- MU.unsafeNew dimension
    pivotValues <- MU.unsafeNew dimension
    resultValue <-
      factorRows
        lowerValues
        diagonalValues
        pivotValues
        0
    case resultValue of
      Left failureValue -> pure (Left failureValue)
      Right () -> do
        frozenValues <- U.unsafeFreeze lowerValues
        frozenDiagonals <- U.unsafeFreeze diagonalValues
        frozenPivots <- U.unsafeFreeze pivotValues
        pure
          ( Right
              IC0Factor
                { ic0FactorDimension = dimension,
                  ic0FactorRowOffsets = ic0PatternRowOffsets symbolicPattern,
                  ic0FactorColumnIndices = ic0PatternColumnIndices symbolicPattern,
                  ic0FactorValues = frozenValues,
                  ic0FactorDiagonal = frozenDiagonals,
                  ic0FactorPivots = frozenPivots
                }
          )
  where
    !rowOffsets = ic0PatternRowOffsets symbolicPattern
    !columnIndices = ic0PatternColumnIndices symbolicPattern
    !matrixValues = ic0PatternValues symbolicPattern
    !matrixDiagonal = ic0PatternDiagonalValues symbolicPattern
    !dimension = U.length matrixDiagonal

    factorRows ::
      MU.MVector s Double ->
      MU.MVector s Double ->
      MU.MVector s Double ->
      Int ->
      ST s (Either SparseIterativeFailure ())
    factorRows lowerValues diagonalValues pivotValues !rowIndex
      | rowIndex >= dimension = pure (Right ())
      | otherwise = do
          let !rowStart = rowOffsets `U.unsafeIndex` rowIndex
              !rowEnd = rowOffsets `U.unsafeIndex` (rowIndex + 1)
          offDiagonalResult <-
            factorStrictLowerRow
              lowerValues
              diagonalValues
              rowIndex
              rowStart
          case offDiagonalResult of
            Left failureValue -> pure (Left failureValue)
            Right () -> do
              correctionValue <- lowerRowSquared lowerValues rowStart rowEnd 0.0
              let !pivotValue =
                    (matrixDiagonal `U.unsafeIndex` rowIndex)
                      + shiftValue
                      - correctionValue
              if not (fieldValueValid pivotValue)
                then pure (Left (SparseNonFiniteUpdate rowIndex rowIndex pivotValue))
                else
                  if pivotValue <= solverEpsilon
                    then
                      pure
                        ( Left
                            ( if ic0PatternSuspectedNullspace symbolicPattern
                                then SparseSuspectedNullspaceUnanchoredLaplacian rowIndex pivotValue
                                else SparseNonpositivePivot rowIndex pivotValue
                            )
                        )
                    else do
                      MU.unsafeWrite pivotValues rowIndex pivotValue
                      MU.unsafeWrite diagonalValues rowIndex (sqrt pivotValue)
                      factorRows
                        lowerValues
                        diagonalValues
                        pivotValues
                        (rowIndex + 1)

    factorStrictLowerRow ::
      MU.MVector s Double ->
      MU.MVector s Double ->
      Int ->
      Int ->
      ST s (Either SparseIterativeFailure ())
    factorStrictLowerRow lowerValues diagonalValues !rowIndex !entryIndex
      | entryIndex >= rowEnd = pure (Right ())
      | otherwise = do
          let !columnIndex = columnIndices `U.unsafeIndex` entryIndex
              !matrixValue = matrixValues `U.unsafeIndex` entryIndex
          correctionValue <-
            lowerIntersectionProduct
              lowerValues
              rowIndex
              columnIndex
              rowStart
              (rowOffsets `U.unsafeIndex` columnIndex)
              0.0
          pivotDiagonal <- MU.unsafeRead diagonalValues columnIndex
          let !factorValue = (matrixValue - correctionValue) / pivotDiagonal
          if not (fieldValueValid factorValue)
            then pure (Left (SparseNonFiniteUpdate rowIndex columnIndex factorValue))
            else do
              MU.unsafeWrite lowerValues entryIndex factorValue
              factorStrictLowerRow
                lowerValues
                diagonalValues
                rowIndex
                (entryIndex + 1)
      where
        !rowStart = rowOffsets `U.unsafeIndex` rowIndex
        !rowEnd = rowOffsets `U.unsafeIndex` (rowIndex + 1)

    lowerIntersectionProduct ::
      MU.MVector s Double ->
      Int ->
      Int ->
      Int ->
      Int ->
      Double ->
      ST s Double
    lowerIntersectionProduct lowerValues !rowIndex !columnIndex !leftEntry !rightEntry !accumulator =
      let !leftEnd = rowOffsets `U.unsafeIndex` (rowIndex + 1)
          !rightEnd = rowOffsets `U.unsafeIndex` (columnIndex + 1)
       in if leftEntry >= leftEnd || rightEntry >= rightEnd
            then pure accumulator
            else
              let !leftColumn = columnIndices `U.unsafeIndex` leftEntry
                  !rightColumn = columnIndices `U.unsafeIndex` rightEntry
               in if leftColumn >= columnIndex || rightColumn >= columnIndex
                    then pure accumulator
                    else
                      case compare leftColumn rightColumn of
                        LT ->
                          lowerIntersectionProduct
                            lowerValues
                            rowIndex
                            columnIndex
                            (leftEntry + 1)
                            rightEntry
                            accumulator
                        EQ -> do
                          leftValue <- MU.unsafeRead lowerValues leftEntry
                          rightValue <- MU.unsafeRead lowerValues rightEntry
                          lowerIntersectionProduct
                            lowerValues
                            rowIndex
                            columnIndex
                            (leftEntry + 1)
                            (rightEntry + 1)
                            (accumulator + leftValue * rightValue)
                        GT ->
                          lowerIntersectionProduct
                            lowerValues
                            rowIndex
                            columnIndex
                            leftEntry
                            (rightEntry + 1)
                            accumulator

lowerRowSquared ::
  MU.MVector s Double ->
  Int ->
  Int ->
  Double ->
  ST s Double
lowerRowSquared lowerValues !entryIndex !endEntry !accumulator
  | entryIndex >= endEntry = pure accumulator
  | otherwise = do
      factorValue <- MU.unsafeRead lowerValues entryIndex
      lowerRowSquared
        lowerValues
        (entryIndex + 1)
        endEntry
        (accumulator + factorValue * factorValue)
{-# INLINE lowerRowSquared #-}

ic0ForwardSolveIntoMutable ::
  IC0Factor ->
  MutableDoubleVector s ->
  MutableDoubleVector s ->
  ST s ()
ic0ForwardSolveIntoMutable factorValue sourceVector targetVector =
  solveRows 0
  where
    !dimension = ic0FactorDimension factorValue
    !rowOffsets = ic0FactorRowOffsets factorValue
    !columnIndices = ic0FactorColumnIndices factorValue
    !factorValues = ic0FactorValues factorValue
    !diagonalValues = ic0FactorDiagonal factorValue

    solveRows !rowIndex
      | rowIndex >= dimension = pure ()
      | otherwise = do
          rhsValue <- MU.unsafeRead sourceVector rowIndex
          knownProduct <-
            lowerKnownProduct
              columnIndices
              factorValues
              targetVector
              (rowOffsets `U.unsafeIndex` rowIndex)
              (rowOffsets `U.unsafeIndex` (rowIndex + 1))
              0.0
          MU.unsafeWrite
            targetVector
            rowIndex
            ( (rhsValue - knownProduct)
                / (diagonalValues `U.unsafeIndex` rowIndex)
            )
          solveRows (rowIndex + 1)
{-# INLINE ic0ForwardSolveIntoMutable #-}

ic0BackwardSolveIntoMutable ::
  IC0Factor ->
  MutableDoubleVector s ->
  MutableDoubleVector s ->
  ST s ()
ic0BackwardSolveIntoMutable factorValue sourceVector targetVector = do
  copyMutableVector sourceVector targetVector
  solveRows (dimension - 1)
  where
    !dimension = ic0FactorDimension factorValue
    !rowOffsets = ic0FactorRowOffsets factorValue
    !columnIndices = ic0FactorColumnIndices factorValue
    !factorValues = ic0FactorValues factorValue
    !diagonalValues = ic0FactorDiagonal factorValue

    solveRows !rowIndex
      | rowIndex < 0 = pure ()
      | otherwise = do
          rhsValue <- MU.unsafeRead targetVector rowIndex
          let !solutionValue = rhsValue / (diagonalValues `U.unsafeIndex` rowIndex)
          MU.unsafeWrite targetVector rowIndex solutionValue
          scatterLowerTranspose
            (rowOffsets `U.unsafeIndex` rowIndex)
            (rowOffsets `U.unsafeIndex` (rowIndex + 1))
            solutionValue
          solveRows (rowIndex - 1)

    scatterLowerTranspose !entryIndex !endEntry !solutionValue
      | entryIndex >= endEntry = pure ()
      | otherwise = do
          let !columnIndex = columnIndices `U.unsafeIndex` entryIndex
              !factorEntry = factorValues `U.unsafeIndex` entryIndex
          targetValue <- MU.unsafeRead targetVector columnIndex
          MU.unsafeWrite
            targetVector
            columnIndex
            (targetValue - factorEntry * solutionValue)
          scatterLowerTranspose
            (entryIndex + 1)
            endEntry
            solutionValue
{-# INLINE ic0BackwardSolveIntoMutable #-}

lowerKnownProduct ::
  U.Vector Int ->
  U.Vector Double ->
  MutableDoubleVector s ->
  Int ->
  Int ->
  Double ->
  ST s Double
lowerKnownProduct columnIndices factorValues targetVector !entryIndex !endEntry !accumulator
  | entryIndex >= endEntry = pure accumulator
  | otherwise = do
      let !columnIndex = columnIndices `U.unsafeIndex` entryIndex
          !factorValue = factorValues `U.unsafeIndex` entryIndex
      targetValue <- MU.unsafeRead targetVector columnIndex
      lowerKnownProduct
        columnIndices
        factorValues
        targetVector
        (entryIndex + 1)
        endEntry
        (accumulator + factorValue * targetValue)
{-# INLINE lowerKnownProduct #-}

findCSRValue :: SparseCSR Double -> Int -> Int -> Maybe Double
findCSRValue sparseMatrix rowIndex columnIndex =
  binarySearch startOffset endOffset
  where
    !rowOffsets = csrRowOffsetsVector sparseMatrix
    !columnIndices = csrColumnIndicesVector sparseMatrix
    !values = csrValuesVector sparseMatrix
    !startOffset = rowOffsets `U.unsafeIndex` rowIndex
    !endOffset = rowOffsets `U.unsafeIndex` (rowIndex + 1)

    binarySearch !lo !hi
      | lo >= hi = Nothing
      | otherwise =
          let !mid = lo + ((hi - lo) `div` 2)
              !midColumn = columnIndices `U.unsafeIndex` mid
           in case compare midColumn columnIndex of
                LT -> binarySearch (mid + 1) hi
                EQ -> Just (values `U.unsafeIndex` mid)
                GT -> binarySearch lo mid
{-# INLINE findCSRValue #-}