using ConvolutionInterpolations
using NumericalIntegration # integrate(x, y, rule): Trapezoidal, SimpsonEven, RombergEven
using BSplineKit # interpolate(x, y, BSplineOrder(k)) and integral(spline)
import Dierckx # qualified: Dierckx also exports `integrate`
import ScatteredInterpolation # qualified: also exports `interpolate`
using HCubature, QuadGK # reference integrals, and integration of the RBF interpolant
using Chairmarks, Printf, Random, Plots
default(size = (720, 400), linewidth = 2, framestyle = :box, fontfamily = "sans-serif")
# 1D rules: each maps (x, y) on a uniform grid to the definite integral over [x[1], x[end]]
trap_rule(x, y) = integrate(x, y, Trapezoidal()) # 2nd order
simpson_rule(x, y) = integrate(x, y, SimpsonEven()) # 4th order
romberg_rule(x, y) = Base.CoreLogging.with_logger(Base.CoreLogging.NullLogger()) do # its convergence warnings are not the point here
integrate(x, y, RombergEven()) # high order, needs n = 2^k + 1 points
end
function bspline_rule(k) # order-k B-spline interpolant, exact spline integral
return (x, y) -> begin
S = BSplineKit.interpolate(x, y, BSplineOrder(k))
I = integral(S)
return I(last(x)) - I(first(x))
end
end
function dierckx_rule(x, y) # FITPACK quintic spline, exact spline integral
spl = Dierckx.Spline1D(collect(x), collect(y); k = 5, s = 0.0)
return Dierckx.integrate(spl, first(x), last(x))
end
# a 1D rule lifted to N-D by integrating along one axis at a time
function nested_integral(rule, axes::NTuple{N,<:AbstractVector}, A::AbstractArray{T,N}) where {T,N}
B = A
for d in N:-1:1
x = axes[d]
B = mapslices(v -> rule(x, v), B; dims = d) # integrate every line along axis d
B = dropdims(B; dims = d) # that axis is gone now
end
return B[] # 0-dimensional array -> scalar
end
function conv_query(F, lo, hi) # the integral over the box [lo, hi]
N = length(lo)
total = 0.0
for corner in Iterators.product(ntuple(_ -> (false, true), N)...)
pt = ntuple(d -> corner[d] ? hi[d] : lo[d], N)
sgn = isodd(N - count(corner)) ? -1.0 : 1.0
total += sgn * F(pt...)
end
return total
end
# the convolution interpolant: the iterated antiderivative once, then inclusion–exclusion over the corners
function conv_box_integral(axes::NTuple{N,<:AbstractVector}, A::AbstractArray{T,N}; kernel = :b7) where {T,N}
grid = N == 1 ? axes[1] : axes
F = convolution_interpolation(grid, A; kernel = kernel, derivative = -1)
return conv_query(F, first.(axes), last.(axes))
end
methods = [
"Trapezoid" => (axes, A) -> nested_integral(trap_rule, axes, A),
"Simpson" => (axes, A) -> nested_integral(simpson_rule, axes, A),
"Romberg" => (axes, A) -> nested_integral(romberg_rule, axes, A),
"Dierckx k=5" => (axes, A) -> nested_integral(dierckx_rule, axes, A),
"BSplineKit k=4" => (axes, A) -> nested_integral(bspline_rule(4), axes, A),
"BSplineKit k=8" => (axes, A) -> nested_integral(bspline_rule(8), axes, A),
"ConvInterp :b7" => (axes, A) -> conv_box_integral(axes, A; kernel = :b7),
"ConvInterp :b13" => (axes, A) -> conv_box_integral(axes, A; kernel = :b13),
]
# the test integrands and their reference integrals
g(x) = exp(x) * cos(4x) + 1 / (1 + 16 * (x - 0.4)^2) # smooth, non-polynomial, non-periodic
I_g = quadgk(g, 0.0, 1.0; rtol = 1e-14)[1] # 1D reference
f_sep(p...) = prod(g, p) # separable N-D integrand, reference I_g^N
f2(x, y) = exp(-3 * (x - 0.4)^2 - 2 * (y - 0.6)^2) * cos(4 * x * y + y) # non-separable 2D integrand
I_f2 = hcubature(p -> f2(p[1], p[2]), [0.0, 0.0], [1.0, 1.0]; rtol = 1e-13)[1];