packages feed

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

module Moonlight.LinAlg.Effect.Harness.Preconditioner
  ( ic0FactorSolveRoundTripLaw,
    ic0RejectsNonpositivePivotLaw,
    preconditionedCgConvergesOnSpdLaw,
  )
where

import Data.Vector.Unboxed qualified as U
import Moonlight.Core (MoonlightError)
import Moonlight.LinAlg
  ( IC0Config (..),
    SparseConjugateGradientConfig (..),
    SparseCSR,
    SparseIterativeFailure (..),
    SparsePreconditionerFamily (..),
    SparseIterativeResult (..),
    cooToCSR,
    csrMatVecVector,
    mkSparseCOO,
    solveSparseCG,
  )
import Moonlight.LinAlg.Effect.Harness.Core
  ( approxTolerance,
    assertApproxList,
    assertRightProperty,
    vectorNorm,
  )
import Test.Tasty.QuickCheck qualified as QC

newtype PositiveDiagonal3 = PositiveDiagonal3 [Double]
  deriving stock (Eq, Show)

newtype RightHandSide3 = RightHandSide3 [Double]
  deriving stock (Eq, Show)

instance QC.Arbitrary PositiveDiagonal3 where
  arbitrary =
    PositiveDiagonal3
      <$> QC.vectorOf 3 (fromIntegral <$> QC.chooseInt (1, 9))

instance QC.Arbitrary RightHandSide3 where
  arbitrary =
    RightHandSide3
      <$> QC.vectorOf 3 (fromIntegral <$> QC.chooseInt (-8, 8))

ic0FactorSolveRoundTripLaw :: QC.Property
ic0FactorSolveRoundTripLaw =
  QC.property ic0FactorSolveRoundTripLawProperty

ic0RejectsNonpositivePivotLaw :: QC.Property
ic0RejectsNonpositivePivotLaw =
  assertRightProperty $ do
    matrixValue <-
      mkSparseCOO
        2
        2
        [(0, 0, 1.0), (0, 1, 2.0), (1, 0, 2.0), (1, 1, 1.0)]
        >>= cooToCSR
    let resultValue =
          solveSparseCG
            (cgConfigWith 8 (IncompleteCholesky0SparsePreconditionerFamily (IC0Config Nothing)))
            matrixValue
            (U.fromList [1.0, 1.0])
            (U.fromList [0.0, 0.0])
    pure
      ( case resultValue of
          Left (SparseNonpositivePivot _ _) -> True
          _ -> False
      )

preconditionedCgConvergesOnSpdLaw :: QC.Property
preconditionedCgConvergesOnSpdLaw =
  assertRightProperty $ do
    matrixValue <- anchoredPathLaplacian 16
    let rhsValues = anchoredPathRightHandSide 16
        resultValue =
          solveSparseCG
            (cgConfigWith 128 (IncompleteCholesky0SparsePreconditionerFamily (IC0Config Nothing)))
            matrixValue
            rhsValues
            (U.replicate 16 0.0)
    pure
      ( case resultValue of
          Right sparseResult ->
            sparseResidualNorm sparseResult <= scgcTolerance (cgConfigWith 128 (IncompleteCholesky0SparsePreconditionerFamily (IC0Config Nothing)))
              && trueResidualNorm matrixValue rhsValues (sparseSolution sparseResult) <= scgcTolerance (cgConfigWith 128 (IncompleteCholesky0SparsePreconditionerFamily (IC0Config Nothing)))
          Left _ -> False
      )

ic0FactorSolveRoundTripLawProperty :: PositiveDiagonal3 -> RightHandSide3 -> QC.Property
ic0FactorSolveRoundTripLawProperty (PositiveDiagonal3 diagonalEntries) (RightHandSide3 rhsEntries) =
  assertRightProperty $ do
    matrixValue <- diagonalMatrix3 diagonalEntries
    let rhsValues = U.fromList rhsEntries
        expectedSolution = zipWith (/) rhsEntries diagonalEntries
        resultValue =
          solveSparseCG
            (cgConfigWith 8 (IncompleteCholesky0SparsePreconditionerFamily (IC0Config Nothing)))
            matrixValue
            rhsValues
            (U.replicate 3 0.0)
    pure
      ( case resultValue of
          Right sparseResult ->
            assertApproxList expectedSolution (U.toList (sparseSolution sparseResult))
              && trueResidualNorm matrixValue rhsValues (sparseSolution sparseResult) <= scgcTolerance (cgConfigWith 8 (IncompleteCholesky0SparsePreconditionerFamily (IC0Config Nothing)))
          Left _ -> False
      )

cgConfigWith :: Int -> SparsePreconditionerFamily -> SparseConjugateGradientConfig
cgConfigWith iterationLimit preconditionerFamily =
  SparseConjugateGradientConfig
    { scgcTolerance = approxTolerance,
      scgcIterationLimit = iterationLimit,
      scgcPreconditionerFamily = preconditionerFamily
    }

diagonalMatrix3 :: [Double] -> Either MoonlightError (SparseCSR Double)
diagonalMatrix3 diagonalEntries =
  mkSparseCOO
    3
    3
    (zipWith (\entryIndex entryValue -> (entryIndex, entryIndex, entryValue)) [0 ..] diagonalEntries)
    >>= cooToCSR

anchoredPathLaplacian :: Int -> Either MoonlightError (SparseCSR Double)
anchoredPathLaplacian dimension =
  mkSparseCOO dimension dimension ((0, 0, 1.0) : concatMap anchoredPathEdgeEntries [0 .. dimension - 2])
    >>= cooToCSR

anchoredPathEdgeEntries :: Int -> [(Int, Int, Double)]
anchoredPathEdgeEntries leftIndex =
  let rightIndex = leftIndex + 1
   in [ (leftIndex, leftIndex, 1.0),
        (leftIndex, rightIndex, -1.0),
        (rightIndex, leftIndex, -1.0),
        (rightIndex, rightIndex, 1.0)
      ]

anchoredPathRightHandSide :: Int -> U.Vector Double
anchoredPathRightHandSide dimension =
  U.generate
    dimension
    ( \indexValue ->
        let entryPhase = fromIntegral (indexValue + 1)
            entrySkew = fromIntegral ((indexValue * 7) `mod` 11)
         in 1.0 + sin entryPhase + 0.125 * entrySkew
    )

trueResidualNorm :: SparseCSR Double -> U.Vector Double -> U.Vector Double -> Double
trueResidualNorm matrixValue rhsValues solutionValues =
  either
    (const (1.0 / 0.0))
    (\productValues -> vectorNorm (U.toList (U.zipWith (-) productValues rhsValues)))
    (csrMatVecVector matrixValue solutionValues)