我最初的GSoC项目是关于实现原生Julia求解器,用于解决由二阶常微分方程(ODE)确定的边值问题(BVP)。我从BVP路径开始,构建了一种射击方法来解决来自初值问题(IVP)的BVP,然后构建了单隐式龙格-库塔(MIRK)方法的雏形。这些求解器位于BoundaryValueDiffEq.jl 存储库中。与其试图直接跳到终点,并讨论如何在MIRK中完成每个细节,我转而探索这些细节如何在二阶ODE中自然地产生。我实现了许多用于动力学IVP的求解器。尽管在GSoC结束时我并没有完全完成我的最初目标,但我已经非常接近了。
首先,存在辛性的概念,因为Labatto(Lobatto IIIA-IIIB)MIRK表格实际上是辛的。基本上,辛性是另一种说法,即守恒积分(能量、角动量等)是守恒的,因此辛积分器专门用于解决由需要能量守恒的动力系统产生的二阶ODE。在动力学IVP上更容易看出辛性实际上包含了什么。例如,开普勒问题的哈密顿量和角动量为
我们可以求解哈密顿方程
以获得开普勒问题的解。
using OrdinaryDiffEq, ForwardDiff, LinearAlgebra
H(q,p) = norm(p)^2/2 - inv(norm(q))
L(q,p) = q[1]*p[2] - p[1]*q[2]
pdot(dp,p,q,params,t) = ForwardDiff.gradient!(dp, q->-H(q, p), q)
qdot(dq,p,q,params,t) = ForwardDiff.gradient!(dq, p-> H(q, p), p)
然后,我们使用具有适当初始条件的Ruth3
辛积分器来解决此问题。
initial_position = [.4, 0]
initial_velocity = [0., 2.]
tspan = (0,20.)
prob = DynamicalODEProblem(pdot, qdot, initial_velocity, initial_position, tspan)
sol = solve(prob, Ruth3(), dt=1//50);
最后,我们通过计算第一积分并绘制它们来分析解。
请注意,辛积分器并不意味着它具有精确的守恒性。辛积分器的解位于辛流形上,但不一定守恒哈密顿量(能量)。能量可以以(准)周期性的方式波动,因此第一积分存在微小的变化。在上述情况下,能量最多变化6e-6
,并且它趋向于恢复。随着dt
变小,变化也减小。角动量完美守恒。更多详细信息请参阅此笔记本。
同样,我在IVP世界中探索了自适应性和密集输出。我实现了几个自适应龙格-库塔-奈斯托罗姆 (RKN) 求解器。MIRK自适应性和RKN自适应性有一个共同的主题,即误差估计,而MIRK通过使用密集输出来实现这一点。计算庞加莱截面是密集输出的实际使用示例。在绘制庞加莱截面时,我们通常需要使用saveat
或ContinuousCallback
,并且两者都需要密集输出才能正常工作。密集输出本质上是ODE的连续解。saveat
使用密集输出在指定时间评估值,因此ODE积分仍然可以是自适应的(积分器不需要达到精确的saveat
点)。ContinuousCallback
在密集输出上执行根查找以找到事件发生的时间。因此,高阶密集输出对于计算准确的saveat
和ContinuousCallback
非常重要。以下是绘制庞加莱截面的两个示例。
Duffing振子是一种具有非线性弹性的受迫振子,其形式为
首先,我们需要用参数写出常微分方程。
using OrdinaryDiffEq, Plots; pgfplots()
function draw_duffing(Γ, α, β, δ, ω)
function driven_pendulum(dv,v,x,p,t)
Γ, α, β, δ, ω = p
dv[1] = Γ*cos(ω*t) - β*x[1]^3 - α*x[1] - δ*v[1]
end
prob = SecondOrderODEProblem(driven_pendulum, [1.5], [0.], (5000., 35000.), (Γ, α, β, δ, ω))
sol = solve(prob, DPRKN6(), saveat=2pi/prob.p[end])
y1, x1 = [map(x->x[i], sol.u[end-2000:end]) for i in 1:2]
scatter(x1, y1, markersize=0.8, leg=false, title="Poincaré surface of duffing oscillator", xlabel="\$x\$", ylabel="\$\\dot{x}\$", color=:black, xlims=(0.5,1.7))
end
draw_duffing(8, 1, 5, 0.02, 0.5)
然后,我们需要在处获得解以绘制庞加莱截面,我们可以通过使用saveat
来实现。
受驱摆是一个周期性受迫摆,其形式为
同样,我们执行与上面相同的操作。
using OrdinaryDiffEq, Plots; pgfplots()
function draw_driven_pendulum(f₀,q,ω)
function driven_pendulum(dv,v,x,p,t)
f₀, q, ω, = p
dv[1] = -sin(x[1]) - q*v[1] + f₀*cos(ω*t)
end
prob = SecondOrderODEProblem(driven_pendulum, [0.], [2pi], (0.,50000.), (f₀,q,ω))
sol = solve(prob, DPRKN6(), saveat=2pi/prob.p[end])
y1, x1 = [map(x->x[i], sol.u[500:end]) for i in 1:2]
scatter(x1.%pi, y1, markersize=0.8, leg=false, title="Poincaré surface of driven pendulum", xlabel="\$\\theta\$", ylabel="\$\\dot{\\theta}\$", color=:black)
end
draw_driven_pendulum(1.12456789, 0.23456789, 0.7425755501794571)
BoundaryValueDiffEq
中的MIRK求解器目前还没有自适应性和密集输出,但是根据我在IVP中学习到的所有内容,大多数部分都已经实现或理解,因此我们可以预期这将在不久的将来完成。以下是如何使用BoundaryValueDiffEq
包的示例。在此示例中,我们将解决以下问题
using BoundaryValueDiffEq
const g = 9.81
L = 1.0
tspan = (0.0,pi/2)
function simplependulum(du,u,p,t)
θ = u[1]
dθ = u[2]
du[1] = dθ
du[2] = -(g/L)*sin(θ)
end
function bc1(residual, u, p, t)
residual[1] = u[end÷2][1] + pi/2 # the solution at the middle of the time span should be -pi/2
residual[2] = u[end][1] - pi/2 # the solution at the end of the time span should be pi/2
end
bvp1 = BVProblem(simplependulum, bc1, [pi/2,pi/2], tspan)
sol1 = solve(bvp1, GeneralMIRK4(), dt=0.05)
更多细节可以在 这里 找到。
我要感谢我的所有导师 Chris Rackauckas、陈嘉豪和 Christoph Ortner 对我的积极回应和指导。尤其是我的导师 Rackauckas,他可以在我在 Slack 上提问后五分钟内回答我的问题。我还要感谢 Julia 社区管理 GSoC 项目和 JuliaCon 2017。