Skip to content

Document package design choices #719

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

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ makedocs(;
pages = [
"Home" => "index.md",
"Overview" => "intro.md",
"Philosophy" => "philosophy.md",
"Manual" => [
"Constructing intervals" => "manual/construction.md",
"Usage" => "manual/usage.md",
Expand Down
45 changes: 24 additions & 21 deletions docs/src/manual/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,37 +91,40 @@ sin(Y)



## Comparisons and set operations
## Comparisons

All comparisons and set operations for `Real` have been purposely disallowed to prevent silent errors. For instance, `x == y` does not implies `x - y == 0` for non-singleton intervals.
If the result of a comparison can be established with guarantee,
it will be return, otherwise, an error is thrown.

```@repl usage
interval(1) < interval(2)
precedes(interval(1), interval(2))
interval(1, 5) < interval(7, 9)
interval(1, 5) < interval(4.99, 9)
interval(1.23) == interval(1.23)
interval(1.23) == interval(4.99, 9)
interval(1.23) == interval(1.2, 1.3)
```

In particular, `if ... else ... end` statements used for floating-points will often break with intervals.

See [Philosophy](@ref) for more details and why this choice was made.


## Set operations

Set operations are all disallowed and error on intervals to avoid ambiguities.
To perform set operations on intervals, use the `*_interval` equivalent explicitly,
e.g. `issubset_interval` instead of `issubset`.


```@repl usage
issubset(interval(1, 2), interval(2))
issubset_interval(interval(1, 2), interval(2))
intersect(interval(1, 2), interval(2))
intersect_interval(interval(1, 2), interval(2))
```

In particular, `if ... else ... end` statements used for floating-points will generally break with intervals.

One can refer to the following:
- `<`: cannot be used with intervals. See instead [`isstrictless`](@ref) or [`strictprecedes`](@ref).
- `==`: allowed if the arguments are singleton intervals, or if at least one argument is not an interval (equivalent to [`isthin`](@ref)). Otherwise, see [`isequal_interval`](@ref).
- `iszero`, `isone`: allowed (equivalent to [`isthinzero`](@ref) and [`isthinone`](@ref) respectively).
- `isinteger`: cannot be used with intervals. See instead [`isthininteger`](ref).
- `isfinite`: cannot be used with intervals. See instead [`isbounded`](@ref).
- `isnan`: cannot be used with intervals. See instead [`isnai`](@ref).
- `in`: allowed if at least one argument is not an interval and the interval argument is a singleton. Otherwise, see [`in_interval`](@ref).
- `issubset`: cannot be used with intervals. See instead [`issubset_interval`](@ref).
- `isdisjoint`: cannot be used with intervals. See instead [`isdisjoint_interval`](@ref).
- `issetequal`: cannot be used with intervals.
- `isempty`: cannot be used with intervals. See instead [`isempty_interval`](@ref).
- `union`: cannot be used with intervals. See instead [`hull`](@ref).
- `intersect`: cannot be used with intervals. See instead [`intersect_interval`](@ref).
- `setdiff`: cannot be used with intervals. See instead [`interiordiff`](@ref).

See [Philosophy](@ref) for more details and why this choice was made.


## Piecewise functions
Expand Down
148 changes: 148 additions & 0 deletions docs/src/philosophy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Philosophy

The goal of the `Interval` type is to be directly used to replace floating point
number in arbitrary julia code, such that in any calculation,
the resulting intervals are guaranteed to bound the true image of the starting
intervals.

So, essentially, we would like `Interval` to act as numbers and
the julia ecosystem has evolved to use `Real` as the default supertype
for numerical types that are not complex.
Therefore, to ensure the widest compatiblity,
our `Interval` type must be a subtype of `Real`.

Then, for any function `f(x::Real)`,
we want the following to hold for all real `x` in the interval `X`
(note that it holds for **all** real numbers in `X`,
even those that can not be represented as floating point numbers):
```math
f(x) \in f(X), \qquad \forall x \in X.
```

At first glance, this is reasonable:
all arithmetic operations are well-defined for both real numbers and intervals,
therefore we can use multiple dispatch to define the interval behavior of
operations such has `+`, `/`, `sin` or `log`.
Then a code written for `Real`s can be used as is with `Interval`s.

However, being a `Real` means way more than just being compatible with
arithmetic operations.
`Real`s are also expected to

1. Be compatible with any other `Number` through promotion.
2. Support comparison operations, such as `==` or `<`.
3. Act as a container of a single element,
e.g. `collect(x)` returns a 0-dimensional array containing `x`.

Each of those points lead to specific design choice for `IntervalArithmetic.jl`,
choices that we detail below.


## Compatibility with other `Number`s

In julia it is expected that `1 + 2.2` silently promoted the integer `1`
to a `Float64` to be able to perform the addition.
Following this logic, it means that `0.1 + interval(2.2, 2.3)` should
silently promote `0.1` to an interval.

However, in this case we can not guarantee that `0.1` is known exactly,
because we do not know how it was produced in the first place.
Following the julia convention is thus in contradiction with providing
guaranteed result.

In this case, we choose to be mostly silent,
the information that a non-interval of unknown origin is recorded in the `NG` flag,
but the calculation is not interrupted, and no warning is printed.

For convenience, we provide the [`ExactReal`](@ref) and [`@exact`](@ref) macro
to allow to explicitly mark a number as being exact,
and not produce the `NG` flag when mixed with intervals.


## Comparison operators

We can extend our above definition of the desired behavior for two real numbers
`x` and `y`, and their respective intervals `X` and `Y`.
With this, we want to have, for any function`f`,
for all `x` in `X` and all `y` in `Y`,
``math
f(x, y) \in f(X, Y), \qquad \forall x \in X, y \in Y.
``

With this in mind, an operation such as `==` can easily be defined for intervals

1. If the intervals are disjoints (`X ∩ Y === ∅`), then `X == Y` is `[false]`.
2. If the intervals both contain a single element,
and that element is the same for both,
`X == Y` is `[true]`.
3. Otherwise, we can not conclude anything, and `X == Y` must be `[false, true]`.

Not that we use intervals in all case, because, according to our definition,
the true result must be contained in the returned interval.
However, this is not convenient, as any `if` statement would error when used
with an interval.
Instead, we have opted to return respectively `false` and `true`
for cases 1 and 2, and to immediately error otherwise.

In this way, we can return a more informative error,
but we only do it when the result is ambiguous.

This has a clear cost, however, in that some expected behaviors do not hold.
For example, an `Interval` is not equal to itself.
Comment on lines +90 to +91
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this can be removed. What you wrote above, seems already very clear.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it doesn't hurt to have an example.


```julia> X = interval(1, 2)
[1.0, 2.0]_com

julia> X == X
ERROR: ArgumentError: `==` is purposely not supported when the intervals are overlapping. See instead `isequal_interval`
Stacktrace:
[1] ==(x::Interval{Float64}, y::Interval{Float64})
@ IntervalArithmetic C:\Users\Kolaru\.julia\packages\IntervalArithmetic\XjBhk\src\intervals\real_interface.jl:86
[2] top-level scope
@ REPL[6]:1.
```


## Intervals as sets

We have taken the perspective to always let `Interval`s act as if they were numbers.
Copy link
Member

@OlivierHnt OlivierHnt May 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to avoid the word "number":

"
We have taken the perspective to always let Intervals behave as numerical values.
"


But they are also sets of numbers,
and it would be nice to use all set operations defined in julia on them.

However, `Real` are also sets. For example, the following is valid

```julia
julia> 3 in 3
true
```

Then what should `3 in interval(2, 6)` do?

For interval as a set, it is clearly `true`.
But for intervals as a subtype of `Real` this is equivalent to
```julia
3 == interval(2, 6)
```
which must either be false (they are not the same things),
or error as the result can not be established.

To be safe, we decided to go one step further and disable
**all** set operations from julia `Base` on intervals.
These operations can instead be performed with the specific `*_interval` function,
for example `in_interval` as a replacement for `in`,
except for `setdiff`.
We can not meaningfully define the set difference of two intervals,
because our intervals are always closed,
while the result of `setdiff` can be open.


# Summary

| | Functions | Behavior | Note |
| :---- | :---- | :---- | :---- |
| Arithmetic operations | `+`, `-`, `*`, `/`, `^` | Interval extension | Produce the `NG` flag when mixed with non-interval |
| Other numeric function | `sin`, `exp`, `sqrt`, etc. | Interval extension | |
| Boolean operations | `==`, `<`, `<=`, `iszero`, `isnan`, `isinteger`, `isfinite` | Error if the result can not be guaranteed to be either `true` or `false` | See [`isequal_interval`](@ref) to test equality of intervals, and [`isbounded`](@ref) to test the finiteness of the elements |
| Set operations | `in`, `issubset`, `isdisjoint`, `issetequal`, `isempty`, `union`, `intersect` | Always error | Use the `*_interval` function instead (e.g. [`in_interval`](@ref))
| Exceptions | `≈`, `setdiff` | Always error | No meaningful interval extension |
9 changes: 9 additions & 0 deletions src/intervals/interval_operations/boolean.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ isequal_interval(x, y, z, w...) = isequal_interval(x, y) & isequal_interval(y, z

isequal_interval(x) = Base.Fix2(isequal_interval, x)

"""
issetequal_interval(x, y)

Return whether the two interval are identical when considered as sets.

Alias of the [`isequal_interval`](@ref) function.
"""
const issetequal_interval = isequal_interval

"""
issubset_interval(x, y)

Expand Down
25 changes: 17 additions & 8 deletions src/intervals/interval_operations/cancellative.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
# IEEE Standard 1788-2015 and required for set-based flavor in Section 10.5.6

"""
cancelminus(x, y)
cancelminus(x, y ; dec = :default)

Compute the unique interval `z` such that `y + z == x`.

The result is decorated by at most `trv` (Section 11.7.1).
The keywork `dec` argument controls the decoration of the result,
it `dec` can be either the decoration of the output,
or a symbol:
- `:default`: if at least one of the input intervals is `ill`,
then the result is `ill`, otherwise it is `trv` (Section 11.7.1).
- `:auto`: the ouptut has the minimal decoration of the inputs.

Implement the `cancelMinus` function of the IEEE Standard 1788-2015 (Section 9.2).
"""
Expand Down Expand Up @@ -36,23 +41,27 @@ function cancelminus(x::BareInterval{T}, y::BareInterval{T}) where {T<:NumTypes}
end
cancelminus(x::BareInterval, y::BareInterval) = cancelminus(promote(x, y)...)

function cancelminus(x::Interval, y::Interval)
function cancelminus(x::Interval, y::Interval ; dec = :default)
r = cancelminus(bareinterval(x), bareinterval(y))
d = min(decoration(x), decoration(y), decoration(r), trv)
t = isguaranteed(x) & isguaranteed(y)
return _unsafe_interval(r, d, t)
return _unsafe_interval(r, set_decoration(dec, x, y, r), t)
end

"""
cancelplus(x, y)
cancelplus(x, y ; dec = :default)

Compute the unique interval `z` such that `y - z == x`; this is semantically
equivalent to `cancelminus(x, -y)`.

The result is decorated by at most `trv` (Section 11.7.1).
The keywork `dec` argument controls the decoration of the result,
it `dec` can be either the decoration of the output,
or a symbol:
- `:default`: if at least one of the input intervals is `ill`,
then the result is `ill`, otherwise it is `trv` (Section 11.7.1).
- `:auto`: the ouptut has the minimal decoration of the inputs.

Implement the `cancelPlus` function of the IEEE Standard 1788-2015 (Section 9.2).
"""
cancelplus(x::BareInterval, y::BareInterval) = cancelminus(x, -y)

cancelplus(x::Interval, y::Interval) = cancelminus(x, -y)
cancelplus(x::Interval, y::Interval ; dec = :default) = cancelminus(x, -y ; dec)
Loading