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