Poincaré maps

In this example, we shall illustrate how to construct a Poincaré map associated with the surface of section $x=0$, $\dot x>0$, for $E=0.1025$ for the Hénon-Heiles system. This is equivalent to find the roots of an appropriate function g(dx, x, params, t). We illustrate the implementation using many initial conditions (Monte Carlo like implementation), and then compare the results with the use of jet transport techniques.

Monte Carlo simulation

The Hénon-Heiles system is a 2-dof Hamiltonian system used to model the (planar) motion of a star around a galactic center. The Hamiltonian is given by $H = (p_x^2+p_y^2)/2 + (x^2+y^2)/2 + \lambda (x^2y-y^3/3)$, from which the equations of motion can be obtained; below we concentrate in the case $\lambda=1$.

# Hamiltonian
V(x,y) = 0.5*( x^2 + y^2 )+( x^2*y - y^3/3)
H(x,y,p,q) = 0.5*(p^2+q^2) + V(x, y)
H(x) = H(x...)

# Equations of motion
function henonheiles!(dq, q, p, t)
    x, y, px, py = q
    dq[1] = px
    dq[2] = py
    dq[3] = -x-2y*x
    dq[4] = -y-(x^2-y^2)
    nothing
end

We set the initial energy, which is a conserved quantity; x0 corresponds to the initial condition, which will be properly adjusted to be in the correct energy surface.

# initial energy and initial condition
const E0 = 0.1025
x0 = [0.0, 0.45335, 0.0, 0.0]

In order to be able to generate (random) initial conditions with the appropriate energy, we write a function px, which depends on x, y, py and the energy E, that returns the value of px>0 for which the initial condition [x, y, px, py] has energy E:

# px: select px0>0 such that E=E0
px(x, E) = sqrt(2(E-V(x[1], x[2]))-x[4]^2)

# px!: in-place version of px; returns the modified initial condition `x0`
function px!(x, E)
    mypx = px(x, E)
    x[3] = mypx
    return x
end

# run px!
px!(x0, E0)
4-element Vector{Float64}:
 0.0
 0.45335
 0.24817464176177093
 0.0

Let's check that the initial condition x0 has actually energy equal to E0, up to roundoff accuracy:

H(x0)
0.1025

The scalar function g, which may depend on the time t, the vector of dependent variables x, the velocities dx, and perhaps some parameters params, following again the convention of DifferentialEquations.jl, defines the surface of section by means of the condition $g(dx, x, params, t) = 0$. Internally, the function g is assumed to return a Tuple{Bool, Taylor1{T}}, where T corresponds to eltype(x[1]) (x::Vector{Taylor1{T}}). In the particular case that the user wishes to discard a particular crossing (or crossings), the function g must return a false value, as will be illustrated below.

For the present example, we are looking for crossings through the surface $x=0$, which corresponds to x[1]==0, restricting the crossings to satisfy $\dot x > 0$. i.e., x[3]>0. We thus define the function g as

# x=0, px>0 section
function g(dx, x, p, t)
    px_ = constant_term(x[3])
    # if px > 0...
    if px_ > zero(px_)
        return (true, x[1])
    else
        #otherwise, discard the crossing
        return (false, x[1])
    end
end
Note

Note that in the definition of g we want to make sure that we only take the "positive" crossings through the surface of section $x=0$; hence the if...else... block.

We initialize some auxiliary arrays, where we shall save the solutions:

# number of initial conditions
nconds = 100
tvSv = Vector{Vector{Float64}}(undef, nconds)
xvSv = Vector{Matrix{Float64}}(undef, nconds)
gvSv = Vector{Vector{Float64}}(undef, nconds)
x_ini = similar(x0)

We generate nconds random initial conditions in a small neighborhood around x0 and integrate the equations of motion from t0=0 to tmax=135, using a polynomial of order 25 and absolute tolerance 1e-25:

using TaylorIntegration

for i in 1:nconds
    rand1 = rand()
    rand2 = rand()
    x_ini .= x0 .+ 0.005 .* [0.0, sqrt(rand1)*cos(2pi*rand2), 0.0, sqrt(rand1)*sin(2pi*rand2)]
    px!(x_ini, E0)   # ensure initial energy is E0

    sol_i = taylorinteg(henonheiles!, g, x_ini, 0.0, 135.0,
        25, 1e-25, maxsteps=30000);
    tvSv[i] = vcat(0.0, sol_i.t)
    xvSv[i] = vcat(transpose(x_ini), sol_i.x)
    gvSv[i] = vcat(0.0, sol_i.gresids)
end

We generate an animation with the solutions

using Plots
poincare_anim1 = @animate for i=1:21
    scatter(map(x->x[i,2], xvSv), map(x->x[i,4], xvSv), label="$(i-1)-th iterate",
        m=(1,stroke(0)), ratio=:equal)
    xlims!(0.09,0.51)
    ylims!(-0.11,0.11)
    xlabel!("y")
    ylabel!("py")
    title!("Hénon-Heiles Poincaré map (21 iterates)")
end
Plots.Animation("/tmp/jl_MPfwiC", ["000001.png", "000002.png", "000003.png", "000004.png", "000005.png", "000006.png", "000007.png", "000008.png", "000009.png", "000010.png"  …  "000012.png", "000013.png", "000014.png", "000015.png", "000016.png", "000017.png", "000018.png", "000019.png", "000020.png", "000021.png"])
gif(poincare_anim1, "poincareanim1.gif", fps = 2)
[ Info: Saved animation to /home/runner/work/TaylorIntegration.jl/TaylorIntegration.jl/docs/build/poincareanim1.gif

Poincaré map for the Hénon Heiles system

Jet transport

Now, we illustrate the use of jet transport techniques in the same example, that is, we propagate a neighborhood around x0, which will be plotted in the Poincaré map. We first define the vector of small increments of the phase space variables, xTN; we fix the maximum order of the polynomial expansion in these variables to be 4. Then, x0TN is the neighborhood in the 4-dimensional phase space around $x0$.

xTN = set_variables("δx δy δpx δpy", numvars=length(x0), order=4)
x0TN = x0 .+ xTN

As it was shown above, $x0$ belongs to the energy surface $H(x0) = E_0 = 0.1025$; yet, as it was defined above, the set of phase space points denoted by x0TN includes points that belong to other energy surfaces. This can be noticed by computing H(x0TN)

H(x0TN)
 0.1025 + 0.2478237775 δy + 0.24817464176177093 δpx + 0.9533499999999999 δx² + 0.046650000000000025 δy² + 0.5 δpx² + 0.5 δpy² + 1.0 δx² δy - 0.3333333333333333 δy³ + 𝒪(‖x‖⁵)

Clearly, the expression above may contain points whose energy is different from E0. As it was done above, we shall fix the px component of x0TN so all points of the neighborhood are in the same energy surface.

px!(x0TN, E0) # Impose that all variations are on the proper energy shell!
H(x0TN)
 0.1025 - 5.551115123125783e-17 δy² - 5.551115123125783e-17 δy³ - 1.7763568394002505e-15 δx² δy² - 4.440892098500626e-16 δy⁴ + 8.881784197001252e-16 δy² δpy² + 𝒪(‖x‖⁵)

We notice that the coefficients of all monomials whose order is not zero are very small, and the constant_term is E0.

In order to properly handle this case, we need to extend the definition of g to be useful for Taylor1{TaylorN{T}} vectors.

#specialized method of g for Taylor1{TaylorN{T}}'s
function g(dx::Array{Taylor1{TaylorN{T}},1}, x::Array{Taylor1{TaylorN{T}},1},
        p, t) where {T<:Number}
    px_ = constant_term(constant_term(x[3]))
    if px_ > zero( T )
        return (true, x[1])
    else
        return (false, x[1])
    end
end

We are now set to carry out the integration.

solTN = taylorinteg(henonheiles!, g, x0TN, 0.0, 135.0, 25, 1e-25, maxsteps=30000);

We define some auxiliary arrays, and then make an animation with the results for plotting.

#some auxiliaries:
tvTN, xvTN, tvSTN, xvSTN, gvSTN = solTN.t, solTN.x, solTN.tevents, solTN.xevents, solTN.gresids
xvSTNaa = Array{Array{TaylorN{Float64},1}}(undef, length(tvSTN)+1 );
xvSTNaa[1] = x0TN
for ind in 2:length(tvSTN)+1
    whatever = xvSTN[ind-1,:]
    xvSTNaa[ind] = whatever
end
tvSTNaa = union([zero(tvSTN[1])], tvSTN);

myrnd  = 0:0.01:1
npoints = length(myrnd)
ncrosses = length(tvSTN)
yS = Array{Float64}(undef, ncrosses+1, npoints)
pS = Array{Float64}(undef, ncrosses+1, npoints)

myrad=0.005
ξy = @. myrad * cos(2pi*myrnd)
ξp = @. myrad * sin(2pi*myrnd)

for indpoint in 1:npoints
    yS[1,indpoint] = x0[2] + ξy[indpoint]
    pS[1,indpoint] = x0[4] + ξp[indpoint]
    mycond = [0.0, ξy[indpoint], 0.0, ξp[indpoint]]
    for indS in 2:ncrosses+1
        temp = evaluate(xvSTNaa[indS], mycond)
        yS[indS,indpoint] = temp[2]
        pS[indS,indpoint] = temp[4]
    end
end

poincare_anim2 = @animate for i=1:21
    scatter(map(x->x[i,2], xvSv), map(x->x[i,4], xvSv), marker=(:circle, stroke(0)),
        markersize=0.01, label="Monte Carlo")
    plot!(yS[i,:], pS[i,:], width=0.1, label="Jet transport")
    xlims!(0.09,0.51)
    ylims!(-0.11,0.11)
    xlabel!("y")
    ylabel!("py")
    title!("Poincaré map: 4th-order jet transport vs Monte Carlo")
end
Plots.Animation("/tmp/jl_l7MCkm", ["000001.png", "000002.png", "000003.png", "000004.png", "000005.png", "000006.png", "000007.png", "000008.png", "000009.png", "000010.png"  …  "000012.png", "000013.png", "000014.png", "000015.png", "000016.png", "000017.png", "000018.png", "000019.png", "000020.png", "000021.png"])
gif(poincare_anim2, "poincareanim2.gif", fps = 2)
[ Info: Saved animation to /home/runner/work/TaylorIntegration.jl/TaylorIntegration.jl/docs/build/poincareanim2.gif

Poincaré map: Jet transport vs Monte Carlo

The next animation is the same as before, adapting the scale.

poincare_anim3 = @animate for i=1:21
    scatter(map(x->x[i,2], xvSv), map(x->x[i,4], xvSv), marker=(:circle, stroke(0)),
        markersize=0.1, label="Monte Carlo")
    plot!(yS[i,:], pS[i,:], width=0.5, label="Jet transport")
    xlabel!("y")
    ylabel!("py")
    title!("Poincaré map: 4th-order jet transport vs Monte Carlo")
end
Plots.Animation("/tmp/jl_SvM9Y9", ["000001.png", "000002.png", "000003.png", "000004.png", "000005.png", "000006.png", "000007.png", "000008.png", "000009.png", "000010.png"  …  "000012.png", "000013.png", "000014.png", "000015.png", "000016.png", "000017.png", "000018.png", "000019.png", "000020.png", "000021.png"])
gif(poincare_anim3, "poincareanim3.gif", fps = 2)
[ Info: Saved animation to /home/runner/work/TaylorIntegration.jl/TaylorIntegration.jl/docs/build/poincareanim3.gif

Poincaré map: Jet transport vs Monte Carlo