-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Define interval arithmetics using broadcasting #55
base: master
Are you sure you want to change the base?
Conversation
So I foresee at least one headache. There's surely an easier demo, but here's one I happened to know: julia> x = 0..3
0..3
julia> 30 in ((((((((x .+ x) .+ x) .+ x) .+ x) .+ x) .+ x) .+ x) .+ x) .+ x
true
julia> y = x ./ 10
0.0..0.3
julia> 3 in ((((((((y .+ y) .+ y) .+ y) .+ y) .+ y) .+ y) .+ y) .+ y) .+ y
false
julia> 0.3 in y
true Lack of associativity with floating-point arithmetic means that some things that should be axiomatic are violated. This is part of why https://github.com/JuliaIntervals/IntervalArithmetic.jl goes to pains to use BigFloats, but of course that also makes that package painfully slow for many applications. This package was originally constructed to be the "non-controversial" core operations involving intervals. Given the problem above, we would have to ask whether introducing this behavior changes that guarantee. Maybe that's OK, but we shouldn't do it lightly. CC @dpsanders |
|
@timholy Isn't it a problem only if something becomes floating-point when you didn't ask? That is to say, if you already have
@dlfivefifty Alternatively, how about defining |
I'm a bit sensitive to this because I ended up spending about a month of my life immersed in the joys of endpoints for ranges (JuliaLang/julia#23194, JuliaLang/julia#18777 among others). I had the same perspective you do, but people didn't like it not being exact (bugs were reported when they were not). That said, given the |
Here's a simpler example: julia> 0.9 ∈ (y .+ y .+ y)
false In fact in IntervalArithmetic.jl we have used Float64 for a long time (unless you explicitly request BigFloat), and a while ago we worked hard to also make that fast, but still with correct rounding (using ErrorfreeArithmetic.jl). Unfortunately the sum of 10 intervals that @timholy used above still takes 85 ns, compared to 3.5ns for this PR (and 1.8 μs for BigFloat intervals -- 20x slower). There doesn't seem to be any reasonable way around the overhead, unfortunately, without messing with the processor's rounding mode, which has its own problems and overhead. Given that there is already a correct implementation available elsewhere, maybe it makes sense to have this non-guaranteed implementation here? But then you are never sure if your result is really correct, which is part / most of the point of interval arithmetic. I do like the broadcasting syntax for interval-as-container. |
Note that julia> 3//10 ∈ y
false |
I'm a total noob when it comes to rigorous interval arithmetic. But is it conceivable to prefer rounding in the opposite direction than IntervalArithmetic.jl? That is to say, is there a situation where you want that the result is guaranteed to not contain incorrect result? I'm asking this because, if you take the definition
My use case is just shifting around two numbers in floating-point precision so I don't need rigorous arithmetic personally. But this does not appear in a tight loop so I don't mind to use more involved definition either. |
I don't think there's a useful situation in which that's true. Consider |
Let me also make it clear that I do think there is room for a fast-and-loose interval arithmetic package that takes floating point at face value; I just think that a package that promises to "get the topology right, and leave it at that" is not the place for such a thing. |
Actually there is a (not very well documented and probably not completely implemented nor tested) "no rounding at all" mode in IntervalArithmetic.jl that satisfies the requirement for a "fast and loose mode". |
Note there is really a need for two different types:
For (2) I would say standard floating point results are found. If someone wants rigorous interval arithmetic they should be using (1) as in IntervalArithmetic.jl. That said, there would be no problem in having this broadcast behaviour implemented outside this package, say a new package IntervalSetsBroadcasting.jl or even just putting it in DomainSets.jl |
My point on the emphasis on the "only if" part is in line with this argument.
I suppose you mean that treating
Note that the second path involves fixing the behavior in julia> Set([1,2]) .+ Set([3,4])
2-element Array{Int64,1}:
6
4 |
The behaviour of broadcasting with |
I'd rather this weren't true in the long run, and we reserved @dpsanders, I learned some stuff about IntervalArithmetic! I'll have to get back to playing with it at some point, it sounds like some of the things I was concerned about are probably not an issue any more. |
This "is it a f(x::Complex) = ...
f(x::Real) = ... Another example is one gets complex rectangles for free if Traits may provide an alternative solution in Julia v2.0 or so, but at the moment that is speculative. |
I am not so sure you can productively separate these. If you write a bit of code thinking about "real Real" numbers, then the following will likely surprise you: julia> x, y = 1..2, 1.5..1.8
([1, 2], [1.5, 1.80001])
julia> x < y, y < x, x == y
(false, false, false) There are no As far as getting traits, I think someone just needs to spend a month on andyferris/Traitor.jl#8. That doesn't help any definitions in Base, unfortunately, but I don't think there are any technical barriers for the package ecosystem to develop a nice trait system. |
The problem is you often need to use interval arithmetic for other package codes. Forcing all packages to use traits to describe real numbers is not realistic, and constructions like In any case, whether its a good idea or not, it's already being done: I think perhaps the long term solution is to make interval an interface, not a type. So perhaps there could be IntervalBase.jl with a few basic functions that other packages implement, and then IntervalSets.jl will be the home of "intervals as sets" and IntervalArithmetic.jl will be the home of "intervals as numbers", with a shared interface: module IntervalBase
"A tuple containing the left and right endpoints of the interval."
function endpoints end
"The left endpoint of the interval."
leftendpoint(d) = endpoints(d)[1]
"The right endpoint of the interval."
rightendpoint(d) = endpoints(d)[2]
"A tuple of `Bool`'s encoding whether the left/right endpoints are closed."
function closedendpoints end
"Is the interval closed at the left endpoint?"
isleftclosed(d) = closedendpoints(d)[1]
"Is the interval closed at the right endpoint?"
isrightclosed(d) = closedendpoints(d)[2]
# open_left and open_right are implemented in terms of closed_* above, so those
# are the only ones that should be implemented for specific intervals
"Is the interval open at the left endpoint?"
isleftopen(d) = !isleftclosed(d)
"Is the interval open at the right endpoint?"
isrightopen(d) = !isrightclosed(d)
# Only closed if closed at both endpoints, and similar for open
isclosed(d) = isleftclosed(d) && isrightclosed(d)
isopen(d) = isleftopen(d) && isrightopen(d)
function infimum end
function supremum end
end |
I know. It's true of ForwardDiff too. I'm just speaking about where we want to go, and less concerned about "what we have now."
In a sense I was originally envisioning that IntervalSets could be the base. IntervalArithmetic defines its own |
WRT to the original question of type piracy (@tkf), let me point out: having another package that "pirates" this one to add arithmetic operations would be a lot like ColorVectorSpace pirating the types defined in ColorTypes and adding arithmetic. I can't remember a single problem that's ever caused. |
Yes I know. I think it didn't work out quite as well as anticipated as IntervalArithmetic.jl doesn't use it, so I'm proposing an even more "bare bones" package, just so there is a common interface (common type hierarchy appears to be impossible). What's unclear is what to do about The end result would be a package like ApproxFun.jl can mix-and-match IntervalSets.jl (for the function domains) and IntervalArithmetic.jl (for computations), using a common interface ( |
Yes, a common interface sounds good in principle. I'm a bit unclear about what would go in it, but if it's practical... |
Basically what I said above: |
In How problematic is it if we pirate |
@dlfivefifty I opened JuliaLang/julia#33282 asking if it makes sense to use broadcasting for set arithmetic. Can you comment there on why you think it is a good way to go, even though the connection to broadcasting with associative data structure is unclear (OK, at least for me)? |
Until JuliaLang/julia#33282 is resolved, how about adding
This would be great. |
If it were just |
The issue is there are two different interval types and |
Ah, yes, of course. |
What do you think about adding custom functions/operators for now? #55 (comment) |
Didn't know that, that's really interesting. Ours also contain the endpoints, of course, but I guess you're saying you "defensively" widen the interval a bit? I guess it comes down to whether you want "safe" or "literal" by default. I bet the |
@tkf, what's the barrier to just using IntervalArithmetic for your use case? |
@timholy It's rather aesthetic one. I just prefer minimalistic dependency if possible and also conceptually I'm using interval-as-a-set rather than interval-as-an-approximation-of-number. I'm using IntervalSets.jl with a few custom operators and I'll keep doing so if this PR is not merged. I just thought I'd upstream my code in case it's useful for others. (Also, rather selfishly, I thought I could get some code review "for free".) |
I view Note that there is a recent package [ For julia> using IntervalArithmetic
julia> x = interval(0, 0.3)
[0, 0.3]
julia> 3//10 ∈ x
false
julia> x = 0..0.3
[0, 0.300001]
julia> @format full
Display parameters:
- format: full
- decorations: false
- significant figures: 6
julia> x
Interval(0.0, 0.30000000000000004)
julia> 3//10 ∈ x
true I have been doubting recently if this is a good idea, since who knows what "the user really wanted". But I certainly believe that there should be some way of doing this using a representation like |
Please see #55 (comment). The reason why I think it's doing interval-as-an-approximation-of-number is that IntervalArithmetic.jl enforces "rounding outward": julia> x = @interval(-5, 5)
[-5, 5]
julia> sign(x)
[-1, 1] Of course, it depends on the definition of calculations with sets. I'm using the definition that works with arbitrary sets, not just intervals:
The answer of |
I agree that the answer is not "correct" for true set operations, but I wouldn't say that's approximating a number (which number is - 1..1 approximating?) Rather, it's just returning the smallest (in the best case) interval that contains all possible results. In this way you can keep everything within the same universe of "calculations with intervals". |
I used the phrase "approximation of number" as I thought that was the main application of IntervalArithmetic.jl. I agree that "calculations with interval" is the best short description. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I gave this a review since you asked. It's beautiful work, as usual!
Not certain we want to merge this, but no harm in looking.
src/interval.jl
Outdated
end | ||
|
||
broadcasted(::typeof(/), d1::AbstractInterval, d2::AbstractInterval) = | ||
MethodError(broadcasted, (/, d1, d2)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
throw
?
# type `AbstractInterval`. | ||
|
||
broadcasted(::typeof(/), i::AbstractInterval, x) = | ||
Interval{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we want
coretype(::Interval{L,R}) where {L,R} = Interval{L,R}
Then this could just be
broadcasted(::typeof(/), i::AbstractInterval, x) = coretype(i)(endpoints(i) ./ x)...)
@timholy Wow, I didn't expect that coming. Thanks a lot! |
I think we can have with the same This is actually what we do with the Regarding the problem of Regarding for example ordering (a property one generally expect from real if a < b
# Do something
else
# Do something else
end likely to silently and unexpectedly fail. |
Ideally ambiguous cases should raise exceptions in comparisons, see also the behavior Boost implements, and what I adapted for NumberIntervals.jl. In any case - if this package is meant for sets, why not use set comparisons, like |
Using broadcasting for set arithmetics is still under discussion JuliaLang/julia#33282. But, if we can do this, a neat way to write this is |
close #32