moonlight-linalg-0.1.0.0: src-dense/Moonlight/LinAlg/Internal/Dense/OneSidedJacobiSVD.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE StrictData #-}
-- | One-sided Jacobi SVD over sealed flat Double workspaces.
module Moonlight.LinAlg.Internal.Dense.OneSidedJacobiSVD
( ThinSvdFailure (..),
ThinSvdResult (..),
thinSvdFullColumnRank,
)
where
import Control.Monad.ST (ST, runST)
import Data.Bifunctor (first)
import Data.List (sortBy)
import Data.Ord (comparing)
import Data.Vector.Storable qualified as S
import Data.Vector.Storable.Mutable qualified as SM
import Moonlight.Core (checkedNonNegativeProduct)
import Moonlight.LinAlg.Internal.Eigen.DenseWork
( MutableDenseWork (..),
dotDenseColumns,
newDenseWork,
readDenseWork,
setIdentityDenseWork,
writeDenseWork,
)
import Moonlight.LinAlg.Internal.Eigen.Kernels
( epsDouble,
finiteDouble,
forIndex,
hypotStable,
)
import Moonlight.LinAlg.Pure.Dense.Flat
( DenseDoubleMatrix,
denseDoubleMatrixShape,
denseDoubleMatrixToRowMajorVector,
trustedDenseDoubleMatrixRowMajor,
)
import Prelude
data ThinSvdFailure
= ThinSvdNonFiniteInput
| ThinSvdDimensionViolation !String
| ThinSvdRankDeficient !Int !Double
| ThinSvdSweepBudgetNonConvergence !Int !Double
deriving stock (Eq, Show)
data ThinSvdResult = ThinSvdResult
{ thinSvdLeftSingularVectors :: !DenseDoubleMatrix,
thinSvdSingularValues :: !(S.Vector Double),
thinSvdRightSingularVectorsTransposed :: !DenseDoubleMatrix
}
deriving stock (Eq, Show)
thinSvdFullColumnRank :: DenseDoubleMatrix -> Either ThinSvdFailure ThinSvdResult
thinSvdFullColumnRank matrixValue
| rowCount < columnCount =
Left (ThinSvdDimensionViolation "thin Jacobi SVD requires row count greater than or equal to column count")
| columnCount > 64 =
Left (ThinSvdDimensionViolation "thin Jacobi SVD supports at most 64 columns")
| S.any (not . finiteDouble) payload =
Left ThinSvdNonFiniteInput
| otherwise = do
matrixEntryCount <- checkedThinSvdCardinality "thin Jacobi SVD matrix" rowCount columnCount
rightEntryCount <- checkedThinSvdCardinality "thin Jacobi SVD right singular vectors" columnCount columnCount
if S.length payload /= matrixEntryCount
then Left (ThinSvdDimensionViolation "thin Jacobi SVD payload does not match its declared matrix shape")
else runST (thinSvdFullColumnRankST rowCount columnCount matrixEntryCount rightEntryCount matrixValue)
where
!(rowCount, columnCount) = denseDoubleMatrixShape matrixValue
payload = denseDoubleMatrixToRowMajorVector matrixValue
checkedThinSvdCardinality :: String -> Int -> Int -> Either ThinSvdFailure Int
checkedThinSvdCardinality context leftCount rightCount =
first
(const (ThinSvdDimensionViolation (context <> " cardinality exceeds non-negative Int range")))
(checkedNonNegativeProduct leftCount rightCount)
thinSvdFullColumnRankST :: Int -> Int -> Int -> Int -> DenseDoubleMatrix -> ST s (Either ThinSvdFailure ThinSvdResult)
thinSvdFullColumnRankST !rowCount !columnCount !matrixEntryCount !rightEntryCount matrixValue = do
leftColumns <- newDenseWork rowCount columnCount
rightVectors <- newDenseWork columnCount columnCount
setIdentityDenseWork rightVectors
copyRowMajorToColumns rowCount columnCount matrixValue leftColumns
sweepResult <- runJacobiSweeps rowCount columnCount leftColumns rightVectors
case sweepResult of
Left err -> pure (Left err)
Right () -> do
singularValues <- singularValuesFromColumns columnCount leftColumns
let !maximumSingular = maximumSingularValue singularValues
!rankTolerance = fromIntegral (max 1 rowCount) * epsDouble * max 1.0 maximumSingular
case firstRankDeficiency rankTolerance singularValues of
Just (columnIndex, singularValue) -> pure (Left (ThinSvdRankDeficient columnIndex singularValue))
Nothing -> Right <$> projectThinSvdResult rowCount columnCount matrixEntryCount rightEntryCount leftColumns rightVectors singularValues
copyRowMajorToColumns :: Int -> Int -> DenseDoubleMatrix -> MutableDenseWork s -> ST s ()
copyRowMajorToColumns !rowCount !columnCount matrixValue columns =
forIndex 0 rowCount $ \rowIndex ->
forIndex 0 columnCount $ \columnIndex ->
writeDenseWork columns rowIndex columnIndex (payload `S.unsafeIndex` (rowIndex * columnCount + columnIndex))
where
payload = denseDoubleMatrixToRowMajorVector matrixValue
{-# INLINE copyRowMajorToColumns #-}
runJacobiSweeps ::
Int ->
Int ->
MutableDenseWork s ->
MutableDenseWork s ->
ST s (Either ThinSvdFailure ())
runJacobiSweeps !rowCount !columnCount leftColumns rightVectors = sweepAt 0
where
!sweepBudget = max 8 (12 * max 1 columnCount)
!pairTolerance = 64.0 * epsDouble
sweepAt !sweepIndex
| columnCount <= 1 = pure (Right ())
| sweepIndex >= sweepBudget = do
finalCross <- maximumNormalizedCross columnCount leftColumns
pure (Left (ThinSvdSweepBudgetNonConvergence sweepBudget finalCross))
| otherwise = do
summary <- sweepColumnPairs rowCount columnCount pairTolerance leftColumns rightVectors
if sweepMaximumCross summary <= pairTolerance
then pure (Right ())
else sweepAt (sweepIndex + 1)
data SweepSummary = SweepSummary
{ sweepMaximumCross :: !Double
}
sweepColumnPairs ::
Int ->
Int ->
Double ->
MutableDenseWork s ->
MutableDenseWork s ->
ST s SweepSummary
sweepColumnPairs !rowCount !columnCount !pairTolerance leftColumns rightVectors =
goLeft 0 0.0
where
goLeft !leftColumn !maximumCross
| leftColumn >= columnCount - 1 = pure (SweepSummary maximumCross)
| otherwise = do
nextMaximum <- goRight leftColumn (leftColumn + 1) maximumCross
goLeft (leftColumn + 1) nextMaximum
goRight !leftColumn !rightColumn !maximumCross
| rightColumn >= columnCount = pure maximumCross
| otherwise = do
alpha <- dotDenseColumns leftColumns leftColumn leftColumn
beta <- dotDenseColumns leftColumns rightColumn rightColumn
gamma <- dotDenseColumns leftColumns leftColumn rightColumn
let !crossValue = normalizedCross alpha beta gamma
if crossValue > pairTolerance
then do
rotateJacobiColumns rowCount leftColumns rightVectors leftColumn rightColumn alpha beta gamma
goRight leftColumn (rightColumn + 1) (max maximumCross crossValue)
else goRight leftColumn (rightColumn + 1) (max maximumCross crossValue)
maximumNormalizedCross :: Int -> MutableDenseWork s -> ST s Double
maximumNormalizedCross !columnCount leftColumns = goLeft 0 0.0
where
goLeft !leftColumn !maximumCross
| leftColumn >= columnCount - 1 = pure maximumCross
| otherwise = do
nextMaximum <- goRight leftColumn (leftColumn + 1) maximumCross
goLeft (leftColumn + 1) nextMaximum
goRight !leftColumn !rightColumn !maximumCross
| rightColumn >= columnCount = pure maximumCross
| otherwise = do
alpha <- dotDenseColumns leftColumns leftColumn leftColumn
beta <- dotDenseColumns leftColumns rightColumn rightColumn
gamma <- dotDenseColumns leftColumns leftColumn rightColumn
goRight leftColumn (rightColumn + 1) (max maximumCross (normalizedCross alpha beta gamma))
normalizedCross :: Double -> Double -> Double -> Double
normalizedCross !alpha !beta !gamma =
let !denominator = sqrt (max 0.0 alpha * max 0.0 beta)
in if denominator <= 0.0
then 0.0
else abs gamma / denominator
{-# INLINE normalizedCross #-}
rotateJacobiColumns ::
Int ->
MutableDenseWork s ->
MutableDenseWork s ->
Int ->
Int ->
Double ->
Double ->
Double ->
ST s ()
rotateJacobiColumns !rowCount leftColumns rightVectors !leftColumn !rightColumn !alpha !beta !gamma = do
let !(cosineValue, sineValue) = jacobiRotation alpha beta gamma
rotateColumnPair rowCount leftColumns leftColumn rightColumn cosineValue sineValue
rotateColumnPair columnCount rightVectors leftColumn rightColumn cosineValue sineValue
where
MutableDenseWork columnCount _ _ = rightVectors
jacobiRotation :: Double -> Double -> Double -> (Double, Double)
jacobiRotation !alpha !beta !gamma =
let !tauValue = (beta - alpha) / (2.0 * gamma)
!tangentValue =
if tauValue < 0.0
then (-1.0) / ((-tauValue) + hypotStable tauValue 1.0)
else 1.0 / (tauValue + hypotStable tauValue 1.0)
!cosineValue = 1.0 / hypotStable 1.0 tangentValue
!sineValue = tangentValue * cosineValue
in (cosineValue, sineValue)
{-# INLINE jacobiRotation #-}
rotateColumnPair :: Int -> MutableDenseWork s -> Int -> Int -> Double -> Double -> ST s ()
rotateColumnPair !rowCount work !leftColumn !rightColumn !cosineValue !sineValue =
forIndex 0 rowCount $ \rowIndex -> do
leftValue <- readDenseWork work rowIndex leftColumn
rightValue <- readDenseWork work rowIndex rightColumn
writeDenseWork work rowIndex leftColumn (cosineValue * leftValue - sineValue * rightValue)
writeDenseWork work rowIndex rightColumn (sineValue * leftValue + cosineValue * rightValue)
{-# INLINE rotateColumnPair #-}
singularValuesFromColumns :: Int -> MutableDenseWork s -> ST s (S.Vector Double)
singularValuesFromColumns !columnCount leftColumns = do
singularValueBuffer <- SM.new columnCount
forIndex 0 columnCount $ \columnIndex -> do
normSquared <- dotDenseColumns leftColumns columnIndex columnIndex
SM.write singularValueBuffer columnIndex (sqrt (max 0.0 normSquared))
S.unsafeFreeze singularValueBuffer
maximumSingularValue :: S.Vector Double -> Double
maximumSingularValue singularValues =
S.foldl' max 0.0 singularValues
firstRankDeficiency :: Double -> S.Vector Double -> Maybe (Int, Double)
firstRankDeficiency !rankTolerance singularValues = go 0
where
go !columnIndex
| columnIndex >= S.length singularValues = Nothing
| otherwise =
let !singularValue = singularValues `S.unsafeIndex` columnIndex
in if singularValue <= rankTolerance
then Just (columnIndex, singularValue)
else go (columnIndex + 1)
projectThinSvdResult ::
Int ->
Int ->
Int ->
Int ->
MutableDenseWork s ->
MutableDenseWork s ->
S.Vector Double ->
ST s ThinSvdResult
projectThinSvdResult !rowCount !columnCount !matrixEntryCount !rightEntryCount leftColumns rightVectors singularValues = do
uBuffer <- SM.new matrixEntryCount
sigmaBuffer <- SM.new columnCount
vtBuffer <- SM.new rightEntryCount
let orderedColumns =
fmap fst
. sortBy (flip (comparing snd))
$ [(columnIndex, singularValues `S.unsafeIndex` columnIndex) | columnIndex <- [0 .. columnCount - 1]]
writeOrderedColumns orderedColumns 0 uBuffer sigmaBuffer vtBuffer
uValues <- S.unsafeFreeze uBuffer
sigmaValues <- S.unsafeFreeze sigmaBuffer
vtValues <- S.unsafeFreeze vtBuffer
pure
ThinSvdResult
{ thinSvdLeftSingularVectors = trustedDenseDoubleMatrixRowMajor rowCount columnCount uValues,
thinSvdSingularValues = sigmaValues,
thinSvdRightSingularVectorsTransposed = trustedDenseDoubleMatrixRowMajor columnCount columnCount vtValues
}
where
writeOrderedColumns orderedColumns !targetColumn uBuffer sigmaBuffer vtBuffer =
case orderedColumns of
[] -> pure ()
sourceColumn : remainingColumns -> do
let !singularValue = singularValues `S.unsafeIndex` sourceColumn
!inverseSingular = 1.0 / singularValue
SM.write sigmaBuffer targetColumn singularValue
forIndex 0 rowCount $ \rowIndex -> do
leftEntry <- readDenseWork leftColumns rowIndex sourceColumn
SM.write uBuffer (rowIndex * columnCount + targetColumn) (leftEntry * inverseSingular)
forIndex 0 columnCount $ \columnIndex -> do
rightEntry <- readDenseWork rightVectors columnIndex sourceColumn
SM.write vtBuffer (targetColumn * columnCount + columnIndex) rightEntry
writeOrderedColumns remainingColumns (targetColumn + 1) uBuffer sigmaBuffer vtBuffer