moonlight-linalg-0.1.0.0: test/dense/DenseFlatSpec.hs
{-# LANGUAGE RecordWildCards #-}
module DenseFlatSpec (tests) where
import Data.Vector.Storable qualified as S
import Data.Vector.Unboxed qualified as U
import Moonlight.Core (MoonlightError (..))
import Moonlight.LinAlg.Internal.Eigen.Residual (ResidualReport (..))
import Moonlight.LinAlg.Internal.Eigen.Symmetric
( CertifiedSymmetricEigenResult (..),
certifySymmetricEigenResult,
symmetricEigenPairsDenseUnchecked,
)
import Moonlight.LinAlg.Pure.Dense.Flat (trustedDenseDoubleMatrixRowMajor)
import Moonlight.LinAlg.Dense
( denseDoubleMatrixShape,
denseDoubleMatrixToRows,
denseDoubleMatrixVectorProduct,
mkDenseDoubleMatrixRowMajor,
mkDenseDoubleMatrixRows,
)
import Moonlight.LinAlg.Native
( denseDoubleLinearSolveLapack,
denseDoubleMatrixProductBlas,
denseDoubleSymmetricEigenpairsLapack,
)
import Moonlight.LinAlg.Spectral
( eigenpairCount,
eigenpairResidualNorms,
eigenpairValues,
)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, testCase)
import Prelude
tests :: TestTree
tests =
testGroup
"Dense flat Double matrix"
[ testCase "row-major constructor rejects invalid payload length" $
assertEqual
"shape error"
(Left (InvariantViolation "dense Double row-major payload length mismatch: expected 6 values but received 5"))
(mkDenseDoubleMatrixRowMajor 2 3 (S.fromList [1.0 .. 5.0])),
testCase "row-major constructor rejects non-finite payloads" $
assertEqual
"finite payload"
(Left (InvariantViolation "dense Double row-major payload requires finite entries"))
(mkDenseDoubleMatrixRowMajor 1 1 (S.fromList [0 / 0])),
testCase "row-major constructor rejects wrapped shape cardinality" $
let wrappedDimension = 2 ^ (32 :: Int)
in assertEqual
"wrapped shape"
(Left (InvariantViolation "dense Double matrix dimensions exceed Int cardinality"))
(mkDenseDoubleMatrixRowMajor wrappedDimension wrappedDimension S.empty),
testCase "row constructor preserves rectangular shape and projection" $
assertEqual
"rows"
(Right ((2, 3), [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]))
(fmap (\matrixValue -> (denseDoubleMatrixShape matrixValue, denseDoubleMatrixToRows matrixValue)) (mkDenseDoubleMatrixRows [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])),
testCase "matrix/vector product uses validated flat storage" $
assertEqual
"matvec"
(Right (S.fromList [140.0, 320.0]))
( do
matrixValue <- mkDenseDoubleMatrixRowMajor 2 3 (S.fromList [1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
denseDoubleMatrixVectorProduct matrixValue (S.fromList [10.0, 20.0, 30.0])
),
testCase "matrix/vector product rejects vector shape mismatch" $
assertEqual
"shape error"
(Left (InvariantViolation "dense Double matrix/vector shape mismatch (matrix=(2,3), vector=2)"))
( do
matrixValue <- mkDenseDoubleMatrixRowMajor 2 3 (S.fromList [1.0 .. 6.0])
denseDoubleMatrixVectorProduct matrixValue (S.fromList [10.0, 20.0])
),
testCase "BLAS matrix product matches reference rows" testDenseDoubleMatrixProductBlas,
testCase "LAPACK dense solve matches reference solution" testDenseDoubleLinearSolveLapack,
testCase "LAPACK dense solve rejects singular input" testDenseDoubleLinearSolveSingular,
testCase "LAPACK dense symmetric eigenpairs are residual certified" testDenseDoubleSymmetricEigenpairsLapack,
testCase "pure dense symmetric eigen certification is explicit" testPureDenseSymmetricEigenCertification
, testCase "pure dense symmetric eigen rejects oversized workspace cardinality" testPureDenseSymmetricEigenWorkspaceOverflow
]
testDenseDoubleMatrixProductBlas :: Assertion
testDenseDoubleMatrixProductBlas = do
productResult <-
case (mkDenseDoubleMatrixRowMajor 2 3 (S.fromList [1.0 .. 6.0]), mkDenseDoubleMatrixRowMajor 3 2 (S.fromList [7.0 .. 12.0])) of
(Right leftMatrix, Right rightMatrix) ->
denseDoubleMatrixProductBlas leftMatrix rightMatrix
(Left err, _) -> pure (Left err)
(_, Left err) -> pure (Left err)
assertEqual
"matrix product rows"
(Right [[58.0, 64.0], [139.0, 154.0]])
(denseDoubleMatrixToRows <$> productResult)
testDenseDoubleLinearSolveLapack :: Assertion
testDenseDoubleLinearSolveLapack = do
solveResult <-
case mkDenseDoubleMatrixRowMajor 2 2 (S.fromList [3.0, 1.0, 1.0, 2.0]) of
Left err -> pure (Left err)
Right matrixValue ->
denseDoubleLinearSolveLapack matrixValue (S.fromList [9.0, 8.0])
assertApproxStorableVector "solution" 1.0e-10 (S.fromList [2.0, 3.0]) solveResult
testDenseDoubleLinearSolveSingular :: Assertion
testDenseDoubleLinearSolveSingular = do
solveResult <-
case mkDenseDoubleMatrixRowMajor 2 2 (S.fromList [1.0, 2.0, 2.0, 4.0]) of
Left err -> pure (Left err)
Right matrixValue ->
denseDoubleLinearSolveLapack matrixValue (S.fromList [1.0, 2.0])
assertEqual
"singular solve"
(Left (InvariantViolation "LAPACK DGESV detected exact singularity at U diagonal 2"))
solveResult
testDenseDoubleSymmetricEigenpairsLapack :: Assertion
testDenseDoubleSymmetricEigenpairsLapack = do
eigenResult <-
case mkDenseDoubleMatrixRowMajor 2 2 (S.fromList [2.0, 0.0, 0.0, 3.0]) of
Left err -> pure (Left err)
Right matrixValue -> denseDoubleSymmetricEigenpairsLapack matrixValue
case eigenResult of
Left err -> assertEqual "eigen success" (Right ()) (Left err)
Right pairs -> do
assertEqual "eigenpair count" 2 (eigenpairCount pairs)
assertApproxUnboxedVector "eigenvalues" 1.0e-10 (U.fromList [2.0, 3.0]) (Right (eigenpairValues pairs))
assertBool
"residuals stay certified"
(U.all (<= 1.0e-10) (eigenpairResidualNorms pairs))
testPureDenseSymmetricEigenCertification :: Assertion
testPureDenseSymmetricEigenCertification = do
let resultValue = do
matrixValue <- mkDenseDoubleMatrixRowMajor 2 2 (S.fromList [2.0, 0.0, 0.0, 3.0])
eigenResult <- symmetricEigenPairsDenseUnchecked 2 matrixValue
case certifySymmetricEigenResult matrixValue eigenResult of
Left err -> Left (InvariantViolation ("unexpected eigen certification failure: " <> show err))
Right certified -> Right certified
case resultValue of
Left err -> assertEqual "certification success" (Right ()) (Left err)
Right CertifiedSymmetricEigenResult {certifiedSymmetricEigenResidualReport = ResidualReport {..}} -> do
assertBool "residual scale stays certified" (residualScaled <= 1.0e7)
assertBool "orthogonality scale stays certified" (residualOrthogonalityScaled <= 1.0e7)
testPureDenseSymmetricEigenWorkspaceOverflow :: Assertion
testPureDenseSymmetricEigenWorkspaceOverflow =
let wrappedDimension = 2 ^ (32 :: Int)
in assertEqual
"oversized eigensolver workspace"
(Left (InvariantViolation "symmetric eigen workspace cardinality exceeds Int range"))
( symmetricEigenPairsDenseUnchecked
wrappedDimension
(trustedDenseDoubleMatrixRowMajor wrappedDimension wrappedDimension S.empty)
)
assertApproxStorableVector :: String -> Double -> S.Vector Double -> Either MoonlightError (S.Vector Double) -> Assertion
assertApproxStorableVector label tolerance expected actualResult =
case actualResult of
Left err -> assertEqual label (Right expected) (Left err)
Right actual ->
assertBool
label
( S.length expected == S.length actual
&& S.and (S.zipWith (\left right -> abs (left - right) <= tolerance) expected actual)
)
assertApproxUnboxedVector :: String -> Double -> U.Vector Double -> Either MoonlightError (U.Vector Double) -> Assertion
assertApproxUnboxedVector label tolerance expected actualResult =
case actualResult of
Left err -> assertEqual label (Right expected) (Left err)
Right actual ->
assertBool
label
( U.length expected == U.length actual
&& U.and (U.zipWith (\left right -> abs (left - right) <= tolerance) expected actual)
)