Skip to content

Commit 0c50276

Browse files
Function space SBP operators (#268)
* first draft for function space operators * some spaces * order alphabetically * don't force basis_function to be passed as vector * allow passing accuracy_order * first (basically) working version * orthonormalization via Gram-Schmidt * add some basic tests * some smaller optimizations * fix import of Options * another fix... * avoid unnecessary anonymous function * use version of spzeros that is compatible with older Julia versions * FunctionSpaceOperator -> function_space_operator * add proper docstring * extend docstring * put constructor function in extension * allow passing options * clarify that the operator is of derivative_order 1 * only Optim has to be loaded * only for Julia 1.9 * remove fallback imports with Requires * use PreallocationTools.jl to reduce allocations * add kwarg derivative-order * add analytical gradient (still much slower than ForwardDiff.jl) * fuse function and gradient evaluation * avoid some type conversions * use div instead of sum * Apply suggestions from code review Co-authored-by: Hendrik Ranocha <[email protected]> * change order of loops * Update ext/SummationByPartsOperatorsOptimForwardDiffExt.jl Co-authored-by: Hendrik Ranocha <[email protected]> * import dot * move optimization_function_and_grad to fix type instability introduced by closure bug * move optimization_function_and_grad! outside (that was it!) * Update ext/SummationByPartsOperatorsOptimForwardDiffExt.jl Co-authored-by: Hendrik Ranocha <[email protected]> * some cleanups * ignore stale dependency PreallocationTools in Aqua * add test with non-equidistant nodes * fix test * remove kwargs x_min and x_max * add experimental feature warning * Update test/matrix_operators_test.jl Co-authored-by: Hendrik Ranocha <[email protected]> * remove l_hat2 since it this case is impossible * remove PreallocationTools again * throw ArgumentError instead of assertion for wrong derivative_order --------- Co-authored-by: Hendrik Ranocha <[email protected]> Co-authored-by: Hendrik Ranocha <[email protected]>
1 parent 96e695d commit 0c50276

7 files changed

+345
-1
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ docs/build
99
docs/src/code_of_conduct.md
1010
docs/src/contributing.md
1111
docs/src/license.md
12+
13+
run
14+
run/*

Project.toml

+4
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ Unrolled = "9602ed7d-8fef-5bc8-8597-8f21381861e8"
2727
BandedMatrices = "aae01518-5342-5314-be14-df237901396f"
2828
DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def"
2929
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
30+
Optim = "429524aa-4258-5aef-a3af-852621145aeb"
3031
StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a"
3132

3233
[extensions]
3334
SummationByPartsOperatorsBandedMatricesExt = "BandedMatrices"
3435
SummationByPartsOperatorsDiffEqCallbacksExt = "DiffEqCallbacks"
3536
SummationByPartsOperatorsForwardDiffExt = "ForwardDiff"
37+
SummationByPartsOperatorsOptimForwardDiffExt = ["Optim", "ForwardDiff"]
3638
SummationByPartsOperatorsStructArraysExt = "StructArrays"
3739

3840
[compat]
@@ -46,6 +48,7 @@ InteractiveUtils = "1"
4648
LinearAlgebra = "1"
4749
LoopVectorization = "0.12.22"
4850
MuladdMacro = "0.2"
51+
Optim = "1"
4952
PolynomialBases = "0.4.15"
5053
PrecompileTools = "1.0.1"
5154
RecursiveArrayTools = "2.11, 3"
@@ -64,4 +67,5 @@ julia = "1.6"
6467
BandedMatrices = "aae01518-5342-5314-be14-df237901396f"
6568
DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def"
6669
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
70+
Optim = "429524aa-4258-5aef-a3af-852621145aeb"
6771
StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
module SummationByPartsOperatorsOptimForwardDiffExt
2+
3+
using Optim: Optim, Options, LBFGS, optimize, minimizer
4+
using ForwardDiff: ForwardDiff
5+
6+
using SummationByPartsOperators: SummationByPartsOperators, GlaubitzNordströmÖffner2023, MatrixDerivativeOperator
7+
using LinearAlgebra: Diagonal, LowerTriangular, dot, diag, norm, mul!
8+
using SparseArrays: spzeros
9+
10+
function SummationByPartsOperators.function_space_operator(basis_functions, nodes::Vector{T},
11+
source::SourceOfCoefficients;
12+
derivative_order = 1, accuracy_order = 0,
13+
options = Options(g_tol = 1e-14, iterations = 10000)) where {T, SourceOfCoefficients}
14+
15+
if derivative_order != 1
16+
throw(ArgumentError("Derivative order $derivative_order not implemented."))
17+
end
18+
sort!(nodes)
19+
weights, D = construct_function_space_operator(basis_functions, nodes, source; options = options)
20+
return MatrixDerivativeOperator(first(nodes), last(nodes), nodes, weights, D, accuracy_order, source)
21+
end
22+
23+
function inner_H1(f, g, f_derivative, g_derivative, nodes)
24+
return sum(f.(nodes) .* g.(nodes) + f_derivative.(nodes) .* g_derivative.(nodes))
25+
end
26+
norm_H1(f, f_derivative, nodes) = sqrt(inner_H1(f, f, f_derivative, f_derivative, nodes))
27+
28+
call_orthonormal_basis_function(A, basis_functions, k, x) = sum([basis_functions[i](x)*A[k, i] for i in 1:k])
29+
30+
# This will orthonormalize the basis functions using the Gram-Schmidt process to reduce the condition
31+
# number of the Vandermonde matrix. The matrix A transfers the old basis functions to the new orthonormalized by
32+
# g(x) = A * f(x), where f(x) is the vector of old basis functions and g(x) is the vector of the new orthonormalized
33+
# basis functions. Analogously, we have g'(x) = A * f'(x).
34+
function orthonormalize_gram_schmidt(basis_functions, basis_functions_derivatives, nodes)
35+
K = length(basis_functions)
36+
37+
A = LowerTriangular(zeros(eltype(nodes), K, K))
38+
39+
basis_functions_orthonormalized = Vector{Function}(undef, K)
40+
basis_functions_orthonormalized_derivatives = Vector{Function}(undef, K)
41+
42+
for k = 1:K
43+
A[k, k] = 1
44+
for j = 1:k-1
45+
g(x) = call_orthonormal_basis_function(A, basis_functions, j, x)
46+
g_derivative(x) = call_orthonormal_basis_function(A, basis_functions_derivatives, j, x)
47+
inner_product = inner_H1(basis_functions[k], g, basis_functions_derivatives[k], g_derivative, nodes)
48+
norm_squared = inner_H1(g, g, g_derivative, g_derivative, nodes)
49+
A[k, :] = A[k, :] - inner_product/norm_squared * A[j, :]
50+
end
51+
52+
basis_functions_orthonormalized[k] = x -> call_orthonormal_basis_function(A, basis_functions, k, x)
53+
basis_functions_orthonormalized_derivatives[k] = x -> call_orthonormal_basis_function(A, basis_functions_derivatives, k, x)
54+
# Normalization
55+
r = norm_H1(basis_functions_orthonormalized[k], basis_functions_orthonormalized_derivatives[k], nodes)
56+
A[k, :] = A[k, :] / r
57+
end
58+
return basis_functions_orthonormalized, basis_functions_orthonormalized_derivatives
59+
end
60+
61+
function vandermonde_matrix(functions, nodes)
62+
N = length(nodes)
63+
K = length(functions)
64+
V = zeros(eltype(nodes), N, K)
65+
for i in 1:N
66+
for j in 1:K
67+
V[i, j] = functions[j](nodes[i])
68+
end
69+
end
70+
return V
71+
end
72+
73+
function create_S(sigma, N)
74+
S = zeros(eltype(sigma), N, N)
75+
set_S!(S, sigma, N)
76+
return S
77+
end
78+
79+
function set_S!(S, sigma, N)
80+
k = 1
81+
for i in 1:N
82+
for j in (i + 1):N
83+
S[i, j] = sigma[k]
84+
S[j, i] = -sigma[k]
85+
k += 1
86+
end
87+
end
88+
end
89+
90+
sig(x) = 1 / (1 + exp(-x))
91+
sig_deriv(x) = sig(x) * (1 - sig(x))
92+
93+
function create_P(rho, x_length)
94+
P = Diagonal(sig.(rho))
95+
P *= x_length / sum(P)
96+
return P
97+
end
98+
99+
function construct_function_space_operator(basis_functions, nodes,
100+
::GlaubitzNordströmÖffner2023;
101+
options = Options(g_tol = 1e-14, iterations = 10000))
102+
K = length(basis_functions)
103+
N = length(nodes)
104+
L = div(N * (N - 1), 2)
105+
basis_functions_derivatives = [x -> ForwardDiff.derivative(basis_functions[i], x) for i in 1:K]
106+
basis_functions_orthonormalized, basis_functions_orthonormalized_derivatives = orthonormalize_gram_schmidt(basis_functions,
107+
basis_functions_derivatives,
108+
nodes)
109+
V = vandermonde_matrix(basis_functions_orthonormalized, nodes)
110+
V_x = vandermonde_matrix(basis_functions_orthonormalized_derivatives, nodes)
111+
# Here, W satisfies W'*W = I
112+
# W = [V; -V_x]
113+
114+
B = spzeros(eltype(nodes), N, N)
115+
B[1, 1] = -1
116+
B[N, N] = 1
117+
118+
R = B * V / 2
119+
x_length = last(nodes) - first(nodes)
120+
S = zeros(eltype(nodes), N, N)
121+
A = zeros(eltype(nodes), N, K)
122+
SV = zeros(eltype(nodes), N, K)
123+
PV_x = zeros(eltype(nodes), N, K)
124+
daij_dsigmak = zeros(eltype(nodes), N, K, L)
125+
daij_drhok = zeros(eltype(nodes), N, K, N)
126+
p = (V, V_x, R, x_length, S, A, SV, PV_x, daij_dsigmak, daij_drhok)
127+
128+
x0 = zeros(L + N)
129+
fg!(F, G, x) = optimization_function_and_grad!(F, G, x, p)
130+
result = optimize(Optim.only_fg!(fg!), x0, LBFGS(), options)
131+
132+
x = minimizer(result)
133+
sigma = x[1:L]
134+
rho = x[(L + 1):end]
135+
S = create_S(sigma, N)
136+
P = create_P(rho, x_length)
137+
weights = diag(P)
138+
Q = S + B/2
139+
D = inv(P) * Q
140+
return weights, D
141+
end
142+
143+
@views function optimization_function_and_grad!(F, G, x, p)
144+
V, V_x, R, x_length, S, A, SV, PV_x, daij_dsigmak, daij_drhok = p
145+
(N, _) = size(R)
146+
L = div(N * (N - 1), 2)
147+
sigma = x[1:L]
148+
rho = x[(L + 1):end]
149+
set_S!(S, sigma, N)
150+
P = create_P(rho, x_length)
151+
mul!(SV, S, V)
152+
mul!(PV_x, P, V_x)
153+
@. A = SV - PV_x + R
154+
if !isnothing(G)
155+
fill!(daij_dsigmak, zero(eltype(daij_dsigmak)))
156+
for k in axes(daij_dsigmak, 3)
157+
for j in axes(daij_dsigmak, 2)
158+
for i in axes(daij_dsigmak, 1)
159+
l_tilde = k + i - N * (i - 1) + div(i * (i - 1), 2)
160+
# same as above, but needs more type conversions
161+
# l_tilde = Int(k + i - (i - 1) * (N - i/2))
162+
if i + 1 <= l_tilde <= N
163+
daij_dsigmak[i, j, k] += V[l_tilde, j]
164+
else
165+
C = N^2 - 3*N + 2*i - 2*k + 1/4
166+
if C >= 0
167+
D = sqrt(C)
168+
D_plus_one_half = D + 0.5
169+
D_plus_one_half_trunc = trunc(D_plus_one_half)
170+
if D_plus_one_half == D_plus_one_half_trunc
171+
int_D_plus_one_half = trunc(Int, D_plus_one_half_trunc)
172+
l_hat = N - int_D_plus_one_half
173+
if 1 <= l_hat <= i - 1
174+
daij_dsigmak[i, j, k] -= V[l_hat, j]
175+
end
176+
end
177+
end
178+
end
179+
end
180+
end
181+
end
182+
sig_rho = sig.(rho)
183+
sig_deriv_rho = sig_deriv.(rho)
184+
sum_sig_rho = sum(sig_rho)
185+
for k in axes(daij_drhok, 3)
186+
for j in axes(daij_drhok, 2)
187+
for i in axes(daij_drhok, 1)
188+
factor1 = x_length * V_x[i, j] / sum_sig_rho^2
189+
factor = factor1 * sig_deriv_rho[k]
190+
if k == i
191+
daij_drhok[i, j, k] = -factor * (sum_sig_rho - sig_rho[k])
192+
else
193+
daij_drhok[i, j, k] = factor * sig_rho[i]
194+
end
195+
end
196+
end
197+
end
198+
for k in axes(daij_dsigmak, 3)
199+
G[k] = 2 * dot(daij_dsigmak[:, :, k], A)
200+
end
201+
for k in axes(daij_drhok, 3)
202+
G[L + k] = 2 * dot(daij_drhok[:, :, k], A)
203+
end
204+
end
205+
if !isnothing(F)
206+
return norm(A)^2
207+
end
208+
end
209+
210+
end # module

src/SummationByPartsOperators.jl

+3-1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ include("fourier_operators.jl")
109109
include("fourier_operators_2d.jl")
110110
include("legendre_operators.jl")
111111
include("matrix_operators.jl")
112+
include("function_space_operators.jl")
112113
include("upwind_operators.jl")
113114
include("SBP_coefficients/MattssonNordström2004.jl")
114115
include("SBP_coefficients/MattssonSvärdNordström2004.jl")
@@ -154,7 +155,7 @@ export periodic_central_derivative_operator, periodic_derivative_operator, deriv
154155
dissipation_operator, var_coef_derivative_operator,
155156
fourier_derivative_operator,
156157
legendre_derivative_operator, legendre_second_derivative_operator,
157-
upwind_operators
158+
upwind_operators, function_space_operator
158159
export UniformMesh1D, UniformPeriodicMesh1D
159160
export couple_continuously, couple_discontinuously
160161
export mul!
@@ -169,6 +170,7 @@ export MattssonNordström2004, MattssonSvärdNordström2004, MattssonSvärdShoey
169170
SharanBradyLivescu2022
170171
export Tadmor1989, MadayTadmor1989, Tadmor1993,
171172
TadmorWaagan2012Standard, TadmorWaagan2012Convergent
173+
export GlaubitzNordströmÖffner2023
172174

173175
export BurgersPeriodicSemidiscretization, BurgersNonperiodicSemidiscretization,
174176
CubicPeriodicSemidiscretization, CubicNonperiodicSemidiscretization,

src/function_space_operators.jl

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
2+
"""
3+
GlaubitzNordströmÖffner2023()
4+
5+
Function space SBP (FSBP) operators given in
6+
- Glaubitz, Nordström, Öffner (2023)
7+
Summation-by-parts operators for general function spaces.
8+
SIAM Journal on Numerical Analysis 61, 2, pp. 733-754.
9+
10+
See also
11+
- Glaubitz, Nordström, Öffner (2024)
12+
An optimization-based construction procedure for function space based
13+
summation-by-parts operators on arbitrary grids.
14+
arXiv, arXiv:2405.08770v1.
15+
16+
See [`function_space_operator`](@ref).
17+
"""
18+
struct GlaubitzNordströmÖffner2023 <: SourceOfCoefficients end
19+
20+
function Base.show(io::IO, source::GlaubitzNordströmÖffner2023)
21+
if get(io, :compact, false)
22+
summary(io, source)
23+
else
24+
print(io,
25+
"Glaubitz, Nordström, Öffner (2023) \n",
26+
" Summation-by-parts operators for general function spaces \n",
27+
" SIAM Journal on Numerical Analysis 61, 2, pp. 733-754. \n",
28+
"See also \n",
29+
" Glaubitz, Nordström, Öffner (2024) \n",
30+
" An optimization-based construction procedure for function \n",
31+
" space based summation-by-parts operators on arbitrary grids \n",
32+
" arXiv, arXiv:2405.08770v1.")
33+
end
34+
end
35+
36+
# This function is extended in the package extension SummationByPartsOperatorsOptimExt
37+
"""
38+
function_space_operator(basis_functions, nodes, source;
39+
derivative_order = 1, accuracy_order = 0,
40+
options = Optim.Options(g_tol = 1e-14, iterations = 10000))
41+
42+
Construct an operator that represents a first-derivative operator in a function space spanned by
43+
the `basis_functions`, which is an iterable of functions. The operator is constructed on the
44+
interval `[x_min, x_max]` with the nodes `nodes`, where `x_min` is taken as the minimal value in
45+
`nodes` and `x_max` the maximal value. Note that the `nodes` will be sorted internally. The
46+
`accuracy_order` is the order of the accuracy of the operator, which can optionally be passed,
47+
but does not have any effect on the operator. The operator is constructed solving an optimization
48+
problem with Optim.jl. You can specify the options for the optimization problem with the `options`
49+
argument, see also the [documentation of Optim.jl](https://julianlsolvers.github.io/Optim.jl/stable/user/config/).
50+
51+
The operator that is returned follows the general interface. Currently, it is wrapped in a
52+
[`MatrixDerivativeOperator`](@ref), but this might change in the future.
53+
In order to use this function, the package `Optim` must be loaded.
54+
55+
See also [`GlaubitzNordströmÖffner2023`](@ref).
56+
57+
!!! compat "Julia 1.9"
58+
This function requires at least Julia 1.9.
59+
60+
!!! warning "Experimental implementation"
61+
This is an experimental feature and may change in future releases.
62+
"""
63+
function function_space_operator end

test/Project.toml

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ BandedMatrices = "aae01518-5342-5314-be14-df237901396f"
44
DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def"
55
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
66
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
7+
Optim = "429524aa-4258-5aef-a3af-852621145aeb"
78
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
89
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
910
SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"
@@ -16,6 +17,7 @@ Aqua = "0.8"
1617
BandedMatrices = "0.15, 0.16, 0.17, 1"
1718
DiffEqCallbacks = "2, 3"
1819
ForwardDiff = "0.10"
20+
Optim = "1"
1921
OrdinaryDiffEq = "5, 6"
2022
SpecialFunctions = "1, 2"
2123
StaticArrays = "1.0"

0 commit comments

Comments
 (0)