Input: kernel function . Output "first integral" of :
for . Some small set of values, to be determined, will be supplied at compile time.
will be specified as a subtype of AbstractKernel
.
For some kernel types, is known analytically. (See Table I in Homer's paper.)
may depend on some compile-time numeric parameters, specified as type parameters.
If the integral is not known analytically, we may have to compute it numerically. This will be done at compile time by staged functions, with the numeric integration results used to compute the coefficients of a Chebyshev polynomial fit, which can then be compiled into an efficient polynomial approximation of . However, to compute a Chebyshev approximation for a function defined on , we will have to perform a coordinate transform from , and the type of coordinate transformation will depend on how fast decays asymptotically as . This decay rate can be specified via the type of K
.
The function will be parameterized by a FirstIntegral{K,n}
type parameterized by an AbstractKernel
type K
and an integer n
.
julia> abstract type AbstractKernel end
Any kernel ~ X^P for X βͺ S and ~ X^Q for X β« S
julia> abstract type PowerLawScaling{P,Q,S} <: AbstractKernel end
julia> immutable FirstIntegral{K<:AbstractKernel,N} end
immutable PowerLaw{p} <: PowerLawScaling{p,p,p} end # rα΅ power law
function (::FirstIntegral{PowerLaw{p},n})(X::Number) where {p,n}
return p >= 0 ? X^p / (1 + n + p) : inv(X^(-p) * (1 + n + p))
end
F = FirstIntegral{PowerLaw{-1}, 3}()
F(3.7)
julia> @code_llvm F(3.7) # best to run this one yourself!
In a general multi-physics BEM package, one might conceivably have a user-specified kernel for which the first integral is not known analytically. Performing the integral numerically at runtime would be too expensive, however, especially since the integrand may have an integrable singularity.
Instead, we will perform the integral at compile-time (or, at least, outside the innermost BEM loops) for various and use these values to compute a Chebyshev interpolating polynomial . This polynomial will then be used to generate an efficient .
There are three tricky points:
will probably be singular as , which means we can't fit it directly to a polynomial. (This is not a problem for how is used, because is always used for integration over domains that do not include .) This will be dealt with by requiring the user to specify the degree of the singularity as : i.e. for . We will factor out this singularity from and fit the remaining (non-singular) function to a polynomial.
We need for , whereas polynomial interpolation requires a finite interval, typically . We will handle this by choosing a coordinate mapping . Because such a coordinate mapping is necessarily singular, however, it will screw up the convergence of polynomial interpolation if we choose the wrong degree of singularity β we want a mapping such that is nonsingular (e.g. a low-degree polynomial in ) as . To choose this, the user will specify a degree of the decay rate as , i.e. for .
is dimensionful (it is a physical distance within one of the triangles or other geometric elements of the BEM basis). Mapping it to a dimensionless inevitably involves choosing a scale of . This should be user-specified (e.g. it can be the median diameter of the BEM elements). That is, for and for .
In summary, the user will specify a PowerLawScaling{p,q,s}
kernel type parameterized by p
, q
, and s
. They will also define a (::PowerLawScaling{p,q,s})(r::Number)
method that computes .
The polynomial fit will be performed as follows, assuming and . First, we let , which eliminates the singularity while still having for . Second, we let [or equivalently ], which maps to , and has the property that as , so the coordinate remapping produces a nice degree-1 polynomial. Finally, we fit to a Chebyshev polynomial , and compute via .
The following routines compute the coefficients of a Chebyshev interpolating polynomial for a function on , where are the first-kind Chebyshev polynomials.
We compute these coefficients by first evaluating at the Chebyshev points for , for which the Chebyshev sum is equivalent to a type-III discrete cosine transform (DCT-III), so that the coefficients are computed by a DCT-II. These are not quite the typical Chebyshev points, which correspond to a DCT-I: the difference is that the DCT-I corresponds to the closed interval , i.e. it includes the endpoints, whereas our function may involve terms that blow up at the endpoints (although the overall function should be okay) so we don't want to evaluate it there.
We also provide a function evalcheb
to evaluate for any by a Clenshaw recurrence, and a macro version @evalcheb
(analogous to Base.@horner
) that generates a completely inlined version of this recurrence for the case where is fixed.
julia> #Pkg.add("FFTW");
julia> using FFTW
chebyshev points (order N) on the interval
julia> chebx(N) = [cos(Ο*(n+0.5)/N) for n in 0:N-1]
chebyshev coefficients for vector of values on points
julia> function chebcoef(f::AbstractVector)
a = FFTW.r2r(f, FFTW.REDFT10) / length(f)
a[1] /= 2
return a
end
Given a function and a tolerance, return enough Chebyshev coefficients to reconstruct to that tolerance on
julia> function chebcoef(f, tol=1e-13)
N = 10
local c
while true
x = chebx(N)
c = chebcoef(float[f(y) for y in x])
# look at last 3 coefs, since individual c's might be zero
if maximum(abs,c[end:end-2]) < tol * maximum(abs,c)
break
end
N *= 2
end
vβ = maximum(abs,c) * tol
return c[1:findlast(v -> abs(v) > tol, c)] # shrink to minimum length
end
julia> function chebcoef(f, tol=1e-13)
N = 10
local c
while true
x = chebx(N)
c = chebcoef(Float64[f(y) for y in x])
# look at last 3 coefs, since individual c's might be zero
if maximum(abs,c[end:end-2]) < tol * maximum(abs,c)
break
end
N *= 2
end
vβ = maximum(abs,c) * tol
return c[1:findlast(v -> abs(v) > tol, c)] # shrink to minimum length
end
Given coefficients , evaluate them for in by Clenshaw recurrence
julia> function evalcheb(x, a)
isempty(a) && throw(BoundsError())
-1 β€ x β€ 1 || throw(DomainError())
bβββ = bβββ = zero(x)
for k = length(a):-1:2
bβ = a[k] + 2x*bβββ - bβββ
bβββ = bβββ
bβββ = bβ
end
return a[1] + x*bβββ - bβββ
end
# inlined version of evalcheb given coefficents a, and x in (-1,1)
julia> macro evalcheb(x, a...)
isempty(a) && throw(BoundsError())
# Clenshaw recurrence, evaluated symbolically:
bβββ = bβββ = 0
for k = length(a):-1:2
bβ = esc(a[k])
if bβββ != 0
bβ = :(muladd(t2, $bβββ, $bβ))
end
if bβββ != 0
bβ = :($bβ - $bβββ)
end
bβββ = bβββ
bβββ = bβ
end
ex = esc(a[1])
if bβββ != 0
ex = :(muladd(t, $bβββ, $ex))
end
if bβββ != 0
ex = :($ex - $bβββ)
end
Expr(:block, :(t = $(esc(x))), :(t2 = 2t), ex)
end
Let's try a simple test case: performing Chebyshev interpolation of :
julia> c = chebcoef(exp)
julia> x = linspace(-1,1,100)
julia> maximum(abs.(Float64[evalcheb(y,c) for y in x] - exp.(x))) # the maximum error on [-1,1]
# check that the evalcheb macro works
julia> evalcheb(0.1234, c[1:4]) - @evalcheb(0.1234, c[1],c[2],c[3],c[4])
# extract parameters from PowerLawScaling type
julia> pqsPowerLawScaling{p,q,s}(::PowerLawScaling{p,q,s}) = (p,q,s)
Extract parameters from PowerLawScaling type
julia> pqsPowerLawScaling{p,q,s}(::PowerLawScaling{p,q,s}) = (p,q,s)
julia> @generated function (::FirstIntegral{P,n}, X::Real) where {P<:PowerLawScaling,n}
# compute the Chebyshev coefficients (of the rescaled π¦β as described above)
K = P()
p,q,s = pqsPowerLawScaling(K)
π¦β = X -> quadgk(w -> w^n * K(w*X), 0,1, abstol=1e-12, reltol=1e-10)[1]
Lβ = p < 0 ? X -> π¦β(X) / (s^p + X^p) : π¦β # scale out X βͺ s singularity
q > 0 && throw(DomainError()) # don't know how to deal with growing kernels
qinv = 1/q
c = chebcoef(ΞΎ -> Lβ((1-ΞΎ)^qinv - 2^qinv), 1e-9)
# return an expression that inlines the evaluation of π¦β via C(ΞΎ)
quote
X <= 0 && throw(DomainError())
ΞΎ = 1 - (X + $(2^qinv))^$q
C = @evalcheb ΞΎ $(c...)
return $p < 0 ? C * (X^$p + $(s^p)) : C
end
end
A simple example where the result is known analytically:
julia> immutable DumbPowerLaw{p,s} <: PowerLawScaling{p,p,s}; end # rα΅ power law
julia> (::FirstIntegral{DumbPowerLaw{p,s}})(r) where {p,s} = r^p
julia> F = FirstIntegral{DumbPowerLaw{-1,1.0},3}()
julia> F(3.7)
julia> @code_llvm F(3.7)
julia> #Pkg.add("PyPlot")
julia> using PyPlot
julia> x = [0.01:.0125:1.0;];
julia> plot(x, map(FirstIntegral{DumbPowerLaw{-1,1.}, 3}(),x))
As an added bonus, I should probably include the other integrals included in section 5.7. Here they are:
Notice that a Helmholtz kernel is also a power law kernel:
import GSL
exprel(n, x) = GSL. sf_exprel_n(n, x)
type Helmholtz{k} <: PowerLaw{-1, -1} end # exp(ikr) / 4 pi r
function(::FirstIntegral{Helmholtz{k}, n})(x) where {k,n}
ikx = im * k * x
return exp(ikx) * exprel(n, -ikx)/(4*pi*x)
end
# magnetic field integral equation
abstract type MFIE{k} <: PowerLaw{-3, -3} end # (ikr - 1) * exp(ikr) / 4pi r^3
function (::FirstIntegral{MFIE{k}, n})(x) where {k,n}
ikx = im * k * x
return exp(ikx) * (im * k * exprel(n - 1, -ikx)/((n - 1)*x) - exprel(n - 2, -ikx)/((n-2)*(x^2)) / (4*pi*x)
end