-
Notifications
You must be signed in to change notification settings - Fork 71
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
Implement piecewise function #689
Conversation
Oh nice, looks great! One thing is that there should maybe be a check to prevent overlapping domains. Eg the two following functions do not behave the same, and are essentially ill-defined julia> foo = Piecewise(
interval(-Inf, 0) => x -> -x,
interval(-1, 1) => x -> 2x,
interval(0, Inf) => x -> x ;
continuous = true
)
julia> goo = Piecewise(
interval(-Inf, 0) => x -> -x,
interval(0, Inf) => x -> x,
interval(-1, 1) => x -> 2x ;
continuous = true
) Moreover, Julia> julia> f = Piecewise(
interval(-1, 0) => identity,
interval(1, 2) => identity);
julia> domain(f)
[-1.0, 2.0]_trv
Julia> julia> g = Piecewise(
interval(-1, 0) => identity,
interval(0, 1) => identity);
julia> domain(g) # should it have the decoration `trv`?
[-1.0, 2.0]_trv One question on the implementation. Currently you can only indicate if a struct Piecewise2{N,T<:NTuple{N}}
pieces :: T
continuous :: NTuple{N,Bool}
end
Piecewise2(pieces::NTuple{N}, continuous::Bool) where {N} = Piecewise2(pieces, ntuple(_ -> continuous, Val(N))) This way you do not hit a |
src/piecewise.jl
Outdated
|
||
function (piecewise::Piecewise)(x::Real) | ||
for (region, f) in piecewise.pieces | ||
in_interval(x, region) && return f(x) |
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.
This doesn't seem completely right: you need to return the union of f(x)
over all the pieces that x
overlaps with, I think.
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.
e.g. the line
return reduce(hull, f(intersect_interval(X, region)) for (region, f) ∈ piecewise.pieces)
in my original implementation in the issue you limked to.
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.
This part is the implementation for non-interval real only, so I think it is fine, as it would be a bit awkward to return an interval for non-interval input.
As @OlivierHnt mentioned, this implementation allows ill-defined functions, but I don't think the answer is the interval-ification of everything.
Yes, that reasonable. To do that properly, we would need open and closed intervals (otherwise the boundary points are always included, and thus points on the boundary of pieces are always duplicated). I am wondering if it would be worth to pull intervals from
Actually yes, here you would have a bug if there is a hole in the middle of the pieces. For the decoration, I don't know. Those intervals, really have not much in common with the arithmetic intervals...
I thought about this a bit, I am not strongly against implementing it. It is just a bit awkward that the order of the pieces matter in the input. |
Ah I did not notice the behaviour when evaluating at the boundary point. So it is also order dependent julia> foo = Piecewise(
interval(-Inf, 0) => x -> -x,
interval(0, Inf) => x -> 3 ;
continuous = false
);
julia> foo(0)
0
julia> goo = Piecewise(
interval(0, Inf) => x -> 3,
interval(-Inf, 0) => x -> -x;
continuous = false
);
julia> goo(0)
3 My first impulse would be not to add a dependency on Intervals.jl. One idea could be to introduce yet an other option to specify the behaviour. Eg
Right, so for julia> Piecewise(interval(0, 1) => x -> 1, interval(10, 11) => x -> 2)(interval(5.))
ERROR: ArgumentError: reducing over an empty collection is not allowed; consider supplying `init` to the reducer Maybe I am overlooking something, but I think that the behaviour for not being in the domain should be the same regardless whether you are in the convex hull or not. Eg compare the above error with julia> Piecewise(interval(0, 1) => x -> 1, interval(10, 11) => x -> 2)(interval(-2))
∅_trv
Yes you are right, maybe
Indeed. |
Mhm I gave it a bit more thoughts. Here is an attempt (order independent) struct Piecewise{T<:Tuple}
pieces :: T
# The symbol indicates whether or not the boudnary is included in the interval:
# Symbol(11) means that the interval contains both extremities
# Symbol(10) means that the interval contains only at the left extremity
# Symbol(01) means that the interval contains only at the right extremity
# Symbol(00) means that the interval does not contain either extremity
# TODO: need to normalize `pieces`
# TODO: missing `continuous` (to determine the decoration `def`, or `com`)
end
Piecewise(pieces...) = Piecewise(pieces)
_subdomains(piecewise::Piecewise) = first.(piecewise.pieces)
function _intersect_subdomain(x::Interval, region::Tuple{Interval,Symbol})
y = IntervalArithmetic.setdecoration(intersect_interval(x, region[1]), decoration(x))
if isempty_interval(y)
return y
elseif region[2] === Symbol(11)
return y
elseif region[2] === Symbol(01)
isthin(y, inf(region[1])) && return emptyinterval(y)
return y
elseif region[2] === Symbol(10)
isthin(y, sup(region[1])) && return emptyinterval(y)
return y
else # region[2] === Symbol(00)
isthin(y, inf(region[1])) | isthin(y, sup(region[1])) && return emptyinterval(y)
return y
end
end
function (piecewise::Piecewise)(x::Interval)
subdoms = _subdomains(piecewise)
vals = _intersect_subdomain.(x, subdoms)
all(v -> isempty_interval(v[1]), vals) && return throw(DomainError("piecewise function was called with $x which is outside of its domain $subdoms"))
return _evaluate(piecewise.pieces, vals, nothing)
end
function _evaluate(p, x, r) # maybe not type stable
isempty_interval(x[1][1]) && return _evaluate(Base.tail(p), Base.tail(x), r)
fx = last(p[1])(x[1][1])
return _evaluate(Base.tail(p), Base.tail(x), IntervalArithmetic.setdecoration(hull(fx, r), min(decoration(fx), decoration(r))))
end
function _evaluate(p, x, ::Nothing) # maybe not type stable
isempty_interval(x[1][1]) && return _evaluate(Base.tail(p), Base.tail(x), nothing)
fx = last(p[1])(x[1][1])
return _evaluate(Base.tail(p), Base.tail(x), fx)
end
function _evaluate(p::Tuple{Pair}, x, r) # maybe not type stable
isempty_interval(x[1][1]) && return r
fx = last(p[1])(x[1][1])
return IntervalArithmetic.setdecoration(hull(fx, r), min(decoration(fx), decoration(r)))
end
function _evaluate(p::Tuple{Pair}, x, ::Nothing) # maybe not type stable
isempty_interval(x[1][1]) && return x[1][1]
return last(p[1])(x[1][1])
end then foo = Piecewise((interval(-Inf, 0), Symbol(11)) => x -> -x,
(interval( 0, Inf), Symbol(01)) => x -> interval(3));
foo(interval(-1, 0))
foo(interval(-1, 1)) # decoration is wrong, currently not implemented
foo(interval(0, 1))
foo(interval(0))
goo = Piecewise((interval(0, 1), Symbol(00)) => x -> interval(1),
(interval(2, 3), Symbol(01)) => x -> interval(3));
goo(interval(0)) # now it throws
goo(interval(1, 2)) # now it throws
goo(interval(2, 3)) |
I will try to implement the ForwardDiff extension for this, it may give some directions as to how to to handle the boundary points. |
In the current version (which still has a bug in the derivative computation), I am using To avoid confusion, I am aliasing julia> slide = Piecewise(
Domain{Open, Closed}(-Inf, -1) => x -> -2x - 1, # `false, true` means open on the left, closed on the right
Domain{Open, Closed}(-1, 0) => x -> x^2,
Domain{Open, Open}(0, Inf) => Constant(0) ;
continuity = [1, 1]
)
Piecewise function with 3 pieces:
(-Inf .. -1.0] -> var"#135#137"()
(-1 .. 0] -> var"#136#138"()
(0.0 .. Inf) -> Constant{Int64}(0) The domains need to be explicitely disjoint and ordered, which is checked at construction. |
One thing I would like to discuss is how necessary it is to add the dependancy on |
It is not striclty necessary, the only things I import are the following: import Intervals
import Intervals: IntervalSet, less_than_disjoint, Open, Closed
const Domain = Intervals.Interval I have no strong feeling about it, it must just be noted that in this case One advantage of pulling the code out is to avoid weird error showing something went wrong with |
Indeed 😞 Maybe if we use what we have for struct Domain{T<:Symbol,S<:Symbol,R<:NumTypes}
left :: T
right :: S
interval :: Interval{R}
Domain{T,S,R}() = ...
end |
I will try that :) I think that it will probably be better to not wrap an Interval here, just because we don't want any of the complication that come with it, just two numbers, bound types and some set operations is enough. If more is needed we can always provide conversion function to our |
src/piecewise.jl
Outdated
return val1 < val2 | ||
end | ||
|
||
Base.in(x::Real, domain::Domain) = rightof(x, lowerbound(domain)) && leftof(x, upperbound(domain)) |
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.
Mhm maybe we should only export Domain
to the user and not overload Base functions for it.
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.
Why not ? They seem quite natural operation to perform.
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 am not entirely sure 😄. My reasoning is based on maintaining consistency : it feels somewhat inconsistent that we have in_interval
for BareInterval
, while for Domain
, we overload Base.in
. This is maybe a "flaw" or something to be improved, I do not know....
However, for now, it seems more prudent to first implement the necessary internal functionalities and then extend them incrementally on 0.22.x (this way we also avoid having to deal with breaking changes).
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.
A Domain
is a set, and never a number, so we don't have the same ambiguity we have for an Interval
. I can't see how it could go wrong (famous words to regrets, I know ^^).
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.
(famous words to regrets, I know ^^)
😆
A
Domain
is a set, and never a number, so we don't have the same ambiguity we have for anInterval
I agree. Note that I was mentioning BareInterval
(not Interval
) which is not a subtype of Number
as well.
With this new |
For clarity and brevity, I would keep it for a follow up PR. A good question though is: would we then define function with limited domains (e.g. |
Interesting. The main question is probably "how bad" this overhead is. |
src/piecewise.jl
Outdated
struct Piecewise | ||
domain::Vector | ||
fs | ||
continuity::Vector{Int} | ||
singularities::Vector | ||
end |
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.
You probably already thought about it, but I would have imagined using Tuple
here instead of Vector
. I think these are good below length 16, or 32?
But maybe an implementation like
struct Piecewise{M,N,T<:NTuple{M},S<:NTuple{M},R<:NTuple{N}}
domains :: T
functions :: S
continuity_orders :: NTuple{N,Int}
singularities :: R
function Piecewise{M,N,T,S,R}(domains, functions, continuity_orders, singularities) where {M,N,T<:NTuple{M},S<:NTuple{M},R<:NTuple{N}}
@assert M == N+1
# more checks...
return new{M,N,T,S,R}(domains, functions, continuity_orders, singularities)
end
end
is messy, or can hurt the performance?
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.
We can use tuples. It's just a bit messy because domains and functions can different types, so the type has whole tuple type as type parameters. But that's mostly a technicality.
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.
Indeed, I let you decide what you think is best 🙂
I've addressed all you comments and added docstrings and doc , therefore (if the doc builds properly), this is ready for a final review and merging. |
src/piecewise.jl
Outdated
end | ||
|
||
function Piecewise(pairs::Vararg{Pair} ; continuity = fill(-1, length(pairs) - 1)) | ||
pairs = collect(pairs) |
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.
No need to collect
since the code now uses Tuple
src/piecewise.jl
Outdated
function Piecewise( | ||
domains::Vector{<:Domain}, | ||
fs, | ||
continuity::Vector{Int} = fill(-1, length(domains) - 1)) |
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.
Maybe these should be:
function Piecewise(
domains::NTuple{N,Domain},
fs,
continuity::Tuple{Vararg{Int}} = ntuple(i -> -1, Val(N-1))) where {N}
# .....
src/piecewise.jl
Outdated
return Piecewise(Tuple(domains), Tuple(fs), Tuple(continuity), Tuple(singularities)) | ||
end | ||
|
||
function Piecewise(pairs::Vararg{Pair} ; continuity = fill(-1, length(pairs) - 1)) |
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.
Similarly, maybe
function Piecewise(pairs::Vararg{Pair,N} ; continuity = ntuple(i -> -1, Val(N-1))) where {N}
#....
Thank you so much @Kolaru for taking the time to address this issue. This is a great new functionality for IntervalArithmetic! 🥳 Aside from my minor comments, the only remaining item for me is that I am not convinced we should overload julia> 1 in bareinterval(1, 2) # no method defined
ERROR: MethodError: [...]
julia> 1 in interval(1, 2) # it defaults to `Base.in` for `Number`
false
julia> 1 in Domain(:closed, :closed, 1, 2) # this should throw the same error as `1 in bareinterval(1, 2)`
true If we have some (internal?) functions -- say |
For now I have remove the overload of base function for I also just did a little final bit of refactoring. |
LGTM 🎉 |
Implement piecewise function, as described in #655 (and handling decorations).
For example, the
abs
function would be defined asIt also introduce the
Constant
constructor, as a workaround forReturns
from Base: the later can not convert its output to interval when the input is an interval, which causes problems.For example:
It can be used for example to define a step function as