The Kepler Problem
The Hamiltonian and the angular momentum for the Kepler problem are
Also, we know that
using OrdinaryDiffEq, LinearAlgebra, ForwardDiff, Plots;
gr();
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)
initial_position = [0.4, 0]
initial_velocity = [0.0, 2.0]
initial_cond = (initial_position, initial_velocity)
initial_first_integrals = (H(initial_cond...), L(initial_cond...))
tspan = (0, 20.0)
prob = DynamicalODEProblem(pdot, qdot, initial_velocity, initial_position, tspan)
sol = solve(prob, KahanLi6(), dt = 1 // 10);
retcode: Success
Interpolation: 3rd order Hermite
t: 201-element Vector{Float64}:
0.0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
⋮
19.200000000000003
19.300000000000004
19.400000000000006
19.500000000000007
19.60000000000001
19.70000000000001
19.80000000000001
19.900000000000013
20.0
u: 201-element Vector{ArrayPartition{Float64, Tuple{Vector{Float64}, Vector{Float64}}}}:
([0.0, 2.0], [0.4, 0.0])
([-0.5830949354540153, 1.8556656829703986], [0.36982713146498514, 0.19503596514776078])
([-0.9788105843777312, 1.5274462532150213], [0.28987830863610903, 0.36495974735762693])
([-1.17547762665905, 1.1751394486895783], [0.18078065407309682, 0.4998457720618293])
([-1.2440239387295458, 0.8720450804540057], [0.05902925334751511, 0.6016956802132387])
([-1.2441259417439434, 0.6289994697149073], [-0.06577256855272472, 0.6762747102291482])
([-1.210142434136089, 0.4368770315976506], [-0.188677607179601, 0.7291942568591364])
([-1.159918613868923, 0.28408169071815415], [-0.30726896099260204, 0.7649583990935568])
([-1.1025329550493486, 0.16100716005909121], [-0.42042727561865095, 0.7869985179897889])
([-1.0426125487031446, 0.06047044972523817], [-0.5276934467175253, 0.7979089270264804])
⋮
([-1.2216434770974924, 1.0146166139269628], [0.12021680827049967, 0.5550113775144959])
([-1.2499499900381474, 0.7423750265723162], [-0.00391841642025021, 0.6423528468283755])
([-1.229831087369157, 0.5265058660314383], [-0.1281828192264333, 0.7053724817632377])
([-1.1861148292768198, 0.355578849211398], [-0.24911096207717606, 0.7491505605432118])
([-1.1314960903669982, 0.21881648422641656], [-0.3650489236780046, 0.7776241823022744])
([-1.0724336821492086, 0.10787192092145223], [-0.4752653800399116, 0.7937719633967358])
([-1.0122234000273318, 0.016617787590257133], [-0.5794991103107785, 0.7998530690972221])
([-0.9525349461453905, -0.059398567433266464], [-0.6777283550437248, 0.7976021348885333])
([-0.8941857396495508, -0.12343221924184411], [-0.7700512224747937, 0.7883718532084002])
Let’s plot the orbit and check the energy and angular momentum variation. We know that energy and angular momentum should be constant, and they are also called first integrals.
plot_orbit(sol) = plot(sol, vars = (3, 4), lab = "Orbit", title = "Kepler Problem Solution")
function plot_first_integrals(sol, H, L)
plot(initial_first_integrals[1] .- map(u -> H(u[2, :], u[1, :]), sol.u),
lab = "Energy variation", title = "First Integrals")
plot!(initial_first_integrals[2] .- map(u -> L(u[2, :], u[1, :]), sol.u),
lab = "Angular momentum variation")
end
analysis_plot(sol, H, L) = plot(plot_orbit(sol), plot_first_integrals(sol, H, L))
analysis_plot (generic function with 1 method)
analysis_plot(sol, H, L)
Let’s try to use a Runge-Kutta-Nyström solver to solve this problem and check the first integrals' variation.
sol2 = solve(prob, DPRKN6()) # dt is not necessary, because unlike symplectic
# integrators DPRKN6 is adaptive
@show sol2.u |> length
analysis_plot(sol2, H, L)
Let’s then try to solve the same problem by the ERKN4
solver, which is specialized for sinusoid-like periodic function
sol3 = solve(prob, ERKN4()) # dt is not necessary, because unlike symplectic
# integrators ERKN4 is adaptive
@show sol3.u |> length
analysis_plot(sol3, H, L)
We can see that ERKN4
does a bad job for this problem, because this problem is not sinusoid-like.
One advantage of using DynamicalODEProblem
is that it can implicitly convert the second order ODE problem to a normal system of first order ODEs, which is solvable for other ODE solvers. Let’s use the Tsit5
solver for the next example.
sol4 = solve(prob, Tsit5())
@show sol4.u |> length
analysis_plot(sol4, H, L)
Note
There is drifting for all the solutions, and high order methods are drifting less because they are more accurate.
Conclusion
Symplectic integrator does not conserve the energy completely at all time, but the energy can come back. In order to make sure that the energy fluctuation comes back eventually, symplectic integrator has to have a fixed time step. Despite the energy variation, symplectic integrator conserves the angular momentum perfectly.
Both Runge-Kutta-Nyström and Runge-Kutta integrator do not conserve energy nor the angular momentum, and the first integrals do not tend to come back. An advantage Runge-Kutta-Nyström integrator over symplectic integrator is that RKN integrator can have adaptivity. An advantage Runge-Kutta-Nyström integrator over Runge-Kutta integrator is that RKN integrator has less function evaluation per step. The ERKN4
solver works best for sinusoid-like solutions.
Manifold Projection
In this example, we know that energy and angular momentum should be conserved. We can achieve this through manifold projection. As the name implies, it is a procedure to project the ODE solution to a manifold. Let’s start with a base case, where manifold projection isn’t being used.
using DiffEqCallbacks
function plot_orbit2(sol)
plot(sol, vars = (1, 2), lab = "Orbit", title = "Kepler Problem Solution")
end
function plot_first_integrals2(sol, H, L)
plot(initial_first_integrals[1] .- map(u -> H(u[1:2], u[3:4]), sol.u),
lab = "Energy variation", title = "First Integrals")
plot!(initial_first_integrals[2] .- map(u -> L(u[1:2], u[3:4]), sol.u),
lab = "Angular momentum variation")
end
analysis_plot2(sol, H, L) = plot(plot_orbit2(sol), plot_first_integrals2(sol, H, L))
function hamiltonian(du, u, params, t)
q, p = u[1:2], u[3:4]
qdot(@view(du[1:2]), p, q, params, t)
pdot(@view(du[3:4]), p, q, params, t)
end
prob2 = ODEProblem(hamiltonian, [initial_position; initial_velocity], tspan)
sol_ = solve(prob2, RK4(), dt = 1 // 5, adaptive = false)
analysis_plot2(sol_, H, L)
There is a significant fluctuation in the first integrals, when there is no manifold projection.
function first_integrals_manifold(residual, u, p, t)
residual[1:2] .= initial_first_integrals[1] - H(u[1:2], u[3:4])
residual[3:4] .= initial_first_integrals[2] - L(u[1:2], u[3:4])
end
cb = ManifoldProjection(first_integrals_manifold)
sol5 = solve(prob2, RK4(), dt = 1 // 5, adaptive = false, callback = cb)
analysis_plot2(sol5, H, L)
We can see that thanks to the manifold projection, the first integrals' variation is very small, although we are using RK4
which is not symplectic. But wait, what if we only project to the energy conservation manifold?
function energy_manifold(residual, u, p, t)
residual[1:2] .= initial_first_integrals[1] - H(u[1:2], u[3:4])
residual[3:4] .= 0
end
energy_cb = ManifoldProjection(energy_manifold)
sol6 = solve(prob2, RK4(), dt = 1 // 5, adaptive = false, callback = energy_cb)
analysis_plot2(sol6, H, L)
There is almost no energy variation, but angular momentum varies quite a bit. How about only project to the angular momentum conservation manifold?
function angular_manifold(residual, u, p, t)
residual[1:2] .= initial_first_integrals[2] - L(u[1:2], u[3:4])
residual[3:4] .= 0
end
angular_cb = ManifoldProjection(angular_manifold)
sol7 = solve(prob2, RK4(), dt = 1 // 5, adaptive = false, callback = angular_cb)
analysis_plot2(sol7, H, L)
Again, we see what we expect.