Numerical solution of partial differential equations using the example of an acoustic wave
In this example, we will consider the numerical solution of a partial differential equation using the example of an acoustic wave oscillation.
The topic of embedded functions will be touched upon to show how you can improve the readability of the code without reducing the speed of its execution.
Let's consider the solution of the string oscillation equation without delving into the code, but only paying attention to its structure.
(The code will be explained in the next chapter).
@views function acoustic_wave_1D_orig()
# physical parameters
lx =
20.0 r, β = 1.0, 1.0
# counts
nx = 200
nvis = 2
# differentials
dx = lx / nx
dt = dx*√(ρ / β)
nt = nx^2 ÷ 100
xc = LinRange(dx / 2, lx - dx / 2, nx)
# initializing arrays
Pr = @. exp(-(xc - lx/4)^2)
Sample = copy(Pr)
Vx = zeros(Float64, nx - 1)
# algorithm cycle
@gif for it = 1:nt
Vx .-= dt ./ ρ .* diff(Pr) ./ dx
Pr[2:end-1] .-= dt ./ β .* diff(Vx) ./ dx
plot(xc, [Sample, Pr]; xlims=(0, lx), ylims=(-1.1, 1.1),
xlabel="lx", ylabel="Concentration",
title="Time = $(round(it*dt,digits=1))")
end every nvis
end
Несмотря на то, что код сопровождается комментариями, можно явно выделить этапы работы функции.
Может возникнуть предположение, что внедрение функций в код под каждый этап замедлит выполнение главной функции.
Действительно, вызов функции занимает значительное время, в сравнении с арифметическими операциями. Однако компиляторы умеют оптимизировать код и делать функции встраиваемыми.
Чтобы проверить, происходит ли вызов функции - воспользуемся макросом @code_llvm, который покажет код, близкий к ассемблеру. (Для большей читабельности можно включить тёмную тему в Engee)
function fa(a,b)
return a+b
end
function fb(a,b,c)
fa(a,b)*c
end
@code_llvm fb(1,2,4)
В строчке %3 = add i64 %1, %0 происходит сложение чисел a и b.
Для того чтобы явно указать компилятору, что не нужно делать оптимизацию встраивания, воспользуемся макросом @noinline
@noinline function fa(a,b)
return a+b
end
function fb(a,b,c)
fa(a,b)*c
end
@code_llvm fb(1,2,4)
Теперь же происходит вызов функции fa, которая складывает a и b:
%3 = call i64 @j_fa_7789(i64 signext %0, i64 signext %1)
Для того чтобы убедиться в том, что встроенная функция работает быстрее, чем вызываемая - напишем следующий код:
@noinline function fa_noinline(a,b)
a + b
end
@inline function fa_inline(a,b)
a + b
end
function test_fa_noinline(N)
sum = 0
for i in 1:N
sum += fa_noinline(i,i)
end
sum
end
function test_fa_inline(N)
sum = 0
for i in 1:N
sum += fa_inline(i,i)
end
sum
end
И подключим библиотеку BenchmarkTools для оценки времени выполнения кода:
Pkg.add("BenchmarkTools")
using BenchmarkTools
N = 10^5
@btime test_fa_noinline($N)
@btime test_fa_inline($N)
Как видим, встроенная функция работает, быстрее.
Уравнение акустической волны
Уравнение акустической волны имеет вид:
где - давление, - время, а - положение частиц в некоторой среде. - скорость звука в этой среде.
Введём некоторые обозначения:
- объёмный модуль сжимаемости (βulk modulus)
- плотность среды.
Тогда исходное уравнение второго порядка:
можно переписать как систему двух уравнений первого порядка:
Numerical solution of the acoustic wave equation
Let's introduce a function defining the physical parameters of our equation:
@doc """
- lx - длина среды, по которой распространяется волна
- ρ - плотность
- β - модуль сжимаемости
"""
physical_parameters(;lx = 20, ρ = 1, β = 1) = (lx,ρ,β)
@doc physical_parameters
We have set it in such a way as to demonstrate the following scenarios:
physical_parameters() # хотим использовать параметры по умолчанию
physical_parameters(lx=100,β=10) # меняем какие-то параметры, остальные оставляем по умолчанию
Note that a tuple is a specific data type that should be understood when to apply.
However, for a small set of data that is worth unpacking, a tuple is one of the best options.
In the function below foo we can see that the compiler has seen that "unpacking" a tuple is just an assignment. a,b,c values x,y and z. And you don't need to make any calls to the elements of the tuple.
function foo(x,y,z)
a,b,c = physical_parameters(;lx = x, ρ=y, β=z)
return a+b+c
end
@code_llvm foo(10,20,30)
If only physical_parameters I didn't return the motorcade (lx, ρ, β), and the vector [lx, ρ, β] then the assembly code would be much larger and take longer to execute.
The finite element method
To solve the system of equations, we will use the [finite difference] method (https://en.wikipedia.org/wiki/Finite_difference_method ) in conjunction with the Euler method.
(Often, to solve such problems, [apply] (https://en.wikipedia.org/wiki/FTCS_scheme ) more accurate [central difference](https://ru.wikipedia.org/wiki/Finite differences))
The essence of the method is to represent a partial differential equation in terms of small but finite differences.:
Selecting the sampling step
Having the length of the "string" lx we can obtain a spatial discretization by dividing the length by the number of partitions, which we determine ourselves.
nx- set by the userdx=lx / nx- spatial sampling step- - the time step of the sampling. His choice affects whether the solution will be sustainable. The values of this step are selected based on Courant conditions. In our case, the speed of sound in the medium acts as a constant.
ntIt can be set by the user, or it can depend on the length, so that at least 1 period always passes.xc- a set of grid centers along which spatial sampling is performed
function differentials(;lx=20,nx=200,β=1,ρ=1)
dx=lx/nx
dt=dx*√(β*ρ)
nt = nx^2 ÷ 100
xc = LinRange(dx / 2, lx - dx / 2, nx)
return(dx=dx,dt=dt,nt=nt,xc=xc)
end
Initializing arrays
Let's set arrays corresponding to pressure and velocity.
We will also save the initial pressure array for subsequent visualization.
function arrays_initializer(xc,lx,nx)
Vx = zeros(Float64, nx - 1)
Pr = @. exp(-(xc - lx / 4)^2)
Sample = copy(Pr)
(Vx=Vx, Pr=Pr, Sample=Sample)
end
Before proceeding to the solution, we'll connect a library for visualizing graphs of solutions and animating them.:
# Pkg.add("Plots")
using Plots, Plots.Measures, Printf
# задаём параметры по умолчанию
default(size=(1500, 600), framestyle=:box, label=false, grid=false, margin=10mm, lw=6, labelfontsize=20, tickfontsize=20, titlefontsize=24)
Finite difference solution
a = a - 1 ~ a -= 1
Let's rewrite it
how
- a scalar showing the value at a given time at the point
For more information, see the picture below.
Similarly, for pressure P.

function show_acoustic_wave((Vx,Pr,Sample),(dx,dt,nt,xc);nvis=2,ρ=1,β=1,lx=20)
@gif for it = 1:nt
Vx .-= dt ./ ρ .* diff(Pr) ./ dx
Pr[2:end-1] .-= dt ./ β .* diff(Vx) ./ dx
plot(xc, [Sample, Pr]; xlims=(0, lx), ylims=(-1.1, 1.1),
xlabel="lx", ylabel="Концентрация",
title="Время = $(round(it*dt,digits=1))")
end every nvis
end
Then the final code will look like this:
@views function acoustic_wave_1D()
lx, ρ, b = physical_parameters() # если мы знаем порядок элементов в кортеже
nx = 200 # количество ячеек в сетке
ds = differentials() # если не знаем, то используем именованный
dx = ds.dx
dt = ds.dt
nt = ds.nt
xc = ds.xc
ars = arrays_initializer(xc,lx,nx)
show_acoustic_wave(ars,ds,nvis=2)
end
@time acoustic_wave_1D()
Let's compare the execution time with our source code.
@views function acoustic_wave_1D_orig()
# физический параметры
lx = 20.0
ρ, β = 1.0, 1.0
# отсчёты
nx = 200
nvis = 2
# дифференциалы
dx = lx / nx
dt = dx*√(ρ / β)
nt = nx^2 ÷ 100
xc = LinRange(dx / 2, lx - dx / 2, nx)
# инициализация массивов
Pr = @. exp(-(xc - lx / 4)^2)
Sample = copy(Pr)
Vx = zeros(Float64, nx - 1)
# цикл работы алгоритма
@gif for it = 1:nt
Vx .-= dt ./ ρ .* diff(Pr) ./ dx
Pr[2:end-1] .-= dt ./ β .* diff(Vx) ./ dx
plot(xc, [Sample, Pr]; xlims=(0, lx), ylims=(-1.1, 1.1),
xlabel="lx", ylabel="Концентрация",
title="Время = $(round(it*dt,digits=1))")
end every nvis
end
@time acoustic_wave_1D_orig()
From which we can conclude that wrapping small code in a function does not significantly affect the speed of its execution. However, the following advantages of dividing the code into functions can be noted:
- Documentation of functions. Documenting functions is a much more effective and expressive tool than commenting on code.
- Some functions may be used by you in different scenarios.
- Creating functions allows you to decompose complex tasks into simple ones, as well as gradually increase the complexity and accuracy of your program. (First, we will insert a simple function, but we will check its operability, and only then we will select more accurate and effective ways to implement it)

