ekmett / intervals

Interval Arithmetic
http://hackage.haskell.org/package/intervals
BSD 2-Clause "Simplified" License
27 stars 13 forks source link

Interval versions of functions that return NaN #21

Closed dmcclean closed 8 years ago

dmcclean commented 10 years ago

Applying the same conservative :: (a -> a) -> (Interval a -> Interval a) -> Property property that I wrote to test the trig functions, I found some other interesting counterexamples that raise a design question.

I expected that the essence of the interval version of, say, sin, is that it's a conservative approximation of the scalar version. So if you pick a value in the interval x and evaluate sin there, the result will be in the interval sin x. (This isn't the entirety of what we want, since we want the "best" such approximation, but it's a start.)

Our current definitions of some functions fail this test for values that are outside the domain. log (-2) is NaN, and log (-3 ... -1) is NaN ... -Infinity (which seems like a strange beast in its own right...), but NaN is not an element of NaN ... -Infinity because of the way the Ord Double instance works.

For the interval type where Empty is an option, it seems like a good one and the current definitions take it. If NaN could somehow be an element of Empty that would be even better, but I'm not sure if there is a parametric way to do that. So all we would do would be to relax the definition of conservativeness that we have in our heads.

For the non empty interval type it is a bit thornier. Returning whole instead of Empty seems like an option. It's conservative, just not very. ;) Might be more user friendly than an exception, but then again maybe an exception is the right thing to do. Perhaps making NaN an element of the strange interval we return now would be OK, but with the same parametricity thorn. I'm not sure what other Floating instances might be lurking out there that don't have NaNish things or where those definitions would leave them. Rounded? Fixed?

dmcclean commented 10 years ago

I changed the definition of log from log (I a b) = (if a > 0 then log a else negInfinity) ... log b to log (I a b) = (if a > 0 then log a else negInfinity) ... (if b > 0 then log b else negInfinity). This is more consistent because it treats intervals with two negative endpoints more like intervals with one negative endpoint.

Then I created a variant of conservative that works when the scalar type is in RealFloat, like this:

let conservativeExceptNaN sf f xs = forAll (choose (inf xs, sup xs)) $ \x -> isNaN (sf x) || (sf x) `elem` (f xs)

I switched the relevant tests from conservative to conservativeExceptNaN.

I feel like this is a decent solution from a test perspective, so the only remaining question is whether we are happy with the design that the interval version of a function f with a limited domain, applied to an interval x that includes values outside that domain, is the hull of f applied to the part of x that is in the domain of f with the nearest value to x that is in the domain of f.

Under this rule, acos (-3 ... -2) is pi ... pi, slightly different from the NaN ... pi that we give now, but more consistent with the pi ... pi that we now give for acos (-3 ... -1).

ekmett commented 10 years ago

Our current definitions of some functions fail this test for values that are outside the domain. log (-2) is NaN, and log (-3 ... -1) is NaN ... -Infinity (which seems like a strange beast in its own right...), but NaN is not an element of NaN ... -Infinity because of the way the Ord Double instance works.

You make a pretty compelling argument there.

ekmett commented 10 years ago

I think if we call a function that returns NaN, our current system has no real way it can provide an interval that would contain the result, so conservativeExceptNaN is as close as we're going to get.

I feel like this is a decent solution from a test perspective, so the only remaining question is whether we are happy with the design that the interval version of a function f with a limited domain, applied to an interval x that includes values outside that domain, is the hull of f applied to the part of x that is in the domain of f with the nearest value to x that is in the domain of f.

How far reaching are the implications of this rule on the API?

dmcclean commented 10 years ago

The existing implementations of log, acos, asin, acosh, and atanh all sometimes return intervals that have NaN as an endpoint. Generally this happens when both endpoints of the argument are outside of the domain, they deal well with just one endpoint outside.

bergey commented 8 years ago

Fixed by https://github.com/ekmett/intervals/pull/36