packages feed

moonlight-linalg-0.1.0.0: src-dense/Moonlight/LinAlg/Pure/Dense/Decomposition.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE RecordWildCards #-}

module Moonlight.LinAlg.Pure.Dense.Decomposition
  ( qrDecompFullColumnRank,
    choleskyDecomp,
    symmetricEigen,
    symmetricEigenPairs,
    thinSvdFullColumnRank,
  )
where

import Data.List (sortBy)
import Data.Ord (comparing)
import Data.Vector.Storable qualified as S
import GHC.TypeNats (KnownNat)
import Moonlight.Core (MoonlightError (..))
import Moonlight.LinAlg.Internal.Dense.DoubleFactorization
  ( choleskyLower,
    qrFullColumnRank,
  )
import Moonlight.LinAlg.Internal.Dense.OneSidedJacobiSVD qualified as JacobiSVD
import Moonlight.LinAlg.Pure.Dense.Rows (transposeRowsExact)
import Moonlight.LinAlg.Internal.Eigen.Input
  ( validateSymmetricEigenInput,
  )
import Moonlight.LinAlg.Internal.Eigen.Symmetric
  ( SymmetricEigenResult (..),
    symmetricEigenPairsDenseUnchecked,
  )
import Moonlight.LinAlg.Pure.Dense.Flat
  ( denseDoubleMatrixShape,
    denseDoubleMatrixToRowMajorVector,
    denseDoubleMatrixToRows,
    mkDenseDoubleMatrixRows,
  )
import Moonlight.LinAlg.Pure.Dense.Types (Matrix, Vector, fromListMatrix, fromListVector, toListMatrix)
import qualified Moonlight.LinAlg.Pure.Dense.Types as DenseTypes
import Prelude

qrDecompFullColumnRank ::
  forall r c.
  (KnownNat r, KnownNat c) =>
  Matrix r c Double ->
  Either MoonlightError (Matrix r c Double, Matrix c c Double)
qrDecompFullColumnRank matrixValue = do
  let (rowCount, columnCount) = DenseTypes.matrixShape matrixValue
  (qValues, rValues) <- qrFullColumnRank rowCount columnCount (toListMatrix matrixValue)
  qMatrix <- fromListMatrix @r @c qValues
  rMatrix <- fromListMatrix @c @c rValues
  pure (qMatrix, rMatrix)

choleskyDecomp ::
  forall n.
  KnownNat n =>
  Matrix n n Double ->
  Either MoonlightError (Matrix n n Double)
choleskyDecomp matrixValue = do
  let (matrixSize, _) = DenseTypes.matrixShape matrixValue
  lowerValues <- choleskyLower matrixSize (toListMatrix matrixValue)
  fromListMatrix @n @n lowerValues

symmetricEigenPairs :: Int -> [[Double]] -> Either MoonlightError [(Double, [Double])]
symmetricEigenPairs matrixSize matrixRows = do
  validateSymmetricEigenInput "symmetric eigen decomposition" matrixSize matrixRows
  matrixValue <- mkDenseDoubleMatrixRows matrixRows
  eigenResultToPairs <$> symmetricEigenPairsDenseUnchecked matrixSize matrixValue

symmetricEigen ::
  forall n.
  KnownNat n =>
  Matrix n n Double ->
  Either MoonlightError (Vector n Double, Matrix n n Double)
symmetricEigen matrixValue = do
  let (matrixSize, _) = DenseTypes.matrixShape matrixValue
  matrixRows <- DenseTypes.matrixToRows matrixValue
  validateSymmetricEigenInput "symmetric eigen decomposition" matrixSize matrixRows
  denseMatrix <- mkDenseDoubleMatrixRows matrixRows
  eigenResult <- symmetricEigenPairsDenseUnchecked matrixSize denseMatrix
  let orderedPairs = sortBy (flip (comparing fst)) (eigenResultToPairs eigenResult)
      eigenvalues = map fst orderedPairs
      eigenvectors = map snd orderedPairs
  eigenvalueVector <- fromListVector @n eigenvalues
  eigenvectorRows <- transposeRowsExact eigenvectors
  eigenvectorMatrix <- fromListMatrix @n @n (concat eigenvectorRows)
  pure (eigenvalueVector, eigenvectorMatrix)

diagonalRows :: [Double] -> [[Double]]
diagonalRows diagonalValues =
  let indexedDiagonalValues = zip [0 :: Int ..] diagonalValues
      size = length diagonalValues
   in map
        (\(rowIndex, diagonalValue) -> map (\columnIndex -> if rowIndex == columnIndex then diagonalValue else 0.0) [0 .. size - 1])
        indexedDiagonalValues

thinSvdFullColumnRank ::
  forall r c.
  (KnownNat r, KnownNat c) =>
  Matrix r c Double ->
  Either MoonlightError (Matrix r c Double, Matrix c c Double, Matrix c c Double)
thinSvdFullColumnRank matrixValue = do
  rows <- DenseTypes.matrixToRows matrixValue
  denseMatrix <- mkDenseDoubleMatrixRows rows
  JacobiSVD.ThinSvdResult {..} <-
    firstMoonlightSvdFailure (JacobiSVD.thinSvdFullColumnRank denseMatrix)
  let singularValues = S.toList thinSvdSingularValues
      sRows = diagonalRows singularValues
      uRows = denseDoubleMatrixToRows thinSvdLeftSingularVectors
      vTRows = denseDoubleMatrixToRows thinSvdRightSingularVectorsTransposed
  uMatrix <- fromListMatrix @r @c (concat uRows)
  sMatrix <- fromListMatrix @c @c (concat sRows)
  vTMatrix <- fromListMatrix @c @c (concat vTRows)
  pure (uMatrix, sMatrix, vTMatrix)

eigenResultToPairs :: SymmetricEigenResult -> [(Double, [Double])]
eigenResultToPairs SymmetricEigenResult {..} =
  fmap eigenPairAt [0 .. matrixSize - 1]
  where
    !(matrixSize, _) = denseDoubleMatrixShape symmetricEigenResultVectors
    eigenvectorPayload = denseDoubleMatrixToRowMajorVector symmetricEigenResultVectors

    eigenPairAt !columnIndex =
      ( symmetricEigenResultValues `S.unsafeIndex` columnIndex,
        fmap
          (\rowIndex -> eigenvectorPayload `S.unsafeIndex` (rowIndex * matrixSize + columnIndex))
          [0 .. matrixSize - 1]
      )

firstMoonlightSvdFailure :: Either JacobiSVD.ThinSvdFailure value -> Either MoonlightError value
firstMoonlightSvdFailure resultValue =
  case resultValue of
    Right value -> Right value
    Left failureValue -> Left (InvariantViolation (thinSvdFailureMessage failureValue))

thinSvdFailureMessage :: JacobiSVD.ThinSvdFailure -> String
thinSvdFailureMessage failureValue =
  case failureValue of
    JacobiSVD.ThinSvdNonFiniteInput ->
      "thin Jacobi SVD requires finite entries"
    JacobiSVD.ThinSvdDimensionViolation message ->
      message
    JacobiSVD.ThinSvdRankDeficient columnIndex singularValue ->
      "thin Jacobi SVD requires full column rank; column "
        <> show columnIndex
        <> " singular value "
        <> show singularValue
        <> " is below rank tolerance"
    JacobiSVD.ThinSvdSweepBudgetNonConvergence sweepBudget maximumCross ->
      "thin Jacobi SVD exhausted "
        <> show sweepBudget
        <> " sweeps; maximum normalized column cross="
        <> show maximumCross