packages feed

moonlight-linalg-0.1.0.0: src-dense/Moonlight/LinAlg/Internal/Dense/DoubleFactorization.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE StrictData #-}

module Moonlight.LinAlg.Internal.Dense.DoubleFactorization
  ( choleskyLower,
    qrFullColumnRank,
    solveSquareLinearSystem,
  )
where

import Control.Monad.ST (ST, runST)
import Data.Bifunctor (first)
import Data.Primitive.PrimArray qualified as PrimArray
import Moonlight.Core
  ( MoonlightError (..),
    checkedNonNegativeProduct,
    fieldValueValid,
  )
import Moonlight.LinAlg.Internal.Eigen.Kernels (hypotStable)
import Moonlight.LinAlg.Internal.Primitives (epsilon)
import Prelude

solveSquareLinearSystem :: Int -> [Double] -> [Double] -> Either MoonlightError [Double]
solveSquareLinearSystem !matrixSize matrixValues rightHandSideValues = do
  matrixEntryCount <- requireSquarePayload "direct solve" matrixSize matrixValues
  requireVectorPayload "direct solve" matrixSize rightHandSideValues
  runST $ do
    matrixWork <- newDoubleArray matrixEntryCount
    rhsWork <- newDoubleArray matrixSize
    pivots <- newIntArray matrixSize
    copyDoubleList matrixWork matrixValues
    copyDoubleList rhsWork rightHandSideValues
    factorResult <- factorPLU matrixSize matrixWork pivots
    case factorResult of
      Left err -> pure (Left err)
      Right () -> do
        applyPivotVector matrixSize pivots rhsWork
        forwardSolveUnitLower matrixSize matrixWork rhsWork
        backResult <- backwardSolveUpper matrixSize matrixWork rhsWork
        case backResult of
          Left err -> pure (Left err)
          Right () -> Right <$> freezeDoubleList rhsWork

qrFullColumnRank :: Int -> Int -> [Double] -> Either MoonlightError ([Double], [Double])
qrFullColumnRank !rowCount !columnCount matrixValues
  | rowCount < columnCount =
      Left (InvariantViolation "full-column-rank QR requires row count greater than or equal to column count")
  | otherwise = do
      matrixEntryCount <- requireMatrixPayload "QR decomposition" rowCount columnCount matrixValues
      upperEntryCount <-
        first
          (const (InvariantViolation "QR upper-factor cardinality exceeds non-negative Int range"))
          (checkedNonNegativeProduct columnCount columnCount)
      runST $ do
        matrixWork <- newDoubleArray matrixEntryCount
        reflectorScalars <- newDoubleArray columnCount
        copyDoubleList matrixWork matrixValues
        setDoubleArray reflectorScalars columnCount 0.0
        factorResult <- factorQR rowCount columnCount matrixWork reflectorScalars
        case factorResult of
          Left err -> pure (Left err)
          Right () -> do
            qValues <- formThinQ rowCount columnCount matrixEntryCount matrixWork reflectorScalars
            rValues <- extractUpperR rowCount columnCount upperEntryCount matrixWork
            pure (Right (qValues, rValues))

choleskyLower :: Int -> [Double] -> Either MoonlightError [Double]
choleskyLower !matrixSize matrixValues = do
  matrixEntryCount <- requireSquarePayload "Cholesky decomposition" matrixSize matrixValues
  runST $ do
    matrixWork <- newDoubleArray matrixEntryCount
    copyDoubleList matrixWork matrixValues
    symmetryResult <- checkSymmetricMatrix matrixSize matrixWork
    case symmetryResult of
      Left err -> pure (Left err)
      Right () -> do
        factorResult <- factorCholesky matrixSize matrixWork
        case factorResult of
          Left err -> pure (Left err)
          Right () -> do
            zeroStrictUpper matrixSize matrixWork
            Right <$> freezeDoubleList matrixWork

