#!/usr/bin/env python3
# pykepler: Numerically solve the restricted 3-body problem and animate the
# movement of the bodies
#
# Copyright (C) 2025 Thomas Guillermo Albers Raviola
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from functools import partial
import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib import rcParams
from matplotlib.animation import FuncAnimation
def unpack_r(rv):
return rv[0:2], rv[2:4], rv[4:6]
def unpack_v(rv):
return rv[6:8], rv[8:10], rv[10:12]
def distance(x1, x2):
return np.sqrt(np.sum((x1 - x2)**2))
def grav_force(r1, r2):
d = distance(r1, r2)
return (r2 - r1) / d**3
#
# Initial value problem has form
# x' = F(x, t)
#
# Set x = [r, v], where r is position of body and v = r'
# Thus, x' = [r', v'] = [v, a] = [v, -G*M*m/abs(d)**2 * e]
#
def func(t, rv, mu):
# Unpack components
r1, r2, r3 = unpack_r(rv)
v1, v2, v3 = unpack_v(rv)
f12 = grav_force(r1, r2)
f13 = grav_force(r1, r3)
f23 = grav_force(r2, r3)
return np.concatenate((v1, v2, v3,
mu[1] * f12 + mu[2] * f13,
-mu[0] * f12 + mu[2] * f23,
-mu[1] * f23 - mu[0] * f13))
def angle(x1, x2):
ux1 = x1 / np.linalg.norm(x1)
ux2 = x2 / np.linalg.norm(x2)
return 180.0 * np.arccos(np.clip(np.dot(ux1, ux2), -1.0, 1.0)) / np.pi
def update(sol, text_artist, tri_artist, pnt_artists, pos_artists, frame):
'''Update plot of the IVP up to the current timestamp'''
if frame > 0:
r1, r2, r3 = unpack_r(sol.y)
r1 = r1[:, frame-1]
r2 = r2[:, frame-1]
r3 = r3[:, frame-1]
tri_artist.set_data([r1[0], r2[0], r3[0], r1[0]],
[r1[1], r2[1], r3[1], r1[1]])
alpha = angle(r2 - r1, r3 - r1)
beta = angle(r1 - r2, r3 - r2)
gamma = angle(r1 - r3, r2 - r3)
a = distance(r2, r3)
b = distance(r3, r1)
c = distance(r2, r1)
fmt = 'α:{:.2f}, β:{:.2f}, γ:{:.2f}\na: {:.2f}, b:{:.2f}, c:{:.2f}'
text_artist.set_text(fmt.format(alpha, beta, gamma, a, b, c))
for artist, r in zip(pos_artists, unpack_r(sol.y)):
artist.set_data(r[0, 0:frame], r[1, 0:frame])
for artist, r in zip(pnt_artists, unpack_r(sol.y)):
artist.set_data([r[0, frame-1]], [r[1, frame-1]])
return [text_artist, tri_artist] + pos_artists + pnt_artists
def animate_ivp(sol, fps=20):
def progress(current_frame, total_frames):
str = '\r Rendering frame [{}/{}] ...'
print(str.format(current_frame + 1, total_frames), end='', flush=True)
fig, ax = plt.subplots(figsize=(10,10), dpi=300)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_xlim(-1, 1)
ax.set_ylim(-1, 1)
pos_artists = []
pnt_artists = []
tri_artist, = ax.plot([], [], ls='--', color='black')
for i in range(3):
pos_artists.append(ax.plot([], [])[0])
pnt_artists.append(ax.plot([], [], '.', label='m{}'.format(i + 1))[0])
text_artist = ax.text(0.05, 0.95, '',
backgroundcolor='#7F7F7F33',
transform=ax.transAxes,
verticalalignment='top') # bbox=props
ax.legend()
frames = np.arange(0, len(sol.t))
animation = FuncAnimation(fig,
partial(update, sol,
text_artist, tri_artist,
pnt_artists, pos_artists),
frames=frames,
interval=sol.t[-1], blit=True, repeat=False)
animation.save('three-body.mp4', fps=fps, progress_callback=progress)
# Reserve newline for progress
print('')
def main():
mu1 = .7
mu2 = 1 - mu1
mu = np.array([mu1, mu2, 1e-12])
omega = 1
ir1 = mu[1] * np.array([-1, 0])
ir2 = mu[0] * np.array([1, 0])
# Trojan satelite at L4 (ahead)
ir3 = np.array([.5 - mu2, np.sqrt(3) / 2])
iv1 = mu[1] * np.array([0, -1])
iv2 = mu[0] * np.array([0, 1])
# yz - zy, zx - xz
iv3 = np.array([- ir3[1], ir3[0]])
irv = np.concatenate((ir1, ir2, ir3, iv1, iv2, iv3))
t_span = (0.0, 4)
t_eval = np.linspace(t_span[0], t_span[1], 1024)
sol = solve_ivp(func, t_span, irv,
t_eval=t_eval, method='DOP853',
rtol=1e-10, atol=1e-20,
args=(mu,))
animate_ivp(sol)
if __name__ == '__main__':
main()