Created
December 22, 2018 13:05
-
-
Save tcbennun/4d71a6a203841e088af191c56b15f1e1 to your computer and use it in GitHub Desktop.
code used to create this video: https://youtu.be/7YmBSqWw7PY
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Copyright (C) 2018 Torin Cooper-Bennun | |
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/>. | |
simulate a double pendulum's motion by numerically integrating the equations of motion as derived from the system's | |
Lagrangian. | |
""" | |
import time | |
import numpy as np | |
import scipy.integrate as integrate | |
import matplotlib.pyplot as plt | |
from matplotlib.animation import FFMpegWriter | |
from matplotlib.collections import LineCollection | |
def f(t, y): | |
""" | |
equations of motion for the double pendulum. motion is defined in terms of the angle of each limb from the vertical | |
(these are the 2 generalised coordinates) and their conjugate momenta. this function calculates the first time | |
derivative of each of these quantities, and returns them as a vector. see | |
<https://en.wikipedia.org/wiki/Double_pendulum#Lagrangian> for the equations. | |
args: | |
t : time (s) [scalar] | |
y : variables of motion: (theta_1, theta_2, p_theta_1, p_theta_2) [np.ndarray, shape (1,)] | |
the limb lengths and masses are taken to be equal to 1m and 1kg respectively. | |
""" | |
m = 1 | |
l = 1 | |
# gravitational acceleration | |
g = 9.81 | |
theta_1, theta_2, p_theta_1, p_theta_2 = y | |
A = 3*np.cos(theta_1 - theta_2) | |
B = 6/(m*l*l) / (16 - A*A) | |
theta_1_dt = B * (2*p_theta_1 - A*p_theta_2) | |
theta_2_dt = B * (8*p_theta_2 - A*p_theta_1) | |
C = -m*l*l/2 | |
D = theta_1_dt*theta_2_dt*np.sin(theta_1 - theta_2) | |
p_theta_1_dt = C*(D + 3*g/l * np.sin(theta_1)) | |
p_theta_2_dt = C*(-D + g/l * np.sin(theta_2)) | |
return np.array([theta_1_dt, theta_2_dt, p_theta_1_dt, p_theta_2_dt]) | |
def double_pendulum_trajectory(theta_1_initial, theta_2_initial, t_0, t_f, N): | |
""" | |
calculate the trajectory of the double pendulum, given the initial angles of the limbs (from the vertical), between | |
times t_0 and t_f (N points in time). | |
""" | |
t_eval = np.linspace(t_0, t_f, N, dtype=np.float64) | |
y_0 = np.array([theta_1_initial, theta_2_initial, 0, 0], dtype=np.float64) | |
return integrate.solve_ivp(f, (t_0, t_f), y_0, t_eval=t_eval) | |
def coordinates_from_trajectory(y_out): | |
""" | |
given integration return value y, which has the trajectory in terms of the angles, convert to x,y and return | |
(assumes l = 1m, m = 1kg, for both limbs) | |
""" | |
l = 1 | |
m = 1 | |
theta_1 = y_out[0] | |
theta_2 = y_out[1] | |
# calculate end position from angles | |
x = l*(np.sin(theta_1) + np.sin(theta_2)) | |
y = -l*(np.cos(theta_1) + np.cos(theta_2)) | |
return x, y | |
def animate_trajectory_coloured(t, y, img_width=1920, img_height=1080, basic=False, fps=30): | |
""" | |
use matplotlib's FFmpeg writer class to create an animation of the double pendulum's trajectory | |
""" | |
if basic: | |
# override fps in basic mode | |
fps = 10 | |
# initialise animation writer | |
metadata = dict(title='double pendulum', artist='tcbennun') | |
writer = FFMpegWriter(fps=fps, metadata=metadata) | |
# pendulum things | |
l = 1 | |
m = 1 | |
theta_1 = y[0] | |
theta_2 = y[1] | |
# get x, y coordinates, and then convert them into an array of line segments | |
x, y = coordinates_from_trajectory(y) | |
# coordinates of the limbs | |
limbs_x = np.array([np.zeros_like(theta_1), l*np.sin(theta_1), x]) | |
limbs_y = np.array([np.zeros_like(theta_1), -l*np.cos(theta_1), y]) | |
# initialise figure | |
dpi = 100 | |
fig, ax = plt.subplots(figsize=(img_width/dpi, img_height/dpi), dpi=dpi) | |
if basic: | |
traj_line, = plt.plot([], [], linewidth=2, color="white") | |
else: | |
# convert coordinates into array of segments (each containing 2 points) | |
points = np.array([x, y]).T.reshape(-1, 1, 2) | |
segments = np.concatenate([points[:-1], points[1:]], axis=1) | |
# use time array for colouring | |
color_arr = 0.5*(t[1:] + t[:-1]) | |
norm = plt.Normalize(color_arr.min(), color_arr.max()) | |
# line object used to render the limbs | |
pend_line, = ax.plot([], [], linewidth=10, color="xkcd:bright blue") | |
# aesthetic display options | |
ax.tick_params(which="both", left=False, bottom=False, labelleft=False, labelbottom=False) | |
ax.set_aspect(1) | |
ax.set_facecolor("black") | |
# ensure the subplot fills the figure, and the aspect ratio matches | |
height = 4.4 | |
width = img_width/img_height * height | |
ax.set_xlim(-width/2, width/2) | |
ax.set_ylim(-height/2, height/2) | |
ax.set_position([0, 0, 1, 1]) | |
# animation fiddling | |
length = t[-1] - t[0] | |
frames = int(length * fps) | |
points = len(t) | |
points_per_frame = int(np.ceil(points/frames)) | |
print(f"{frames} frames to process. Recording...") | |
t0 = time.perf_counter() | |
# produce the animation | |
prev_i = 0 | |
with writer.saving(fig, "writer_test.mp4", dpi): | |
for i in np.arange(0, points, points_per_frame): | |
print(f"{t[i]:.3f} seconds of {length} ({t[i]/length*100:.1f}%)... ", end="\r") | |
if basic: | |
traj_line.set_data(x[:i], y[:i]) | |
else: | |
# need to add a new bunch of segments upon each frame | |
lc = LineCollection(segments[prev_i:i], cmap="RdYlBu", norm=norm) | |
lc.set_array(color_arr[prev_i:i]) | |
lc.set_linewidth(2) | |
ax.add_collection(lc) | |
pend_line.set_data(limbs_x[:, i], limbs_y[:, i]) | |
writer.grab_frame() | |
prev_i = i | |
t1 = time.perf_counter() | |
print(f"\nDone in {t1-t0:.3f} s.") | |
def dancing_flower_1(): | |
t_max = 30 | |
time_res = 1/1000 | |
ret = double_pendulum_trajectory(np.pi - 0.1, 0.1, 0, t_max, int(np.ceil(t_max/time_res))) | |
# visualise_aesthetic(ret.t, ret.y, 1080, 1080) | |
print("Animating...") | |
animate_trajectory_coloured(ret.t, ret.y, 3840, 2160, fps=60) | |
if __name__ == "__main__": | |
dancing_flower_1() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment