Engee 文档
Notebook

不同初始条件下的差分方程组解法

本例考虑了在不同初始条件(n.u.)下求解微分方程(d.u.)的两种方法:

  1. 使用循环for 并改变 n.u.
  2. 使用EnsembleProblem 以多线程模式求解

问题描述

考虑描述捕食者-猎物模型的 Lotka-Volterra 公式:

$$\begin{cases} \frac{d x}{dt} = \alpha x - \beta x y \\ \frac{d y}{dt} = \delta xy - \gamma y \\ \end{cases}$$

其中 $x$ 和$y$ 分别是受害种群和捕食种群

$t$ - 时间

$\alpha, \beta, \delta, \gamma$ - 常数参数,描述物种代表之间的相互作用。

为明确起见,让我们把它们取为$\alpha = \gamma = 1, \beta =0.01, \delta = 0.02$ 。

在这个问题中,n.d.是物种种群的规模。通过求解 d.u.,我们可以观察种群随时间的变化。

让我们考虑一下需要哪些软件包来解决我们的问题:

-OrdinaryDiffEq 是一个可以求解普通 d.u.的软件包,无需软件包插件即可运行DifferentialEquations -Plots 可以在一行中显示微分方程的解 -Distributed 允许将函数放在不同的处理器内核上,以提高计算效率

In [ ]:
Pkg.add(["DifferentialEquations", "OrdinaryDiffEq", "Distributed", "BenchmarkTools"])
In [ ]:
using OrdinaryDiffEq, Plots

普通微分方程的形式为 $$\frac{du}{dt}=f(u,,p,t)$$


要解决这个问题,需要确定以下初始数据:

f - 函数本身(D.U. 结构)

u0 - 初始条件

tspan - 求解的时间间隔

p - 系统的参数,这些参数也决定了系统的形式

kwargs - 传递给解法的关键字


有两种方法可以指定函数f

  1. f(du,u,p,t) - 从内存中高效提取,但可能不受不允许突变软件包的支持。
  2. f(u,p,t) - 返回du 。适用于禁止突变的软件包。
In [ ]:
function lotka(du, u, p, t) 
    du[1] = p[1] * u[1] - p[2] * u[1] * u[2]
    du[2] = p[4] * u[1] * u[2] - p[3] * u[2]  
end
α = 1; β = 0.01; γ = 1; δ = 0.02;
p = [α, β, γ, δ]
tspan = (0.0, 6.5)
u0 = [50; 50]
prob = ODEProblem(lotka, u0, tspan, p)
Out[0]:
ODEProblem with uType Vector{Int64} and tType Float64. In-place: true
timespan: (0.0, 6.5)
u0: 2-element Vector{Int64}:
 50
 50

设定问题后,我们开始求解。函数solve 允许使用不同的求解器(solver)求解 ODE。在我们的例子中,我们有一个可以用常数步求解的 ODE。作为参数,我们将传递问题集、求解器Tsit5() 和参数saveat=0.01 ,后者表示保存解的步长。

In [ ]:
sol = solve(prob, Tsit5(), saveat=0.01)
Out[0]:
retcode: Success
Interpolation: 1st order linear
t: 651-element Vector{Float64}:
 0.0
 0.01
 0.02
 0.03
 0.04
 0.05
 0.06
 0.07
 0.08
 0.09
 0.1
 0.11
 0.12
 ⋮
 6.39
 6.4
 6.41
 6.42
 6.43
 6.44
 6.45
 6.46
 6.47
 6.48
 6.49
 6.5
u: 651-element Vector{Vector{Float64}}:
 [50.0, 50.0]
 [50.25062391161723, 50.00125211549466]
 [50.50249138370918, 50.00501692739517]
 [50.755595782113325, 50.01130738411668]
 [51.009930152636706, 50.020136767794746]
 [51.26548722105585, 50.031518694285374]
 [51.52225939311683, 50.04546711316499]
 [51.78023875453521, 50.06199630773043]
 [52.039417070996095, 50.081120894998996]
 [52.29978578815409, 50.10285582570839]
 [52.56133603163334, 50.127216384316746]
 [52.82405860702751, 50.15421818900265]
 [53.08794399989971, 50.18387719166507]
 ⋮
 [47.63881663670754, 50.10217268569618]
 [47.877146554422595, 50.07972805051839]
 [48.11676888713707, 50.05968794764736]
 [48.35768016469967, 50.04206191833404]
 [48.59987678709717, 50.026859716639514]
 [48.84335502445415, 50.01409130943499]
 [49.08811101703379, 50.00376687640185]
 [49.33414077523645, 49.99589681003162]
 [49.581440179601316, 49.99049171562596]
 [49.83000498080491, 49.98756241129669]
 [50.07983079966195, 49.98711992796579]
 [50.33091312712557, 49.98917550936536]

现在让我们显示解决方案,并自动指定图例和坐标轴签名t

In [ ]:
plot(sol)
Out[0]:

