geomancy-0.4.0.0: test/Support/Ball.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{- | Ball arithmetic
Also known as mid-rad interval arithmetic, is an extension of floating-point arithmetic
in which an error bound is attached to each variable.
-}
module Support.Ball
( Ball(..)
, exactly
, balls
, balls44
, widen
, narrow
, isExactly
, sumAnyOrder
, libmUlps
, Outside(..)
, outside
, enclosed
, Disjoint(..)
, disjoint
, consistent
) where
import Data.Foldable (toList)
import Linear (M44)
import Linear.Epsilon (Epsilon(..))
data Ball p = Ball
{ center :: {-# UNPACK #-} !Double
, radius :: {-# UNPACK #-} !Double
}
deriving (Eq, Show)
exactly :: Real p => p -> Ball p
exactly x = Ball (realToFrac x) 0
balls :: (Functor f, Real p) => f p -> f (Ball p)
balls = fmap exactly
balls44 :: Real p => M44 p -> M44 (Ball p)
balls44 = fmap balls
widen :: Ball p -> Ball Double
widen (Ball m r) = Ball m r
narrow :: Ball Double -> Ball Float
narrow (Ball m r)
| r == 0, representable @Float m = Ball m 0
| otherwise = rounded m r
isExactly :: Double -> Ball p -> Bool
isExactly x (Ball m r) = r == 0 && m == x
-- | Assume `sin`, `cos` and `tan` are accurate to 1 ulp.
libmUlps :: Double
libmUlps = 1
unitRoundoff :: forall p. RealFloat p => Double
unitRoundoff = 2 ^^ negate (floatDigits (0 :: p))
smallestSubnormal :: forall p. RealFloat p => Double
smallestSubnormal = encodeFloat 1 (fst (floatRange (0 :: p)) - floatDigits (0 :: p))
representable :: forall p. RealFloat p => Double -> Bool
representable x = realToFrac (realToFrac x :: p) == x
gamma :: Int -> Double -> Double
gamma n u = nu / (1 - nu)
where
nu = fromIntegral n * u
radiusSlack :: Double -> Double
radiusSlack r = r * (1 + 16 * unitRoundoff @Double)
rounded :: forall p. RealFloat p => Double -> Double -> Ball p
rounded m propagated =
Ball m $ radiusSlack (propagated + midpointRounding + simulatedRounding)
where
midpointRounding = unitRoundoff @Double * abs m + smallestSubnormal @Double
simulatedRounding =
unitRoundoff @p * (abs m + propagated + midpointRounding) + smallestSubnormal @p
libmRounded :: forall p. RealFloat p => Double -> Double -> Ball p
libmRounded m propagated =
Ball m $ radiusSlack (propagated + midpointError + simulatedError)
where
midpointError = libmError @Double (abs m)
simulatedError = libmError @p (abs m + propagated + midpointError)
libmError :: forall p. RealFloat p => Double -> Double
libmError magnitude = libmUlps * (2 * unitRoundoff @p * magnitude + smallestSubnormal @p)
twoSum :: Double -> Double -> (Double, Double)
twoSum a b = (s, (a - (s - b')) + (b - b'))
where
s = a + b
b' = s - a
exactProduct :: Double -> Double -> Double -> Bool
exactProduct a b prod =
snd (twoProduct a b) == 0 && toRational a * toRational b == toRational prod
twoProduct :: Double -> Double -> (Double, Double)
twoProduct a b = (p, ((ah * bh - p) + ah * bl + al * bh) + al * bl)
where
p = a * b
(ah, al) = split a
(bh, bl) = split b
split x = (hi, x - hi)
where
c = 134217729 * x
hi = c - (c - x)
plus :: forall p. RealFloat p => Ball p -> Ball p -> Ball p
plus a@(Ball ma ra) b@(Ball mb rb)
| isExactly 0 a = b
| isExactly 0 b = a
| ra == 0, rb == 0, roundoff == 0, representable @p s = Ball s 0
| otherwise = rounded s (ra + rb)
where
(s, roundoff) = twoSum ma mb
times :: forall p. RealFloat p => Ball p -> Ball p -> Ball p
times a@(Ball ma ra) b@(Ball mb rb)
| isExactly 0 a || isExactly 0 b = Ball 0 0
| isExactly 1 a = b
| isExactly 1 b = a
| isExactly (-1) a = negate b
| isExactly (-1) b = negate a
| ra == 0, rb == 0, exactProduct ma mb prod, representable @p prod = Ball prod 0
| otherwise = rounded prod (abs ma * rb + abs mb * ra + ra * rb)
where
prod = ma * mb
sumAnyOrder :: forall p. RealFloat p => [Ball p] -> Ball p
sumAnyOrder summands =
case filter (not . isExactly 0) summands of
[] -> Ball 0 0
[single] -> single
nonZero -> anyOrder nonZero
where
anyOrder xs = Ball m $ radiusSlack (propagated + midpointRounding + simulatedRounding)
where
m = sum $ map center xs
propagated = (1 + gamma additions (unitRoundoff @Double)) * sum (map radius xs)
magnitudes = sum $ map (abs . center) xs
additions = length xs - 1
midpointRounding =
gamma additions (unitRoundoff @Double) * magnitudes +
fromIntegral additions * smallestSubnormal @Double
simulatedRounding =
gamma additions (unitRoundoff @p) * (magnitudes + propagated + midpointRounding) +
fromIntegral additions * smallestSubnormal @p
literal :: forall p. RealFloat p => Rational -> Ball p
literal q
| exact = Ball m 0
| otherwise = roundedLiteral m (realToFrac (fromRational q :: p))
where
m = fromRational q
exact = toRational m == q && toRational (fromRational q :: p) == q
roundedLiteral :: Double -> Double -> Ball p
roundedLiteral m simulated =
Ball m $ radiusSlack (max (abs (simulated - m)) midpointRounding)
where
midpointRounding = unitRoundoff @Double * abs m + smallestSubnormal @Double
instance RealFloat p => Num (Ball p) where
(+) = plus
a - b = plus a (negate b)
(*) = times
negate (Ball m r) = Ball (negate m) r
abs (Ball m r) = Ball (abs m) r
signum = error "Ball.signum: no enclosure for a discontinuous function"
fromInteger = literal . fromInteger
instance RealFloat p => Fractional (Ball p) where
a@(Ball ma ra) / b@(Ball mb rb)
| isExactly 1 b = a
| abs mb <= 2 * rb = error "Ball./: divisor is too close to zero to bound the quotient"
| ra == 0, rb == 0, exactQuotient = Ball quotient 0
| otherwise = rounded quotient (((abs quotient + smallestSubnormal @Double) * rb + ra) / (abs mb - rb))
where
quotient = ma / mb
exactQuotient =
toRational quotient == toRational ma / toRational mb && representable @p quotient
fromRational = literal
instance RealFloat p => Floating (Ball p) where
pi = roundedLiteral pi (realToFrac (pi :: p))
sqrt (Ball m r)
| r == 0, exactProduct root root m, representable @p root = Ball root 0
| m > 2 * r = rounded root (r / root)
| m + r < 0 = error "Ball.sqrt: negative argument"
| otherwise = rounded (upper / 2) (upper / 2)
where
root = sqrt m
upper = sqrt (m + r)
sin x@(Ball m r)
| isExactly 0 x = Ball 0 0
| otherwise = libmRounded (sin m) r
cos x@(Ball m r)
| isExactly 0 x = Ball 1 0
| otherwise = libmRounded (cos m) r
tan x
| isExactly 0 x = Ball 0 0
| otherwise = Ball m $ radiusSlack (r + libmError @p (abs m + r))
where
Ball m r = sin (widen x) / cos (widen x)
exp = unsupported "exp"
log = unsupported "log"
asin = unsupported "asin"
acos = unsupported "acos"
atan = unsupported "atan"
sinh = unsupported "sinh"
cosh = unsupported "cosh"
asinh = unsupported "asinh"
acosh = unsupported "acosh"
atanh = unsupported "atanh"
unsupported :: String -> a
unsupported name = error $ "Ball." <> name <> ": no enclosure implemented"
instance (RealFloat p, Epsilon p) => Epsilon (Ball p) where
nearZero (Ball m r)
| nearest == farthest = nearest
| otherwise = error "Ball.nearZero: the ball straddles the threshold"
where
nearest = nearZero (realToFrac (max 0 (abs m - r)) :: p)
farthest = nearZero (realToFrac (abs m + r) :: p)
data Outside p = Outside
{ actual :: p
, enclosure :: Ball p
}
deriving (Eq, Show)
outside :: RealFloat p => Ball p -> p -> Maybe (Outside p)
outside ball@(Ball m r) x
| isNaN x || isInfinite x = Just $ Outside x ball
| abs (realToFrac x - m) <= r = Nothing
| otherwise = Just $ Outside x ball
enclosed :: (Foldable f, Foldable g, RealFloat p) => f (Ball p) -> g p -> [Maybe (Outside p)]
enclosed bs xs = zipWith outside (toList bs) (toList xs)
data Disjoint p = Disjoint (Ball p) (Ball p)
deriving (Eq, Show)
disjoint :: Ball p -> Ball p -> Maybe (Disjoint p)
disjoint a b
| abs (center a - center b) <= radius a + radius b = Nothing
| otherwise = Just $ Disjoint a b
consistent :: (Foldable f, Foldable g) => f (Ball p) -> g (Ball p) -> [Maybe (Disjoint p)]
consistent as bs = zipWith disjoint (toList as) (toList bs)