packages feed

probability-polynomial-1.0.1.0: test/Numeric/Function/PiecewiseSpec.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -Wno-orphans #-}
{-# OPTIONS_GHC -Wno-missing-methods #-}

{-|
Copyright   : Predictable Network Solutions Ltd., 2020-2024
License     : BSD-3-Clause
-}
module Numeric.Function.PiecewiseSpec
    ( spec
    , genInterval
    , genPiecewise
    ) where

import Prelude

import Data.Function.Class
    ( eval
    )
import Numeric.Function.Piecewise
    ( Piecewise
    , fromAscPieces
    , fromInterval
    , intervals
    , toAscPieces
    , translateWith
    , trim
    , zipPointwise
    )
import Test.Hspec
    ( Spec
    , describe
    , it
    )
import Test.QuickCheck
    ( Arbitrary
    , Gen
    , Positive (..)
    , (===)
    , (.&&.)
    , arbitrary
    , frequency
    , listOf
    , property
    )

import qualified Data.Function.Class as Fun

{-----------------------------------------------------------------------------
    Tests
------------------------------------------------------------------------------}
spec :: Spec
spec = do
    describe "Test consistency" $ do
      describe "Linear" $ do
        it "eval . translate" $ property $
            \p y x ->
                evalLinear (translateLinear y p) x
                    ===  evalLinear p (x - y)

      describe "Interval" $ do
        it "member intersect" $ property $
            \x y z ->
                member z (intersect x y)  ===  (member z x && member z y)

    describe "fromInterval" $ do
        it "zero" $ property $
            \(x :: Rational) (o :: Constant) ->
                eval (fromInterval (x, x) o) x  ===  0

        it "intervals" $ property $
            \(x :: Rational) (Positive d) (o :: Constant) ->
                let y = x + d
                in  intervals (fromInterval (x,y) o) === [(x,y)]

        it "eval" $ property $
            \(x :: Rational) (Positive d) (o :: Linear) z ->
                let y = x + d
                    p = fromInterval (x, y) o
                in 
                    eval p z
                        === (if x <= z && z < y then eval o z else 0)

    describe "mergeBy" $ do
        it "(p + negate p) trims to 0" $ property $
            \(p :: Piecewise Linear) ->
                let z = trim (p + negate p)
                in  null (toAscPieces z) === True
                    .&&. eval z 0 === 0

    describe "translateWith" $ do
        it "eval . translate" $ property $
            \(p :: Piecewise Linear) x y ->
                eval (translateWith translateLinear y p) x
                    === eval p (x - y)

    describe "zipPointwise" $ do
        it "intersects intervals" $ property $
            \p (q :: Piecewise Constant) ->
                allIntervals (zipPointwise (+) p q)
                === [ i
                    | ip <- allIntervals p
                    , iq <- allIntervals q
                    , let i = intersect ip iq
                    , i /= Empty
                    ]

        it "eval, +" $ property $
            \p (q :: Piecewise Linear) x ->
                eval (zipPointwise (+) p q) x
                === (eval p x + eval q x)

        it "eval, *" $ property $
            \p (q :: Piecewise Constant) x ->
                eval (zipPointwise (*) p q) x
                === (eval p x * eval q x)

    describe "instance Num (Piecewise Q Constant)" $ do
        it "(+)" $ property $
            \p (q :: Piecewise Constant) x ->
                eval (p + q) x
                === (eval p x + eval q x)

        it "(*)" $ property $
            \p (q :: Piecewise Constant) x ->
                eval (p * q) x
                === (eval p x * eval q x)

        it "negate" $ property $
            \(p :: Piecewise Constant) x ->
                eval (negate p) x
                === negate (eval p x)

        it "abs" $ property $
            \(p :: Piecewise Constant) x ->
                eval (abs p) x
                === abs (eval p x)

        it "signum" $ property $
            \(p :: Piecewise Constant) x ->
                eval (signum p) x
                === signum (eval p x)

{-----------------------------------------------------------------------------
    Helper types
    Constant and linear functions
------------------------------------------------------------------------------}
type Q = Rational

-- | Constant function
newtype Constant = Constant Q
    deriving (Eq, Show)

instance Num Constant where
    Constant a1 + Constant a2 = Constant (a1 + a2)
    Constant a1 * Constant a2 = Constant (a1 * a2)
    negate (Constant a) = Constant (negate a)
    abs (Constant a) = Constant (abs a)
    signum (Constant a) = Constant (signum a)
    fromInteger n = Constant (fromInteger n)

instance Fun.Function Constant where
    type instance Domain Constant = Q
    type instance Codomain Constant = Q
    eval (Constant a) _ = a

-- | Linear function with a constant and a slope
data Linear = Linear Q Q
    deriving (Eq, Show)

instance Num Linear where
    Linear a1 b1 + Linear a2 b2 = Linear (a1 + a2) (b1 + b2)
    negate (Linear a b) = Linear (negate a) (negate b)
    fromInteger n = Linear 0 (fromInteger n)

instance Fun.Function Linear where
    type instance Domain Linear = Q
    type instance Codomain Linear = Q
    eval = evalLinear

translateLinear :: Q -> Linear -> Linear
translateLinear y (Linear a b) = Linear a (b - a*y)

evalLinear :: Linear -> Q -> Q
evalLinear (Linear a b) x = a*x + b

{-----------------------------------------------------------------------------
    Helper types
    Intervals
------------------------------------------------------------------------------}
-- | Interval on the real number line.
-- This type does not represent all interval types,
-- only those that are relevant to our purposes here.
data Interval
    = All
    | Empty
    | Before Q  -- exclusive
    | After Q   -- inclusive
    | FromTo Q Q
    deriving (Eq, Show)

-- | Definition of membership.
member :: Q -> Interval -> Bool
member _ All = True
member _ Empty = False
member z (Before y) = z < y
member z (After x) = x <= z
member z (FromTo x y) = x <= z && z < y

-- | The intersection of two 'Interval' is again an 'Interval'.
intersect :: Interval -> Interval -> Interval
intersect All x = x
intersect x All = x
intersect Empty _ = Empty
intersect _ Empty = Empty
intersect (Before y1) (Before y2) = Before (min y1 y2)
intersect (Before y1) (After x2) = mkFromTo x2 y1
intersect (Before y1) (FromTo x2 y2) = mkFromTo x2 (min y1 y2)
intersect (After x1) (After x2) = After (max x1 x2)
intersect (After x1) (Before y2) = mkFromTo x1 y2
intersect (After x1) (FromTo x2 y2) = mkFromTo (max x1 x2) y2
intersect (FromTo x1 y1) (Before y2) = mkFromTo x1 (min y1 y2)
intersect (FromTo x1 y1) (After x2) = mkFromTo (max x1 x2) y1
intersect (FromTo x1 y1) (FromTo x2 y2) = mkFromTo (max x1 x2) (min y1 y2)

-- | Smart constructor,
-- returns 'Empty' if the endpoint does not come after the starting point.
mkFromTo :: Q -> Q -> Interval
mkFromTo x y = if x < y then FromTo x y else Empty

-- | Return all intervals, 
allIntervals :: Fun.Domain o ~ Q => Piecewise o -> [Interval]
allIntervals pieces
    | null xs = [All]
    | otherwise = [Before xmin] <> map (uncurry FromTo) is <> [After xmax]
  where
    xs = map fst (toAscPieces pieces)
    is = zip xs (drop 1 xs)
    xmin = minimum xs
    xmax = maximum xs

{-----------------------------------------------------------------------------
    Random generators
------------------------------------------------------------------------------}
instance Arbitrary Constant where
    arbitrary = Constant <$> arbitrary

instance Arbitrary Linear where
    arbitrary = Linear <$> arbitrary <*> arbitrary

genInterval :: Gen (Q,Q)
genInterval = do
    x <- arbitrary
    Positive d <- arbitrary
    pure (x, x + d)

genFromTo :: Gen Interval
genFromTo = uncurry FromTo <$> genInterval

instance Arbitrary Interval where
    arbitrary = frequency
        [ (1, pure All)
        , (1, pure Empty)
        , (3, Before <$> arbitrary)
        , (3, After <$> arbitrary)
        , (20, genFromTo)
        ]

-- | A list of disjoint and sorted elements.
newtype DisjointSorted a = DisjointSorted [a]
    deriving (Eq, Show)

genDisjointSorted :: Gen (DisjointSorted Rational)
genDisjointSorted =
    DisjointSorted . drop 1 . scanl (\s (Positive d) -> s + d) 0
        <$> listOf arbitrary

instance Arbitrary (DisjointSorted Rational) where
    arbitrary = genDisjointSorted

genPiecewise :: Fun.Domain o ~ Rational => Gen o -> Gen (Piecewise o)
genPiecewise gen = do
    DisjointSorted xs <- genDisjointSorted
    os <- mapM (const gen) xs
    pure $ fromAscPieces $ zip xs os

instance
    (Fun.Domain o ~ Rational, Arbitrary o)
    => Arbitrary (Piecewise o)
  where
    arbitrary = genPiecewise arbitrary