packages feed

arrayfire-0.8.0.0: test/ArrayFire/NumericalSpec.hs

{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications    #-}
-- | Numerical algorithm tests that exercise broad API surface area.
-- Each test has a known exact answer derived from mathematics, so failures
-- indicate either a bug in the library or a precision regression.
module ArrayFire.NumericalSpec where

import qualified ArrayFire as A
import           Data.Function ((&))
import           Test.Hspec
import           Test.Hspec.QuickCheck (prop)
import           Test.QuickCheck       (NonEmptyList (..))

tol :: Double
tol = 1e-4

shouldBeApprox :: Double -> Double -> Expectation
shouldBeApprox x y = abs (x - y) < tol `shouldBe` True

spec :: Spec
spec = describe "Numerical algorithms" $ do

  -- ∫₀^π sin(x) dx = 2  (midpoint rectangle rule)
  -- Exercises: arange, sin, sumAll, scalar, *, +
  describe "Rectangle-rule integration" $ do
    it "approximates integral of sin over [0,pi] = 2" $ do
      let n      = 10000 :: Int
          h      = pi / fromIntegral n
          is     = A.arange @Double [n] (-1)         -- [0,1,...,n-1]
          xs     = (is + A.scalar 0.5) * A.scalar h  -- midpoints
          result = h * A.sumAll (sin xs)
      result `shouldBeApprox` 2.0

  -- Power iteration on A = [[2,1],[1,2]]
  -- Exact dominant eigenvalue = 3, eigenvector = [1,1]/√2
  -- Exercises: matrix, matmul, sumAll, *, /, scalar, sqrt, Haskell iterate
  describe "Power iteration" $ do
    it "converges to dominant eigenvalue 3 of [[2,1],[1,2]]" $ do
      let a       = A.matrix @Double (2,2) [[2,1],[1,2]]
          v0      = A.matrix @Double (2,1) [[1,1]]
          norm2 v = sqrt @Double (A.sumAll (v * v))
          norm v  = v / A.scalar (norm2 v)
          step v  = norm (A.mm a v)
          vFinal  = iterate step (norm v0) !! 30
          av      = A.mm a vFinal
          -- Rayleigh quotient: v^T A v
          lambda  = A.sumAll (vFinal * av)
      lambda `shouldBeApprox` 3.0

  -- Geometric series: Σ(k=0..19) 0.5^k = (1 - 0.5^20)/(1 - 0.5)
  -- Exercises: arange, (**), sumAll, scalar
  describe "Geometric series" $ do
    it "sum of 0.5^k for k=0..19 matches closed form" $ do
      let n        = 20 :: Int
          ks       = A.arange @Double [n] (-1)
          terms    = A.scalar 0.5 ** ks
          result   = A.sumAll terms
          expected = (1.0 - 0.5 ^ n) / (1.0 - 0.5)
      result `shouldBeApprox` expected

  -- Centered-difference moving average on u = [1..10]:
  --   avg_i = (u[i-1] + u[i+1]) / 2  for i = 1..8
  -- For an arithmetic sequence, this equals u[i] exactly.
  -- Exercises: vector, (!), range, +, /, scalar
  describe "Slice-based centered differences" $ do
    it "moving average of arithmetic sequence equals interior values" $ do
      let u    = A.vector @Double 10 [1..10]
          avg  = (u A.! A.range 0 7 + u A.! A.range 2 9) / A.scalar 2.0
      avg `shouldBe` u A.! A.range 1 8

  -- Slice assignment: overwrite interior of a zero vector.
  -- Exercises: vector, &, (.~), !, range, toList
  describe "Slice assignment" $ do
    it "(.~) writes src into interior slice, leaves boundaries unchanged" $ do
      let u      = A.vector @Double 6 (repeat 0.0)
          src    = A.vector @Double 4 [1,2,3,4]
          result = u & A.range 1 4 A..~ src
      A.toList result `shouldBe` [0,1,2,3,4,0]

  -- Sample statistics of [1..100].
  -- mean([1..100]) = 50.5  (exact by Gauss's formula)
  -- sum = n * mean must hold exactly.
  -- Exercises: vector, meanAll, sumAll
  describe "Statistical identities" $ do
    it "mean of [1..100] = 50.5" $ do
      A.meanAll (A.vector @Double 100 [1..100]) `shouldBeApprox` 50.5
    it "sumAll = n * meanAll" $ do
      let arr   = A.vector @Double 100 [1..100]
          m     = A.meanAll arr
          s     = A.sumAll  arr
      s `shouldBeApprox` (100 * m)
    it "variance of a constant array is 0" $ do
      A.varAll (A.vector @Double 50 (repeat 7.0)) A.Population `shouldBeApprox` 0.0

  -- Sum of first n squares: Σ(k=1..n) k² = n(n+1)(2n+1)/6
  -- Exercises: iota, *, +, scalar, sumAll
  describe "Sum of squares" $ do
    it "Sigma k^2 for k=1..100 matches closed form n(n+1)(2n+1)/6" $ do
      let n        = 100 :: Int
          ks       = A.iota @Double [n] [] + A.scalar 1.0  -- [1,2,...,n]
          result   = A.sumAll (ks * ks)
          expected = fromIntegral (n * (n+1) * (2*n+1)) / 6.0
      result `shouldBeApprox` expected

  -- Parseval's theorem: ||x||² = (1/N)||X||²  where X = FFT(x)
  -- Uses a complex Dirac delta: |x|² = 1, FFT is a flat spectrum |X[k]|² = 1 each.
  -- Exercises: mkArray, fft, conjg, real, sumAll, *
  describe "Parseval's theorem" $ do
    it "time-domain and frequency-domain energies agree" $ do
      let n       = 64 :: Int
          -- Dirac delta: all energy in first sample
          xs      = A.mkArray @(A.Complex Double) [n] (1 : repeat 0)
          -- time-domain energy: Σ |x[k]|² = 1
          tEnergy = A.sumAll (A.real (xs * A.conjg xs) :: A.Array Double)
          -- frequency-domain energy: (1/N) Σ |X[k]|² = (1/N)*N = 1
          xf      = A.fft xs 1.0 n
          fEnergy = (1.0 / fromIntegral n) * (A.sumAll (A.real (xf * A.conjg xf) :: A.Array Double))
      tEnergy `shouldBeApprox` 1.0
      tEnergy `shouldBeApprox` fEnergy

  describe "sumAll = n * meanAll (property)" $ do
    prop "sumAll = n * meanAll for any non-empty list of Double" $ \(NonEmpty xs) ->
      let n   = length xs
          arr = A.vector @Double n xs
          s   = A.sumAll arr
          m   = A.meanAll arr
      in abs (s - fromIntegral n * m) < 1e-9 + 1e-6 * abs s