packages feed

moonlight-linalg-0.1.0.0: src-eigen/Moonlight/LinAlg/Internal/Eigen/Tridiagonal.hs

{-# LANGUAGE BangPatterns #-}

module Moonlight.LinAlg.Internal.Eigen.Tridiagonal
  ( canonicalizeEigenvectorSigns,
    eigenpairsFromMutable,
    newIdentityEigenvectors,
    orthonormalizeDegenerateClusters,
    sortEigenpairsAscending,
    solveTridiagonalEigenvectors,
  )
where

import Control.Monad (when)
import Control.Monad.ST (ST)
import Data.Primitive.PrimArray
  ( MutablePrimArray,
    readPrimArray,
    writePrimArray,
  )
import Moonlight.Core (MoonlightError (..), fieldValueValid)
import Moonlight.LinAlg.Internal.Eigen.DenseWork
  ( MutableDenseWork (..),
    newDenseWork,
    setIdentityDenseWork,
  )
import Moonlight.LinAlg.Internal.Eigen.Kernels
  ( copySignMagnitude,
    epsDouble,
    forIndex,
    hypotStable,
    maxFiniteDouble,
    safeMinimumDouble,
  )
import Prelude

newIdentityEigenvectors :: Int -> ST s (MutableDenseWork s)
newIdentityEigenvectors !matrixSize = do
  eigenvectors <- newDenseWork matrixSize matrixSize
  setIdentityDenseWork eigenvectors
  pure eigenvectors

solveTridiagonalEigenvectors ::
  Int ->
  MutablePrimArray s Double ->
  MutablePrimArray s Double ->
  MutableDenseWork s ->
  ST s (Either MoonlightError ())
solveTridiagonalEigenvectors !matrixSize diagonalValues offDiagonalValues eigenvectors = do
  when (matrixSize > 0) $
    writePrimArray offDiagonalValues (matrixSize - 1) 0.0
  scaleValue <- scaleTridiagonal matrixSize diagonalValues offDiagonalValues
  solveResult <- solveAllIndices 0
  unscaleDiagonal matrixSize diagonalValues scaleValue
  pure solveResult
  where
    !iterationLimit = max 64 (matrixSize * 128)

    solveAllIndices !splitIndex
      | splitIndex >= matrixSize = pure (Right ())
      | otherwise = do
          indexResult <- convergeIndex splitIndex 0
          case indexResult of
            Left err -> pure (Left err)
            Right () -> solveAllIndices (splitIndex + 1)

    convergeIndex !splitIndex !iterationCount
      | iterationCount >= iterationLimit = do
          maxOffDiagonal <- maximumOffDiagonalMagnitude splitIndex matrixSize offDiagonalValues
          pure
            ( Left
                ( InvariantViolation
                    ( "tridiagonal eigensolve exhausted implicit-QL iteration budget at block "
                        <> show (splitIndex, matrixSize - 1)
                        <> " after "
                        <> show iterationCount
                        <> " iterations; max off-diagonal="
                        <> show maxOffDiagonal
                    )
                )
            )
      | otherwise = do
          activeIndex <- findActiveSplit splitIndex
          if activeIndex == splitIndex
            then pure (Right ())
            else do
              implicitQLStep splitIndex activeIndex diagonalValues offDiagonalValues eigenvectors
              convergeIndex splitIndex (iterationCount + 1)

    findActiveSplit !splitIndex = go splitIndex
      where
        go !candidateIndex
          | candidateIndex >= matrixSize - 1 = pure (matrixSize - 1)
          | otherwise = do
              offDiagonal <- readPrimArray offDiagonalValues candidateIndex
              leftDiagonal <- readPrimArray diagonalValues candidateIndex
              rightDiagonal <- readPrimArray diagonalValues (candidateIndex + 1)
              if negligibleOffDiagonal offDiagonal leftDiagonal rightDiagonal
                then writePrimArray offDiagonalValues candidateIndex 0.0 >> pure candidateIndex
                else go (candidateIndex + 1)

implicitQLStep ::
  Int ->
  Int ->
  MutablePrimArray s Double ->
  MutablePrimArray s Double ->
  MutableDenseWork s ->
  ST s ()
implicitQLStep !splitIndex !activeIndex diagonalValues offDiagonalValues (MutableDenseWork eigenvectorRowCount _ eigenvectorPayload) = do
  leftDiagonal <- readPrimArray diagonalValues splitIndex
  nextDiagonal <- readPrimArray diagonalValues (splitIndex + 1)
  leftOffDiagonal <- readPrimArray offDiagonalValues splitIndex
  activeDiagonal <- readPrimArray diagonalValues activeIndex
  let !shiftRatio = (nextDiagonal - leftDiagonal) / (2.0 * leftOffDiagonal)
      !shiftRadius = hypotStable shiftRatio 1.0
      !shiftDenominator = shiftRatio + copySignMagnitude shiftRadius shiftRatio
      !initialShift = activeDiagonal - leftDiagonal + (leftOffDiagonal / shiftDenominator)
  sweepDown (activeIndex - 1) 1.0 1.0 initialShift 0.0
  where
    sweepDown !indexValue !previousCosine !previousSine !currentShift !currentCorrection
      | indexValue < splitIndex = do
          updatedLeftDiagonal <- readPrimArray diagonalValues splitIndex
          writePrimArray diagonalValues splitIndex (updatedLeftDiagonal - currentCorrection)
          writePrimArray offDiagonalValues splitIndex currentShift
          writePrimArray offDiagonalValues activeIndex 0.0
      | otherwise = do
          currentOffDiagonal <- readPrimArray offDiagonalValues indexValue
          currentDiagonal <- readPrimArray diagonalValues indexValue
          nextDiagonal <- readPrimArray diagonalValues (indexValue + 1)
          let !fValue = previousSine * currentOffDiagonal
              !bValue = previousCosine * currentOffDiagonal
          rotateWithGivens indexValue bValue currentDiagonal nextDiagonal currentCorrection fValue currentShift

    rotateWithGivens !indexValue !bValue !currentDiagonal !nextDiagonal !currentCorrection !fValue !gValue
      | fValue == 0.0 && gValue == 0.0 =
          finishRotation indexValue bValue currentDiagonal nextDiagonal currentCorrection 1.0 0.0 0.0
      | abs fValue >= abs gValue =
          let !normalizedCosine = gValue / fValue
              !radius = hypotStable normalizedCosine 1.0
              !sineValue = 1.0 / radius
              !cosineValue = normalizedCosine * sineValue
           in finishRotation indexValue bValue currentDiagonal nextDiagonal currentCorrection cosineValue sineValue (fValue * radius)
      | otherwise =
          let !normalizedSine = fValue / gValue
              !radius = hypotStable normalizedSine 1.0
              !cosineValue = 1.0 / radius
              !sineValue = normalizedSine * cosineValue
           in finishRotation indexValue bValue currentDiagonal nextDiagonal currentCorrection cosineValue sineValue (gValue * radius)

    finishRotation !indexValue !bValue !currentDiagonal !nextDiagonal !currentCorrection !nextCosine !nextSine !updatedOffDiagonal = do
      let !nextDiagonalBase = nextDiagonal - currentCorrection
          !rotationRadius = ((currentDiagonal - nextDiagonalBase) * nextSine) + (2.0 * nextCosine * bValue)
          !nextCorrection = nextSine * rotationRadius
          !updatedNextDiagonal = nextDiagonalBase + nextCorrection
          !nextShift = (nextCosine * rotationRadius) - bValue
      writePrimArray offDiagonalValues (indexValue + 1) updatedOffDiagonal
      writePrimArray diagonalValues (indexValue + 1) updatedNextDiagonal
      rotateEigenvectorColumnsAt indexValue (indexValue + 1) nextCosine nextSine
      sweepDown (indexValue - 1) nextCosine nextSine nextShift nextCorrection

    rotateEigenvectorColumnsAt !leftColumn !rightColumn !cosineValue !sineValue = rotateRows 0
      where
        !leftBase = leftColumn * eigenvectorRowCount
        !rightBase = rightColumn * eigenvectorRowCount
        rotateRows !rowIndex
          | rowIndex >= eigenvectorRowCount = pure ()
          | otherwise = do
              leftEntry <- readPrimArray eigenvectorPayload (leftBase + rowIndex)
              rightEntry <- readPrimArray eigenvectorPayload (rightBase + rowIndex)
              writePrimArray eigenvectorPayload (leftBase + rowIndex) (cosineValue * leftEntry - sineValue * rightEntry)
              writePrimArray eigenvectorPayload (rightBase + rowIndex) (sineValue * leftEntry + cosineValue * rightEntry)
              rotateRows (rowIndex + 1)

scaleTridiagonal :: Int -> MutablePrimArray s Double -> MutablePrimArray s Double -> ST s Double
scaleTridiagonal !matrixSize diagonalValues offDiagonalValues = do
  maximumMagnitude <- maximumTridiagonalMagnitude matrixSize diagonalValues offDiagonalValues
  let !safeMaximum = sqrt maxFiniteDouble * 0.25
      !safeMinimum = sqrt safeMinimumDouble / epsDouble
      !scaleValue
        | maximumMagnitude == 0.0 = 1.0
        | maximumMagnitude > safeMaximum = safeMaximum / maximumMagnitude
        | maximumMagnitude < safeMinimum = safeMinimum / maximumMagnitude
        | otherwise = 1.0
  when (scaleValue /= 1.0) $ do
    forIndex 0 matrixSize $ \indexValue -> do
      diagonalEntry <- readPrimArray diagonalValues indexValue
      writePrimArray diagonalValues indexValue (scaleValue * diagonalEntry)
    forIndex 0 (max 0 (matrixSize - 1)) $ \indexValue -> do
      offDiagonalEntry <- readPrimArray offDiagonalValues indexValue
      writePrimArray offDiagonalValues indexValue (scaleValue * offDiagonalEntry)
  pure scaleValue
{-# INLINE scaleTridiagonal #-}

unscaleDiagonal :: Int -> MutablePrimArray s Double -> Double -> ST s ()
unscaleDiagonal !matrixSize diagonalValues !scaleValue =
  when (scaleValue /= 1.0) $ do
    let !inverseScale = 1.0 / scaleValue
    forIndex 0 matrixSize $ \indexValue -> do
      diagonalEntry <- readPrimArray diagonalValues indexValue
      writePrimArray diagonalValues indexValue (inverseScale * diagonalEntry)
{-# INLINE unscaleDiagonal #-}

maximumTridiagonalMagnitude :: Int -> MutablePrimArray s Double -> MutablePrimArray s Double -> ST s Double
maximumTridiagonalMagnitude !matrixSize diagonalValues offDiagonalValues = do
  diagonalMaximum <- maximumArrayMagnitude 0 matrixSize diagonalValues 0.0
  maximumArrayMagnitude 0 (max 0 (matrixSize - 1)) offDiagonalValues diagonalMaximum
{-# INLINE maximumTridiagonalMagnitude #-}

maximumOffDiagonalMagnitude :: Int -> Int -> MutablePrimArray s Double -> ST s Double
maximumOffDiagonalMagnitude !startIndex !matrixSize offDiagonalValues =
  maximumArrayMagnitude startIndex (max startIndex (matrixSize - 1)) offDiagonalValues 0.0
{-# INLINE maximumOffDiagonalMagnitude #-}

maximumArrayMagnitude :: Int -> Int -> MutablePrimArray s Double -> Double -> ST s Double
maximumArrayMagnitude !startIndex !stopIndex arrayValues !initialMaximum = go startIndex initialMaximum
  where
    go !indexValue !currentMaximum
      | indexValue >= stopIndex = pure currentMaximum
      | otherwise = do
          entryValue <- readPrimArray arrayValues indexValue
          go (indexValue + 1) (max currentMaximum (abs entryValue))
{-# INLINE maximumArrayMagnitude #-}

negligibleOffDiagonal :: Double -> Double -> Double -> Bool
negligibleOffDiagonal !offDiagonal !leftDiagonal !rightDiagonal =
  abs offDiagonal <= (64.0 * epsDouble * (abs leftDiagonal + abs rightDiagonal)) + safeMinimumDouble
{-# INLINE negligibleOffDiagonal #-}

sortEigenpairsAscending :: Int -> MutablePrimArray s Double -> MutableDenseWork s -> ST s ()
sortEigenpairsAscending !matrixSize diagonalValues eigenvectors =
  forIndex 0 matrixSize $ \targetIndex -> do
    minimumIndex <- findMinimumIndex targetIndex (targetIndex + 1)
    when (minimumIndex /= targetIndex) $ do
      targetValue <- readPrimArray diagonalValues targetIndex
      minimumValue <- readPrimArray diagonalValues minimumIndex
      writePrimArray diagonalValues targetIndex minimumValue
      writePrimArray diagonalValues minimumIndex targetValue
      swapDenseColumnsTight eigenvectors targetIndex minimumIndex
  where
    findMinimumIndex !bestIndex !candidateIndex
      | candidateIndex >= matrixSize = pure bestIndex
      | otherwise = do
          bestValue <- readPrimArray diagonalValues bestIndex
          candidateValue <- readPrimArray diagonalValues candidateIndex
          if candidateValue < bestValue
            then findMinimumIndex candidateIndex (candidateIndex + 1)
            else findMinimumIndex bestIndex (candidateIndex + 1)

orthonormalizeDegenerateClusters :: Int -> MutablePrimArray s Double -> MutableDenseWork s -> ST s (Either MoonlightError ())
orthonormalizeDegenerateClusters !matrixSize diagonalValues eigenvectors = processCluster 0
  where
    processCluster !clusterStart
      | clusterStart >= matrixSize = pure (Right ())
      | otherwise = do
          clusterStop <- findClusterStop clusterStart (clusterStart + 1)
          clusterResult <- orthonormalizeColumns clusterStart clusterStop
          case clusterResult of
            Left err -> pure (Left err)
            Right () -> processCluster clusterStop

    findClusterStop !clusterStart !candidateIndex
      | candidateIndex >= matrixSize = pure matrixSize
      | otherwise = do
          leftValue <- readPrimArray diagonalValues (candidateIndex - 1)
          rightValue <- readPrimArray diagonalValues candidateIndex
          if sameEigenCluster leftValue rightValue
            then findClusterStop clusterStart (candidateIndex + 1)
            else pure candidateIndex

    orthonormalizeColumns !clusterStart !clusterStop = normalizeColumnAt clusterStart
      where
        normalizeColumnAt !columnIndex
          | columnIndex >= clusterStop = pure (Right ())
          | otherwise = do
              subtractPriorColumns clusterStart columnIndex
              normalized <- normalizeEigenvectorColumn eigenvectors columnIndex
              if normalized
                then normalizeColumnAt (columnIndex + 1)
                else pure (Left (InvariantViolation ("symmetric eigen decomposition produced zero eigenvector at column " <> show columnIndex)))

    subtractPriorColumns !priorIndex !columnIndex
      | priorIndex >= columnIndex = pure ()
      | otherwise = do
          projection <- dotDenseColumnsTight eigenvectors priorIndex columnIndex
          addScaledColumn eigenvectors priorIndex columnIndex (negate projection)
          subtractPriorColumns (priorIndex + 1) columnIndex

sameEigenCluster :: Double -> Double -> Bool
sameEigenCluster !leftValue !rightValue =
  abs (leftValue - rightValue) <= 128.0 * epsDouble * max 1.0 (max (abs leftValue) (abs rightValue))
{-# INLINE sameEigenCluster #-}

addScaledColumn :: MutableDenseWork s -> Int -> Int -> Double -> ST s ()
addScaledColumn (MutableDenseWork rowCount _ payload) !sourceColumn !targetColumn !scaleValue =
  forIndex 0 rowCount $ \rowIndex -> do
    sourceEntry <- readPrimArray payload (sourceBase + rowIndex)
    targetEntry <- readPrimArray payload (targetBase + rowIndex)
    writePrimArray payload (targetBase + rowIndex) (targetEntry + scaleValue * sourceEntry)
  where
    !sourceBase = sourceColumn * rowCount
    !targetBase = targetColumn * rowCount
{-# INLINE addScaledColumn #-}

normalizeEigenvectorColumn :: forall s. MutableDenseWork s -> Int -> ST s Bool
normalizeEigenvectorColumn eigenvectors !columnIndex = do
  normValue <- columnNorm eigenvectors columnIndex
  if normValue <= 0.0 || not (fieldValueValid normValue)
    then pure False
    else scaleDenseColumnTight eigenvectors columnIndex (1.0 / normValue) >> pure True
  where
    columnNorm :: MutableDenseWork s -> Int -> ST s Double
    columnNorm (MutableDenseWork rowCount _ payload) !targetColumn = go 0 0.0 1.0
      where
        !targetBase = targetColumn * rowCount
        go !rowIndex !scaleValue !scaledSum
          | rowIndex >= rowCount =
              if scaleValue == 0.0
                then pure 0.0
                else pure (scaleValue * sqrt scaledSum)
          | otherwise = do
              entryValue <- readPrimArray payload (targetBase + rowIndex)
              let !entryAbs = abs entryValue
              if entryAbs == 0.0
                then go (rowIndex + 1) scaleValue scaledSum
                else
                  if scaleValue < entryAbs
                    then
                      let !scaledRatio = scaleValue / entryAbs
                       in go (rowIndex + 1) entryAbs (1.0 + scaledSum * scaledRatio * scaledRatio)
                    else
                      let !scaledRatio = entryAbs / scaleValue
                       in go (rowIndex + 1) scaleValue (scaledSum + scaledRatio * scaledRatio)
{-# INLINE normalizeEigenvectorColumn #-}

canonicalizeEigenvectorSigns :: Int -> MutableDenseWork s -> ST s ()
canonicalizeEigenvectorSigns !matrixSize eigenvectors =
  forIndex 0 matrixSize $ \columnIndex -> do
    (_, maximumMagnitude, representativeValue) <- maximumMagnitudeInColumn eigenvectors columnIndex
    when (maximumMagnitude > 0.0 && representativeValue < 0.0) $
      scaleDenseColumnTight eigenvectors columnIndex (-1.0)

maximumMagnitudeInColumn :: MutableDenseWork s -> Int -> ST s (Int, Double, Double)
maximumMagnitudeInColumn (MutableDenseWork rowCount _ payload) !columnIndex = go 0 0 0.0 0.0
  where
    !columnBase = columnIndex * rowCount
    go !rowIndex !bestIndex !bestMagnitude !bestValue
      | rowIndex >= rowCount = pure (bestIndex, bestMagnitude, bestValue)
      | otherwise = do
          entryValue <- readPrimArray payload (columnBase + rowIndex)
          let !entryMagnitude = abs entryValue
          if entryMagnitude > bestMagnitude
            then go (rowIndex + 1) rowIndex entryMagnitude entryValue
            else go (rowIndex + 1) bestIndex bestMagnitude bestValue
{-# INLINE maximumMagnitudeInColumn #-}

swapDenseColumnsTight :: MutableDenseWork s -> Int -> Int -> ST s ()
swapDenseColumnsTight (MutableDenseWork rowCount _ payload) !leftColumn !rightColumn =
  when (leftColumn /= rightColumn) $
    forIndex 0 rowCount $ \rowIndex -> do
      leftValue <- readPrimArray payload (leftBase + rowIndex)
      rightValue <- readPrimArray payload (rightBase + rowIndex)
      writePrimArray payload (leftBase + rowIndex) rightValue
      writePrimArray payload (rightBase + rowIndex) leftValue
  where
    !leftBase = leftColumn * rowCount
    !rightBase = rightColumn * rowCount
{-# INLINE swapDenseColumnsTight #-}

dotDenseColumnsTight :: MutableDenseWork s -> Int -> Int -> ST s Double
dotDenseColumnsTight (MutableDenseWork rowCount _ payload) !leftColumn !rightColumn = go 0 0.0
  where
    !leftBase = leftColumn * rowCount
    !rightBase = rightColumn * rowCount
    go !rowIndex !accumulator
      | rowIndex >= rowCount = pure accumulator
      | otherwise = do
          leftValue <- readPrimArray payload (leftBase + rowIndex)
          rightValue <- readPrimArray payload (rightBase + rowIndex)
          go (rowIndex + 1) (accumulator + leftValue * rightValue)
{-# INLINE dotDenseColumnsTight #-}

scaleDenseColumnTight :: MutableDenseWork s -> Int -> Double -> ST s ()
scaleDenseColumnTight (MutableDenseWork rowCount _ payload) !columnIndex !scaleValue =
  forIndex 0 rowCount $ \rowIndex -> do
    entryValue <- readPrimArray payload (columnBase + rowIndex)
    writePrimArray payload (columnBase + rowIndex) (scaleValue * entryValue)
  where
    !columnBase = columnIndex * rowCount
{-# INLINE scaleDenseColumnTight #-}

eigenpairsFromMutable :: Int -> MutablePrimArray s Double -> MutableDenseWork s -> ST s [(Double, [Double])]
eigenpairsFromMutable !matrixSize diagonalValues (MutableDenseWork rowCount _ eigenvectorPayload) = collectColumns 0 []
  where
    collectColumns !columnIndex !revPairs
      | columnIndex >= matrixSize = pure (reverse revPairs)
      | otherwise = do
          eigenvalue <- readPrimArray diagonalValues columnIndex
          eigenvector <- collectColumnEntries columnIndex 0 []
          collectColumns (columnIndex + 1) ((eigenvalue, eigenvector) : revPairs)

    collectColumnEntries !columnIndex !rowIndex !revEntries
      | rowIndex >= matrixSize = pure (reverse revEntries)
      | otherwise = do
          entryValue <- readPrimArray eigenvectorPayload (rowIndex + columnIndex * rowCount)
          collectColumnEntries columnIndex (rowIndex + 1) (entryValue : revEntries)