Skip to content

Commit b3c5458

Browse files
committed
integral equation solvers
1 parent ab731fd commit b3c5458

31 files changed

Lines changed: 1443 additions & 190 deletions

Project.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ version = "0.2.5"
44

55
[deps]
66
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
7+
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
78
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
9+
NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce"
810
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
911
RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
1012
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
13+
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
14+
SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"
1115
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
1216
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
1317

src/IO/plotsIO.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,18 @@ end
8282
end
8383
end
8484

85+
@recipe function f(Q::NystromMesh)
86+
@series begin
87+
mesh(Q)
88+
end
89+
@series begin
90+
aspect_ratio --> :equal
91+
label := "Quadrature nodes"
92+
seriestype := :scatter
93+
PlotPoints(), collect(qcoords(Q))
94+
end
95+
end
96+
8597
@recipe function f(msh::GenericMesh)
8698
return msh, domain(msh)
8799
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#=
2+
Functionality related to defining and solving integral equations, with a
3+
strong focus on boundary integral equations
4+
=#
5+
6+
include("nystrommesh.jl")
7+
include("kernels.jl")
8+
include("integralpotential.jl")
9+
include("integraloperator.jl")
10+
include("density.jl")
11+
include("dim.jl")

src/IntegralEquations/adaptive.jl

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#=
2+
Adaptive quadrature technique to compute a sparse correction to integral
3+
operators when the kernel is weakly singular. The technique consists of:
4+
1. Identifying a priorily the entries of the integral operator which need
5+
correction. Those are given by entries whose target point and source element
6+
lie closer than a certain threshold
7+
2. For each element in the source surface, and for each target point close
8+
to that element, compute the matrix entries by performing an adaptive
9+
integration
10+
=#
11+
12+
"""
13+
near_interaction_list_adaptive(X,Y::AbstractMesh; atol)
14+
15+
For each element `el` of type `E` in `Y`, return the indices of the points in `X` which
16+
are closer than `atol` to the `center` of `el`.
17+
18+
This function returns a dictionary where e.g. `Dict[E][5] --> Vector{Int}` gives
19+
the indices of points in `X` which are closer than atol to the center of the
20+
fifth element of type `E`.
21+
"""
22+
function near_interaction_list(X, M::AbstractMesh; atol)
23+
dict = Dict{DataType,Vector{Vector{Int}}}()
24+
for E in keys(M)
25+
pts = [center(el) for el in M[E]]
26+
kdtree = KDTree(pts)
27+
push!(dict, E => inrange(kdtree, X, atol))
28+
end
29+
return dict
30+
end
31+
32+
function adaptive_correction(iop::IntegralOperator)
33+
X, Y = target_surface(iop), source_surface(iop)
34+
# maximum element size
35+
lmax = -Inf
36+
dict_near = near_interaction_list(dofs(X), Y; atol=0.1)
37+
return
38+
end
39+
40+
function assemble_gk(iop; compress=Matrix)
41+
X, Y = target_surface(iop), source_surface(iop)
42+
@timeit_debug "assemble dense part" begin
43+
out = compress(iop)
44+
end
45+
@timeit_debug "compute near interaction list" begin
46+
dict_near = near_interaction_list(dofs(X), Y; atol=0.1)
47+
end
48+
@timeit_debug "compute singular correction" begin
49+
correction = singular_weights_gk(iop, dict_near)
50+
end
51+
return axpy!(1, correction, out) # out <-- out + correction
52+
end
53+
54+
function singular_weights_gk(iop::IntegralOperator, dict_near)
55+
X, Y = target_surface(iop), source_surface(iop)
56+
T = eltype(iop)
57+
Is = Int[]
58+
Js = Int[]
59+
Vs = T[]
60+
# loop over all integration elements by types
61+
for E in keys(Y)
62+
iter = ElementIterator(Y, E)
63+
qreg = Y.etype2qrule[E]
64+
lag_basis = lagrange_basis(qnodes(qreg))
65+
@timeit_debug "singular weights" begin
66+
_singular_weights_gk!(Is, Js, Vs, iop, iter, dict_near, lag_basis, qreg)
67+
end
68+
end
69+
Sp = sparse(Is, Js, Vs, size(iop)...)
70+
return Sp
71+
end
72+
73+
function _singular_weights_gk!(Is, Js, Vs, iop, iter, dict_near, lag_basis, qreg)
74+
yi = qnodes(qreg)
75+
K = kernel(iop)
76+
X = target_surface(iop)
77+
Y = source_surface(iop)
78+
E = eltype(iter)
79+
list_near = dict_near[E]
80+
# Over the lines below we loop over all element, find which target nodes
81+
# need to be regularized, and compute the regularized weights on the source
82+
# points for that target point.
83+
for (n, el) in enumerate(iter) # loop over elements
84+
jglob = Y.elt2dof[E][:, n]
85+
for (i, jloc) in list_near[n] # loop over near targets
86+
ys = yi[jloc]
87+
xdof = dofs(X)[i]
88+
for (m, l) in enumerate(lag_basis) # loop over lagrange basis
89+
val, _ = quadgk(0, ys[1], 1; atol=1e-14) do
90+
y = el(ŷ)
91+
jac = jacobian(el, ŷ)
92+
μ = integration_measure(jac)
93+
ydof = NystromDOF(y, -1.0, jac, -1, -1)
94+
return K(xdof, ydof) * μ * l(ŷ)
95+
end
96+
# push data to sparse matrix
97+
push!(Is, i)
98+
j = jglob[m]
99+
push!(Js, j)
100+
push!(Vs, val - iop[i, j])
101+
end
102+
end
103+
end
104+
return Is, Js, Vs
105+
end

