-
-
Notifications
You must be signed in to change notification settings - Fork 30
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
add Trapezoidal rule #173
add Trapezoidal rule #173
Conversation
src/Integrals.jl
Outdated
import SciMLBase.IntegralProblem # this is type piracy, and belongs in SciMLBase | ||
function IntegralProblem(y::AbstractArray, lb, ub, args...; kwargs...) | ||
IntegralProblem{false}(y, lb, ub, args...; kwargs...) | ||
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.
Yeah move this over to SciMLBase
src/Integrals.jl
Outdated
function construct_grid(prob, alg, lb, ub, dim) | ||
x = alg.spec | ||
@assert length(ub) == length(lb) == 1 "Multidimensional integration is not supported with the Trapezoidal method" | ||
if x isa Integer | ||
grid = range(lb[1], ub[1], length=x) | ||
else | ||
grid = x | ||
@assert ndims(grid) == 1 "Multidimensional integration is not supported with the Trapezoidal method" | ||
end | ||
|
||
@assert lb[1] ≈ grid[begin] "Lower bound in `IntegralProblem` must coincide with that of the grid" | ||
@assert ub[1] ≈ grid[end] "Upper bound in `IntegralProblem` must coincide with that of the grid" | ||
if is_sampled_problem(prob) | ||
@assert size(prob.f, dim) == length(grid) "Integrand and grid must be of equal length along the integrated dimension" | ||
@assert axes(prob.f, dim) == axes(grid,1) "Grid and integrand array must use same indexing along integrated dimension" | ||
end | ||
return grid | ||
end | ||
|
||
@inline myselectdim(y::AbstractArray{T,dims}, d, i) where {T,dims} = selectdim(y, d, i) | ||
@inline myselectdim(y::AbstractArray{T,1}, _, i) where {T} = @inbounds y[i] | ||
|
||
@inline dimension(::Val{D}) where D = D | ||
function __solvebp_call(prob::IntegralProblem, alg::Trapezoidal{S, D}, sensealg, lb, ub, p; kwargs...) where {S,D} | ||
# since all AbstractRange types are equidistant by design, we can rely on that | ||
@assert prob.batch == 0 | ||
# using `Val`s for dimensionality is required to make `selectdim` not allocate | ||
dim = dimension(D) | ||
p = p | ||
if is_sampled_problem(prob) | ||
@assert alg.spec isa AbstractArray "For pre-sampled problems where the integrand is an array, the integration grid must also be specified by an array." | ||
end | ||
|
||
grid = construct_grid(prob, alg, lb, ub, dim) | ||
|
||
err = Inf64 | ||
if is_sampled_problem(prob) | ||
data = prob.f | ||
# inlining is required in order to not allocate | ||
@inline function integrand(i) | ||
# integrate along dimension `dim`, returning a n-1 dimensional array, or scalar if n=1 | ||
myselectdim(data, dim, i) | ||
end | ||
else | ||
if isinplace(prob) | ||
y = zeros(eltype(lb), prob.nout) | ||
integrand = i -> @inbounds (prob.f(y, grid[i], p); y) | ||
else | ||
integrand = i -> @inbounds prob.f(grid[i], p) | ||
end | ||
end | ||
|
||
firstidx, lastidx = firstindex(grid), lastindex(grid) | ||
|
||
out = integrand(firstidx) | ||
|
||
if isbits(out) | ||
# fast path for equidistant grids | ||
if grid isa AbstractRange | ||
dx = grid[begin+1] - grid[begin] | ||
out /= 2 | ||
for i in (firstidx+1):(lastidx-1) | ||
out += integrand(i) | ||
end | ||
out += integrand(lastidx)/2 | ||
out *= dx | ||
# irregular grids: | ||
else | ||
out *= (grid[firstidx + 1] - grid[firstidx]) | ||
for i in (firstidx+1):(lastidx-1) | ||
@inbounds out += integrand(i) * (grid[i + 1] - grid[i-1]) | ||
end | ||
out += integrand(lastidx) * (grid[lastidx] - grid[lastidx-1]) | ||
out /= 2 | ||
end | ||
else # same, but inplace, broadcasted | ||
out = copy(out) # to prevent aliasing | ||
if grid isa AbstractRange | ||
dx = grid[begin+1] - grid[begin] | ||
out ./= 2 | ||
for i in (firstidx+1):(lastidx-1) | ||
out .+= integrand(i) | ||
end | ||
out .+= integrand(lastidx) ./ 2 | ||
out .*= dx | ||
else | ||
out .*= (grid[firstidx + 1] - grid[firstidx]) | ||
for i in (firstidx+1):(lastidx-1) | ||
@inbounds out .+= integrand(i) .* (grid[i + 1] - grid[i-1]) | ||
end | ||
out .+= integrand(lastidx) .* (grid[lastidx] - grid[lastidx-1]) | ||
out ./= 2 | ||
end | ||
end | ||
return SciMLBase.build_solution(prob, alg, out, err, retcode = ReturnCode.Success) | ||
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.
This implementation of Trapezoid should be in a separate file trapezoid.jl that implements the method, and is then include
d into the top level file.
src/Integrals.jl
Outdated
if is_sampled_problem(prob) | ||
data = prob.f | ||
# inlining is required in order to not allocate | ||
@inline function integrand(i) |
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.
@inline function integrand(i) | |
integrand = function (i) |
It should be an anonymous function if in an if statement
Just small organizational things, but otherwise looks great! |
Yes, let's get that merged and bumped first, then bump the lower bound and build off of that. |
Though I think more data is actually required, since it may need to know the |
Is it also possible to implement a method to return a vector of integrated values - i.e. from beginning to that point - like in https://github.com/dextorious/NumericalIntegration.jl/blob/master/src/NumericalIntegration.jl#L44? |
Yeah, that is possible of course... We can define a |
Ah so you prefer the x-data to be inside the problem, not the method. I guess that makes sense. So the API should be: x = 0.0:0.1:1.0
y = x.^2
method = Trapezoidal()
problem = DataIntegralProblem(x, y; dim=1) # dim optional
solve(problem, method)
f = x -> x^2
method = Trapezoidal()
problem = IntegralProblem(f, 0.0, 1.0)
solve(problem, method; x = 0.0:0.1:1.0) Do you agree? |
Yes, it's part of the problem definition, so it should be defined in |
To be clear, all |
Alright, so that means we need to put an I will change this in the PR to SciMLBase. EDIT: just did. Will continue this implementation when that is merged. (I dont know how to develop multiple interdependent packages at the same time). |
It sounds like this PR is suggesting 2 things:
Point 1 is novel and should be addressed in the pr. Point 2 could be addressed in a separate pr because it is a generalization of what is already implemented in the FastGaussQuadrature.jl extension. An api for this could be struct QuadratureFunction{Q} <: IntegralAlgorithm
q::Q
n::Int
end which accepts a function, |
Regarding point 2, I agree that integrating a function with the trapezoidal (or any other) quadrature rule is a different problem to that of integrating data. However i do think we should at least try to get the api for the former to be consistent with the current one of this package, so something of the form
The |
Yes that would be nice to have.
Yes |
So given the """
trapz(n::Integer)
Return the weights and nodes on the standard interval [-1,1] of the [trapezoidal
rule](https://en.wikipedia.org/wiki/Trapezoidal_rule).
"""
function trapz(n::Integer)
@assert n > 1
r = range(-1, 1, length=n)
x = collect(r)
halfh = step(r)/2
h = step(r)
w = [ (i == 1) || (i == n) ? halfh : h for i in 1:n ]
return (x, w)
end
struct Trapezoidal <: IntegralAlgorithm end # use this constructor for DataIntegralProblems
Trapezoidal(n) = QuadratureFunction(trapz, n) # use this constructor for IntegralProblems and you could implement The |
I'll follow up with a |
BTW, the name Trapezoidal is already used in OrdinaryDiffEq so we should probably prevent the name clash. |
How about |
👍 I like that suggestion.
Awesome! It's great to see this finally getting some love again. |
This looks great, and i agree this would be a nice way to implement many rules. In the simple cases though, it may be possible to avoid allocating. eg, |
Yes, it is not necessary for |
Sorry for the delay, here is a rewrite of the original code. If approved I can straightforwardly implement also Simpson's rule. The trapezoidal rule for integrating functions can use a combination of this code, and the Let me know how it can be improved. |
Apparently the `eachslice behaviour changed in 1.9 causing tests to fail...
We should also implement the |
@IlianPihlajamaa I'm not sure how to commit to your pr, so see my fork for how to add the |
I realized I would have to make a PR to your branch |
Looks great! If you make a PR to my branch I will merge it! I don't know an easier way to include your work. Do you? |
Yes, see https://github.com/IlianPihlajamaa/Integrals.jl/pull/1 |
add init interface for SampledIntegralProblem
sol3 = solve!(cache) | ||
``` | ||
|
||
For multi-dimensional datasets, the integration dimension can also be changed |
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.
oh that's pretty cool.
function SciMLBase.init(prob::SampledIntegralProblem, | ||
alg::SciMLBase.AbstractIntegralAlgorithm; | ||
kwargs...) | ||
NamedTuple(kwargs) == NamedTuple() || throw(ArgumentError("There are no keyword arguments allowed to `solve`")) |
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.
that's odd, why not? There are many keyword arguments that would go here?
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.
@lxvm is there a specific reason why not, or did you just not expect it to ever be necessary?
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.
The current implementation of the solver doesn't use any keyword arguments, so I didn't want an api with keywords. If future algorithms for sampled integral problems need them I would expect this to change, but there are no convergence criteria for this kind of problem, so I wasn't expecting any
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.
Oh just the sampled data methods. Okay, yeah for now might as well throw. We always try to throw if keyword arguments that are incorrect so this is good.
Just a few comments, other than that I think it's pretty ready to go.
Where is this? I didn't see a piracy. |
It's already in SciMLBase |
Co-authored-by: Christopher Rackauckas <[email protected]>
oh it wasn't checked 😅 |
Co-authored-by: Christopher Rackauckas <[email protected]>
Co-authored-by: Christopher Rackauckas <[email protected]>
Yes, my bad :) |
@IlianPihlajamaa What does this PR do about multidimensional grids? We can certainly define a multidimensional trapezoidal rule on regular grids as a product of 1d rules, but what would the API be? Would the user pass in a If we don't want to support this then should we check |
The constructor in SciMLBase already guarantees that |
Thanks, that sounds good. I agree that multidimensional domains should be handled separately, and the user would probably have to supply some extra information about the domain. Congrats on the very nice PR! |
!! Not yet ready to merge !!
EDIT: I think it's ready now!
To do:
I have added a trapezoidal rule for integrating sampled data. Let me know what you think. If/once you agree with the code, I will write docs (and add Simpson's as well in the same way). Right now, I only have a docstring for
Trapezoidal
.Additionally, I have some duplicate code for in- and out-of-place operations, that I don't really know how to combine. See lines 224-241 of Integrals.jl, do you have an idea how to streamline such code?
The trapezoidal rule supports integration of pre-sampled data, stored in an array, as well as integration of
functions. It does not support batching or integration over multidimensional spaces.
To use the trapezoidal rule to integrate a function on a regular grid with
n
points:To use the trapezoidal rule to integrate a function on an predefined irregular grid, see the following example.
Note that the lower and upper bound of integration must coincide with the first and last element of the grid.
To use the Trapezoidal rule to integrate a set of sampled data, see the following example.
By default, the integration occurs over the first dimension of the input array.
In order to make integration over arrays work, I needed to add a method (because
isinplace(f::AbstractArray)
is not defined:I think this belongs in SciMLBase. I will make a PR there too if you agree this is the right approach.