packages feed

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

{-# LANGUAGE AllowAmbiguousTypes #-}

-- | Test helpers: Arbitrary instances, approximate equality, matrix generators.
module Test.Types
  ( -- * Approximate equality
    (~=)
  , matApproxEq
  , vecApproxEq
    -- * Matrix generators
  , genMatrix
  , genVector
  , genSPDMatrix
  , genUpperTriangular
  , genLowerTriangular
  , genSymmetric
    -- * Tolerance
  , defaultTol
    -- * Standard test matrices
  , hilbertMatrix
  , wilkinsonMatrix
  , frankMatrix
  , hadamardMatrix
    -- * Generators with controlled properties
  , genMatrixWithCond
  , genNearSingularMatrix
  , genClusteredEigenMatrix
  , genSPDMatrixWithCond
  ) where

import qualified Data.Massiv.Array as M
import Data.Massiv.Array (Ix2(..), Sz(..), Comp(..))
import GHC.TypeNats (KnownNat, natVal)
import Data.Proxy (Proxy(..))
import Data.Bits (popCount)
import qualified Data.Bits as Bits
import Test.QuickCheck

import Numeric.LinearAlgebra.Massiv.Types
import Numeric.LinearAlgebra.Massiv.Internal
import Numeric.LinearAlgebra.Massiv.BLAS.Level3 (matMul, transpose)
import Numeric.LinearAlgebra.Massiv.Orthogonal.QR (qr)

-- | Default tolerance for floating-point comparisons.
defaultTol :: Double
defaultTol = 1e-8

-- | Approximate equality for scalars.
(~=) :: Double -> Double -> Bool
x ~= y = abs (x - y) < defaultTol * (1 + abs x + abs y)

-- | Approximate equality for matrices.
matApproxEq :: forall m n. (KnownNat m, KnownNat n)
            => Matrix m n M.P Double -> Matrix m n M.P Double -> Bool
matApproxEq a b =
  let r = dimVal @m
      c = dimVal @n
  in all (\(i, j) -> (a ! (i, j)) ~= (b ! (i, j)))
     [(i, j) | i <- [0..r-1], j <- [0..c-1]]

-- | Approximate equality for vectors.
vecApproxEq :: forall n. KnownNat n
            => Vector n M.P Double -> Vector n M.P Double -> Bool
vecApproxEq a b =
  let nn = dimVal @n
  in all (\i -> (a !. i) ~= (b !. i)) [0..nn-1]

-- | Generate a random m×n matrix with entries in [-10, 10].
genMatrix :: forall m n. (KnownNat m, KnownNat n) => Gen (Matrix m n M.P Double)
genMatrix = do
  let r = fromIntegral (natVal (Proxy @m))
      c = fromIntegral (natVal (Proxy @n))
  entries <- vectorOf (r * c) (choose (-10, 10))
  pure $ makeMatrix @m @n @M.P $ \i j -> entries !! (i * c + j)

-- | Generate a random n-element vector with entries in [-10, 10].
genVector :: forall n. KnownNat n => Gen (Vector n M.P Double)
genVector = do
  let nn = fromIntegral (natVal (Proxy @n))
  entries <- vectorOf nn (choose (-10, 10))
  pure $ makeVector @n @M.P $ \i -> entries !! i

-- | Generate a symmetric positive definite matrix: A = BBᵀ + εI.
genSPDMatrix :: forall n. KnownNat n => Gen (Matrix n n M.P Double)
genSPDMatrix = do
  let nn = fromIntegral (natVal (Proxy @n))
  entries <- vectorOf (nn * nn) (choose (-5, 5))
  let b = makeMatrix @n @n @M.P $ \i j -> entries !! (i * nn + j)
      -- BBᵀ + εI
      epsilon = 0.1 :: Double
  pure $ makeMatrix @n @n @M.P $ \i j ->
    let bbT = foldl' (\acc k -> acc + (b ! (i, k)) * (b ! (j, k))) 0 [0..nn-1]
    in bbT + if i == j then epsilon else 0

-- | Generate an upper triangular matrix with nonzero diagonal.
genUpperTriangular :: forall n. KnownNat n => Gen (Matrix n n M.P Double)
genUpperTriangular = do
  let nn = fromIntegral (natVal (Proxy @n))
  entries <- vectorOf (nn * nn) (choose (-10, 10))
  diags <- vectorOf nn (choose (1, 10))  -- Ensure nonzero diagonal
  pure $ makeMatrix @n @n @M.P $ \i j ->
    if i == j then diags !! i
    else if i < j then entries !! (i * nn + j)
    else 0

-- | Generate a lower triangular matrix with nonzero diagonal.
genLowerTriangular :: forall n. KnownNat n => Gen (Matrix n n M.P Double)
genLowerTriangular = do
  let nn = fromIntegral (natVal (Proxy @n))
  entries <- vectorOf (nn * nn) (choose (-10, 10))
  diags <- vectorOf nn (choose (1, 10))
  pure $ makeMatrix @n @n @M.P $ \i j ->
    if i == j then diags !! i
    else if i > j then entries !! (i * nn + j)
    else 0

-- | Generate a symmetric matrix.
genSymmetric :: forall n. KnownNat n => Gen (Matrix n n M.P Double)
genSymmetric = do
  let nn = fromIntegral (natVal (Proxy @n))
  entries <- vectorOf (nn * nn) (choose (-10, 10))
  pure $ makeMatrix @n @n @M.P $ \i j ->
    if i <= j then entries !! (i * nn + j)
    else entries !! (j * nn + i)

------------------------------------------------------------------------
-- Standard test matrices
------------------------------------------------------------------------

-- | Hilbert matrix of order /n/: \(H_{ij} = 1/(i+j+1)\).
--
-- The Hilbert matrix is symmetric positive definite with an
-- exponentially growing condition number. Its inverse has integer
-- entries.
--
-- /Reference:/ Higham (2002), Section 28.1; GVL4 p. 128.
hilbertMatrix :: forall n. KnownNat n => Matrix n n M.P Double
hilbertMatrix = makeMatrix @n @n @M.P $ \i j ->
  1 / fromIntegral (i + j + 1)

-- | Wilkinson matrix \(W_n\): symmetric tridiagonal with
-- diagonal \(|i - \lfloor n/2 \rfloor|\) and unit sub/superdiagonal.
--
-- Has near-degenerate eigenvalue pairs that stress eigenvalue solvers.
--
-- /Reference:/ Higham (2002), Section 28.6; MATLAB @gallery('wilk', n)@.
wilkinsonMatrix :: forall n. KnownNat n => Matrix n n M.P Double
wilkinsonMatrix =
  let nn = dimVal @n
      mid = nn `div` 2
  in makeMatrix @n @n @M.P $ \i j ->
    if i == j then fromIntegral (abs (i - mid))
    else if abs (i - j) == 1 then 1
    else 0

-- | Frank matrix of order /n/: upper Hessenberg with
-- \(\det(F) = 1\) and known positive real eigenvalues.
--
-- \(F_{ij} = n - \max(i,j)\) for \(j \ge i-1\), zero otherwise
-- (0-indexed).
--
-- /Reference:/ Higham (2002), Section 28.5; Frank (1958).
frankMatrix :: forall n. KnownNat n => Matrix n n M.P Double
frankMatrix =
  let nn = dimVal @n
  in makeMatrix @n @n @M.P $ \i j ->
    if j >= i - 1 && i - 1 >= 0 || j >= i
    then fromIntegral (nn - max i j)
    else 0

-- | Hadamard matrix of order /n/ via the Sylvester\/Walsh construction.
--
-- \(H_{ij} = (-1)^{\mathrm{popcount}(i \mathbin{\&} j)}\)
--
-- Produces a proper Hadamard matrix when /n/ is a power of 2,
-- satisfying \(H^T H = n I\). All entries are \(\pm 1\).
--
-- /Reference:/ Higham (2002), Section 28.3.
hadamardMatrix :: forall n. KnownNat n => Matrix n n M.P Double
hadamardMatrix = makeMatrix @n @n @M.P $ \i j ->
  if even (popCount ((Bits..&.) i j)) then 1 else -1

------------------------------------------------------------------------
-- Generators with controlled properties
------------------------------------------------------------------------

-- | Generate a matrix with prescribed 2-norm condition number /κ/.
--
-- Uses the @randsvd@ construction: \(A = U \Sigma V^T\) where
-- \(U\) and \(V\) are random orthogonal matrices obtained from
-- QR factorization of random matrices, and the singular values
-- are geometrically spaced from 1 to \(1/\kappa\).
--
-- /Reference:/ Higham (2002), Section 28.3; Fasi & Higham,
-- "Generating Extreme-Scale Matrices With Specified Singular
-- Values or Condition Number," SIAM J. Sci. Comput. 43(5), 2021.
genMatrixWithCond :: forall m n. (KnownNat m, KnownNat n)
  => Double -> Gen (Matrix m n M.P Double)
genMatrixWithCond kappa = do
  uRaw <- genMatrix @m @m
  vRaw <- genMatrix @n @n
  let (u, _) = qr uRaw
      (v, _) = qr vRaw
      mm = dimVal @m
      nn = dimVal @n
      minDim = min mm nn
      -- Singular values geometrically spaced: sigma_0 = 1, sigma_{minDim-1} = 1/kappa
      sigma = makeMatrix @m @n @M.P $ \i j ->
        if i == j && i < minDim
        then if minDim <= 1 then 1
             else let t = fromIntegral i / fromIntegral (minDim - 1)
                  in kappa ** (-t)
        else 0
  pure $ matMul u (matMul sigma (transpose v))

-- | Generate a near-singular matrix with smallest singular value /ε/.
--
-- Wrapper around 'genMatrixWithCond' with \(\kappa = 1/\varepsilon\).
genNearSingularMatrix :: forall n. KnownNat n
  => Double -> Gen (Matrix n n M.P Double)
genNearSingularMatrix smallSV = genMatrixWithCond @n @n (1 / smallSV)

-- | Generate a symmetric matrix with clustered eigenvalues near /c/.
--
-- Constructs \(A = Q \Lambda Q^T\) where \(\Lambda\) is diagonal
-- with entries \(c + i \cdot 10^{-6}\) and \(Q\) is a random
-- orthogonal matrix. This stresses iterative eigenvalue solvers
-- that must resolve near-degenerate eigenvalue pairs.
genClusteredEigenMatrix :: forall n. KnownNat n
  => Double -> Gen (Matrix n n M.P Double)
genClusteredEigenMatrix cluster = do
  qRaw <- genMatrix @n @n
  let (q, _) = qr qRaw
      qt = transpose q
      lambda = makeMatrix @n @n @M.P $ \i j ->
        if i == j then cluster + fromIntegral i * 1e-6 else 0
  pure $ matMul q (matMul lambda qt)

-- | Generate an SPD matrix with prescribed condition number /κ/.
--
-- Uses the @randsvd@ construction with \(U = V\) (ensuring symmetry):
-- \(A = Q \Lambda Q^T\) where eigenvalues are geometrically spaced
-- from 1 to \(1/\kappa\).
genSPDMatrixWithCond :: forall n. KnownNat n
  => Double -> Gen (Matrix n n M.P Double)
genSPDMatrixWithCond kappa = do
  qRaw <- genMatrix @n @n
  let (q, _) = qr qRaw
      qt = transpose q
      nn = dimVal @n
      lambda = makeMatrix @n @n @M.P $ \i j ->
        if i == j
        then if nn <= 1 then 1
             else let t = fromIntegral i / fromIntegral (nn - 1)
                  in kappa ** (-t)
        else 0
  pure $ matMul q (matMul lambda qt)