packages feed

cdar-mBound-0.1.0.0: test/Test.hs

{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, StandaloneDeriving, DeriveGeneric #-}
import Test.Tasty
import Test.Tasty.SmallCheck as SC
import Test.Tasty.QuickCheck as QC
import Test.Tasty.HUnit
import Test.SmallCheck.Series as SC
import GHC.Generics

import Control.Applicative (ZipList (..))
import Control.Monad (liftM, liftM3, liftM4)
import Data.Functor

import Data.List
import Data.Ord
import Data.Fixed (mod')
import Data.CDAR


-- Main test

main = defaultMain tests

tests :: TestTree
tests = testGroup "Tests" [dyadic, localOption (mkTimeout 500000) approx, creal, unitTests]
  -- [dyadic
  -- ,approx
  -- ,creal
  -- ,unitTests]

-- General auxiliary properties.

idempotent :: Eq a => (a -> a) -> a -> Bool
idempotent f = \x -> f(x) == f(f(x))

autoinverse :: Eq a => (a -> a) -> a -> Bool
autoinverse f = \x -> x == f(f(x))

dyadic :: TestTree
dyadic = testGroup "Dyadic" [propertiesD]

deriving instance Generic Dyadic
instance (Monad m) => Serial m Dyadic

instance Arbitrary Dyadic where
    arbitrary = do a <- arbitrary
                   s <- choose (-1000,100)
                   return (a :^ s)

propertiesD :: TestTree
propertiesD = testGroup "Properties of Dyadic" [scPropD, qcPropD]

scPropD = testGroup "(checked by SmallCheck)"
  [ SC.testProperty "compare" $
      \x y -> compare (x :: Dyadic) (y :: Dyadic) == (compare (toRational x) (toRational y))
  , SC.testProperty "multiplication" $
      \a b -> toRational (a*b :: Dyadic) == (toRational a)*(toRational b)
  , SC.testProperty "addition" $
      \a b -> toRational (a+b :: Dyadic) == (toRational a)+(toRational b)
  , SC.testProperty "negate" $
      \a -> (a :: Dyadic) + negate a == 0
  , SC.testProperty "negation exists" $
      \a b -> (a :: Dyadic) + b == 0 SC.==> a == negate b
  , SC.testProperty "negation autoinverse" $ autoinverse (negate :: Dyadic -> Dyadic)
  , SC.testProperty "abs idempotent" $ idempotent (abs :: Dyadic -> Dyadic)
  , SC.testProperty "signum idempotent" $ idempotent (signum :: Dyadic -> Dyadic)
  , SC.testProperty "signum in {-1,0,1}" $ \x -> signum (x :: Dyadic) `elem` [-1,0,1]
  , SC.testProperty "negate abs signum" $
      \a -> (a :: Dyadic) + (negate (signum a) * (abs a)) == 0
  , SC.testProperty "read show" $
      \a -> (a :: Dyadic) == read (show a)
  , SC.testProperty "shift left identity" $ \d@(a :^ s) (SC.NonNegative n) -> shiftD (s - n) d == d
  , SC.testProperty "sqrt" $ \d@(a :^ s) -> sqrtD (min 0 (2*s)) (d*d) == abs d
  ]

qcPropD = testGroup "(checked by QuickCheck)"
  [ QC.testProperty "compare" $ 
      \x y -> compare (x :: Dyadic) (y :: Dyadic) == (compare (toRational x) (toRational y))
  , QC.testProperty "multiplication" $
      \a b -> toRational (a*b :: Dyadic) == (toRational a)*(toRational b)
  , QC.testProperty "addition" $
      \a b -> toRational (a+b :: Dyadic) == (toRational a)+(toRational b)
  , QC.testProperty "negate" $
      \a -> (a :: Dyadic) + negate a == 0
  , QC.testProperty "negate abs signum" $
      \a -> (a :: Dyadic) + (negate (signum a) * (abs a)) == 0:^0
  , QC.testProperty "read show" $
      \a -> (a :: Dyadic) == read (show a)
  , QC.testProperty "negation autoinverse" $ autoinverse (negate :: Dyadic -> Dyadic)
  , QC.testProperty "abs idempotent" $ idempotent (abs :: Dyadic -> Dyadic)
  , QC.testProperty "signum idempotent" $ idempotent (signum :: Dyadic -> Dyadic)
  , QC.testProperty "signum in {-1,0,1}" $ \x -> signum (x :: Dyadic) `elem` [-1,0,1]
  , QC.testProperty "shift left identity" $ \d@(a :^ s) (QC.NonNegative n) -> shiftD (s - n) d == d
  , QC.testProperty "sqrt" $ \d@(a :^ s) -> sqrtD (min 0 (2*s)) (d*d) == abs d
  ]

{-
Multiplication and addition of Approx is not tested. Those two operations should return the exact Approx of the result.

There are no test making sure that the loss of information is acceptable for reciprocal, exp, log, ... .
-}
approx :: TestTree
approx = testGroup "Approx" [scPropA, qcPropA]

instance Monad m => Serial m Approx where
  series = cons0 Bottom \/ (quadToApprox <$> series)
    where
      quadToApprox (SC.Positive mb, m, SC.NonNegative e, s) = approxMB mb m e s

instance Arbitrary Approx where
    arbitrary = frequency [(10,
                            liftM3 approxAutoMB
                                   arbitrary 
                                   (return 0)
                                   (choose (-100,20))),
                           (25,
                            liftM4 approxMB
                                   (choose (10,100000))
                                   arbitrary 
                                   (elements [1,2,3,254,255,256,257,510,511,512,513,767,768,1020,1021,1022,1023,1024,1025])
                                   (choose (-100,20))),
                           (60,
                            liftM4 approxMB
                                   (choose (10,100000))
                                   arbitrary 
                                   (choose (0,10))
                                   (choose (-100,20))),
                           (5, return Bottom)]

scPropA :: TestTree
scPropA = testGroup "(checked by smallcheck)"
  [ SC.testProperty "read . show = id" $ \a -> (a :: Approx) == read (show a)
  , SC.testProperty "diameter" $ \a -> diameter a == upperBound a - lowerBound a
  , SC.testProperty "fromDyadic exact and centre" $ \d -> let a = fromDyadic d in exact a && Just d == centre a
  , SC.testProperty "abs negate sign" $ \a -> approximatedBy 0 $ (a ::Approx) + (negate (signum a) * (abs a))
  , SC.testProperty "a+(-a) contains 0" $ \a -> 0 `approximatedBy` ((a :: Approx) + negate a)
  , SC.testProperty "toApprox r contains r" $ \r -> approximatedBy r $ toApprox 20 (r :: Rational)
  , SC.testProperty "1/(1/a) contains a" $ \a -> not (0 `approximatedBy` a) SC.==> 
                                                       a `better` (1/(1/a))
  , SC.testProperty "a*(1/a) contains 1" $ \a -> not (0 `approximatedBy` a) SC.==> 1 `approximatedBy` (a*(1/a))
  , SC.testProperty "boundErrorTerm" $
    \a -> case a of
            Approx _ _ _ _ -> let b@(Approx _ _ e _) = boundErrorTerm a
                              in (a `better` b) && (e < 1024) -- 1024 is errorBound when errorBits is 10
            Bottom         -> better a (boundErrorTerm a)
  , SC.testProperty "limitSize" $
    \a -> case a of
            Approx _ _ _ _ -> let b@(Approx _ _ _ s) = limitSize 2 a
                              in (a `better` b) && (s >= -2)
            Bottom         -> better a (limitSize 2 a)
  , SC.testProperty "sqrt" $ \a -> let b = abs a in better b $ (sqrtA b)^2
  , SC.testProperty "recipA" $ \a -> let b = abs a in 0 `approximatedBy` b || (lowerBound (recipA b) * upperBound b <= 1
                                                                               && upperBound (recipA b) * lowerBound b >= 1)
  ]

qcPropA :: TestTree
qcPropA = testGroup "(checked by quickcheck)"
  [ QC.testProperty "read . show = id" $ \a -> (a :: Approx) == read (show a)
  , QC.testProperty "diameter" $ \a -> diameter a == upperBound a - lowerBound a
  , QC.testProperty "fromDyadic exact and centre" $ \d -> let a = fromDyadic d in exact a && Just d == centre a
  , QC.testProperty "abs negate sign" $ \a -> approximatedBy 0 $ (a ::Approx) + (negate (signum a) * (abs a))
  , QC.testProperty "addition precision" $ \a b -> 
      collect (precision (boundErrorTerm (a+b)) - (precision a `min` precision b))
              True
{-
  , QC.testProperty "multiplication precision" $ \a b -> 
      collect (precision (boundErrorTerm (a*b)) - (precision a `min` precision b))
              True
-}
  , QC.testProperty "a+(-a) contains 0" $ \a -> 0 `approximatedBy` ((a :: Approx) + negate a)
  , QC.testProperty "toApprox r contains r" $ \r -> approximatedBy r $ toApprox 20 (r :: Rational)
  , QC.testProperty "1/(1/a) contains a" $ \a -> not (0 `approximatedBy` a) QC.==> 
                                                       a `better` (1/(1/a))
  , QC.testProperty "a*(1/a) contains 1" $ \a -> not (0 `approximatedBy` a) QC.==> 1 `approximatedBy` (a*(1/a))
  , QC.testProperty "boundErrorTerm" $
    \a -> case a of 
            Approx _ _ _ _ -> let b@(Approx _ _ e _) = boundErrorTerm a
                              in (a `better` b) && (e < 1024) -- 1024 is errorBound when errorBits is 10
            Bottom         -> better a (boundErrorTerm a)
  , QC.testProperty "limitSize" $
    \a -> case a of 
            Approx _ _ _ _ -> let b@(Approx _ _ _ s) = limitSize 2 a
                              in (a `better` b) && (s >= -2)
            Bottom       ->   better a (limitSize 2 a)
  , QC.testProperty "values" $ \a -> let types = a :: Approx in collect a True
  , QC.testProperty "sqr . sqrt" $ \a -> let b = abs a in b `better` sqrA (sqrtA b)
  , QC.testProperty "sqrt . sqr" $ \a -> abs a `better` sqrtA (sqrA a)
  , QC.testProperty "recipA" $ \a -> let b = abs a in 0 `approximatedBy` b || (lowerBound (recipA b) * upperBound b <= 1
                                                                               && upperBound (recipA b) * lowerBound b >= 1)
  ]

creal :: TestTree
creal = testGroup "CReal" [propertiesCR]

genCRApprox :: Gen Approx -> Gen CR
genCRApprox = liftM (CR . pure)

genCR :: Gen CR
genCR = do
  x <- choose (0,1) :: Gen Double
  let (m,_) = decodeFloat x
  s <- choose (-125,-42) :: Gen Int
  s' <- choose (-56,-51) :: Gen Int
  frequency [(2, return . CR . pure $ approxAutoMB m 0 s)
            ,(2, return . CR . pure $ approxAutoMB m 0 s')
            ,(2, return . CR . pure $ approxAutoMB (-m) 0 s)
            ,(2, return . CR . pure $ approxAutoMB (-m) 0 s')
            ,(1, return . CR . pure $ approxAutoMB m 1 s)
            ,(1, return . CR . pure $ approxAutoMB m 1 s')
            ,(1, return . CR . pure $ approxAutoMB (-m) 1 s)
            ,(1, return . CR . pure $ approxAutoMB (-m) 1 s')
            ]

genCROpen :: CR -> CR -> Gen CR
genCROpen a b = do
  x <- choose (0,1) :: Gen Double
  return $ a + (b-a) * fromDoubleAsExactValue x

genCRClosed :: CR -> CR -> Gen CR
genCRClosed a b = do
  x <- choose (0,1) :: Gen Double
  frequency [(96, return $ a + (b-a) * fromDoubleAsExactValue x)
            ,(2, return a)
            ,(2, return b)
            ]

instance Arbitrary CR where
  arbitrary = genCR

instance Show CR where
  show (CR x) = showA . head . getZipList $ x

checkCRN :: Int -> CR -> CR -> Bool
checkCRN n (CR x) (CR y) = and $ zipWith better (take n $ getZipList x) (take n $ getZipList y)

checkFun :: (CR -> CR) -> CR -> Bool
checkFun f x = let a = require 1 x
                   l = lowerA a
                   u = upperA a
                   fx = require 1 (f x)
               in better (require 200 . f . CR . pure $ l) fx &&
                  better (require 200 . f . CR . pure $ u) fx

propertiesCR :: TestTree
propertiesCR = testGroup "Properties of CReal" [scPropCR, qcPropCR]

scPropCR = testGroup "(checked by smallCheck)"
  []

{-
Some of the test should hold for larger domains than tested over. Consider,
for example, atanh . tanh: with an input of -1000, tanh will give essentially
-1 with some error bound. Given that atanh is computed by a logarithm applied
to (x+1)/(x-1), which is essentially 0, we may exhaust memory before a good
enough result is computed.
-}
qcPropCR = testGroup "(checked by quickCheck)"
  [ QC.testProperty "values" $ \x -> let types = x :: CR in collect (show x) True
  , QC.testProperty "Pythagorean identity" $ QC.forAll (genCRClosed (-pi) pi) $ \x -> checkCRN 5 1 ((sin x)^2 + (cos x)^2)
  , QC.testProperty "^2 . sqrt" $ QC.forAll (genCRClosed 0 100) $ \x -> checkCRN 5 x ((sqrt x)^2)
  , QC.testProperty "sqrt . ^2" $ \x -> checkCRN 5 (abs x) (sqrt (x^2))
  , QC.testProperty "log . exp" $ \x -> checkCRN 5 x (log (exp x))
  , QC.testProperty "exp . log" $ QC.forAll (genCROpen 0 100) $ \x -> checkCRN 5 x (exp (log x))
  , QC.testProperty "asin . sin" $ QC.forAll (genCROpen (-pi/2) (pi/2)) $ \x -> checkCRN 5 x (asin (sin x))
  , QC.testProperty "sin . asin" $ QC.forAll (genCROpen (-1) 1) $ \x -> checkCRN 5 x (sin (asin x))
  , QC.testProperty "acos . cos" $ QC.forAll (genCROpen 0 pi) $ \x -> checkCRN 5 x (acos (cos x))
  , QC.testProperty "cos . acos" $ QC.forAll (genCROpen (-1) 1) $ \x -> checkCRN 5 x (cos (acos x))
  , QC.testProperty "atan . tan" $ QC.forAll (genCROpen (-pi/2) (pi/2)) $ \x -> checkCRN 5 x (atan (tan x))
  , QC.testProperty "tan . atan" $ \x -> checkCRN 1 x (tan (atan x))
  , QC.testProperty "asinh . sinh" $ \x -> checkCRN 5 x (asinh (sinh x))
  , QC.testProperty "sinh . asinh" $ \x -> checkCRN 5 x (sinh (asinh x))
  , QC.testProperty "acosh . cosh" $ QC.forAll (genCRClosed 0 10) $ \x -> checkCRN 5 x (acosh (cosh x))
  , QC.testProperty "cosh . acosh" $ QC.forAll (genCRClosed 1 10) $ \x -> checkCRN 5 x (cosh (acosh x))
  , QC.testProperty "atanh . tanh" $ QC.forAll (genCRClosed (-100) 100) $ \x -> checkCRN 5 x (atanh (tanh x))
  , QC.testProperty "tanh . atanh" $ QC.forAll (genCROpen (-1) 1) $ \x -> checkCRN 5 x (tanh (atanh x))
  , QC.testProperty "recip . recip" $ \x -> checkCRN 5 x (1/(1/x))
  , QC.testProperty "sqrt" $ QC.forAll (genCRClosed 0 100) $ checkFun sqrt
  , QC.testProperty "sin" $ checkFun sin
  , QC.testProperty "cos" $ checkFun cos
  , QC.testProperty "tan" $ checkFun tan
  , QC.testProperty "exp" $ QC.forAll (genCRClosed (-100) 100) $ checkFun exp
  , QC.testProperty "log" $ QC.forAll (genCROpen 0 100) $ checkFun log
  , QC.testProperty "asin" $ QC.forAll (genCRClosed (-1) 1) $ checkFun asin
  , QC.testProperty "acos" $ QC.forAll (genCRClosed (-1) 1) $ checkFun asin
  , QC.testProperty "atan" $ checkFun atan
  , QC.testProperty "sinh" $ QC.forAll (genCRClosed (-100) 100) $ checkFun sinh
  , QC.testProperty "asinh" $ checkFun asinh
  , QC.testProperty "cosh" $ QC.forAll (genCRClosed 0 10) $ checkFun cosh
  , QC.testProperty "acosh" $ QC.forAll (genCRClosed 1 10) $ checkFun acosh
  , QC.testProperty "tanh" $ QC.forAll (genCRClosed (-10) 10) $ checkFun tanh
  , QC.testProperty "atanh" $ QC.forAll (genCROpen (-1) 1) $ checkFun atanh
  ]

unitTests = testGroup "Unit tests"
  [ testCase "showA 1" $ showA (Approx 3 7 2 2) @?= "3~"
  , testCase "showA 2" $ showA (Approx 3 7 2 3) @?= "~~"
  , testCase "showA (Approx 99 2 0)" $ showA (Approx 8 99 2 0) @?= "10~"
  , testCase "sin π/2 = 1 (1000 bits)" $ approximatedBy 1 (require 1000 . sin $ pi/2) @?= True
  ]