linear-massiv-0.1.0.0: test/Test/Orthogonal.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
module Test.Orthogonal (orthogonalTests) 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.Level3 (matMul, transpose, mSub)
import Numeric.LinearAlgebra.Massiv.Orthogonal.Householder
import Numeric.LinearAlgebra.Massiv.Orthogonal.Givens
import Numeric.LinearAlgebra.Massiv.Orthogonal.QR (qr, qrGivens)
import Numeric.LinearAlgebra.Massiv.Orthogonal.LeastSquares
import Numeric.LinearAlgebra.Massiv.BLAS.Level2 (matvec)
import Numeric.LinearAlgebra.Massiv.Norms (normFrob, vnorm2)
import Test.Types
import Test.Residuals
orthogonalTests :: TestTree
orthogonalTests = testGroup "Orthogonal"
[ testGroup "Householder"
[ testProperty "Householder zeros subdiagonal" prop_householderZeros
, testProperty "Householder matrix orthogonal" prop_householderOrthogonal
]
, testGroup "Givens"
[ testProperty "Givens zeros target" prop_givensZeros
, testProperty "Givens preserves norm" prop_givensNorm
]
, testGroup "QR"
[ testProperty "A = QR reconstruction" prop_qrReconstruction
, testProperty "Q orthogonal" prop_qOrthogonal
, testProperty "R upper triangular" prop_rUpperTriangular
, testCase "3x3 QR known" test_qrKnown
]
, testGroup "Least Squares"
[ testCase "overdetermined system" test_leastSquares
]
, testGroup "QR scaled residuals"
[ testProperty "QR scaled residual 5x5" prop_qrScaledResidual5
, testProperty "Q orthogonality residual 5x5" prop_qOrthogonalityResidual5
, testProperty "Q orthogonality residual 10x10" prop_qOrthogonalityResidual10
]
, testGroup "QR reconstruction (larger)"
[ testProperty "QR reconstruction 10x10" prop_qrReconstruction10x10
, testProperty "QR reconstruction 10x5 rectangular" prop_qrReconstruction10x5
]
, testGroup "Givens vs Householder"
[ testProperty "Givens matches Householder 5x5" prop_givensMatchesHouseholder
]
, testGroup "Least squares (property)"
[ testProperty "normal equations 10x5" prop_leastSquares10x5
]
]
-- Householder tests
prop_householderZeros :: Property
prop_householderZeros = forAll (genVector @4) $ \x ->
let (v, beta) = householderVector x
h = householderMatrix @4 @M.P v beta
hx = matvec h x
-- All entries below first should be ~0
in all (\i -> abs (hx !. i) < 1e-6) [1..3]
prop_householderOrthogonal :: Property
prop_householderOrthogonal = forAll (genVector @3) $ \x ->
let (v, beta) = householderVector x
h = householderMatrix @3 @M.P v beta
ht = transpose h
hht = matMul h ht
eye = identityMatrix @3 @M.P :: Matrix 3 3 M.P Double
in matApproxEq @3 @3 hht eye
-- Givens tests
prop_givensZeros :: Property
prop_givensZeros = forAll ((,) <$> choose (-10, 10) <*> choose (-10, 10)) $ \(a, b) ->
let (c, s) = givensRotation a b
-- Convention: [c, -s; s, c] * [a; b] = [r; 0]
zero = s * a + c * b -- should be ~0
in abs zero < (1e-10 :: Double)
prop_givensNorm :: Property
prop_givensNorm = forAll ((,) <$> choose (-10, 10) <*> choose (-10, 10)) $ \(a, b) ->
let (c, s) = givensRotation a (b :: Double)
r = c * a - s * b -- r = sqrt(a² + b²)
in abs (r * r - (a * a + b * b)) < 1e-8
-- QR tests
prop_qrReconstruction :: Property
prop_qrReconstruction = forAll (genMatrix @4 @3) $ \a ->
let (q, r) = qr a
qr_ = matMul q r
in matApproxEq @4 @3 a qr_
prop_qOrthogonal :: Property
prop_qOrthogonal = forAll (genMatrix @3 @3) $ \a ->
let (q, _) = qr a
qt = transpose q
qtq = matMul qt q
eye = identityMatrix @3 @M.P :: Matrix 3 3 M.P Double
in matApproxEq @3 @3 qtq eye
prop_rUpperTriangular :: Property
prop_rUpperTriangular = forAll (genMatrix @3 @3) $ \a ->
let (_, r) = qr a
in all (\(i, j) -> abs (r ! (i, j)) < 1e-8)
[(i, j) | i <- [0..2], j <- [0..2], i > j]
test_qrKnown :: Assertion
test_qrKnown = do
let a = makeMatrix @3 @3 @M.P $ \i j -> case (i,j) of
(0,0) -> 12; (0,1) -> -51; (0,2) -> 4
(1,0) -> 6; (1,1) -> 167; (1,2) -> -68
(2,0) -> -4; (2,1) -> 24; (2,2) -> -41
_ -> 0 :: Double
(q, r) = qr a
qr_ = matMul q r
assertBool "QR reconstruction" $ matApproxEq @3 @3 a qr_
-- Least squares test
test_leastSquares :: Assertion
test_leastSquares = do
-- Overdetermined system: 4 equations, 2 unknowns
-- A = [[1,1],[1,2],[1,3],[1,4]], b = [6,5,7,10]
-- Least squares fit: y = a + bx
let a = makeMatrix @4 @2 @M.P $ \i j -> case (i,j) of
(0,0) -> 1; (0,1) -> 1
(1,0) -> 1; (1,1) -> 2
(2,0) -> 1; (2,1) -> 3
(3,0) -> 1; (3,1) -> 4
_ -> 0 :: Double
b = makeVector @4 @M.P $ \i -> case i of
0 -> 6; 1 -> 5; 2 -> 7; _ -> 10 :: Double
x = leastSquaresQR a b
-- The solution should minimize ‖Ax - b‖₂
-- With these values: x ≈ [3.5, 1.4]
assertBool "intercept reasonable" $ abs (x !. 0 - 3.5) < 0.5
assertBool "slope reasonable" $ abs (x !. 1 - 1.4) < 0.5
-- QR scaled residual tests
prop_qrScaledResidual5 :: Property
prop_qrScaledResidual5 = forAll (genMatrix @5 @5) $ \a ->
let (q, r) = qr a
in scaledResidualQR a q r < 100
prop_qOrthogonalityResidual5 :: Property
prop_qOrthogonalityResidual5 = forAll (genMatrix @5 @5) $ \a ->
let (q, _) = qr a
in orthogonalityResidual @5 q < 100
prop_qOrthogonalityResidual10 :: Property
prop_qOrthogonalityResidual10 = withMaxSuccess 20 $ forAll (genMatrix @10 @10) $ \a ->
let (q, _) = qr a
in orthogonalityResidual @10 q < 100
-- QR reconstruction (larger)
prop_qrReconstruction10x10 :: Property
prop_qrReconstruction10x10 = forAll (genMatrix @10 @10) $ \a ->
let (q, r) = qr a
in normFrob (mSub a (matMul q r)) / (normFrob a + 1e-15) < 1e-6
prop_qrReconstruction10x5 :: Property
prop_qrReconstruction10x5 = withMaxSuccess 20 $ forAll (genMatrix @10 @5) $ \a ->
let (q, r) = qr a
in normFrob (mSub a (matMul q r)) / (normFrob a + 1e-15) < 1e-6
-- Givens vs Householder
prop_givensMatchesHouseholder :: Property
prop_givensMatchesHouseholder = forAll (genMatrix @5 @5) $ \a ->
let (q1, r1) = qr a
(q2, r2) = qrGivens a
a1 = matMul q1 r1
a2 = matMul q2 r2
in normFrob (mSub a1 a2) / (normFrob a1 + 1e-15) < 1e-6
-- Least squares (property)
prop_leastSquares10x5 :: Property
prop_leastSquares10x5 = forAll ((,) <$> genMatrix @10 @5 <*> genVector @10) $ \(a, b) ->
let x = leastSquaresQR a b
-- Compute Ax as a 10-vector
ax = matvec a x
-- Compute residual r = Ax - b
r_vec = makeVector @10 @M.P $ \i -> (ax !. i) - (b !. i)
-- Compute A^T r as a 5-vector
at = transpose a
atr = matvec at r_vec
-- Normal equations: A^T(Ax - b) should be approximately zero
in vnorm2 atr / (vnorm2 b + 1e-15) < 1e-4