symtegration-0.6.1: test/Symtegration/ErrorDouble.hs
-- |
-- Description: Floating-point numbers with error ranges.
-- Copyright: Copyright 2024 Yoo Chung
-- License: Apache-2.0
-- Maintainer: dev@chungyc.org
--
-- Floating-point numbers with error bars.
-- Basically each number is a pair of 'Double' values denoting a range.
-- These are used to determine whether an expression is too sensitive
-- to small divergences in floating-point computations. By avoiding such
-- expressions, one can avoid situations where a mathematically equivalent
-- reformulation of a mathematical expression can end up with vastly different results.
module Symtegration.ErrorDouble
( DoubleWithError,
sensitiveFunction,
sensitiveExpression,
)
where
import Data.Foldable1 qualified as Foldable1
import Data.List.NonEmpty (NonEmpty (..))
import Data.Text (Text)
import Symtegration.Symbolic
-- | Floating-point number with a range of values simulating floating-point divergences.
newtype DoubleWithError = DE (Double, Double) deriving (Eq, Ord, Show)
-- | Relative size of error to introduce to an individual 'Double' value.
errorSize :: Double
errorSize = 1e-5
-- | The amount of relative error we will tolerate.
errorTolerance :: Double
errorTolerance = 1e-3
-- | Add some error to a 'Double' value.
includeError :: Double -> DoubleWithError
includeError 0 = DE (-errorSize, errorSize)
includeError x
| u <= v = DE (u, v)
| otherwise = DE (v, u)
where
u = x * (1 - errorSize)
v = x * (1 + errorSize)
-- | The relative size of error present in a 'DoubleWithError' value.
relativeError :: DoubleWithError -> Double
relativeError (DE (u, 0)) = abs u
relativeError (DE (0, v)) = abs v
relativeError (DE (u, v)) = abs (u - v) / (abs u + abs v)
-- | Returns whether the given function is sensitive at the given value.
-- I.e., given an error in the value, whether the error will grow too great in the result.
sensitiveFunction :: (DoubleWithError -> DoubleWithError) -> Double -> Bool
sensitiveFunction f x = not isNotSensitive
where
y = f $ includeError x
-- We want to say it is too sensitive if either is NaN, so don't use >= directly.
isNotSensitive = relativeError y < errorTolerance
-- | Returns whether the given expression is sensitive at the given assignment of values.
-- I.e., given an error in the values, whether the error will grow too great in the result.
sensitiveExpression :: Expression -> (Text -> Maybe Double) -> Bool
sensitiveExpression e m = not isNotSensitive
where
y = evaluate e t
t s
| (Just x') <- m s = Just $ includeError x'
| otherwise = Just $ includeError 0
-- We want to say it is too sensitive if either is NaN, so don't use >= directly.
isNotSensitive
| (Just y') <- y = relativeError y' < errorTolerance
| otherwise = False
binOp :: (Double -> Double -> Double) -> DoubleWithError -> DoubleWithError -> DoubleWithError
binOp f (DE (u, v)) (DE (u', v')) = DE (Foldable1.minimum bounds, Foldable1.maximum bounds)
where
bounds = f u u' :| [f u v', f v u', f v v']
unOp :: (Double -> Double) -> DoubleWithError -> DoubleWithError
unOp f (DE (u, v)) = DE (min u' v', max u' v')
where
u' = f u
v' = f v
instance Num DoubleWithError where
(+) = binOp (+)
(-) = binOp (-)
(*) = binOp (*)
negate = unOp negate
abs = unOp abs
signum = unOp signum
fromInteger = includeError . fromInteger
instance Fractional DoubleWithError where
(/) = binOp (/)
recip = unOp recip
fromRational = includeError . fromRational
instance Floating DoubleWithError where
pi = includeError pi
exp = unOp exp
log = unOp log
(**) = binOp (**)
logBase = binOp logBase
sin = unOp sin
cos = unOp cos
tan = unOp tan
asin = unOp asin
acos = unOp acos
atan = unOp atan
sinh = unOp sinh
cosh = unOp cosh
tanh = unOp tanh
asinh = unOp asinh
acosh = unOp acosh
atanh = unOp atanh