packages feed

symtegration-0.6.1: test/Symtegration/FiniteDouble.hs

-- |
-- Description: A variant of 'Double' without infinities or multiple zeroes.
-- Copyright: Copyright 2024 Yoo Chung
-- License: Apache-2.0
-- Maintainer: dev@chungyc.org
--
-- 'FiniteDouble' is a variant of 'Double' which avoids sensitivities
-- which result in what would otherwise be equivalent mathematical functions
-- result in significantly different results.  Basically, it ensures that
-- any finite value resulting from a calculation on finite values does
-- not involve any infinities during the calculation.
--
-- A value of NaN compares equal to any other NaN, which makes it possible
-- to check whether two supposedly equivalent functions both return NaN.
--
-- The functions operating on 'FiniteDouble' are not allowed to return
-- infinities, which prevents seemingly equivalent functions from returning
-- completely different results.  For the same reason, only a positive zero
-- is allowed to be returned.
--
-- These are examples of seemingly equivalent functions which can return
-- significantly different finite results, which 'FiniteDouble' prevents:
--
-- *   @(atan (m / (0 * z)))@ and @(atan (m / 0))@
--
-- *   @d / (0 - (cosh (exp ((logBase f c) / (e * 0)))))@ and
--     @d / (0 - (cosh (exp ((logBase f c) / 0))))@
--
-- *   @tanh (s ** (f / (0 * k)))@ and @tanh (s ** (f / 0))@
module Symtegration.FiniteDouble (FiniteDouble (..), isFinite, Exact (..), Near (..)) where

import Test.QuickCheck

-- | A variant of 'Double' which only allows finite
newtype FiniteDouble = FiniteDouble Double

instance Show FiniteDouble where
  show (FiniteDouble x) = show x

instance Eq FiniteDouble where
  (FiniteDouble x) == (FiniteDouble y)
    | isNaN x && isNaN y = True
    | otherwise = x == y

instance Ord FiniteDouble where
  (FiniteDouble x) <= (FiniteDouble y)
    | isNaN x && isNaN y = True
    | otherwise = x <= y

instance Num FiniteDouble where
  (+) = binOp (+)
  (*) = binOp (*)
  (-) = binOp (-)
  abs = unaryOp abs
  signum = unaryOp signum
  fromInteger n = FiniteDouble $ fromInteger n

instance Fractional FiniteDouble where
  (/) = binOp (/)
  fromRational q = FiniteDouble $ fromRational q

instance Floating FiniteDouble where
  pi = FiniteDouble pi
  exp = unaryOp exp
  log = unaryOp log
  sin = unaryOp sin
  cos = unaryOp cos
  asin = unaryOp asin
  acos = unaryOp acos
  atan = unaryOp atan
  sinh = unaryOp sinh
  cosh = unaryOp cosh
  asinh = unaryOp asinh
  acosh = unaryOp acosh
  atanh = unaryOp atanh
  (**) = binOp (**)

instance Real FiniteDouble where
  toRational (FiniteDouble x) = toRational x

instance RealFrac FiniteDouble where
  properFraction (FiniteDouble x) = (n, FiniteDouble f)
    where
      (n, f) = properFraction x

instance RealFloat FiniteDouble where
  floatRadix (FiniteDouble x) = floatRadix x
  floatDigits (FiniteDouble x) = floatDigits x
  floatRange (FiniteDouble x) = floatRange x
  decodeFloat (FiniteDouble x) = decodeFloat x
  encodeFloat x y = FiniteDouble $ encodeFloat x y
  isNaN (FiniteDouble x) = isNaN x || isInfinite x
  isInfinite (FiniteDouble x) = isInfinite x
  isDenormalized (FiniteDouble x) = isDenormalized x
  isNegativeZero (FiniteDouble x) = isNegativeZero x
  isIEEE (FiniteDouble x) = isIEEE x

instance Arbitrary FiniteDouble where
  arbitrary = FiniteDouble <$> arbitrary
  shrink (FiniteDouble x) = FiniteDouble <$> shrink x

-- | Returns whether a number is finite and not a NaN.
isFinite :: FiniteDouble -> Bool
isFinite (FiniteDouble x)
  | isNaN x = False
  | isInfinite x = False
  | otherwise = True

binOp :: (Double -> Double -> Double) -> FiniteDouble -> FiniteDouble -> FiniteDouble
binOp op (FiniteDouble x) (FiniteDouble y)
  | isInfinite v = FiniteDouble nan
  | -0 <- v = FiniteDouble 0
  | otherwise = FiniteDouble v
  where
    v = x `op` y
    nan = 0 / 0

unaryOp :: (Double -> Double) -> FiniteDouble -> FiniteDouble
unaryOp op (FiniteDouble x)
  | isInfinite v = FiniteDouble nan
  | -0 <- v = FiniteDouble 0
  | otherwise = FiniteDouble v
  where
    v = op x
    nan = 0 / 0

-- | Wrapper type over 'FiniteDouble' so that the same return values are compared as equal.
-- In other words, a NaN compared to a NaN will be considered equal.
-- Used for comparing that two implementations apply the exact same operations.
newtype Exact = Exact FiniteDouble deriving (Show)

instance Eq Exact where
  (Exact x) == (Exact y)
    | isNaN x && isNaN y = True
    | otherwise = x == y

-- | Wrapper type for comparing whether 'FiniteDouble' values are close enough.
-- Intended for testing whether two supposedly equivalent functions return
-- values which are close enough.
newtype Near = Near FiniteDouble deriving (Show)

instance Eq Near where
  (==) (Near (FiniteDouble x)) (Near (FiniteDouble y))
    | isNaN x && isNaN y = True
    | isInfinite x || isInfinite y = x == y
    | x == 0 || y == 0 || x == (-0) || y == (-0) = x - y < threshold
    | otherwise = (x - y) / y < threshold
    where
      threshold = 1e-3

instance Ord Near where
  compare (Near x'@(FiniteDouble x)) (Near y'@(FiniteDouble y))
    | x' == y' = EQ
    | otherwise = compare x y