Skip to content

Commit

Permalink
add trapezoidal rule for sampled data, attempt 2
Browse files Browse the repository at this point in the history
  • Loading branch information
IlianPihlajamaa committed Sep 19, 2023
1 parent 913f7f7 commit aa677c4
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 55 deletions.
1 change: 1 addition & 0 deletions docs/pages.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pages = ["index.md",
"Tutorials" => Any["tutorials/numerical_integrals.md",
"tutorials/differentiating_integrals.md"],
"Basics" => Any["basics/IntegralProblem.md",
"basics/SampledIntegralProblem.md",
"basics/solve.md",
"basics/FAQ.md"],
"Solvers" => Any["solvers/IntegralSolvers.md"],
Expand Down
73 changes: 73 additions & 0 deletions docs/src/basics/SampledIntegralProblem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Integrating pre-sampled data

In some cases, instead of a function that acts as integrand,
one only possesses a list of data points `y` at a set of sampling
locations `x`, that must be integrated. This package contains functionality
for doing that.

## Example

Say, by some means we have generated a dataset `x` and `y`:
```example 1
using Integrals # hide
f = x -> x^2
x = range(0, 1, length=20)
y = f.(x)
```

Now, we can integrate this data set as follows:

```example 1
problem = SampledIntegralProblem(y, x)
method = TrapezoidalRule()
solve(problem, method)
```

The exact aswer is of course \$ 1/3 \$.

## Details

### Non-equidistant grids

If the sampling points `x` are provided as an `AbstractRange`
(constructed with the `range` function for example), faster methods are used that take advantage of
the fact that the points are equidistantly spaced. Otherwise, general methods are used for
non-uniform grids.

Example:

```example 2
using Integrals # hide
f = x -> x^7
x = [0.0; sort(rand(1000)); 1.0]
y = f.(x)
problem = SampledIntegralProblem(y, x)
method = TrapezoidalRule()
solve(problem, method)
```

### Evaluating multiple integrals at once

If the provided data set `y` is a multidimensional array, the integrals are evaluated across only one
of its axes. For performance reasons, the last axis of the array `y` is chosen by default, but this can be modified with the `dim`
keyword argument to the problem definition.

```example 3
using Integrals # hide
f1 = x -> x^2
f2 = x -> x^3
f3 = x -> x^4
x = range(0, 1, length=20)
y = [f1.(x) f2.(x) f3.(x)]
problem = SampledIntegralProblem(y, x; dim=1)
method = TrapezoidalRule()
solve(problem, method)
```

### Supported methods

Right now, only the `TrapezoidalRule` is supported, [see wikipedia](https://en.wikipedia.org/wiki/Trapezoidal_rule).

```@docs
TrapezoidalRule
```
1 change: 1 addition & 0 deletions src/Integrals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ include("init.jl")
include("algorithms.jl")
include("infinity_handling.jl")
include("quadrules.jl")
include("sampled.jl")
include("trapezoidal.jl")

abstract type QuadSensitivityAlg end
Expand Down
15 changes: 14 additions & 1 deletion src/algorithms.jl
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,23 @@ end
TrapezoidalRule
Struct for evaluating an integral via the trapezoidal rule.
Example with sampled data:
```
using Integrals
f = x -> x^2
x = range(0, 1, length=20)
y = f.(x)
problem = SampledIntegralProblem(y, x)
method = TrapezoidalRul()
solve(problem, method)
```
"""
struct TrapezoidalRule <: SciMLBase.AbstractIntegralAlgorithm
end

"""
QuadratureRule(q; n=250)
Expand Down
3 changes: 0 additions & 3 deletions src/common.jl
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,3 @@ function __solvebp_call(cache::IntegralCache, args...; kwargs...)
__solvebp_call(build_problem(cache), args...; kwargs...)
end

@inline _selectdim(y::AbstractArray{T, dims}, d, i) where {T, dims} = selectdim(y, d, i)
@inline _selectdim(y::AbstractArray{T, 1}, _, i) where {T} = @inbounds y[i]
@inline dimension(::Val{D}) where {D} = D
57 changes: 57 additions & 0 deletions src/sampled.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
abstract type AbstractWeights end

# must have field `n` for length, and a field `h` for stepsize
abstract type UniformWeights <: AbstractWeights end
@inline Base.iterate(w::UniformWeights) = (0 == w.n) ? nothing : (w[1], 1)
@inline Base.iterate(w::UniformWeights, i) = (i == w.n) ? nothing : (w[i+1], i+1)
Base.length(w::UniformWeights) = w.n
Base.eltype(w::UniformWeights) = typeof(w.h)
Base.size(w::UniformWeights) = (length(w), )

Check warning on line 9 in src/sampled.jl

View check run for this annotation

Codecov / codecov/patch

src/sampled.jl#L5-L9

Added lines #L5 - L9 were not covered by tests

# must contain field `x` which are the sampling points
abstract type NonuniformWeights <: AbstractWeights end
@inline Base.iterate(w::NonuniformWeights) = (0 == length(w.x)) ? nothing : (w[firstindex(w.x)], firstindex(w.x))
@inline Base.iterate(w::NonuniformWeights, i) = (i == lastindex(w.x)) ? nothing : (w[i+1], i+1)
Base.length(w::NonuniformWeights) = length(w.x)
Base.eltype(w::NonuniformWeights) = eltype(w.x)
Base.size(w::NonuniformWeights) = (length(w), )

Check warning on line 17 in src/sampled.jl

View check run for this annotation

Codecov / codecov/patch

src/sampled.jl#L13-L17

Added lines #L13 - L17 were not covered by tests

_eachslice(data::AbstractArray; dims=ndims(data)) = eachslice(data; dims=dims)
_eachslice(data::AbstractArray{T, 1}; dims=ndims(data)) where T = data


# these can be removed when the Val(dim) is removed from SciMLBase
dimension(::Val{D}) where {D} = D
dimension(D::Int) = D

Check warning on line 25 in src/sampled.jl

View check run for this annotation

Codecov / codecov/patch

src/sampled.jl#L25

Added line #L25 was not covered by tests


function evalrule(data::AbstractArray, weights, dim)
f = _eachslice(data, dims=dim)
firstidx, lastidx = firstindex(f), lastindex(f)
out = f[firstidx]*weights[firstidx]
if isbits(out)
for i in firstidx+1:lastidx
@inbounds out += f[i]*weights[i]
end
else
for i in firstidx+1:lastidx
@inbounds out .+= f[i] .* weights[i]
end
end
return out

end


# can be reused for other sampled rules
function __solvebp_call(prob::SampledIntegralProblem, alg::TrapezoidalRule; kwargs...)
dim = dimension(prob.dim)
err = nothing
data = prob.y
grid = prob.x
weights = find_weights(grid, alg)
I = evalrule(data, weights, dim)
return SciMLBase.build_solution(prob, alg, I, err, retcode = ReturnCode.Success)
end


70 changes: 19 additions & 51 deletions src/trapezoidal.jl
Original file line number Diff line number Diff line change
@@ -1,55 +1,23 @@
function __solvebp_call(prob::SampledIntegralProblem, alg::TrapezoidalRule; kwargs...)
dim = dimension(prob.dim)
err = Inf64
data = prob.y
grid = prob.x
# inlining is required in order to not allocate
integrand = @inline function (i)
# integrate along dimension `dim`, returning a n-1 dimensional array, or scalar if n=1
_selectdim(data, dim, i)
end
struct TrapezoidalUniformWeights <: UniformWeights
n::Int
h::Float64
end

@inline Base.getindex(w::TrapezoidalUniformWeights, i) = ifelse((i == 1) || (i == w.n), w.h*0.5 , w.h)

firstidx, lastidx = firstindex(grid), lastindex(grid)

out = integrand(firstidx)
struct TrapezoidalNonuniformWeights{X<:AbstractArray} <: NonuniformWeights
x::X
end

if isbits(out)
# fast path for equidistant grids
if grid isa AbstractRange
dx = step(grid)
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)
@inline function Base.getindex(w::TrapezoidalNonuniformWeights, i)
x = w.x
(i == firstindex(x)) && return (x[i + 1] - x[i])*0.5
(i == lastindex(x)) && return (x[i] - x[i - 1])*0.5
return (x[i + 1] - x[i - 1])*0.5
end

function find_weights(x::AbstractVector, ::TrapezoidalRule)
x isa AbstractRange && return TrapezoidalUniformWeights(length(x), step(x))
return TrapezoidalNonuniformWeights(x)
end
2 changes: 2 additions & 0 deletions test/sampled_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ using Integrals, Test
# single dimensional y
y = f.(grid)
prob = SampledIntegralProblem(y, grid)
@show solve(prob, method()).u, exact
error = solve(prob, method()).u .- exact
@test all(error .< 10^-4)

# along dim=2
y = f.([grid grid]')
prob = SampledIntegralProblem(y, grid; dim=2)
@show solve(prob, method()).u, exact
error = solve(prob, method()).u .- exact
@test all(error .< 10^-4)
end
Expand Down

0 comments on commit aa677c4

Please sign in to comment.