-
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
base: master
Are you sure you want to change the base?
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 |
|
||
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. |
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