Visualising Marchal's family
In a few talks, I presented a visualisation of what I call the Marchal family of quasi-periodic orbits. This visualisation was done in GLMakie, a great Julia package for advanced interactive visualisation.
Below, I show the code for the visualisation, which is 276 lines long; the #region, #endregion and ## comments are for ease of use within VS Code with the Julia extension. The imported data is in a 250 Mo file (data_trajs_eps_01_33_J_000_160.jld in the code below); I have no convenient way of sharing it right now; however, if you contact me I will gladly share it!
The Project.toml is really short:
[deps]
GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" And here is the code:
#region Setup
dir = @__DIR__
using Pkg; Pkg.activate(dir)
using GLMakie
using JLD
using StaticArrays
Makie.inline!(false)
set_theme!(Theme(fontsize = 24))
function rot1(θ)
return @SMatrix(
[1 0 0;
0 cos(θ) -sin(θ);
0 sin(θ) cos(θ)])
end
function rot3(θ)
return @SMatrix(
[cos(θ) -sin(θ) 0;
sin(θ) cos(θ) 0;
0 0 1])
end
acos_clamped(val) = acos(clamp(val, -1, 1))
masses(ε_m) = (1-2ε_m, ε_m, ε_m)
vec_arr3_to_arr4(vec) = [vec[i][j, k, l] for
i in eachindex(vec), j in axes(vec[1], 1),
k in axes(vec[1], 2), l in axes(vec[1], 3)
]
arr3_vec_to_arr4(arr) = [arr[i, j, k][l] for
i in axes(arr, 1), j in axes(arr, 2),
k in axes(arr, 3), l in eachindex(arr[1, 1, 1])
]
function hill2cart_helio_norot(x, masses)
rt1, rt2, G1, G2, r1, r2, w1, w2, C, Ω1 = x
x_cart = zeros(eltype(x), 18)
J = acos_clamped((C^2 - G1^2 - G2^2) / (2 * G1 * G2))
# rt1 *= (masses[1] + masses[2]) / masses[2]
# rt2 *= (masses[1] + masses[3]) / masses[3]
# G1 *= (masses[1] + masses[2]) / masses[2]
# G2 *= (masses[1] + masses[3]) / masses[3]
i1 = acos_clamped((G1 + G2 * cos(J)) / C)
i2 = acos_clamped((G2 + G1 * cos(J)) / C)
#i1 = J / 2
#i2 = J / 2
Ω2 = Ω1 + π
R1 = rot3(Ω1) * rot1(i1) * [r1 * cos(w1), r1 * sin(w1), 0]
R2 = rot3(Ω2) * rot1(i2) * [r2 * cos(w2), r2 * sin(w2), 0]
Rt1 = rot3(Ω1) * rot1(i1) * rot3(w1) * [rt1, G1/r1, 0]
Rt2 = rot3(Ω2) * rot1(i2) * rot3(w2) * [rt2, G2/r2, 0]
#= Rt1 = rot3(Ω1) * rot1(i1) * [
rt1 * cos(w1) - G1 * sin(w1) / r1,
rt1 * sin(w1) + G1 * cos(w1) / r1,
0
]
Rt2 = rot3(Ω2) * rot1(i2) * [
rt2 * cos(w2) - G2 * sin(w2) / r2,
rt2 * sin(w2) + G2 * cos(w2) / r2,
0
] =#
x_cart[4:6] = R1
x_cart[7:9] = R2
x_cart[13:15] = Rt1# ./ masses[2]
x_cart[16:18] = Rt2# ./ masses[3]
return x_cart
end
function hill2cart_helio_draconitic(x, masses)
return hill2cart_helio_norot(vcat(x[1:9], 0.), masses)
end
function hill2cart_bary(x, masses)
x_cart = hill2cart_helio_norot(x, masses)
m0, m1, m2 = masses
x_bary = zero(x_cart)
x_bary[4:6] = x_cart[4:6] - (m2 / (m0 + m2)) * x_cart[7:9]
x_bary[4:6] ./= (1 + m1/(m0+m2))
x_bary[7:9] = x_cart[7:9] - (m1 / (m0 + m1)) * x_cart[4:6]
x_bary[7:9] ./= (1 + m2/(m0+m1))
x_bary[1:3] = -(m1 * x_bary[4:6] + m2 * x_bary[7:9]) / m0
x_bary[13:15] = x_cart[13:15]
x_bary[16:18] = x_cart[16:18]
x_bary[10:12] = -(x_bary[13:15] + x_bary[16:18])
return x_bary
end
function rot3_point(vec, angle)
return reduce(vcat,
[rot3(angle) * vec[3i .+ (1:3)] for i in 0:5]
)
end
function rot3_trajs_arr4(trajs, angles)
return [
rot3_point(trajs[i, j, k, :], angles[i, j, k])
for i in axes(trajs, 1), j in axes(trajs, 2), k in axes(trajs, 3)
] |> arr3_vec_to_arr4
end
#endregion
##
#region Load trajectories
data_trajs_file = joinpath(dir, "data", "data_trajs_eps_01_33_J_000_160.jld")
data_trajs = load(data_trajs_file)
vec_ts = data_trajs["vec_ts"]
vec_ε_m = data_trajs["vec_ε_m"]
vec_Js = data_trajs["vec_Js"]
arr_eigvals = data_trajs["arr_eigvals"]
arr_trajs = data_trajs["arr_trajs"]
##
vec_masses = [masses(eps_m) for eps_m in vec_ε_m]
trajs_helio_draco = [
hill2cart_helio_draconitic(arr_trajs[ieps, iJ, it, :], vec_masses[ieps])
for ieps in eachindex(vec_ε_m), iJ in eachindex(vec_Js), it in eachindex(vec_ts)
] |> arr3_vec_to_arr4
trajs_helio_norot = [
hill2cart_helio_norot(arr_trajs[ieps, iJ, it, :], vec_masses[ieps])
for ieps in eachindex(vec_ε_m), iJ in eachindex(vec_Js), it in eachindex(vec_ts)
] |> arr3_vec_to_arr4
trajs_bary = [
hill2cart_bary(arr_trajs[ieps, iJ, it, :], vec_masses[ieps])
for ieps in eachindex(vec_ε_m), iJ in eachindex(vec_Js), it in eachindex(vec_ts)
] |> arr3_vec_to_arr4
t_lag = 1.
angles_antirot = [
(
2π - arr_trajs[ieps, iJ, end, 10]
) / t_lag * vec_ts[it] for ieps in eachindex(vec_ε_m), iJ in eachindex(vec_Js), it in eachindex(vec_ts)
]
trajs_bary_antirot = rot3_trajs_arr4(trajs_bary, angles_antirot);
##
arr_to_traj_points(arr_traj) = map(
i_body->[
Point3f(arr_traj[i, j, k, 3i_body .+ (1:3)]...)
for i in axes(arr_traj, 1), j in axes(arr_traj, 2),
k in axes(arr_traj, 3)
],
0:2
)
traj_points_helio_draco = arr_to_traj_points(trajs_helio_draco)
traj_points_helio_norot = arr_to_traj_points(trajs_helio_norot)
traj_points_bary = arr_to_traj_points(trajs_bary)
traj_points_bary_antirot = arr_to_traj_points(trajs_bary_antirot)
traj_points = [
traj_points_helio_draco,
traj_points_helio_norot,
traj_points_bary,
traj_points_bary_antirot,
];
#endregion
##
#region 3D figure
fig_3D = Figure()#; backgroundcolor=:lightgrey)
ax_3D = Axis3(fig_3D[1, 1])
slider_3D = Slider(fig_3D[1, 2];
range=eachindex(vec_Js),
horizontal=false,
linewidth=20,
startvalue=1
)
i_J = lift(identity, slider_3D.value)
text_J = lift(i_J) do iJ
return "J = $(
round(rad2deg(vec_Js[iJ]); sigdigits=3))°"
end
label_3D = Label(fig_3D[2, 2],
text_J
)
play_button = Button(fig_3D[2, 1], label="Play", tellwidth=false)
is_playing = false
end_n = length(vec_ts)
function play!(_)
if !is_playing
@async for i in eachindex(vec_ts)
if i == 1
global is_playing = true
elseif i == end_n
global is_playing = false
end
i_t[] = i
sleep(0.05)
end
end
end
on(play!, play_button.clicks)
on(events(ax_3D.scene).keyboardbutton) do event
if event.action == Keyboard.press || event.action == Keyboard.repeat
if event.key == Keyboard.space
play!(nothing)
end
end
end
slider_ε = Slider(
fig_3D[3, 1];
range=eachindex(vec_ε_m),
horizontal=true,
linewidth=20,
startvalue=1
)
i_ε = lift(identity, slider_ε.value)
text_ε = lift(i_ε) do ieps
return "ε_m = $(
round(vec_ε_m[ieps]; sigdigits=3))"
end
label_ε = Label(fig_3D[3, 2], text_ε, tellwidth=false, width=160)
slider_typetraj = Slider(
fig_3D[4, 1];
range=1:4,
horizontal=true,
linewidth=20,
startvalue=1
)
i_typetraj = lift(identity, slider_typetraj.value)
text_typetraj = lift(i_typetraj) do itraj
return ["Helio rotating", "Helio", "Bary", "Bary antirot"][itraj]
end
label_typetraj = Label(fig_3D[4, 2], text_typetraj)
get_points(new_iJ, i_ε, i_traj, i_body) = traj_points[i_traj][i_body][
i_ε, new_iJ, :]
plotted_mass0 = lift(
(iJ, ieps, i_traj)->get_points(iJ, ieps[], i_traj[], 1),
i_J, i_ε, i_typetraj)
plotted_mass1 = lift(
(iJ, ieps, i_traj)->get_points(iJ, ieps[], i_traj[], 2),
i_J, i_ε, i_typetraj)
plotted_mass2 = lift(
(iJ, ieps, i_traj)->get_points(iJ, ieps[], i_traj[], 3),
i_J, i_ε, i_typetraj)
limits!(ax_3D, (-1.5, 1.5), (-1.5, 1.5), (-1.5, 1.5))
lines!(ax_3D, plotted_mass0; linewidth=1)
lines!(ax_3D, plotted_mass1; linewidth=1)
lines!(ax_3D, plotted_mass2; linewidth=1)
i_t = Observable(1)
points = lift(i_t, plotted_mass0, plotted_mass1, plotted_mass2) do it, traj0, traj1, traj2
return [traj0[i_t[]], traj1[i_t[]], traj2[i_t[]], traj0[i_t[]]]
end
scatter!(ax_3D, points)
lines!(ax_3D, points;
linestyle=:dash
)
fig_3D
##
for i in eachindex(vec_ts)
i_t[] = i
sleep(0.05)
end
##
#endregion
## CC BY-SA 4.0 Alexandre Prieur. Last modified: February 18, 2026. Website built with Franklin.jl and the Julia programming language.