packages feed

linear-massiv-0.1.0.0: test/Test/Solve.hs

{-# LANGUAGE AllowAmbiguousTypes #-}

module Test.Solve (solveTests) where

import Test.Tasty
import Test.Tasty.QuickCheck
import Test.Tasty.HUnit
import qualified Data.Massiv.Array as M

import Numeric.LinearAlgebra.Massiv.Types
import Numeric.LinearAlgebra.Massiv.Internal
import Numeric.LinearAlgebra.Massiv.BLAS.Level2 (matvec)
import Numeric.LinearAlgebra.Massiv.BLAS.Level3 (matMul, transpose)
import Numeric.LinearAlgebra.Massiv.Solve.Triangular
import Numeric.LinearAlgebra.Massiv.Solve.LU
import Numeric.LinearAlgebra.Massiv.Solve.Cholesky
import Numeric.LinearAlgebra.Massiv.Solve.Banded (tridiagSolve)
import Numeric.LinearAlgebra.Massiv.Eigen.Symmetric (symmetricEigen)
import Test.Types
import Test.Residuals

solveTests :: TestTree
solveTests = testGroup "Solve"
  [ testGroup "Triangular"
    [ testProperty "forward sub: Lx = b roundtrip" prop_forwardSubRoundtrip
    , testProperty "back sub: Ux = b roundtrip" prop_backSubRoundtrip
    ]
  , testGroup "LU"
    [ testProperty "PA = LU reconstruction" prop_luReconstruction
    , testProperty "luSolve: Ax = b roundtrip" prop_luSolveRoundtrip
    , testCase "3x3 LU known" test_luKnown
    ]
  , testGroup "Cholesky"
    [ testProperty "A = GGᵀ reconstruction" prop_choleskyReconstruction
    , testProperty "choleskySolve roundtrip" prop_choleskySolveRoundtrip
    ]
  , testGroup "Tridiagonal"
    [ testCase "tridiag solve known" test_tridiagKnown
    ]
  , testGroup "Ill-conditioned"
    [ testCase "LU on Hilbert 5×5" test_luHilbert
    , testProperty "Cholesky near-singular SPD" prop_choleskyNearSingular
    ]
  , testGroup "Randsvd"
    [ testProperty "luSolve with randsvd matrix" prop_luSolveRandsvd
    , testProperty "choleskySolve with randsvd SPD" prop_choleskySolveRandsvd
    ]
  , testGroup "Scaled Residuals"
    [ testProperty "LU residual bound 3×3" prop_luResidualBound
    , testProperty "Cholesky residual bound 3×3" prop_choleskyResidualBound
    ]
  , testGroup "Cross-module"
    [ testCase "det ≈ product of eigenvalues" test_detEqualsEigenProduct
    ]
  , testGroup "Larger Sizes"
    [ testProperty "LU reconstruction 5×5" prop_luReconstruction5
    , testProperty "Cholesky reconstruction 5×5" prop_choleskyReconstruction5
    , testProperty "luSolve 10×10" prop_luSolve10
    ]
  ]

-- Triangular tests

prop_forwardSubRoundtrip :: Property
prop_forwardSubRoundtrip = forAll ((,) <$> genLowerTriangular @3 <*> genVector @3) $ \(l, b) ->
  let x = forwardSub l b
      b' = matvec l x
  in vecApproxEq @3 b b'

prop_backSubRoundtrip :: Property
prop_backSubRoundtrip = forAll ((,) <$> genUpperTriangular @3 <*> genVector @3) $ \(u, b) ->
  let x = backSub u b
      b' = matvec u x
  in vecApproxEq @3 b b'

-- LU tests

prop_luReconstruction :: Property
prop_luReconstruction = forAll (genMatrix @3 @3) $ \a ->
  let (luMat, pivArr) = lu a
      nn = 3 :: Int
      l = makeMatrix @3 @3 @M.P $ \i j ->
        if i == j then 1
        else if i > j then luMat ! (i, j)
        else 0 :: Double
      u = makeMatrix @3 @3 @M.P $ \i j ->
        if i <= j then luMat ! (i, j)
        else 0 :: Double
      lu_ = matMul l u
      -- PA
      pa = makeMatrix @3 @3 @M.P $ \i j ->
        a ! (M.index' pivArr i, j)
  in matApproxEq @3 @3 pa lu_

prop_luSolveRoundtrip :: Property
prop_luSolveRoundtrip = forAll ((,) <$> genMatrix @3 @3 <*> genVector @3) $ \(a, b) ->
  -- Only test for non-singular matrices (skip near-singular)
  let (_, pivArr) = lu a
      luMat = fst (lu a)
      -- Check diagonal of U
      diagOk = all (\i -> abs (luMat ! (i, i)) > 1e-6) [0..2]
  in diagOk ==>
    let x = luSolve a b
        b' = matvec a x
    in vecApproxEq @3 b b'

test_luKnown :: Assertion
test_luKnown = do
  -- A = [[2,1,1],[4,3,3],[8,7,9]]
  let a = makeMatrix @3 @3 @M.P $ \i j -> case (i,j) of
            (0,0) -> 2; (0,1) -> 1; (0,2) -> 1
            (1,0) -> 4; (1,1) -> 3; (1,2) -> 3
            (2,0) -> 8; (2,1) -> 7; (2,2) -> 9
            _ -> 0 :: Double
      b = makeVector @3 @M.P $ \i -> case i of
            0 -> 1; 1 -> 1; _ -> 1 :: Double
      x = luSolve a b
      b' = matvec a x
  assertBool "LU solve roundtrip" $ vecApproxEq @3 b b'

-- Cholesky tests

prop_choleskyReconstruction :: Property
prop_choleskyReconstruction = forAll (genSPDMatrix @3) $ \a ->
  let g = cholesky a
      gt = transpose g
      ggt = matMul g gt
  in matApproxEq @3 @3 a ggt

prop_choleskySolveRoundtrip :: Property
prop_choleskySolveRoundtrip = forAll ((,) <$> genSPDMatrix @3 <*> genVector @3) $ \(a, b) ->
  let x = choleskySolve a b
      b' = matvec a x
  in vecApproxEq @3 b b'

-- Tridiagonal test

test_tridiagKnown :: Assertion
test_tridiagKnown = do
  -- Tridiagonal SPD: A = [[2,-1,0],[-1,2,-1],[0,-1,2]]
  -- b = [1, 0, 1]
  -- x should be [1, 1, 1]
  let diag_ = makeVector @3 @M.P $ \_ -> 2 :: Double
      supdiag = makeVector @3 @M.P $ \i -> if i < 2 then -1 else 0 :: Double
      b = makeVector @3 @M.P $ \i -> case i of { 0 -> 1; 1 -> 0; _ -> 1 } :: Double
      x = tridiagSolve diag_ supdiag b
      -- Verify Ax = b manually: 2*1 + (-1)*1 = 1, (-1)*1 + 2*1 + (-1)*1 = 0, (-1)*1 + 2*1 = 1
  assertBool "x(0) ~ 1" $ (x !. 0) ~= 1
  assertBool "x(1) ~ 1" $ (x !. 1) ~= 1
  assertBool "x(2) ~ 1" $ (x !. 2) ~= 1

------------------------------------------------------------------------
-- Ill-conditioned tests
------------------------------------------------------------------------

-- | Test 1: LU solve on the 5×5 Hilbert matrix (very ill-conditioned).
test_luHilbert :: Assertion
test_luHilbert = do
  let h = hilbertMatrix @5
      x = makeVector @5 @M.P $ \i -> fromIntegral (i + 1)
      b = matvec h x
      x' = luSolve h b
      residual = scaledResidualLinear @5 h x' b
  assertBool ("LU Hilbert residual too large: " ++ show residual)
    (residual < 1e-4)

-- | Test 2: Cholesky on near-singular SPD matrix (cond = 1e4).
prop_choleskyNearSingular :: Property
prop_choleskyNearSingular = withMaxSuccess 20 $
  forAll (genSPDMatrixWithCond @5 1e4) $ \a ->
    let g = cholesky a
        residual = scaledResidualCholesky @5 a g
    in counterexample ("Cholesky residual = " ++ show residual) $
       residual < 1000

------------------------------------------------------------------------
-- Randsvd tests
------------------------------------------------------------------------

-- | Test 3: LU solve with a randsvd matrix (cond = 100).
prop_luSolveRandsvd :: Property
prop_luSolveRandsvd = withMaxSuccess 30 $
  forAll ((,) <$> genMatrixWithCond @5 @5 100.0 <*> genVector @5) $ \(a, b) ->
    let x = luSolve a b
        residual = scaledResidualLinear @5 a x b
    in counterexample ("LU randsvd residual = " ++ show residual) $
       residual < 0.01

-- | Test 4: Cholesky solve with a randsvd SPD matrix (cond = 100).
prop_choleskySolveRandsvd :: Property
prop_choleskySolveRandsvd = withMaxSuccess 20 $
  forAll ((,) <$> genSPDMatrixWithCond @5 100.0 <*> genVector @5) $ \(a, b) ->
    let x = choleskySolve a b
        residual = scaledResidualLinear @5 a x b
    in counterexample ("Cholesky randsvd residual = " ++ show residual) $
       residual < 0.01

------------------------------------------------------------------------
-- Scaled residual bound tests
------------------------------------------------------------------------

-- | Test 5: LU residual bound for well-conditioned 3×3 random matrices.
prop_luResidualBound :: Property
prop_luResidualBound = forAll ((,) <$> genMatrix @3 @3 <*> genVector @3) $ \(a, b) ->
  let (luMat, _) = lu a
      diagOk = all (\i -> abs (luMat ! (i, i)) > 1e-6) [0..2]
  in diagOk ==>
    let x = luSolve a b
        residual = scaledResidualLinear @3 a x b
    in counterexample ("LU residual = " ++ show residual) $
       residual < 1e-6

-- | Test 6: Cholesky residual bound for well-conditioned 3×3 SPD matrices.
prop_choleskyResidualBound :: Property
prop_choleskyResidualBound = forAll ((,) <$> genSPDMatrix @3 <*> genVector @3) $ \(a, b) ->
  let x = choleskySolve a b
      residual = scaledResidualLinear @3 a x b
  in counterexample ("Cholesky residual = " ++ show residual) $
     residual < 1e-6

------------------------------------------------------------------------
-- Cross-module tests
------------------------------------------------------------------------

-- | Test 7: Determinant equals the product of eigenvalues for a known SPD matrix.
--
-- We use a diagonal-dominant matrix: diag(4,3,2,1) + 0.1*ones(4,4).
test_detEqualsEigenProduct :: Assertion
test_detEqualsEigenProduct = do
  let a = makeMatrix @4 @4 @M.P $ \i j ->
            let diag_ = case i of { 0 -> 4; 1 -> 3; 2 -> 2; _ -> 1 } :: Double
            in (if i == j then diag_ else 0) + 0.1
      d = det a
      (eigenvals, _) = symmetricEigen a 200 1e-12
      eigenProd = product [ eigenvals !. i | i <- [0..3] ]
      relErr = abs (d - eigenProd) / (abs d + 1e-15)
  assertBool ("det vs eigenproduct relative error too large: " ++ show relErr)
    (relErr < 1e-4)

------------------------------------------------------------------------
-- Larger-size tests
------------------------------------------------------------------------

-- | Test 8: LU reconstruction at 5×5 (like existing 3×3 test).
prop_luReconstruction5 :: Property
prop_luReconstruction5 = forAll (genMatrix @5 @5) $ \a ->
  let (luMat, pivArr) = lu a
      l = makeMatrix @5 @5 @M.P $ \i j ->
        if i == j then 1
        else if i > j then luMat ! (i, j)
        else 0 :: Double
      u = makeMatrix @5 @5 @M.P $ \i j ->
        if i <= j then luMat ! (i, j)
        else 0 :: Double
      lu_ = matMul l u
      pa = makeMatrix @5 @5 @M.P $ \i j ->
        a ! (M.index' pivArr i, j)
  in matApproxEq @5 @5 pa lu_

-- | Test 9: Cholesky reconstruction at 5×5 (like existing 3×3 test).
prop_choleskyReconstruction5 :: Property
prop_choleskyReconstruction5 = forAll (genSPDMatrix @5) $ \a ->
  let g = cholesky a
      gt = transpose g
      ggt = matMul g gt
  in matApproxEq @5 @5 a ggt

-- | Test 10: LU solve at 10×10 with diagonal guard.
prop_luSolve10 :: Property
prop_luSolve10 = withMaxSuccess 20 $
  forAll ((,) <$> genMatrix @10 @10 <*> genVector @10) $ \(a, b) ->
    let (luMat, _) = lu a
        diagOk = all (\i -> abs (luMat ! (i, i)) > 1e-6) [0..9]
    in diagOk ==>
      let x = luSolve a b
          residual = scaledResidualLinear @10 a x b
      in counterexample ("LU 10x10 residual = " ++ show residual) $
         residual < 1e-6