1{-# LANGUAGE BangPatterns #-}
2{-# LANGUAGE CPP          #-}
3{-# LANGUAGE ViewPatterns #-}
4-- | Tests for Statistics.Math
5module Tests.SpecFunctions (
6  tests
7  ) where
8
9import Control.Monad
10import Data.List
11import Data.Maybe
12import qualified Data.Vector as V
13import           Data.Vector   ((!))
14import qualified Data.Vector.Unboxed as U
15
16import Test.QuickCheck  hiding (choose,within)
17import Test.Tasty
18import Test.Tasty.QuickCheck   (testProperty)
19import Test.Tasty.HUnit
20
21import Tests.Helpers
22import Tests.SpecFunctions.Tables
23import Numeric.SpecFunctions
24import Numeric.SpecFunctions.Internal   (factorialTable)
25import Numeric.MathFunctions.Comparison (within,relativeError,ulpDistance)
26import Numeric.MathFunctions.Constants  (m_epsilon,m_tiny)
27
28erfTol,erfcTol,erfcLargeTol :: Int
29-- Pure haskell implementation is not very good
30#if !defined(USE_SYSTEM_ERF) || defined(__GHCJS__)
31erfTol       = 4
32erfcTol      = 4
33erfcLargeTol = 64
34-- Macos's erf is slightly worse that GNU one
35#elif defined(darwin_HOST_OS)
36erfTol       = 2
37erfcTol      = 2
38erfcLargeTol = 2
39#else
40erfTol       = 1
41erfcTol      = 2
42erfcLargeTol = 2
43#endif
44
45isGHCJS :: Bool
46#if defined(__GHCJS__)
47isGHCJS = True
48#else
49isGHCJS = False
50#endif
51
52isWindows :: Bool
53#if defined(mingw32_HOST_OS)
54isWindows = True
55#else
56isWindows = False
57#endif
58
59
60tests :: TestTree
61tests = testGroup "Special functions"
62  [ testGroup "erf"
63    [ -- implementation from numerical recipes loses presision for
64      -- large arguments
65      testCase "erfc table" $
66        forTable "tests/tables/erfc.dat" $ \[x, exact] ->
67          checkTabularPure erfcTol (show x) exact (erfc x)
68    , testCase "erfc table [large]" $
69        forTable "tests/tables/erfc-large.dat" $ \[x, exact] ->
70          checkTabularPure erfcLargeTol (show x) exact (erfc x)
71      --
72    , testCase "erf table" $
73        forTable "tests/tables/erf.dat" $ \[x, exact] -> do
74          checkTabularPure erfTol (show x) exact (erf x)
75    , testProperty "id = erfc . invErfc" invErfcIsInverse
76    , testProperty "id = invErfc . erfc" invErfcIsInverse2
77    , testProperty "invErf  = erf^-1"    invErfIsInverse
78    ]
79  --
80  , testGroup "log1p & Co"
81    [ testCase "expm1 table" $
82        forTable "tests/tables/expm1.dat" $ \[x, exact] ->
83          checkTabularPure 2 (show x) exact (expm1 x)
84    , testCase "log1p table" $
85        forTable "tests/tables/log1p.dat" $ \[x, exact] ->
86          checkTabularPure 1 (show x) exact (log1p x)
87    ]
88  ----------------
89  , testGroup "gamma function"
90    [ testCase "logGamma table [fractional points" $
91        forTable "tests/tables/loggamma.dat" $ \[x, exact] -> do
92          checkTabularPure 2 (show x) exact (logGamma x)
93    , testProperty "Gamma(x+1) = x*Gamma(x)" $ gammaReccurence
94    , testCase     "logGamma is expected to be precise at 1e-15 level" $
95        forM_ [3..10000::Int] $ \n -> do
96          let exact = logFactorial (n-1)
97              val   = logGamma (fromIntegral n)
98          checkTabular 8 (show n) exact val
99    ]
100  ----------------
101  , testGroup "incomplete gamma"
102    [ testCase "incompleteGamma table" $
103        forTable "tests/tables/igamma.dat" $ \[a,x,exact] -> do
104          let err | a < 10    = 16
105                  | a <= 101  = case () of
106                      _| isGHCJS   -> 64
107                       | isWindows -> 64
108                       | otherwise -> 32
109                  | a == 201  = 200
110                  | otherwise = 32
111          checkTabularPure err (show (a,x)) exact (incompleteGamma a x)
112    , testProperty "incomplete gamma - increases" $
113        \(abs -> s) (abs -> x) (abs -> y) -> s > 0 ==> monotonicallyIncreases (incompleteGamma s) x y
114    , testProperty "0 <= gamma <= 1"               incompleteGammaInRange
115    , testProperty "gamma(1,x) = 1 - exp(-x)"      incompleteGammaAt1Check
116    , testProperty "invIncompleteGamma = gamma^-1" invIGammaIsInverse
117    ]
118  ----------------
119  , testGroup "beta function"
120    [ testCase "logBeta table" $
121        forTable "tests/tables/logbeta.dat" $ \[p,q,exact] ->
122          let errEst
123                -- For Stirling approx. errors are very good
124                | b > 10          = 2
125                -- Partial Stirling approx
126                | a > 10 = case () of
127                    _| b >= 1    -> 4
128                     | otherwise -> 2 * est
129                -- sum of logGamma
130                | otherwise = case () of
131                    _| a <= 1 && b <= 1 -> 8
132                     | a >= 1 && b >= 1 -> 8
133                     | otherwise        -> 2 * est
134                where
135                  a = max p q
136                  b = min p q
137                  --
138                  est = ceiling
139                      $ abs (logGamma a) + abs (logGamma b) + abs (logGamma (a + b))
140                      / abs (logBeta a b)
141          in checkTabularPure errEst (show (p,q)) exact (logBeta p q)
142    , testCase "logBeta factorial" betaFactorial
143    , testProperty "beta(1,p) = 1/p"   beta1p
144    -- , testProperty "beta recurrence"   betaRecurrence
145    ]
146  ----------------
147  , testGroup "incomplete beta"
148    [ testCase "incompleteBeta table" $
149        forM_ tableIncompleteBeta $ \(p,q,x,exact) ->
150          checkTabular 64 (show (x,p,q)) (incompleteBeta p q x) exact
151    , testCase "incompleteBeta table with p > 3000 and q > 3000" $
152        forM_ tableIncompleteBetaP3000 $ \(x,p,q,exact) ->
153          checkTabular 7000 (show (x,p,q)) (incompleteBeta p q x) exact
154    --
155    , testProperty "0 <= I[B] <= 1" incompleteBetaInRange
156    , testProperty "ibeta symmetry" incompleteBetaSymmetry
157    , testCase "Regression #68" $ do
158        let a = 1
159            b = 0.3
160            p = 0.3
161            x = invIncompleteBeta a b p
162        assertBool "Inversion OK" $ incompleteBeta a b x `ulpDistance` p < 4
163    -- XXX FIXME DISABLED due to failures
164    -- , testProperty "invIncompleteBeta  = B^-1" $ invIBetaIsInverse
165    ]
166  ----------------
167  , testGroup "digamma"
168    [ testAssertion "digamma is expected to be precise at 1e-14 [integers]"
169        $ digammaTestIntegers 1e-14
170      -- Relative precision is lost when digamma(x) ≈ 0
171    , testCase "digamma is expected to be precise at 1e-12" $
172      forTable "tests/tables/digamma.dat" $ \[x, exact] ->
173        checkTabularPure 2048
174          (show x) (digamma x) exact
175    ]
176  ----------------
177  , testGroup "factorial"
178    [ testCase "Factorial table" $
179      forM_ [0 .. 170] $ \n -> do
180        checkTabular 1
181          (show n)
182          (fromIntegral (factorial' n))
183          (factorial (fromIntegral n :: Int))
184      --
185    , testCase "Log factorial from integer" $
186      forM_ [2 .. 170] $ \n -> do
187        checkTabular 1
188          (show n)
189          (log $ fromIntegral $ factorial' n)
190          (logFactorial (fromIntegral n :: Int))
191    , testAssertion "Factorial table is OK"
192    $ U.length factorialTable == 171
193    , testCase "Log factorial table" $
194      forTable "tests/tables/factorial.dat" $ \[i,exact] ->
195        checkTabularPure 3
196          (show i) (logFactorial (round i :: Int)) exact
197    ]
198  ----------------
199  , testGroup "combinatorics"
200    [ testCase "choose table" $
201      forM_ [0 .. 1000] $ \n ->
202        forM_ [0 .. n]  $ \k -> do
203          checkTabular (if isWindows then 3072 else 2048)
204            (show (n,k))
205            (fromIntegral $ choose' n k)
206            (choose (fromInteger n) (fromInteger k))
207    --
208    , testCase "logChoose == log . choose" $
209      forM_ [0 .. 1000] $ \n ->
210        forM_ [0 .. n]  $ \k -> do
211          checkTabular 2
212            (show (n,k))
213            (log $ choose n k)
214            (logChoose n k)
215    ]
216    ----------------------------------------------------------------
217    -- Self tests
218  , testGroup "self-test"
219    [ testProperty "Self-test: 0 <= range01 <= 1" $ \x -> let f = range01 x in f <= 1 && f >= 0
220    ]
221  ]
222
223----------------------------------------------------------------
224-- efr tests
225----------------------------------------------------------------
226
227roundtrip_erfc_invErfc,
228  roundtrip_invErfc_erfc,
229  roundtrip_erf_invErf
230  :: (Double,Double)
231#if !defined(USE_SYSTEM_ERF) || defined(__GHCJS__)
232roundtrip_erfc_invErfc = (2,8)
233roundtrip_invErfc_erfc = (8,4)
234roundtrip_erf_invErf   = (128,128)
235#elif defined(darwin_HOST_OS)
236roundtrip_erfc_invErfc = (4,4)
237roundtrip_invErfc_erfc = (4,4)
238roundtrip_erf_invErf   = (2,2)
239#else
240roundtrip_erfc_invErfc = (2,2)
241roundtrip_invErfc_erfc = (2,2)
242roundtrip_erf_invErf   = (1,1)
243#endif
244
245-- id ≈ erfc . invErfc
246invErfcIsInverse :: Double -> Property
247invErfcIsInverse ((*2) . range01 -> x)
248  = (not $ isInfinite x) ==>
249  ( counterexample ("x        = " ++ show x )
250  $ counterexample ("y        = " ++ show y )
251  $ counterexample ("x'       = " ++ show x')
252  $ counterexample ("calc.err = " ++ show (delta, delta-e'))
253  $ counterexample ("ulps     = " ++ show (ulpDistance x x'))
254  $ ulpDistance x x' <= round delta
255  )
256  where
257    (e,e') = roundtrip_erfc_invErfc
258    delta  = e' + e * abs ( y / x  *  2 / sqrt pi * exp( -y*y ))
259    y  = invErfc x
260    x' = erfc y
261
262-- id ≈ invErfc . erfc
263invErfcIsInverse2 :: Double -> Property
264invErfcIsInverse2 x
265  = (not $ isInfinite x') ==>
266    (y > m_tiny)          ==>
267    (x /= 0) ==>
268    counterexample ("x        = " ++ show x )
269  $ counterexample ("y        = " ++ show y )
270  $ counterexample ("x'       = " ++ show x')
271  $ counterexample ("calc.err = " ++ show delta)
272  $ counterexample ("ulps     = " ++ show (ulpDistance x x'))
273  $ ulpDistance x x' <= delta
274  where
275    (e,e') = roundtrip_invErfc_erfc
276    delta  = round
277           $ e' + e * abs (y / x  /  (2 / sqrt pi * exp( -x*x )))
278    y  = erfc x
279    x' = invErfc y
280
281-- id ≈ erf . invErf
282invErfIsInverse :: Double -> Property
283invErfIsInverse a
284  = (x /= 0) ==>
285    counterexample ("x        = " ++ show x )
286  $ counterexample ("y        = " ++ show y )
287  $ counterexample ("x'       = " ++ show x')
288  $ counterexample ("calc.err = " ++ show delta)
289  $ counterexample ("ulps     = " ++ show (ulpDistance x x'))
290  $ ulpDistance x x' <= delta
291  where
292    (e,e') = roundtrip_erf_invErf
293    delta  = round
294           $ e + e' * abs (y / x  *  2 / sqrt pi * exp ( -y * y ))
295    x  | a < 0     = - range01 a
296       | otherwise =   range01 a
297    y  = invErf x
298    x' = erf y
299
300----------------------------------------------------------------
301-- QC tests
302----------------------------------------------------------------
303
304-- B(p,q) = (x - 1)!(y-1)! / (x + y - 1)!
305betaFactorial :: IO ()
306betaFactorial = do
307  forM_ prod $ \(p,q,facP,facQ,facProd) -> do
308    let exact = fromIntegral (facQ * facP)
309              / fromIntegral facProd
310    checkTabular 16 (show (p,q))
311      (logBeta (fromIntegral p) (fromIntegral q))
312      (log exact)
313  where
314    prod    = [ (p,q,facP,facQ, factorial' (p + q - 1))
315              | (p,facP) <- facList
316              , (q,facQ) <- facList
317              , p + q < 170
318              , not (p == 1 && q== 1)
319              ]
320    facList = [(p,factorial' (p-1)) | p <- [1 .. 170]]
321
322-- B(1,p) = 1/p
323beta1p :: Double -> Property
324beta1p (abs -> p)
325  = p > 2 ==>
326    counterexample ("p    = " ++ show p)
327  $ counterexample ("logB = " ++ show lb)
328  $ counterexample ("err  = " ++ show d)
329  $ d <= 24
330  where
331    lb = logBeta 1 p
332    d  = ulpDistance lb (- log p)
333
334{-
335-- B(p+1,q) = B(p,q) · p/(p+q)
336betaRecurrence :: Double -> Double -> Property
337betaRecurrence (abs -> p) (abs -> q)
338  = p > 0  &&  q > 0  ==>
339    counterexample ("p          = " ++ show p)
340  $ counterexample ("q          = " ++ show q)
341  $ counterexample ("log B(p,q) = " ++ show (logBeta p q))
342  $ counterexample ("log B(p+1,q) = " ++ show (logBeta (p+1) q))
343  $ counterexample ("err        = " ++ show d)
344  $ d <= 128
345  where
346    logB  = logBeta p q + log (p / (p + q))
347    logB' = logBeta (p + 1) q
348    d     = ulpDistance logB logB'
349-}
350
351-- Γ(x+1) = x·Γ(x)
352gammaReccurence :: Double -> Property
353gammaReccurence x
354  = x > 0  ==>  err < errEst
355    where
356      g1     = logGamma x
357      g2     = logGamma (x+1)
358      err    = abs (g2 - g1 - log x)
359      -- logGamma apparently is not as precise for small x. See #59 for details
360      errEst = max 1e-14
361             $ 2 * m_epsilon * sum (map abs [ g1 , g2 , log x ])
362
363-- γ(s,x) is in [0,1] range
364incompleteGammaInRange :: Double -> Double -> Property
365incompleteGammaInRange (abs -> s) (abs -> x) =
366  x >= 0 && s > 0  ==> let i = incompleteGamma s x in i >= 0 && i <= 1
367
368-- γ(1,x) = 1 - exp(-x)
369-- Since Γ(1) = 1 normalization doesn't make any difference
370incompleteGammaAt1Check :: Double -> Bool
371incompleteGammaAt1Check (abs -> x) =
372  ulpDistance (incompleteGamma 1 x) (-expm1(-x)) < 16
373
374-- invIncompleteGamma is inverse of incompleteGamma
375invIGammaIsInverse :: Double -> Double -> Property
376invIGammaIsInverse (abs -> a) (range01 -> p) =
377  a > m_tiny && p > m_tiny && p < 1 && x > m_tiny  ==>
378    ( counterexample ("a    = " ++ show a )
379    $ counterexample ("p    = " ++ show p )
380    $ counterexample ("x    = " ++ show x )
381    $ counterexample ("p'   = " ++ show p')
382    $ counterexample ("err  = " ++ show (ulpDistance p p'))
383    $ counterexample ("est  = " ++ show est)
384    $ ulpDistance p p' <= est
385    )
386  where
387    x  = invIncompleteGamma a p
388    f' = exp ( log x * (a-1) - x - logGamma a)
389    p' = incompleteGamma    a x
390    -- FIXME: Test should be rechecked when #42 is fixed
391    (e,e') = (32,32)
392    est    = round
393           $ e' + e * abs (x / p * f')
394
395-- I(x;p,q) is in [0,1] range
396incompleteBetaInRange :: Double -> Double -> Double -> Property
397incompleteBetaInRange (abs -> p) (abs -> q) (range01 -> x) =
398  p > 0 && q > 0  ==> let i = incompleteBeta p q x in i >= 0 && i <= 1
399
400-- I(0.5; p,p) = 0.5
401incompleteBetaSymmetry :: Double -> Property
402incompleteBetaSymmetry (abs -> p) =
403    p > 0 ==>
404    counterexample ("p   = " ++ show p)
405  $ counterexample ("ib  = " ++ show ib)
406  $ counterexample ("err = " ++ show d)
407  $ counterexample ("est = " ++ show est)
408  $ d <= est
409  where
410    est | p < 1     = 80
411        | p < 10    = 200
412        | otherwise = round $ 6 * p
413    d  = ulpDistance ib 0.5
414    ib = incompleteBeta p p 0.5
415
416-- invIncompleteBeta is inverse of incompleteBeta
417invIBetaIsInverse :: Double -> Double -> Double -> Property
418invIBetaIsInverse (abs -> p) (abs -> q) (range01 -> x) =
419  p > 0 && q > 0  ==> ( counterexample ("p   = " ++ show p )
420                      $ counterexample ("q   = " ++ show q )
421                      $ counterexample ("x   = " ++ show x )
422                      $ counterexample ("x'  = " ++ show x')
423                      $ counterexample ("a   = " ++ show a)
424                      $ counterexample ("err = " ++ (show $ abs $ (x - x') / x))
425                      $ abs (x - x') <= 1e-12
426                      )
427  where
428    x' = incompleteBeta    p q a
429    a  = invIncompleteBeta p q x
430
431-- Table for digamma function:
432--
433-- Uses equality ψ(n) = H_{n-1} - γ where
434--   H_{n} = Σ 1/k, k = [1 .. n]     - harmonic number
435--   γ     = 0.57721566490153286060  - Euler-Mascheroni number
436digammaTestIntegers :: Double -> Bool
437digammaTestIntegers eps
438  = all (uncurry $ eq eps) $ take 3000 digammaInt
439  where
440    ok approx exact = approx
441    -- Harmonic numbers starting from 0
442    harmN = scanl (\a n -> a + 1/n) 0 [1::Rational .. ]
443    gam   = 0.57721566490153286060
444    -- Digamma values
445    digammaInt = zipWith (\i h -> (digamma i, realToFrac h - gam)) [1..] harmN
446
447
448----------------------------------------------------------------
449-- Unit tests
450----------------------------------------------------------------
451
452-- Lookup table for fact factorial calculation. It has fixed size
453-- which is bad but it's OK for this particular case
454factorial_table :: V.Vector Integer
455factorial_table = V.generate 2000 (\n -> product [1..fromIntegral n])
456
457-- Exact implementation of factorial
458factorial' :: Integer -> Integer
459factorial' n = factorial_table ! fromIntegral n
460
461-- Exact albeit slow implementation of choose
462choose' :: Integer -> Integer -> Integer
463choose' n k = factorial' n `div` (factorial' k * factorial' (n-k))
464
465-- Truncate double to [0,1]
466range01 :: Double -> Double
467range01 = abs . (snd :: (Integer, Double) -> Double) . properFraction
468
469
470readTable :: FilePath -> IO [[Double]]
471readTable
472  = fmap (fmap (fmap read . words) . lines)
473  . readFile
474
475forTable :: FilePath -> ([Double] -> Maybe String) -> IO ()
476forTable path fun = do
477  rows <- readTable path
478  case mapMaybe fun rows of
479    []   -> return ()
480    errs -> assertFailure $ intercalate "---\n" errs
481
482checkTabular :: Int -> String -> Double -> Double -> IO ()
483checkTabular prec x exact val =
484  case checkTabularPure prec x exact val of
485    Nothing -> return ()
486    Just s  -> assertFailure s
487
488checkTabularPure :: Int -> String -> Double -> Double -> Maybe String
489checkTabularPure prec x exact val
490  | within prec exact val = Nothing
491  | otherwise             = Just $ unlines
492      [ " x         = " ++ x
493      , " expected  = " ++ show exact
494      , " got       = " ++ show val
495      , " ulps diff = " ++ show (ulpDistance exact val)
496      , " err.est.  = " ++ show prec
497      ]
498