packages feed

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

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

module Moonlight.LinAlg.Internal.Eigen.Residual
  ( ResidualReport (..),
    residualReportPassesSymmetricEigenLimits,
    symmetricEigenResidual,
  )
where

import Data.Vector.Storable qualified as S
import Moonlight.LinAlg.Internal.Eigen.Kernels (epsDouble)
import Moonlight.LinAlg.Pure.Dense.Flat
  ( DenseDoubleMatrix,
    denseDoubleMatrixShape,
    denseDoubleMatrixToRowMajorVector,
  )
import Prelude

data ResidualReport = ResidualReport
  { residualMatrixNorm :: !Double,
    residualFrobenius :: !Double,
    residualOrthogonality :: !Double,
    residualScaled :: !Double,
    residualOrthogonalityScaled :: !Double
  }
  deriving stock (Eq, Show)

residualReportPassesSymmetricEigenLimits :: ResidualReport -> Bool
residualReportPassesSymmetricEigenLimits report =
  residualScaled report <= 1.0e7
    && residualOrthogonalityScaled report <= 1.0e7

symmetricEigenResidual :: DenseDoubleMatrix -> S.Vector Double -> DenseDoubleMatrix -> ResidualReport
symmetricEigenResidual matrixValue eigenvalues eigenvectors =
  let !(matrixSize, _) = denseDoubleMatrixShape matrixValue
      !matrixNorm = frobeniusNorm matrixValue
      !residualNorm = residualFrobeniusNorm matrixValue eigenvalues eigenvectors
      !orthogonalityNorm = orthogonalityFrobeniusNorm eigenvectors
      !dimensionScale = max 1.0 (fromIntegral matrixSize)
      !residualDenominator = max 1.0 matrixNorm * dimensionScale * epsDouble
      !orthogonalityDenominator = dimensionScale * epsDouble
   in ResidualReport
        { residualMatrixNorm = matrixNorm,
          residualFrobenius = residualNorm,
          residualOrthogonality = orthogonalityNorm,
          residualScaled = residualNorm / residualDenominator,
          residualOrthogonalityScaled = orthogonalityNorm / orthogonalityDenominator
        }

frobeniusNorm :: DenseDoubleMatrix -> Double
frobeniusNorm matrixValue =
  S.foldl' accumulateScaledNorm scaledNormZero (denseDoubleMatrixToRowMajorVector matrixValue)
    |> scaledNormValue

residualFrobeniusNorm :: DenseDoubleMatrix -> S.Vector Double -> DenseDoubleMatrix -> Double
residualFrobeniusNorm matrixValue eigenvalues eigenvectors =
  residualColumn 0 scaledNormZero |> scaledNormValue
  where
    !(matrixSize, _) = denseDoubleMatrixShape matrixValue
    matrixPayload = denseDoubleMatrixToRowMajorVector matrixValue
    eigenvectorPayload = denseDoubleMatrixToRowMajorVector eigenvectors

    residualColumn !columnIndex !normState
      | columnIndex >= matrixSize = normState
      | otherwise =
          residualRow columnIndex 0 normState
            |> residualColumn (columnIndex + 1)

    residualRow !columnIndex !rowIndex !normState
      | rowIndex >= matrixSize = normState
      | otherwise =
          let !lambdaValue = eigenvalues `S.unsafeIndex` columnIndex
              !vectorEntry = eigenvectorAt rowIndex columnIndex
              !residualEntry = matrixVectorEntry rowIndex columnIndex - lambdaValue * vectorEntry
           in residualRow columnIndex (rowIndex + 1) (accumulateScaledNorm normState residualEntry)

    matrixVectorEntry !rowIndex !columnIndex =
      dotAt 0 0.0
      where
        !rowOffset = rowIndex * matrixSize

        dotAt !entryIndex !accumulator
          | entryIndex >= matrixSize = accumulator
          | otherwise =
              let !matrixEntry = matrixPayload `S.unsafeIndex` (rowOffset + entryIndex)
                  !vectorEntry = eigenvectorAt entryIndex columnIndex
               in dotAt (entryIndex + 1) (accumulator + matrixEntry * vectorEntry)

    eigenvectorAt !rowIndex !columnIndex =
      eigenvectorPayload `S.unsafeIndex` (rowIndex * matrixSize + columnIndex)

orthogonalityFrobeniusNorm :: DenseDoubleMatrix -> Double
orthogonalityFrobeniusNorm eigenvectors =
  orthogonalityColumn 0 scaledNormZero |> scaledNormValue
  where
    !(matrixSize, _) = denseDoubleMatrixShape eigenvectors
    eigenvectorPayload = denseDoubleMatrixToRowMajorVector eigenvectors

    orthogonalityColumn !leftColumn !normState
      | leftColumn >= matrixSize = normState
      | otherwise =
          orthogonalityPair leftColumn leftColumn normState
            |> orthogonalityColumn (leftColumn + 1)

    orthogonalityPair !leftColumn !rightColumn !normState
      | rightColumn >= matrixSize = normState
      | otherwise =
          let !targetValue = if leftColumn == rightColumn then 1.0 else 0.0
              !weightValue = if leftColumn == rightColumn then 1.0 else sqrt 2.0
              !entryValue = weightValue * (columnDot leftColumn rightColumn - targetValue)
           in orthogonalityPair leftColumn (rightColumn + 1) (accumulateScaledNorm normState entryValue)

    columnDot !leftColumn !rightColumn = go 0 0.0
      where
        go !rowIndex !accumulator
          | rowIndex >= matrixSize = accumulator
          | otherwise =
              let !leftValue = eigenvectorAt rowIndex leftColumn
                  !rightValue = eigenvectorAt rowIndex rightColumn
               in go (rowIndex + 1) (accumulator + leftValue * rightValue)

    eigenvectorAt !rowIndex !columnIndex =
      eigenvectorPayload `S.unsafeIndex` (rowIndex * matrixSize + columnIndex)

data ScaledNorm = ScaledNorm !Double !Double

scaledNormZero :: ScaledNorm
scaledNormZero = ScaledNorm 0.0 1.0

accumulateScaledNorm :: ScaledNorm -> Double -> ScaledNorm
accumulateScaledNorm (ScaledNorm !scaleValue !scaledSum) !entryValue =
  let !entryAbs = abs entryValue
   in if entryAbs == 0.0
        then ScaledNorm scaleValue scaledSum
        else
          if scaleValue < entryAbs
            then
              let !scaledRatio = scaleValue / entryAbs
               in ScaledNorm entryAbs (1.0 + scaledSum * scaledRatio * scaledRatio)
            else
              let !scaledRatio = entryAbs / scaleValue
               in ScaledNorm scaleValue (scaledSum + scaledRatio * scaledRatio)
{-# INLINE accumulateScaledNorm #-}

scaledNormValue :: ScaledNorm -> Double
scaledNormValue (ScaledNorm !scaleValue !scaledSum) =
  if scaleValue == 0.0
    then 0.0
    else scaleValue * sqrt scaledSum
{-# INLINE scaledNormValue #-}

(|>) :: a -> (a -> b) -> b
(|>) value function = function value
{-# INLINE (|>) #-}