newDoubleArray :: Int -> ST s (PrimArray.MutablePrimArray s Double)
newDoubleArray = PrimArray.newPrimArray
{-# INLINE newDoubleArray #-}

newIntArray :: Int -> ST s (PrimArray.MutablePrimArray s Int)
newIntArray = PrimArray.newPrimArray
{-# INLINE newIntArray #-}

setDoubleArray :: PrimArray.MutablePrimArray s Double -> Int -> Double -> ST s ()
setDoubleArray !target !entryCount !entryValue =
  PrimArray.setPrimArray target 0 entryCount entryValue
{-# INLINE setDoubleArray #-}

copyDoubleList :: PrimArray.MutablePrimArray s Double -> [Double] -> ST s ()
copyDoubleList !target = go 0
  where
    go !_ [] = pure ()
    go !entryIndex (entryValue : restValues) = do
      PrimArray.writePrimArray target entryIndex entryValue
      go (entryIndex + 1) restValues
{-# INLINE copyDoubleList #-}

freezeDoubleList :: PrimArray.MutablePrimArray s Double -> ST s [Double]
freezeDoubleList !values =
  PrimArray.primArrayToList <$> PrimArray.unsafeFreezePrimArray values
{-# INLINE freezeDoubleList #-}

rowMajorIndex :: Int -> Int -> Int -> Int
rowMajorIndex !columnCount !rowIndex !columnIndex =
  rowIndex * columnCount + columnIndex
{-# INLINE rowMajorIndex #-}

readMatrix :: Int -> PrimArray.MutablePrimArray s Double -> Int -> Int -> ST s Double
readMatrix !columnCount !matrixValues !rowIndex !columnIndex =
  PrimArray.readPrimArray matrixValues (rowMajorIndex columnCount rowIndex columnIndex)
{-# INLINE readMatrix #-}

writeMatrix :: Int -> PrimArray.MutablePrimArray s Double -> Int -> Int -> Double -> ST s ()
writeMatrix !columnCount !matrixValues !rowIndex !columnIndex !entryValue =
  PrimArray.writePrimArray matrixValues (rowMajorIndex columnCount rowIndex columnIndex) entryValue
{-# INLINE writeMatrix #-}

readMatrixWithColumnCount :: Int -> PrimArray.MutablePrimArray s Double -> Int -> Int -> ST s Double
readMatrixWithColumnCount = readMatrix
{-# INLINE readMatrixWithColumnCount #-}

writeMatrixWithColumnCount :: Int -> PrimArray.MutablePrimArray s Double -> Int -> Int -> Double -> ST s ()
writeMatrixWithColumnCount = writeMatrix
{-# INLINE writeMatrixWithColumnCount #-}

requireSquarePayload :: String -> Int -> [Double] -> Either MoonlightError Int
requireSquarePayload label matrixSize matrixValues = do
  expectedLength <-
    first
      (const (InvariantViolation (label <> " square cardinality exceeds non-negative Int range")))
      (checkedNonNegativeProduct matrixSize matrixSize)
  if length matrixValues /= expectedLength
    then
      Left
        ( InvariantViolation
            ( label
                <> " square payload length mismatch: expected "
                <> show expectedLength
                <> " values but received "
                <> show (length matrixValues)
            )
        )
    else requireFiniteEntries label matrixValues *> Right expectedLength

requireMatrixPayload :: String -> Int -> Int -> [Double] -> Either MoonlightError Int
requireMatrixPayload label rowCount columnCount matrixValues = do
  expectedLength <-
    first
      (const (InvariantViolation (label <> " dense cardinality exceeds non-negative Int range")))
      (checkedNonNegativeProduct rowCount columnCount)
  if length matrixValues /= expectedLength
    then
      Left
        ( InvariantViolation
            ( label
                <> " dense payload length mismatch: expected "
                <> show expectedLength
                <> " values but received "
                <> show (length matrixValues)
            )
        )
    else requireFiniteEntries label matrixValues *> Right expectedLength

requireVectorPayload :: String -> Int -> [Double] -> Either MoonlightError ()
requireVectorPayload label vectorSize vectorValues
  | vectorSize < 0 =
      Left (InvariantViolation (label <> " requires a non-negative vector size"))
  | length vectorValues /= vectorSize =
      Left
        ( InvariantViolation
            ( label
                <> " vector payload length mismatch: expected "
                <> show vectorSize
                <> " values but received "
                <> show (length vectorValues)
            )
        )
  | otherwise =
      requireFiniteEntries label vectorValues

requireFiniteEntries :: String -> [Double] -> Either MoonlightError ()
requireFiniteEntries label values =
  if all fieldValueValid values
    then Right ()
    else Left (InvariantViolation (label <> " requires finite entries"))

checkSymmetricMatrix :: Int -> PrimArray.MutablePrimArray s Double -> ST s (Either MoonlightError ())
checkSymmetricMatrix !matrixSize !matrixWork = goRow 0
  where
    tolerance = sqrt epsilon

    goRow !rowIndex
      | rowIndex >= matrixSize = pure (Right ())
      | otherwise = goColumn rowIndex (rowIndex + 1)

    goColumn !rowIndex !columnIndex
      | columnIndex >= matrixSize = goRow (rowIndex + 1)
      | otherwise = do
          leftValue <- readMatrix matrixSize matrixWork rowIndex columnIndex
          rightValue <- readMatrix matrixSize matrixWork columnIndex rowIndex
          if abs (leftValue - rightValue) <= tolerance
            then goColumn rowIndex (columnIndex + 1)
            else pure (Left (InvariantViolation "Cholesky decomposition requires a symmetric matrix"))
{-# INLINE checkSymmetricMatrix #-}

factorPLU ::
  Int ->
  PrimArray.MutablePrimArray s Double ->
  PrimArray.MutablePrimArray s Int ->
  ST s (Either MoonlightError ())
factorPLU !matrixSize !matrixWork !pivotRows = go 0
  where
    go !pivotIndex
      | pivotIndex >= matrixSize = pure (Right ())
      | otherwise = do
          (selectedRow, selectedMagnitude) <- findPivotRow matrixSize matrixWork pivotIndex
          if selectedMagnitude <= epsilon
            then pure (Left (InvariantViolation ("direct solve failed during PLU factorization: non-invertible pivot at column " <> show pivotIndex)))
            else do
              PrimArray.writePrimArray pivotRows pivotIndex selectedRow
              swapMatrixRows matrixSize matrixWork pivotIndex selectedRow
              pivotValue <- readMatrix matrixSize matrixWork pivotIndex pivotIndex
              eliminatePLUColumn matrixSize matrixWork pivotIndex pivotValue
              go (pivotIndex + 1)

findPivotRow :: Int -> PrimArray.MutablePrimArray s Double -> Int -> ST s (Int, Double)
findPivotRow !matrixSize !matrixWork !columnIndex = do
  firstValue <- readMatrix matrixSize matrixWork columnIndex columnIndex
  go (columnIndex + 1) columnIndex (abs firstValue)
  where
    go !rowIndex !bestRow !bestMagnitude
      | rowIndex >= matrixSize = pure (bestRow, bestMagnitude)
      | otherwise = do
          candidateValue <- readMatrix matrixSize matrixWork rowIndex columnIndex
          let !candidateMagnitude = abs candidateValue
          if candidateMagnitude > bestMagnitude
            then go (rowIndex + 1) rowIndex candidateMagnitude
            else go (rowIndex + 1) bestRow bestMagnitude
{-# INLINE findPivotRow #-}

swapMatrixRows :: Int -> PrimArray.MutablePrimArray s Double -> Int -> Int -> ST s ()
swapMatrixRows !columnCount !matrixWork !leftRow !rightRow
  | leftRow == rightRow = pure ()
  | otherwise = go 0
  where
    go !columnIndex
      | columnIndex >= columnCount = pure ()
      | otherwise = do
          leftValue <- readMatrix columnCount matrixWork leftRow columnIndex
          rightValue <- readMatrix columnCount matrixWork rightRow columnIndex
          writeMatrix columnCount matrixWork leftRow columnIndex rightValue
          writeMatrix columnCount matrixWork rightRow columnIndex leftValue
          go (columnIndex + 1)
{-# INLINE swapMatrixRows #-}

eliminatePLUColumn :: Int -> PrimArray.MutablePrimArray s Double -> Int -> Double -> ST s ()
eliminatePLUColumn !matrixSize !matrixWork !pivotIndex !pivotValue =
  goRow (pivotIndex + 1)
  where
    goRow !rowIndex
      | rowIndex >= matrixSize = pure ()
      | otherwise = do
          factorEntry <- readMatrix matrixSize matrixWork rowIndex pivotIndex
          let !multiplier = factorEntry / pivotValue
          writeMatrix matrixSize matrixWork rowIndex pivotIndex multiplier
          updateTrailingRow rowIndex multiplier (pivotIndex + 1)
          goRow (rowIndex + 1)

    updateTrailingRow !rowIndex !multiplier !columnIndex
      | columnIndex >= matrixSize = pure ()
      | otherwise = do
          currentValue <- readMatrix matrixSize matrixWork rowIndex columnIndex
          pivotRowValue <- readMatrix matrixSize matrixWork pivotIndex columnIndex
          writeMatrix matrixSize matrixWork rowIndex columnIndex (currentValue - multiplier * pivotRowValue)
          updateTrailingRow rowIndex multiplier (columnIndex + 1)
{-# INLINE eliminatePLUColumn #-}

applyPivotVector :: Int -> PrimArray.MutablePrimArray s Int -> PrimArray.MutablePrimArray s Double -> ST s ()
applyPivotVector !matrixSize !pivotRows !rhsWork = go 0
  where
    go !pivotIndex
      | pivotIndex >= matrixSize = pure ()
      | otherwise = do
          selectedRow <- PrimArray.readPrimArray pivotRows pivotIndex
          swapRhsEntries rhsWork pivotIndex selectedRow
          go (pivotIndex + 1)
{-# INLINE applyPivotVector #-}

swapRhsEntries :: PrimArray.MutablePrimArray s Double -> Int -> Int -> ST s ()
swapRhsEntries !rhsWork !leftIndex !rightIndex
  | leftIndex == rightIndex = pure ()
  | otherwise = do
      leftValue <- PrimArray.readPrimArray rhsWork leftIndex
      rightValue <- PrimArray.readPrimArray rhsWork rightIndex
      PrimArray.writePrimArray rhsWork leftIndex rightValue
      PrimArray.writePrimArray rhsWork rightIndex leftValue
{-# INLINE swapRhsEntries #-}

forwardSolveUnitLower :: Int -> PrimArray.MutablePrimArray s Double -> PrimArray.MutablePrimArray s Double -> ST s ()
forwardSolveUnitLower !matrixSize !matrixWork !rhsWork = goRow 0
  where
    goRow !rowIndex
      | rowIndex >= matrixSize = pure ()
      | otherwise = do
          contribution <- lowerDot rowIndex 0 0.0
          rhsValue <- PrimArray.readPrimArray rhsWork rowIndex
          PrimArray.writePrimArray rhsWork rowIndex (rhsValue - contribution)
          goRow (rowIndex + 1)

    lowerDot !rowIndex !columnIndex !accumulator
      | columnIndex >= rowIndex = pure accumulator
      | otherwise = do
          lowerValue <- readMatrix matrixSize matrixWork rowIndex columnIndex
          solvedValue <- PrimArray.readPrimArray rhsWork columnIndex
          lowerDot rowIndex (columnIndex + 1) (accumulator + lowerValue * solvedValue)
{-# INLINE forwardSolveUnitLower #-}

backwardSolveUpper :: Int -> PrimArray.MutablePrimArray s Double -> PrimArray.MutablePrimArray s Double -> ST s (Either MoonlightError ())
backwardSolveUpper !matrixSize !matrixWork !rhsWork = goRow (matrixSize - 1)
  where
    goRow !rowIndex
      | rowIndex < 0 = pure (Right ())
      | otherwise = do
          contribution <- upperDot rowIndex (rowIndex + 1) 0.0
          diagonalValue <- readMatrix matrixSize matrixWork rowIndex rowIndex
          rhsValue <- PrimArray.readPrimArray rhsWork rowIndex
          if abs diagonalValue <= epsilon
            then pure (Left (InvariantViolation "direct solve failed during backward substitution: zero diagonal pivot"))
            else do
              PrimArray.writePrimArray rhsWork rowIndex ((rhsValue - contribution) / diagonalValue)
              goRow (rowIndex - 1)

    upperDot !rowIndex !columnIndex !accumulator
      | columnIndex >= matrixSize = pure accumulator
      | otherwise = do
          upperValue <- readMatrix matrixSize matrixWork rowIndex columnIndex
          solvedValue <- PrimArray.readPrimArray rhsWork columnIndex
          upperDot rowIndex (columnIndex + 1) (accumulator + upperValue * solvedValue)
{-# INLINE backwardSolveUpper #-}

factorQR ::
  Int ->
  Int ->
  PrimArray.MutablePrimArray s Double ->
  PrimArray.MutablePrimArray s Double ->
  ST s (Either MoonlightError ())
factorQR !rowCount !columnCount !matrixWork !reflectorScalars = go 0
  where
    go !columnIndex
      | columnIndex >= columnCount = pure (Right ())
      | otherwise = do
          reflectorResult <- makeHouseholderReflector rowCount columnCount matrixWork reflectorScalars columnIndex
          case reflectorResult of
            Left err -> pure (Left err)
            Right tauValue -> do
              applyQRReflectorToRemainder rowCount columnCount matrixWork columnIndex tauValue
              go (columnIndex + 1)

makeHouseholderReflector ::
  Int ->
  Int ->
  PrimArray.MutablePrimArray s Double ->
  PrimArray.MutablePrimArray s Double ->
  Int ->
  ST s (Either MoonlightError Double)
makeHouseholderReflector !rowCount !columnCount !matrixWork !reflectorScalars !columnIndex = do
  alphaValue <- readMatrix columnCount matrixWork columnIndex columnIndex
  tailNorm <- columnTailNorm rowCount columnCount matrixWork columnIndex
  if tailNorm == 0.0
    then
      if abs alphaValue <= epsilon
        then pure (Left (InvariantViolation "QR decomposition failed: dependent or zero column encountered"))
        else do
          PrimArray.writePrimArray reflectorScalars columnIndex 0.0
          pure (Right 0.0)
    else do
      let !normValue = hypotStable alphaValue tailNorm
          !betaValue =
            if alphaValue < 0.0 || isNegativeZero alphaValue
              then normValue
              else negate normValue
      if abs betaValue <= epsilon
        then pure (Left (InvariantViolation "QR decomposition failed: dependent or zero column encountered"))
        else do
          let !tauValue = (betaValue - alphaValue) / betaValue
              !scaleValue = 1.0 / (alphaValue - betaValue)
          scaleReflectorTail rowCount columnCount matrixWork columnIndex scaleValue
          writeMatrix columnCount matrixWork columnIndex columnIndex betaValue
          PrimArray.writePrimArray reflectorScalars columnIndex tauValue
          pure (Right tauValue)
{-# INLINE makeHouseholderReflector #-}

columnTailNorm :: Int -> Int -> PrimArray.MutablePrimArray s Double -> Int -> ST s Double
columnTailNorm !rowCount !columnCount !matrixWork !columnIndex =
  go (columnIndex + 1) 0.0 1.0
  where
    go !rowIndex !scaleValue !sumSquares
      | rowIndex >= rowCount = pure (scaleValue * sqrt sumSquares)
      | otherwise = do
          entryValue <- readMatrix columnCount matrixWork rowIndex columnIndex
          let !entryMagnitude = abs entryValue
          if entryMagnitude == 0.0
            then go (rowIndex + 1) scaleValue sumSquares
            else
              if scaleValue < entryMagnitude
                then
                  let !scaled = scaleValue / entryMagnitude
                   in go (rowIndex + 1) entryMagnitude (1.0 + sumSquares * scaled * scaled)
                else
                  let !scaled = entryMagnitude / scaleValue
                   in go (rowIndex + 1) scaleValue (sumSquares + scaled * scaled)
{-# INLINE columnTailNorm #-}

scaleReflectorTail :: Int -> Int -> PrimArray.MutablePrimArray s Double -> Int -> Double -> ST s ()
scaleReflectorTail !rowCount !columnCount !matrixWork !columnIndex !scaleValue =
  go (columnIndex + 1)
  where
    go !rowIndex
      | rowIndex >= rowCount = pure ()
      | otherwise = do
          entryValue <- readMatrix columnCount matrixWork rowIndex columnIndex
          writeMatrix columnCount matrixWork rowIndex columnIndex (entryValue * scaleValue)
          go (rowIndex + 1)
{-# INLINE scaleReflectorTail #-}

applyQRReflectorToRemainder :: Int -> Int -> PrimArray.MutablePrimArray s Double -> Int -> Double -> ST s ()
applyQRReflectorToRemainder !rowCount !columnCount !matrixWork !reflectorIndex !tauValue
  | tauValue == 0.0 = pure ()
  | otherwise = goColumn (reflectorIndex + 1)
  where
    goColumn !targetColumn
      | targetColumn >= columnCount = pure ()
      | otherwise = do
          dotValue <- matrixReflectorDot rowCount columnCount matrixWork reflectorIndex targetColumn
          let !scaledDot = tauValue * dotValue
          pivotValue <- readMatrix columnCount matrixWork reflectorIndex targetColumn
          writeMatrix columnCount matrixWork reflectorIndex targetColumn (pivotValue - scaledDot)
          updateTail targetColumn scaledDot (reflectorIndex + 1)
          goColumn (targetColumn + 1)

    updateTail !targetColumn !scaledDot !rowIndex
      | rowIndex >= rowCount = pure ()
      | otherwise = do
          reflectorEntry <- readMatrix columnCount matrixWork rowIndex reflectorIndex
          targetEntry <- readMatrix columnCount matrixWork rowIndex targetColumn
          writeMatrix columnCount matrixWork rowIndex targetColumn (targetEntry - reflectorEntry * scaledDot)
          updateTail targetColumn scaledDot (rowIndex + 1)
{-# INLINE applyQRReflectorToRemainder #-}

matrixReflectorDot :: Int -> Int -> PrimArray.MutablePrimArray s Double -> Int -> Int -> ST s Double
matrixReflectorDot !rowCount !columnCount !matrixWork !reflectorIndex !targetColumn = do
  pivotValue <- readMatrix columnCount matrixWork reflectorIndex targetColumn
  go (reflectorIndex + 1) pivotValue
  where
    go !rowIndex !accumulator
      | rowIndex >= rowCount = pure accumulator
      | otherwise = do
          reflectorEntry <- readMatrix columnCount matrixWork rowIndex reflectorIndex
          targetEntry <- readMatrix columnCount matrixWork rowIndex targetColumn
          go (rowIndex + 1) (accumulator + reflectorEntry * targetEntry)
{-# INLINE matrixReflectorDot #-}

formThinQ ::
  Int ->
  Int ->
  Int ->
  PrimArray.MutablePrimArray s Double ->
  PrimArray.MutablePrimArray s Double ->
  ST s [Double]
formThinQ !rowCount !columnCount !matrixEntryCount !matrixWork !reflectorScalars = do
  qWork <- newDoubleArray matrixEntryCount
  setDoubleArray qWork matrixEntryCount 0.0
  setThinIdentity rowCount columnCount qWork
  applyReflectors (columnCount - 1) qWork
  freezeDoubleList qWork
  where
    applyReflectors !reflectorIndex !qWork
      | reflectorIndex < 0 = pure ()
      | otherwise = do
          tauValue <- PrimArray.readPrimArray reflectorScalars reflectorIndex
          applyQRReflectorToQ rowCount columnCount matrixWork qWork reflectorIndex tauValue
          applyReflectors (reflectorIndex - 1) qWork
{-# INLINE formThinQ #-}

setThinIdentity :: Int -> Int -> PrimArray.MutablePrimArray s Double -> ST s ()
setThinIdentity !rowCount !columnCount !qWork =
  go 0
  where
    diagonalCount = min rowCount columnCount

    go !diagonalIndex
      | diagonalIndex >= diagonalCount = pure ()
      | otherwise = do
          writeMatrixWithColumnCount columnCount qWork diagonalIndex diagonalIndex 1.0
          go (diagonalIndex + 1)
{-# INLINE setThinIdentity #-}

applyQRReflectorToQ ::
  Int ->
  Int ->
  PrimArray.MutablePrimArray s Double ->
  PrimArray.MutablePrimArray s Double ->
  Int ->
  Double ->
  ST s ()
applyQRReflectorToQ !rowCount !columnCount !matrixWork !qWork !reflectorIndex !tauValue
  | tauValue == 0.0 = pure ()
  | otherwise = goColumn reflectorIndex
  where
    goColumn !targetColumn
      | targetColumn >= columnCount = pure ()
      | otherwise = do
          dotValue <- qReflectorDot targetColumn reflectorIndex 0.0
          let !scaledDot = tauValue * dotValue
          pivotValue <- readMatrixWithColumnCount columnCount qWork reflectorIndex targetColumn
          writeMatrixWithColumnCount columnCount qWork reflectorIndex targetColumn (pivotValue - scaledDot)
          updateTail targetColumn scaledDot (reflectorIndex + 1)
          goColumn (targetColumn + 1)

    qReflectorDot !targetColumn !rowIndex !accumulator
      | rowIndex >= rowCount = pure accumulator
      | rowIndex == reflectorIndex = do
          qEntry <- readMatrixWithColumnCount columnCount qWork rowIndex targetColumn
          qReflectorDot targetColumn (rowIndex + 1) (accumulator + qEntry)
      | otherwise = do
          reflectorEntry <- readMatrix columnCount matrixWork rowIndex reflectorIndex
          qEntry <- readMatrixWithColumnCount columnCount qWork rowIndex targetColumn
          qReflectorDot targetColumn (rowIndex + 1) (accumulator + reflectorEntry * qEntry)

    updateTail !targetColumn !scaledDot !rowIndex
      | rowIndex >= rowCount = pure ()
      | otherwise = do
          reflectorEntry <- readMatrix columnCount matrixWork rowIndex reflectorIndex
          qEntry <- readMatrixWithColumnCount columnCount qWork rowIndex targetColumn
          writeMatrixWithColumnCount columnCount qWork rowIndex targetColumn (qEntry - reflectorEntry * scaledDot)
          updateTail targetColumn scaledDot (rowIndex + 1)
{-# INLINE applyQRReflectorToQ #-}

extractUpperR :: Int -> Int -> Int -> PrimArray.MutablePrimArray s Double -> ST s [Double]
extractUpperR !rowCount !columnCount !upperEntryCount !matrixWork = do
  rWork <- newDoubleArray upperEntryCount
  setDoubleArray rWork upperEntryCount 0.0
  goRow 0 rWork
  freezeDoubleList rWork
  where
    goRow !rowIndex !rWork
      | rowIndex >= columnCount = pure ()
      | otherwise = do
          goColumn rowIndex rowIndex rWork
          goRow (rowIndex + 1) rWork

    goColumn !rowIndex !columnIndex !rWork
      | columnIndex >= columnCount = pure ()
      | rowIndex >= rowCount = pure ()
      | otherwise = do
          entryValue <- readMatrix columnCount matrixWork rowIndex columnIndex
          writeMatrix columnCount rWork rowIndex columnIndex entryValue
          goColumn rowIndex (columnIndex + 1) rWork
{-# INLINE extractUpperR #-}

factorCholesky :: Int -> PrimArray.MutablePrimArray s Double -> ST s (Either MoonlightError ())
factorCholesky !matrixSize !matrixWork = goColumn 0
  where
    goColumn !columnIndex
      | columnIndex >= matrixSize = pure (Right ())
      | otherwise = do
          diagonalContribution <- lowerSelfDot columnIndex 0 0.0
          diagonalInput <- readMatrix matrixSize matrixWork columnIndex columnIndex
          let !diagonalResidual = diagonalInput - diagonalContribution
          if diagonalResidual <= 0.0 || not (fieldValueValid diagonalResidual)
            then pure (Left (InvariantViolation "Cholesky decomposition failed: matrix is not positive-definite"))
            else do
              let !diagonalValue = sqrt diagonalResidual
              writeMatrix matrixSize matrixWork columnIndex columnIndex diagonalValue
              updateColumnTail columnIndex diagonalValue (columnIndex + 1)
              goColumn (columnIndex + 1)

    lowerSelfDot !rowIndex !columnIndex !accumulator
      | columnIndex >= rowIndex = pure accumulator
      | otherwise = do
          lowerValue <- readMatrix matrixSize matrixWork rowIndex columnIndex
          lowerSelfDot rowIndex (columnIndex + 1) (accumulator + lowerValue * lowerValue)

    lowerCrossDot !leftRow !rightRow !columnIndex !accumulator
      | columnIndex >= rightRow = pure accumulator
      | otherwise = do
          leftValue <- readMatrix matrixSize matrixWork leftRow columnIndex
          rightValue <- readMatrix matrixSize matrixWork rightRow columnIndex
          lowerCrossDot leftRow rightRow (columnIndex + 1) (accumulator + leftValue * rightValue)

    updateColumnTail !columnIndex !diagonalValue !rowIndex
      | rowIndex >= matrixSize = pure ()
      | otherwise = do
          crossContribution <- lowerCrossDot rowIndex columnIndex 0 0.0
          inputValue <- readMatrix matrixSize matrixWork rowIndex columnIndex
          writeMatrix matrixSize matrixWork rowIndex columnIndex ((inputValue - crossContribution) / diagonalValue)
          updateColumnTail columnIndex diagonalValue (rowIndex + 1)
{-# INLINE factorCholesky #-}

zeroStrictUpper :: Int -> PrimArray.MutablePrimArray s Double -> ST s ()
zeroStrictUpper !matrixSize !matrixWork = goRow 0
  where
    goRow !rowIndex
      | rowIndex >= matrixSize = pure ()
      | otherwise = do
          goColumn rowIndex (rowIndex + 1)
          goRow (rowIndex + 1)

    goColumn !rowIndex !columnIndex
      | columnIndex >= matrixSize = pure ()
      | otherwise = do
          writeMatrix matrixSize matrixWork rowIndex columnIndex 0.0
          goColumn rowIndex (columnIndex + 1)
{-# INLINE zeroStrictUpper #-}