现在我们来显示相位轨迹--相位坐标(xy )随时间的变化。图中,初始位置为红色,求解结束时为蓝色。

In [ ]:
scatter(
sol[1, :], sol[2, :],                           # x и y
legend=false, marker_z=sol.t, color=:RdYlBu,    # marker_z позволяет изменять цвет маркера от t
xlabel="Жертва x", ylabel="Хищник y",           # RdYlBu - от Красного до Синего через Жёлтый
title="Фазовая траектория системы")
Out[0]:

既然我们已经能够描绘出一对 n.u. 的相位轨迹,那么让我们构建一个相位图--这些轨迹的集合。

为此,我们将使用循环for ,逐渐增加捕食者种群的初始规模。

In [ ]:
tspan=(0., 15.)
initial_conditions = range(10, stop=400, length=40)
Out[0]:
10.0:10.0:400.0
In [ ]:
plt = plot()
for y0 in initial_conditions
    u0 = [50; y0]
    prob = ODEProblem(lotka, u0, tspan, p)
    sol = solve(prob, Tsit5(), saveat=0.01)
    plot!(plt, sol[1, :], sol[2, :], title="Фазовый портрет системы", xlabel="Жертва", ylabel="Хищник" ,legend=false)
end

display(plt)

多线程构建相位图

现在,让我们考虑相位肖像构建任务的另一种变体。为此,让我们连接标准软件包Distributed ,它允许以分布式方式执行任务。我们将把任务分配给 4 个线程(进程)。

In [ ]:
using Distributed
N_PROCS = 4  # зададим количество процессов
addprocs(N_PROCS)
Out[0]:
4-element Vector{Int64}:
 2
 3
 4
 5

连接DifferentialEquations - 用于求解各种类型 d.u. 的软件包,然后使用函数EnsembleProb ,通过调用函数prob_func 来设置初始条件的变化。

例如,可以随机改变初始条件: ``朱利亚 函数 prob_func(prob, i, repeat) @.prob.u0 = randn() * prob.u0 prob 结束

或使用外部数据容器:
```julia
函数 prob_func(prob, i, repeat)

    remake(prob, u0=[50, initial_conditions[i]])

结束
In [ ]:
using DifferentialEquations
u0 = [50; 10];
prob = ODEProblem(lotka, u0, tspan, p);

function prob_func(prob, i, repeat)
    remake(prob, u0=[50, initial_conditions[i]])
end
ensemble_prob = EnsembleProblem(prob, prob_func=prob_func)
Out[0]:
EnsembleProblem with problem ODEProblem

提出问题后,我们使用solve 得到解。但这次返回给我们的不是单一的 d.u. 解,而是一整套模拟sim 。请注意,我们在solve 中添加了参数:

EnsembleThreads() - 这意味着分布将在处理器线程上进行,而不是在集群中的不同计算机上进行。 trajectories=N_TRAJECTS - 我们希望运行的模拟次数

In [ ]:
N_TRAJECTS = length(initial_conditions)
sim = solve(ensemble_prob, Tsit5(), EnsembleThreads(), trajectories=N_TRAJECTS, saveat=0.01);

遍历模拟集中的每个解,绘制相位图

In [ ]:
plt = plot()
for sol in sim.u
    plot!(plt, sol[1, :], sol[2, :],  title="Фазовый портрет системы", xlabel="Жертва", ylabel="Хищник" ,legend=false)
end

display(plt)

执行时间比较

现在让我们比较一下在循环中和使用多线程时的代码执行时间。

为此,让我们连接BenchmarkTools 库,设置我们要比较的函数,然后使用宏@btime 运行我们的函数若干次,计算它们的平均执行时间。

In [ ]:
using BenchmarkTools

function for_loop_solution()
    for y0 in range(10, stop=400, length=20)
        u0 = [50; y0]
        prob = ODEProblem(lotka, u0, tspan, p)
        solve(prob, Tsit5(), saveat=0.01)
    end
end

function ensemble_prob_solution()
    prob = ODEProblem(lotka, u0, tspan, p)

    initial_conditions = range(10, stop=400, length=20)
     function prob_func(prob, i, repeat)
        remake(prob, u0=[50, initial_conditions[i]])
    end
    ensemble_prob = EnsembleProblem(prob, prob_func=prob_func)
    solve(ensemble_prob, Tsit5(), EnsembleThreads(), trajectories=20, saveat=0.01)
end

@btime for_loop_solution()
@btime ensemble_prob_solution()
[];  #заглушка чтобы не выводить EnsembleProblem в ячейку вывода
  8.418 ms (33260 allocations: 3.59 MiB)
  1.869 ms (31424 allocations: 3.53 MiB)

结论

根据运行时间评估的结果,ensemble_prob_solution 快 4 倍,这与分配给任务的内核数量相符。

由此我们可以得出结论,在求解具有不同初始条件的微分方程系统时,for 循环应优于EnsemleProblem 和使用软件包Distributed 进行的分布式计算。