src/IntegralEquations/density.jl

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""
2+
struct Density{T,S} <: AbstractVector{T}
3+
4+
Discrete density with values `vals` on an `AbstractMesh`
5+
"""
6+
struct Density{V,S<:AbstractMesh} <: AbstractVector{V}
7+
vals::Vector{V}
8+
mesh::S
9+
end
10+
11+
# AbstractArray interface
12+
Base.size::Density) = size(vals(σ))
13+
Base.getindex::Density, args...) = getindex(vals(σ), args...)
14+
Base.setindex!::Density, args...) = setindex!(vals(σ), args...)
15+
Base.similar::Density) = Density(similar(vals(σ)), mesh(σ))
16+
17+
vals::Density) = σ.vals
18+
mesh::Density) = σ.mesh
19+
20+
function Density(f::Function, X::NystromMesh)
21+
vals = [f(dof) for dof in qnodes(X)]
22+
return Density(vals, X)
23+
end
24+
25+
function γ₀(f, X::NystromMesh)
26+
return Density(x -> f(coords(x)), X)
27+
end
28+
29+
function γ₁(f, X::NystromMesh)
30+
return Density(dof -> f(coords(dof), normal(dof)), X)
31+
end

src/IntegralEquations/dim.jl

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
Base.@kwdef struct DimParameters
2+
sources_oversample_factor::Float64 = 3
3+
sources_radius_multiplier::Float64 = 5
4+
end
5+
6+
"""
7+
dim_correction(pde,X,Y,S,D[;location,p,derivative])
8+
9+
Given a `pde` and a (possibly innacurate) discretizations of its single and
10+
double-layer operators `S` and `D` with domain `Y` and range `X`, compute
11+
corrections `δS` and `δD` such that `S + δS` and `D + δD` are more accurate
12+
approximations of the underlying integral operators.
13+
14+
The following optional keyword arguments are available:
15+
- `location`: whether the target `X` lies on, inside, or outside the domain `Y`.
16+
- `p::DimParameters`: parameters associated with the density interpolation
17+
method
18+
- `derivative`: if true, compute the correction to the adjoint double-layer and
19+
hypersingular operators instead. In this case, `S` and `D` should contain a
20+
discretization of adjoint double-layer and hypersingular operators,
21+
respectively.
22+
"""
23+
function dim_correction(pde, X, Y, S, D; location=:onsurface, p=DimParameters(),
24+
derivative=false)
25+
T = eltype(S)
26+
isvectorial = T <: SMatrix
27+
msg = "unrecognized value for kw `location`: received $location.
28+
Valid options are `:onsurface`, `:inside` and `:outside`."
29+
σ = location === :onsurface ? -0.5 :
30+
location === :inside ? -1 : location === :outside ? 0 : error(msg)
31+
dict_near = nearest_point_to_element(X, Y)
32+
# find first an appropriate set of source points to center the monopoles
33+
qmax = sum(size(etype2qtags(Y, E), 1) for E in keys(Y))
34+
ns = ceil(Int, p.sources_oversample_factor * qmax)
35+
pts = qcoords(Y)
36+
bbox = HyperRectangle(pts)
37+
xc = center(bbox)
38+
r = p.sources_radius_multiplier * radius(bbox)
39+
if ambient_dimension(Y) == 2
40+
xs = uniform_points_circle(ns, r, xc)
41+
elseif ambient_dimension(Y) == 3
42+
xs = fibonnaci_points_sphere(ns, r, xc)
43+
else
44+
notimplemented()
45+
end
46+
# compute traces of monopoles on the source mesh
47+
Xnodes = qnodes(X)
48+
Ynodes = qnodes(Y)
49+
G = SingleLayerKernel(pde, T)
50+
γ₁G = AdjointDoubleLayerKernel(pde, T)
51+
γ₀B = Matrix{T}(undef, length(Ynodes), ns)
52+
γ₁B = Matrix{T}(undef, length(Ynodes), ns)
53+
for k in 1:ns
54+
for j in 1:length(Ynodes)
55+
γ₀B[j, k] = G(Ynodes[j], xs[k])
56+
γ₁B[j, k] = γ₁G(Ynodes[j], xs[k])
57+
end
58+
end
59+
# integrate the monopoles/dipoles over Y with target on X. This is the
60+
# slowest step, and passing a custom S,D can accelerate this computation
61+
Θ = S * γ₁B - D * γ₀B
62+
for k in 1:ns
63+
for i in 1:length(Xnodes)
64+
if derivative
65+
Θ[i, k] += σ * γ₁G(Xnodes[i], xs[k])
66+
else
67+
Θ[i, k] += σ * G(Xnodes[i], xs[k])
68+
end
69+
end
70+
end
71+
# finally compute the corrected weights as sparse matrices
72+
Is, Js, Ss, Ds = Int[], Int[], T[], T[]
73+
dict_near = nearest_point_to_element(X, Y)
74+
for E in keys(Y)
75+
qtags = etype2qtags(Y, E)
76+
near_list = dict_near[E]
77+
nq, ne = size(qtags)
78+
@assert length(near_list) == ne
79+
M = Matrix{T}(undef, 2nq, ns)
80+
for n in 1:ne # loop over elements of type E
81+
# if there is nothing near, skip immediately to next element
82+
isempty(near_list[n]) && continue
83+
# the weights for points in near_list[n] must be corrected when
84+
# integrating over the current element
85+
jglob = @view qtags[:, n]
86+
M0 = @view γ₀B[jglob, :]
87+
M1 = @view γ₁B[jglob, :]
88+
copy!(view(M, 1:nq, :), M0)
89+
copy!(view(M, (nq + 1):(2nq), :), M1)
90+
F = qr!(blockmatrix_to_matrix(M))
91+
for i in near_list[n]
92+
Θi = @view Θ[i:i, :]
93+
W_ = (blockmatrix_to_matrix(Θi) / F.R) * adjoint(F.Q)
94+
W = matrix_to_blockmatrix(W_, T)
95+
for k in 1:nq
96+
push!(Is, i)
97+
push!(Js, jglob[k])
98+
push!(Ss, -W[nq + k]) # single layer corresponds to α=0,β=-1
99+
push!(Ds, W[k]) # double layer corresponds to α=1,β=0
100+
end
101+
end
102+
end
103+
end
104+
δS = sparse(Is, Js, Ss)
105+
δD = sparse(Is, Js, Ds)
106+
return δS, δD
107+
end
108+
109+
"""
110+
nearest_point_to_element(X::NystroMesh,Y::NystromMesh; tol)
111+
112+
For each element `el`, return a list with the indices of all points in `X` for
113+
which `el` is the nearest element. Ignore indices for which the distance exceeds `tol`.
114+
"""
115+
function nearest_point_to_element(X, Y::NystromMesh; tol=Inf)
116+
y = collect(qcoords(Y))
117+
kdtree = KDTree(y)
118+
dict = Dict(j => Int[] for j in 1:length(y))
119+
for i in eachindex(X)
120+
qtag, d = nn(kdtree, X[i])
121+
d > tol || push!(dict[qtag], i)
122+
end
123+
# dict[j] now contains indices in X for which the j quadrature node in Y is
124+
# the closest. Next we reverse the map
125+
etype2nearlist = Dict{DataType,Vector{Vector{Int}}}()
126+
for E in keys(Y)
127+
tags = etype2qtags(Y, E)
128+
nq, ne = size(tags)
129+
etype2nearlist[E] = nearlist = [Int[] for _ in 1:ne]
130+
for j in 1:ne # loop over each element of type E
131+
for q in 1:nq # loop over qnodes in the element
132+
qtag = tags[q, j]
133+
append!(nearlist[j], dict[qtag])
134+
end
135+
end
136+
end
137+
return etype2nearlist
138+
end
139+
function nearest_point_to_element(X::NystromMesh, Y::NystromMesh)
140+
if X === Y
141+
# when both surfaces are the same, the "near points" of an element are
142+
# simply its own quadrature points
143+
dict = Dict{DataType,Vector{Vector{Int}}}()
144+
for E in keys(Y)
145+
idx_dofs = etype2qtags(Y, E)
146+
dict[E] = map(i -> collect(i), eachcol(idx_dofs))
147+
end
148+
else
149+
dict = nearest_point_to_element(collect(qcoords(X)), Y)
150+
end
151+
return dict
152+
end
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""
2+
struct IntegralOperator{T} <: AbstractMatrix{T}
3+
4+
A discrete linear integral operator given by
5+
```math
6+
I[u](x) = \\int_{\\Gamma\\_s} K(x,y)u(y) ds_y, x \\in \\Gamma_{t}
7+
```
8+
where ``\\Gamma_s`` and ``\\Gamma_t`` are the source and target domains, respectively.
9+
"""
10+
struct IntegralOperator{T} <: AbstractMatrix{T}
11+
kernel::AbstractKernel
12+
target_mesh::AbstractMesh
13+
source_mesh::AbstractMesh
14+
end
15+
16+
kernel(iop::IntegralOperator) = iop.kernel
17+
target_mesh(iop::IntegralOperator) = iop.target_mesh
18+
source_mesh(iop::IntegralOperator) = iop.source_mesh
19+
20+
function IntegralOperator(k, X, Y=X)
21+
T = return_type(k)
22+
msg = """IntegralOperator of nonbits being created"""
23+
isbitstype(T) || (@warn msg)
24+
return IntegralOperator{T}(k, X, Y)
25+
end
26+
27+
function Base.size(iop::IntegralOperator)
28+
X = target_mesh(iop)
29+
Y = source_mesh(iop)
30+
return (length(qnodes(X)), length(qnodes(Y)))
31+
end
32+
33+
function Base.getindex(iop::IntegralOperator, i::Integer, j::Integer)
34+
k = kernel(iop)
35+
targets = qnodes(target_mesh(iop))
36+
sources = qnodes(source_mesh(iop))
37+
return k(targets[i], sources[j]) * weight(sources[j])
38+
end
39+
40+
# convenience constructors
41+
SingleLayerOperator(op::AbstractPDE, X, Y=X) = IntegralOperator(SingleLayerKernel(op), X, Y)
42+
DoubleLayerOperator(op::AbstractPDE, X, Y=X) = IntegralOperator(DoubleLayerKernel(op), X, Y)
43+
function AdjointDoubleLayerOperator(op::AbstractPDE, X, Y=X)
44+
return IntegralOperator(AdjointDoubleLayerKernel(op), X, Y)
45+
end
46+
function HyperSingularOperator(op::AbstractPDE, X, Y=X)
47+
return IntegralOperator(HyperSingularKernel(op), X, Y)
48+
end

0 commit comments

Comments
 (0)