Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
trixi-framework
GitHub Repository: trixi-framework/Trixi.jl
Path: blob/main/docs/literate/src/files/differentiable_programming.jl
5591 views
1
#src # Differentiable programming
2
using Test: @test #src
3
4
# [Julia and its ecosystem provide some tools for differentiable programming](https://sinews.siam.org/Details-Page/scientific-machine-learning-how-julia-employs-differentiable-programming-to-do-it-best).
5
# Trixi.jl is designed to be flexible, extendable, and composable with Julia's growing ecosystem for
6
# scientific computing and machine learning. Thus, the ultimate goal is to have fast implementations
7
# that allow automatic differentiation (AD) without too much hassle for users. If some parts do not
8
# meet these requirements, please feel free to open an issue or propose a fix in a PR.
9
10
# In the following, we will walk through some examples demonstrating how to differentiate through
11
# Trixi.jl.
12
13
# ## Forward mode automatic differentiation
14
15
# Trixi.jl integrates well with [ForwardDiff.jl](https://github.com/JuliaDiff/ForwardDiff.jl)
16
# for forward mode AD.
17
18
# ### Computing the Jacobian
19
# The high-level interface to compute the Jacobian this way is [`jacobian_ad_forward`](@ref).
20
# First, we load the required packages and compute the Jacobian of a semidiscretization
21
# of the compressible Euler equations, a system of nonlinear conservation laws.
22
23
using Trixi, LinearAlgebra, Plots
24
25
equations = CompressibleEulerEquations2D(1.4)
26
27
solver = DGSEM(3, flux_central)
28
mesh = TreeMesh((-1.0, -1.0), (1.0, 1.0), initial_refinement_level = 2, n_cells_max = 10^5,
29
periodicity = true)
30
31
semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition_density_wave, solver;
32
boundary_conditions = boundary_condition_periodic)
33
34
J = jacobian_ad_forward(semi);
35
size(J)
36
@test size(J) == (1024, 1024) #src
37
38
# Next, we compute the eigenvalues of the Jacobian.
39
40
λ = eigvals(J)
41
scatter(real.(λ), imag.(λ), label = "central flux")
42
43
# As you can see here, the maximal real part is close to zero.
44
45
relative_maximum = maximum(real, λ) / maximum(abs, λ)
46
@test 3.0e-10 < relative_maximum < 9.0e-10 #src
47
48
# Interestingly, if we add dissipation by switching to the `flux_lax_friedrichs`
49
# at the interfaces, the maximal real part of the eigenvalues increases.
50
51
solver = DGSEM(3, flux_lax_friedrichs)
52
semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition_density_wave, solver;
53
boundary_conditions = boundary_condition_periodic)
54
55
J = jacobian_ad_forward(semi)
56
λ = eigvals(J)
57
58
scatter!(real.(λ), imag.(λ), label = "Lax-Friedrichs flux")
59
60
# Although the maximal real part is still somewhat small, it's larger than for
61
# the purely central discretization.
62
63
relative_maximum = maximum(real, λ) / maximum(abs, λ)
64
@test 1.0e-5 < relative_maximum < 5.0e-5 #src
65
66
# However, we should be careful when using this analysis, since the eigenvectors
67
# are not necessarily well-conditioned.
68
69
λ, V = eigen(J)
70
condition_number = cond(V)
71
@test 1.0e6 < condition_number < 5.0e6 #src
72
73
# In one space dimension, the situation is a bit different.
74
75
equations = CompressibleEulerEquations1D(1.4)
76
77
solver = DGSEM(3, flux_central)
78
mesh = TreeMesh((-1.0,), (1.0,), initial_refinement_level = 6, n_cells_max = 10^5,
79
periodicity = true)
80
81
semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition_density_wave, solver;
82
boundary_conditions = boundary_condition_periodic)
83
84
J = jacobian_ad_forward(semi)
85
86
λ = eigvals(J)
87
88
scatter(real.(λ), imag.(λ), label = "central flux")
89
90
# Here, the maximal real part is basically zero to machine accuracy.
91
92
relative_maximum = maximum(real, λ) / maximum(abs, λ)
93
@test 1.0e-17 < relative_maximum < 2.0e-15 #src
94
95
# Moreover, the eigenvectors are not as ill-conditioned as in 2D.
96
97
λ, V = eigen(J)
98
condition_number = cond(V)
99
@test 200 < condition_number < 4000 #src
100
101
# If we add dissipation, the maximal real part is still approximately zero.
102
103
solver = DGSEM(3, flux_lax_friedrichs)
104
semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition_density_wave, solver;
105
boundary_conditions = boundary_condition_periodic)
106
107
J = jacobian_ad_forward(semi)
108
λ = eigvals(J)
109
110
scatter!(real.(λ), imag.(λ), label = "Lax-Friedrichs flux")
111
112
# As you can see from the plot generated above, the maximal real part is still
113
# basically zero to machine precision.
114
115
relative_maximum = maximum(real, λ) / maximum(abs, λ)
116
@test -1.0e-15 < relative_maximum < 1.0e-15 #src
117
118
# Let's check the condition number of the eigenvectors.
119
120
λ, V = eigen(J)
121
122
condition_number = cond(V)
123
@test 70_000 < condition_number < 200_000 #src
124
125
# Note that the condition number of the eigenvector matrix increases but is
126
# still smaller than for the example in 2D.
127
128
# ### Computing other derivatives
129
130
# It is also possible to compute derivatives of other dependencies using AD in Trixi.jl. For example,
131
# you can compute the gradient of an entropy-dissipative semidiscretization with respect to the
132
# ideal gas constant of the compressible Euler equations as described in the following. This example
133
# is also available as the elixir
134
# [`examples/special_elixirs/elixir_euler_ad.jl`](https://github.com/trixi-framework/Trixi.jl/blob/main/examples/special_elixirs/elixir_euler_ad.jl)
135
136
# First, we create a semidiscretization of the compressible Euler equations.
137
138
using Trixi, LinearAlgebra, ForwardDiff
139
140
equations = CompressibleEulerEquations2D(1.4)
141
142
"""
143
initial_condition_isentropic_vortex(x, t, equations::CompressibleEulerEquations2D)
144
145
The classical isentropic vortex test case of
146
- Chi-Wang Shu (1997)
147
Essentially Non-Oscillatory and Weighted Essentially Non-Oscillatory
148
Schemes for Hyperbolic Conservation Laws
149
[NASA/CR-97-206253](https://ntrs.nasa.gov/citations/19980007543)
150
"""
151
function initial_condition_isentropic_vortex(x, t, equations::CompressibleEulerEquations2D)
152
inicenter = SVector(0.0, 0.0) # initial center of the vortex
153
iniamplitude = 5.0 # size and strength of the vortex
154
155
rho = 1.0 # base flow
156
v1 = 1.0
157
v2 = 1.0
158
vel = SVector(v1, v2)
159
p = 25.0
160
161
rt = p / rho # ideal gas equation
162
t_loc = 0.0
163
164
cent = inicenter + vel * t_loc # shift advection of center to handle periodic BC, but only for v1 = v2 = 1.0
165
cent = x - cent # distance to center point
166
cent = SVector(-cent[2], cent[1])
167
168
r2 = cent[1]^2 + cent[2]^2
169
du = iniamplitude / (2 * π) * exp(0.5 * (1 - r2)) # vel. perturbation
170
dtemp = -(equations.gamma - 1) / (2 * equations.gamma * rt) * du^2 # isentropic
171
172
rho = rho * (1 + dtemp)^(1 / (equations.gamma - 1))
173
vel = vel + du * cent
174
v1, v2 = vel
175
p = p * (1 + dtemp)^(equations.gamma / (equations.gamma - 1))
176
177
prim = SVector(rho, v1, v2, p)
178
return prim2cons(prim, equations)
179
end
180
181
mesh = TreeMesh((-1.0, -1.0), (1.0, 1.0), initial_refinement_level = 2, n_cells_max = 10^5,
182
periodicity = true)
183
184
solver = DGSEM(3, flux_lax_friedrichs, VolumeIntegralFluxDifferencing(flux_ranocha))
185
186
semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition_isentropic_vortex,
187
solver;
188
boundary_conditions = boundary_condition_periodic)
189
190
u0_ode = Trixi.compute_coefficients(0.0, semi)
191
size(u0_ode)
192
@test size(u0_ode) == (1024,) #src
193
194
# Next, we compute the Jacobian using `ForwardDiff.jacobian`.
195
196
J = ForwardDiff.jacobian((du_ode, γ) -> begin
197
equations_inner = CompressibleEulerEquations2D(first(γ))
198
semi_inner = Trixi.remake(semi, equations = equations_inner,
199
uEltype = eltype(γ))
200
Trixi.rhs!(du_ode, u0_ode, semi_inner, 0.0)
201
end, similar(u0_ode), [1.4]); # γ needs to be an `AbstractArray`
202
203
round.(extrema(J), sigdigits = 2)
204
@test round.(extrema(J), sigdigits = 2) == (-220.0, 220.0) #src
205
206
# Note that we create a semidiscretization `semi` at first to determine the state `u0_ode` around
207
# which we want to perform the linearization. Next, we wrap the RHS evaluation inside a closure
208
# and pass that to `ForwardDiff.jacobian`. There, we need to make sure that the internal caches
209
# are able to store dual numbers from ForwardDiff.jl by setting `uEltype` appropriately. A similar
210
# approach is used by [`jacobian_ad_forward`](@ref).
211
212
# Note that the ideal gas constant does not influence the semidiscrete rate of change of the
213
# density, as demonstrated by
214
215
norm(J[1:4:end])
216
@test norm(J[1:4:end]) == 0.0 #src
217
218
# Here, we used some knowledge about the internal memory layout of Trixi.jl, an array of structs
219
# with the conserved variables as fastest-varying index in memory.
220
221
# ## Differentiating through a complete simulation
222
223
# It is also possible to differentiate through a complete simulation. As an example, let's differentiate
224
# the total energy of a simulation using the linear scalar advection equation with respect to the
225
# wave number (frequency) of the initial data.
226
227
using Trixi, OrdinaryDiffEqLowOrderRK, ForwardDiff, Plots
228
229
function energy_at_final_time(k) # k is the wave number of the initial condition
230
equations = LinearScalarAdvectionEquation2D(1.0, -0.3)
231
mesh = TreeMesh((-1.0, -1.0), (1.0, 1.0), initial_refinement_level = 3,
232
n_cells_max = 10^4, periodicity = true)
233
solver = DGSEM(3, flux_lax_friedrichs)
234
initial_condition = (x, t, equation) -> begin
235
x_trans = Trixi.x_trans_periodic_2d(x - equation.advection_velocity * t)
236
return SVector(sinpi(k * sum(x_trans)))
237
end
238
semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition, solver;
239
uEltype = typeof(k),
240
boundary_conditions = boundary_condition_periodic)
241
ode = semidiscretize(semi, (0.0, 1.0))
242
sol = solve(ode, FRK65(), dt = 0.05, adaptive = false, save_everystep = false)
243
return Trixi.integrate(energy_total, sol.u[end], semi)
244
end
245
246
k_values = range(0.9, 1.1, length = 101)
247
248
plot(k_values, energy_at_final_time.(k_values), label = "Energy")
249
250
# You see a plot of a curve that resembles a parabola with local maximum around `k = 1.0`.
251
# Why's that? Well, the domain is fixed but the wave number changes. Thus, if the wave number is
252
# not chosen as an integer, the initial condition will not be a smooth periodic function in the
253
# given domain. Hence, the dissipative surface flux (`flux_lax_friedrichs` in this example)
254
# will introduce more dissipation. In particular, it will introduce more dissipation for "less smooth"
255
# initial data, corresponding to wave numbers `k` further away from integers.
256
257
# We can compute the discrete derivative of the energy at the final time with respect to the wave
258
# number `k` as follows.
259
260
round(ForwardDiff.derivative(energy_at_final_time, 1.0), sigdigits = 2)
261
@test round(ForwardDiff.derivative(energy_at_final_time, 1.0), sigdigits = 2) == 1.4e-5 #src
262
263
# This is rather small and we can treat it as zero in comparison to the value of this derivative at
264
# other wave numbers `k`.
265
266
dk_values = ForwardDiff.derivative.((energy_at_final_time,), k_values);
267
268
plot(k_values, dk_values, label = "Derivative")
269
270
# If you remember basic calculus, a sufficient condition for a local maximum is that the first derivative
271
# vanishes and the second derivative is negative. We can also check this discretely.
272
273
second_derivative = round(ForwardDiff.derivative(k -> Trixi.ForwardDiff.derivative(energy_at_final_time,
274
k), 1.0),
275
sigdigits = 2)
276
@test second_derivative -0.9 #src
277
278
# Having seen this application, let's break down what happens step by step.
279
280
function energy_at_final_time(k) # k is the wave number of the initial condition
281
equations = LinearScalarAdvectionEquation2D(1.0, -0.3)
282
mesh = TreeMesh((-1.0, -1.0), (1.0, 1.0), initial_refinement_level = 3,
283
n_cells_max = 10^4, periodicity = true)
284
solver = DGSEM(3, flux_lax_friedrichs)
285
initial_condition = (x, t, equation) -> begin
286
x_trans = Trixi.x_trans_periodic_2d(x - equation.advection_velocity * t)
287
return SVector(sinpi(k * sum(x_trans)))
288
end
289
semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition, solver;
290
uEltype = typeof(k),
291
boundary_conditions = boundary_condition_periodic)
292
ode = semidiscretize(semi, (0.0, 1.0))
293
sol = solve(ode, FRK65(), dt = 0.05, adaptive = false, save_everystep = false)
294
return Trixi.integrate(energy_total, sol.u[end], semi)
295
end
296
297
k = 1.0
298
round(ForwardDiff.derivative(energy_at_final_time, k), sigdigits = 2)
299
@test round(ForwardDiff.derivative(energy_at_final_time, 1.0), sigdigits = 2) == 1.4e-5 #src
300
301
# When calling `ForwardDiff.derivative(energy_at_final_time, k)` with `k=1.0`, ForwardDiff.jl
302
# will basically use the chain rule and known derivatives of existing basic functions
303
# to calculate the derivative of the energy at the final time with respect to the
304
# wave number `k` at `k0 = 1.0`. To do this, ForwardDiff.jl uses dual numbers, which
305
# basically store the result and its derivative w.r.t. a specified parameter at the
306
# same time. Thus, we need to make sure that we can treat these `ForwardDiff.Dual`
307
# numbers everywhere during the computation. Fortunately, generic Julia code usually
308
# supports these operations. The most basic problem for a developer is to ensure
309
# that all types are generic enough, in particular the ones of internal caches.
310
311
# The first step in this example creates some basic ingredients of our simulation.
312
equations = LinearScalarAdvectionEquation2D(1.0, -0.3)
313
mesh = TreeMesh((-1.0, -1.0), (1.0, 1.0), initial_refinement_level = 3, n_cells_max = 10^4,
314
periodicity = true)
315
solver = DGSEM(3, flux_lax_friedrichs);
316
317
# These do not have internal caches storing intermediate values of the numerical
318
# solution, so we do not need to adapt them. In fact, we could also define them
319
# outside of `energy_at_final_time` (but would need to take care of globals or
320
# wrap everything in another function).
321
322
# Next, we define the initial condition
323
initial_condition = (x, t, equation) -> begin
324
x_trans = Trixi.x_trans_periodic_2d(x - equation.advection_velocity * t)
325
return SVector(sinpi(k * sum(x_trans)))
326
end;
327
# as a closure capturing the wave number `k` passed to `energy_at_final_time`.
328
# If you call `energy_at_final_time(1.0)`, `k` will be a `Float64`. Thus, the
329
# return values of `initial_condition` will be `SVector`s of `Float64`s. When
330
# calculating the `ForwardDiff.derivative`, `k` will be a `ForwardDiff.Dual` number.
331
# Hence, the `initial_condition` will return `SVector`s of `ForwardDiff.Dual`
332
# numbers.
333
334
# The semidiscretization `semi` uses some internal caches to avoid repeated allocations
335
# and speed up the computations, e.g. for numerical fluxes at interfaces. Thus, we
336
# need to tell Trixi.jl to allow `ForwardDiff.Dual` numbers in these caches. That's what
337
# the keyword argument `uEltype=typeof(k)` in
338
semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition, solver;
339
uEltype = typeof(k),
340
boundary_conditions = boundary_condition_periodic)
341
342
# does. This is basically the only part where you need to modify your standard Trixi.jl
343
# code to enable automatic differentiation. From there on, the remaining steps
344
ode = semidiscretize(semi, (0.0, 1.0))
345
sol = solve(ode, FRK65(), dt = 0.05, adaptive = false, save_everystep = false)
346
round(Trixi.integrate(energy_total, sol.u[end], semi), sigdigits = 5)
347
@test round(Trixi.integrate(energy_total, sol.u[end], semi), sigdigits = 5) == 0.25 #src
348
349
# do not need any modifications since they are sufficiently generic (and enough effort
350
# has been spend to allow general types inside these calls).
351
352
# ## Propagating errors using Measurements.jl
353
354
# [![Error bars by Randall Munroe](https://imgs.xkcd.com/comics/error_bars.png)](https://xkcd.com/2110/)
355
# "Error bars" by Randall Munroe, linked from https://xkcd.com/2110
356
357
# Similar to AD, Trixi.jl also allows propagating uncertainties using linear error propagation
358
# theory via [Measurements.jl](https://github.com/JuliaPhysics/Measurements.jl).
359
# As an example, let's create a system representing the linear advection equation
360
# in 1D with an uncertain velocity. Then, we create a semidiscretization using a
361
# sine wave as initial condition, solve the ODE, and plot the resulting uncertainties
362
# in the primitive variables.
363
364
using Trixi, OrdinaryDiffEqLowOrderRK, Measurements, Plots, LaTeXStrings
365
366
equations = LinearScalarAdvectionEquation1D(1.0 ± 0.1)
367
368
mesh = TreeMesh((-1.0,), (1.0,), n_cells_max = 10^5, initial_refinement_level = 5,
369
periodicity = true)
370
371
solver = DGSEM(3)
372
373
semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition_convergence_test,
374
solver, uEltype = Measurement{Float64},
375
boundary_conditions = boundary_condition_periodic)
376
377
ode = semidiscretize(semi, (0.0, 1.5))
378
379
sol = solve(ode, BS3(); ode_default_options()...);
380
381
plot(sol)
382
383
# You should see a plot where small error bars are shown around the extrema
384
# and larger error bars are shown in the remaining parts.
385
# This result is in accordance with expectations. Indeed, the uncertain propagation
386
# speed will affect the extrema less since the local variation of the solution is
387
# relatively small there. In contrast, the local variation of the solution is large
388
# around the turning points of the sine wave, so the uncertainties will be relatively
389
# large there.
390
391
# All this is possible due to allowing generic types and having good abstractions
392
# in Julia that allow packages to work together seamlessly.
393
394
# ## Finite difference approximations
395
396
# Trixi.jl provides the convenience function [`jacobian_fd`](@ref) to approximate the Jacobian
397
# via central finite differences.
398
399
using Trixi, LinearAlgebra
400
401
equations = CompressibleEulerEquations2D(1.4)
402
403
solver = DGSEM(3, flux_central)
404
405
mesh = TreeMesh((-1.0, -1.0), (1.0, 1.0), initial_refinement_level = 2, n_cells_max = 10^5,
406
periodicity = true)
407
408
semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition_density_wave, solver;
409
boundary_conditions = boundary_condition_periodic)
410
411
J_fd = jacobian_fd(semi)
412
413
J_ad = jacobian_ad_forward(semi)
414
415
relative_difference = norm(J_fd - J_ad) / size(J_fd, 1)
416
@test relative_difference < 7.0e-7 #src
417
418
# This discrepancy is of the expected order of magnitude for central finite difference approximations.
419
420
# ## Automatic Jacobian sparsity detection and coloring
421
422
# When solving large sparse nonlinear ODE systems originating from spatial discretizations
423
# with compact stencils such as the DG method with implicit time integrators,
424
# exploiting the sparsity of the Jacobian can lead to significant speedups in the Newton-Raphson solver.
425
# Similarly, steady-state problems can also be solved faster.
426
427
# [Trixi.jl](https://github.com/trixi-framework/Trixi.jl) supports efficient Jacobian computations by leveraging the
428
# [SparseConnectivityTracer.jl](https://github.com/adrhill/SparseConnectivityTracer.jl)
429
# and [SparseMatrixColorings.jl](https://github.com/gdalle/SparseMatrixColorings.jl) packages.
430
# These tools allow to detect the sparsity pattern of the Jacobian and compute the
431
# optional coloring vector for efficient Jacobian evaluations.
432
# These are then handed over to the ODE solver from [OrdinaryDiffEq.jl](https://github.com/SciML/OrdinaryDiffEq.jl).
433
434
# Below is a minimal example in 1D, showing how to use these packages with Trixi.jl.
435
# First, we define the the `equation` and `mesh` as for an ordinary simulation:
436
437
using Trixi
438
439
advection_velocity = 1.0
440
equation = LinearScalarAdvectionEquation1D(advection_velocity)
441
442
mesh = TreeMesh((-1.0,), (1.0,), initial_refinement_level = 4, n_cells_max = 10^4,
443
periodicity = true)
444
445
# We define the basic floating point type used for the actual simulation
446
# and construct the solver:
447
float_type = Float64
448
solver = DGSEM(polydeg = 3, surface_flux = flux_godunov, RealT = float_type)
449
450
# Next, we set up the sparsity detection. For this we need
451
using SparseConnectivityTracer # For Jacobian sparsity pattern
452
453
# We use the [global `TracerSparsityDetector()`](https://adrianhill.de/SparseConnectivityTracer.jl/stable/user/global_vs_local/) here.
454
jac_detector = TracerSparsityDetector()
455
456
# Next, we retrieve the right element type corresponding to `float_type` for the Jacobian sparsity detection.
457
# For more details, see the API documentation of
458
# [`jacobian_eltype`](https://adrianhill.de/SparseConnectivityTracer.jl/stable/user/api/#SparseConnectivityTracer.jacobian_eltype).
459
jac_eltype = jacobian_eltype(float_type, jac_detector)
460
461
# Now we can construct the semidiscretization for sparsity detection with `jac_eltype` as the
462
# datatype for the working arrays and helper datastructures.
463
semi_jac_type = SemidiscretizationHyperbolic(mesh, equation,
464
initial_condition_convergence_test, solver;
465
boundary_conditions = boundary_condition_periodic,
466
uEltype = jac_eltype) # Supply sparsity detection datatype here
467
468
tspan = (0.0, 1.0) # Re-used later in `rhs!` evaluation
469
ode_jac_type = semidiscretize(semi_jac_type, tspan)
470
u0_ode = ode_jac_type.u0
471
du_ode = similar(u0_ode)
472
473
# Wrap the RHS for sparsity detection to match the expected signature `f!(du, u)` required by
474
# [`jacobian_sparsity`](https://adrianhill.de/SparseConnectivityTracer.jl/stable/user/api/#ADTypes.jacobian_sparsity).
475
rhs_wrapped! = (du, u) -> Trixi.rhs!(du, u, semi_jac_type, tspan[1])
476
jac_prototype = jacobian_sparsity(rhs_wrapped!, du_ode, u0_ode, jac_detector)
477
478
# Optionally, we can also compute the coloring vector to reduce Jacobian evaluations
479
# to `1 + maximum(coloring_vec)` for finite differencing and `maximum(coloring_vec)` for algorithmic differentiation.
480
# For this, we need
481
using SparseMatrixColorings
482
483
# We partition by columns as we are using finite differencing here.
484
# One would also partition by columns if forward-based algorithmic differentiation were used,
485
# and only partition by rows if reverse-mode AD were used.
486
# See also [the documentation of the now deprecated SparseDiffTools.jl](https://github.com/JuliaDiff/SparseDiffTools.jl?tab=readme-ov-file#matrix-coloring) package,
487
# the predecessor in spirit to SparseConnectivityTracer.jl and SparseMatrixColorings.jl, for more information.
488
coloring_prob = ColoringProblem(; structure = :nonsymmetric, partition = :column)
489
coloring_alg = GreedyColoringAlgorithm(; decompression = :direct)
490
coloring_result = coloring(jac_prototype, coloring_prob, coloring_alg)
491
coloring_vec = column_colors(coloring_result)
492
493
# Now, set up the actual semidiscretization for the simulation.
494
# The datatype is automatically retrieved from the solver (in this case `float_type = Float64`).
495
semi_float_type = SemidiscretizationHyperbolic(mesh, equation,
496
initial_condition_convergence_test, solver;
497
boundary_conditions = boundary_condition_periodic)
498
# Supply the sparse Jacobian prototype and the optional coloring vector.
499
# Internally, an [`ODEFunction`](https://docs.sciml.ai/DiffEqDocs/stable/types/ode_types/#SciMLBase.ODEFunction)
500
# with `jac_prototype = jac_prototype` and `colorvec = coloring_vec` is created.
501
ode_jac_sparse = semidiscretize(semi_float_type, tspan,
502
jac_prototype = jac_prototype,
503
colorvec = coloring_vec)
504
505
# You can now solve the ODE problem efficiently with an implicit solver.
506
# Currently we are bound to finite differencing here.
507
using OrdinaryDiffEqSDIRK, ADTypes
508
sol = solve(ode_jac_sparse, TRBDF2(; autodiff = AutoFiniteDiff()), dt = 0.1,
509
save_everystep = false);
510
511
# ## Linear systems
512
513
# When a linear PDE is discretized using a linear scheme such as a standard DG method,
514
# the resulting semidiscretization yields an affine ODE of the form
515
516
# ```math
517
# \partial_t u(t) = A u(t) - b,
518
# ```
519
520
# where `A` is a linear operator ("matrix") and `b` is a vector. Trixi.jl allows you
521
# to obtain this linear structure in a matrix-free way by using [`linear_structure`](@ref).
522
# The resulting operator `A` can be used in multiplication, e.g. `mul!` from
523
# LinearAlgebra, converted to a sparse matrix using `sparse` from SparseArrays,
524
# or converted to a dense matrix using `Matrix` for detailed eigenvalue analyses.
525
# For example,
526
527
using Trixi, LinearAlgebra, Plots
528
529
equations = LinearScalarAdvectionEquation2D(1.0, -0.3)
530
531
solver = DGSEM(3, flux_lax_friedrichs)
532
533
mesh = TreeMesh((-1.0, -1.0), (1.0, 1.0), initial_refinement_level = 2, n_cells_max = 10^5,
534
periodicity = true)
535
536
semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition_convergence_test,
537
solver;
538
boundary_conditions = boundary_condition_periodic)
539
540
A, b = linear_structure(semi)
541
542
size(A), size(b)
543
@test (size(A), size(b)) == ((256, 256), (256,)) #src
544
545
# Next, we compute the eigenvalues of the linear operator.
546
547
λ = eigvals(Matrix(A))
548
549
scatter(real.(λ), imag.(λ))
550
551
# As you can see here, the maximal real part is close to machine precision.
552
553
λ = eigvals(Matrix(A))
554
relative_maximum = maximum(real, λ) / maximum(abs, λ)
555
@test relative_maximum < 1.0e-15 #src
556
557
# Since the linear structure defines the action of the linear matrix-alike operator `A`
558
# on a vector, Krylov-subspace based iterative solvers can be employed to efficiently solve
559
# the resulting linear system.
560
# For instance, one may use the [Krylov.jl](https://github.com/JuliaSmoothOptimizers/Krylov.jl) package to solve
561
# e.g. steady-stage problems, i.e., problems where ``\partial_t u(t) = 0``.
562
# Note that the present problem does not possess an actual steady state.
563
564
# Anyways, to solve the linear system ``A u = b``, one can use for instance the GMRES method:
565
using Krylov
566
567
u_steady_state, solve_stats = gmres(A, b)
568
569
# ## Package versions
570
571
# These results were obtained using the following versions.
572
573
using InteractiveUtils
574
versioninfo()
575
576
using Pkg
577
Pkg.status(["Trixi", "OrdinaryDiffEqLowOrderRK", "OrdinaryDiffEqSDIRK",
578
"Plots", "LaTeXStrings",
579
"ForwardDiff", "SparseConnectivityTracer", "SparseMatrixColorings",
580
"ADTypes",
581
"Krylov", "LinearAlgebra",
582
"Measurements"],
583
mode = PKGMODE_MANIFEST)
584
585