packages feed

moonlight-linalg-0.1.0.0: src-laws/Moonlight/LinAlg/Effect/Harness/KrylovSpectral.hs

{-# LANGUAGE DataKinds #-}

module Moonlight.LinAlg.Effect.Harness.KrylovSpectral
  ( arnoldiRelationHoldsLaw,
    arnoldiBasisOrthonormalLaw,
    lanczosProjectionTridiagonalLaw,
    lanczosBasisOrthonormalLaw,
    thickRestartLockedPairsResidualBoundedLaw,
    selectedPairsResidualBoundedLaw,
    selectedPairsClusterOrthonormalLaw,
    tridiagonalSelectedValuesAgreeWithAllPairsLaw,
    diagonalSpectralValuesExactLaw,
    pathLaplacianSpectralValuesClosedFormLaw,
    eigenRequestRejectsOversubscriptionLaw,
  )
where

import Data.Bifunctor (first)
import Data.Vector qualified as Box
import Data.Vector.Unboxed qualified as U
import Moonlight.Core (fieldValueValid)
import Moonlight.LinAlg
  ( EigenRequest (..),
    EigenSolveConfig,
    Eigenpairs,
    LinearOperator,
    OperatorSymmetry (..),
    SpectrumEnd (..),
    arnoldi,
    arnoldiBasisColumns,
    arnoldiHessenbergRows,
    defaultArnoldiConfig,
    defaultEigenSolveConfig,
    defaultLanczosConfig,
    diagonalLinearOperator,
    eigenpairCount,
    eigenpairResidualNorms,
    eigenpairValues,
    eigenpairVectorAt,
    lanczosAlphaDiagonal,
    lanczosBasisColumns,
    lanczosBetaOffDiagonal,
    lanczosStepsCompleted,
    lanczosSymmetric,
    mkNonNegativeConfigTolerance,
    mkPositiveCount,
    mkSparseCOO,
    cooToCSR,
    pathLaplacianLinearOperator,
    runOperatorU,
    selfAdjointCSRLinearOperator,
    solveEigenRequest,
    withArnoldiIterations,
    withEigenFallbackInitialVector,
    withEigenFallbackLanczosConfig,
    withLanczosIterations,
    withLanczosTolerance,
  )
import Moonlight.LinAlg.Effect.Harness.Core
  ( approxTolerance,
    assertApproxList,
    assertApproxListWith,
    assertRightProperty,
    orthonormalTolerance,
    residualTolerance,
  )
import Test.Tasty.QuickCheck qualified as QC

arnoldiRelationHoldsLaw :: QC.Property
arnoldiRelationHoldsLaw =
  assertRightProperty $ do
    iterationCount <- mapLeftShow (mkPositiveCount 2)
    operatorValue <- mapLeftShow (diagonalLinearOperator (U.fromList [1.0, 3.0]))
    decomposition <- mapLeftShow (arnoldi (withArnoldiIterations iterationCount defaultArnoldiConfig) operatorValue (U.fromList [1.0, 1.0]))
    relationHolds operatorValue (arnoldiBasisColumns decomposition) (arnoldiHessenbergRows decomposition)

arnoldiBasisOrthonormalLaw :: QC.Property
arnoldiBasisOrthonormalLaw =
  assertRightProperty $ do
    iterationCount <- mapLeftShow (mkPositiveCount 3)
    operatorValue <- mapLeftShow (diagonalLinearOperator (U.fromList [1.0, 2.0, 4.0]))
    decomposition <- mapLeftShow (arnoldi (withArnoldiIterations iterationCount defaultArnoldiConfig) operatorValue (U.fromList [1.0, 1.0, 1.0]))
    pure (orthonormalColumns (arnoldiBasisColumns decomposition))

lanczosProjectionTridiagonalLaw :: QC.Property
lanczosProjectionTridiagonalLaw =
  assertRightProperty $ do
    iterationCount <- mapLeftShow (mkPositiveCount 3)
    operatorValue <- mapLeftShow (pathLaplacianLinearOperator 4)
    decomposition <- mapLeftShow (lanczosSymmetric (withLanczosIterations iterationCount defaultLanczosConfig) operatorValue (U.fromList [1.0, 0.0, 0.0, 0.0]))
    let stepCount = lanczosStepsCompleted decomposition
    pure
      ( stepCount > 0
          && length (lanczosAlphaDiagonal decomposition) == stepCount
          && length (lanczosBetaOffDiagonal decomposition) == max 0 (stepCount - 1)
          && Box.length (lanczosBasisColumns decomposition) == stepCount
      )

lanczosBasisOrthonormalLaw :: QC.Property
lanczosBasisOrthonormalLaw =
  assertRightProperty $ do
    iterationCount <- mapLeftShow (mkPositiveCount 4)
    operatorValue <- mapLeftShow (pathLaplacianLinearOperator 5)
    decomposition <- mapLeftShow (lanczosSymmetric (withLanczosIterations iterationCount defaultLanczosConfig) operatorValue (U.fromList [1.0, 0.5, 0.25, 0.125, 0.0625]))
    pure (orthonormalColumns (lanczosBasisColumns decomposition))

thickRestartLockedPairsResidualBoundedLaw :: QC.Property
thickRestartLockedPairsResidualBoundedLaw =
  assertRightProperty $ do
    countValue <- mapLeftShow (mkPositiveCount 3)
    operatorValue <- genericPentadiagonalOperator 18
    solveConfig <- restartedSolveConfig 5 approxTolerance (restartSeedVector 18)
    pairs <- mapLeftShow (solveEigenRequest solveConfig operatorValue (EigenpairsRequest SmallestEigenvalues countValue))
    pure (eigenpairCount pairs == 3 && eigenpairResidualsBounded pairs)

selectedPairsResidualBoundedLaw :: QC.Property
selectedPairsResidualBoundedLaw =
  assertRightProperty $ do
    countValue <- mapLeftShow (mkPositiveCount 2)
    operatorValue <- tridiagonalOperator [2.0, 2.5, 3.0, 3.5, 4.0] [-0.31, -0.27, -0.23, -0.19]
    pairs <- mapLeftShow (solveEigenRequest defaultEigenSolveConfig operatorValue (EigenpairsRequest SmallestEigenvalues countValue))
    pure (eigenpairCount pairs == 2 && eigenpairResidualsBounded pairs)

selectedPairsClusterOrthonormalLaw :: QC.Property
selectedPairsClusterOrthonormalLaw =
  assertRightProperty $ do
    countValue <- mapLeftShow (mkPositiveCount 3)
    operatorValue <- mapLeftShow (diagonalLinearOperator (U.fromList [2.0, 2.0, 2.0, 5.0]))
    pairs <- mapLeftShow (solveEigenRequest defaultEigenSolveConfig operatorValue (EigenpairsRequest SmallestEigenvalues countValue))
    pure (eigenpairCount pairs == 3 && orthonormalEigenpairs pairs)

tridiagonalSelectedValuesAgreeWithAllPairsLaw :: QC.Property
tridiagonalSelectedValuesAgreeWithAllPairsLaw =
  assertRightProperty $ do
    selectedCount <- mapLeftShow (mkPositiveCount 3)
    fullCount <- mapLeftShow (mkPositiveCount 5)
    operatorValue <- tridiagonalOperator [2.0, 2.5, 3.0, 3.5, 4.0] [-0.31, -0.27, -0.23, -0.19]
    selectedValues <- mapLeftShow (solveEigenRequest defaultEigenSolveConfig operatorValue (EigenvaluesRequest SmallestEigenvalues selectedCount))
    allPairs <- mapLeftShow (solveEigenRequest defaultEigenSolveConfig operatorValue (EigenpairsRequest SmallestEigenvalues fullCount))
    pure (assertApproxListWith residualTolerance (U.toList selectedValues) (take 3 (U.toList (eigenpairValues allPairs))))

diagonalSpectralValuesExactLaw :: QC.Property
diagonalSpectralValuesExactLaw =
  assertRightProperty $ do
    countValue <- mapLeftShow (mkPositiveCount 2)
    operatorValue <- mapLeftShow (diagonalLinearOperator (U.fromList [3.0, -2.0, 7.0, 1.0]))
    values <- mapLeftShow (solveEigenRequest defaultEigenSolveConfig operatorValue (EigenvaluesRequest SmallestEigenvalues countValue))
    pure (U.toList values == [-2.0, 1.0])

pathLaplacianSpectralValuesClosedFormLaw :: QC.Property
pathLaplacianSpectralValuesClosedFormLaw =
  assertRightProperty $ do
    countValue <- mapLeftShow (mkPositiveCount 3)
    operatorValue <- mapLeftShow (pathLaplacianLinearOperator 5)
    values <- mapLeftShow (solveEigenRequest defaultEigenSolveConfig operatorValue (EigenvaluesRequest SmallestEigenvalues countValue))
    pure (assertApproxList (pathLaplacianValues 5 [0, 1, 2]) (U.toList values))

eigenRequestRejectsOversubscriptionLaw :: QC.Property
eigenRequestRejectsOversubscriptionLaw =
  assertRightProperty $ do
    countValue <- mapLeftShow (mkPositiveCount 4)
    operatorValue <- mapLeftShow (diagonalLinearOperator (U.fromList [1.0, 2.0, 3.0]))
    let resultValue = solveEigenRequest defaultEigenSolveConfig operatorValue (EigenvaluesRequest SmallestEigenvalues countValue)
    pure
      ( case resultValue of
          Left _ -> True
          Right _ -> False
      )

relationHolds ::
  LinearOperator symmetry ->
  Box.Vector (U.Vector Double) ->
  Box.Vector (U.Vector Double) ->
  Either String Bool
relationHolds operatorValue basisColumns hessenbergRows =
  let basisValues = Box.toList basisColumns
      hessenbergValues = U.toList <$> Box.toList hessenbergRows
      stepCount = Box.length hessenbergRows - 1
      relationAt columnIndex = do
        basisVector <- maybeToEither ("missing Arnoldi basis column " <> show columnIndex) (entryAt columnIndex basisValues)
        imageVector <- mapLeftShow (runOperatorU operatorValue basisVector)
        coefficients <- traverse (maybeToEither ("missing Arnoldi coefficient at column " <> show columnIndex) . entryAt columnIndex) hessenbergValues
        pure
          ( assertApproxList
              (U.toList imageVector)
              (U.toList (linearCombinationU (take (length basisValues) coefficients) basisValues))
              && assertApproxList [0.0] (drop (length basisValues) coefficients)
          )
   in fmap and (traverse relationAt [0 .. stepCount - 1])

orthonormalColumns :: Box.Vector (U.Vector Double) -> Bool
orthonormalColumns columns =
  and
    [ assertApproxListWith orthonormalTolerance [expectedValue] [vectorDotU leftColumn rightColumn]
      | (leftIndex, leftColumn) <- zip [0 :: Int ..] (Box.toList columns),
        (rightIndex, rightColumn) <- zip [0 :: Int ..] (Box.toList columns),
        leftIndex <= rightIndex,
        let expectedValue = if leftIndex == rightIndex then 1.0 else 0.0
    ]

orthonormalEigenpairs :: Eigenpairs -> Bool
orthonormalEigenpairs pairs =
  case traverse (`eigenpairVectorAt` pairs) [0 .. eigenpairCount pairs - 1] of
    Left _ -> False
    Right columns -> orthonormalColumns (Box.fromList columns)

eigenpairResidualsBounded :: Eigenpairs -> Bool
eigenpairResidualsBounded pairs =
  U.all (\residualNorm -> fieldValueValid residualNorm && residualNorm <= residualTolerance) (eigenpairResidualNorms pairs)

restartedSolveConfig :: Int -> Double -> U.Vector Double -> Either String EigenSolveConfig
restartedSolveConfig iterationLimit toleranceValue seedVector = do
  iterationCount <- mapLeftShow (mkPositiveCount iterationLimit)
  toleranceBound <- mapLeftShow (mkNonNegativeConfigTolerance toleranceValue)
  let lanczosConfig = withLanczosTolerance toleranceBound (withLanczosIterations iterationCount defaultLanczosConfig)
  pure (withEigenFallbackInitialVector seedVector (withEigenFallbackLanczosConfig lanczosConfig defaultEigenSolveConfig))

tridiagonalOperator :: [Double] -> [Double] -> Either String (LinearOperator 'SelfAdjointOperator)
tridiagonalOperator diagonalEntries offDiagonalEntries =
  mapLeftShow
    ( selfAdjointCSRLinearOperator
        =<< (mkSparseCOO (length diagonalEntries) (length diagonalEntries) (tridiagonalEntries diagonalEntries offDiagonalEntries) >>= cooToCSR)
    )

tridiagonalEntries :: [Double] -> [Double] -> [(Int, Int, Double)]
tridiagonalEntries diagonalEntries offDiagonalEntries =
  zipWith (\entryIndex entryValue -> (entryIndex, entryIndex, entryValue)) [0 ..] diagonalEntries
    <> concat
      ( zipWith
          ( \entryIndex entryValue ->
              [(entryIndex, entryIndex + 1, entryValue), (entryIndex + 1, entryIndex, entryValue)]
          )
          [0 ..]
          offDiagonalEntries
      )

genericPentadiagonalOperator :: Int -> Either String (LinearOperator 'SelfAdjointOperator)
genericPentadiagonalOperator dimension =
  mapLeftShow
    ( selfAdjointCSRLinearOperator
        =<< (mkSparseCOO dimension dimension (genericPentadiagonalEntries dimension) >>= cooToCSR)
    )

genericPentadiagonalEntries :: Int -> [(Int, Int, Double)]
genericPentadiagonalEntries dimension =
  diagonalEntries <> firstOffDiagonalEntries <> secondOffDiagonalEntries
  where
    diagonalEntries =
      (\rowIndex -> (rowIndex, rowIndex, 4.0 + 0.03 * fromIntegral (rowIndex `mod` 7)))
        <$> [0 .. dimension - 1]
    firstOffDiagonalEntries =
      symmetricBandEntries dimension 1 (\rowIndex -> -1.0 - 0.01 * fromIntegral (rowIndex `mod` 5))
    secondOffDiagonalEntries =
      symmetricBandEntries dimension 2 (\rowIndex -> -0.2 - 0.005 * fromIntegral (rowIndex `mod` 3))

symmetricBandEntries :: Int -> Int -> (Int -> Double) -> [(Int, Int, Double)]
symmetricBandEntries dimension offset entryValueAt =
  concat
    ( ( \rowIndex ->
          let columnIndex = rowIndex + offset
              entryValue = entryValueAt rowIndex
           in [(rowIndex, columnIndex, entryValue), (columnIndex, rowIndex, entryValue)]
      )
        <$> [0 .. dimension - offset - 1]
    )

restartSeedVector :: Int -> U.Vector Double
restartSeedVector dimension =
  U.generate dimension (\indexValue -> 1.0 / fromIntegral (indexValue + 1))

pathLaplacianValues :: Int -> [Int] -> [Double]
pathLaplacianValues dimension =
  fmap (\modeIndex -> 2.0 - 2.0 * cos (pi * fromIntegral modeIndex / fromIntegral dimension))

linearCombinationU :: [Double] -> [U.Vector Double] -> U.Vector Double
linearCombinationU coefficients basisVectors =
  case basisVectors of
    [] -> U.empty
    firstVector : _ ->
      foldr (U.zipWith (+)) (U.replicate (U.length firstVector) 0.0) (zipWith (\coefficient vectorValue -> U.map (* coefficient) vectorValue) coefficients basisVectors)

vectorDotU :: U.Vector Double -> U.Vector Double -> Double
vectorDotU leftVector rightVector =
  U.sum (U.zipWith (*) leftVector rightVector)

entryAt :: Int -> [value] -> Maybe value
entryAt targetIndex values =
  case drop targetIndex values of
    entryValue : _ -> Just entryValue
    [] -> Nothing

maybeToEither :: failure -> Maybe value -> Either failure value
maybeToEither failureValue value =
  case value of
    Just presentValue -> Right presentValue
    Nothing -> Left failureValue

mapLeftShow :: Show failure => Either failure value -> Either String value
mapLeftShow = first show