The basis of a pendulum wave is that, in a certain time (the time of the whole "dance" - let's call this $\Gamma$) the pendulum on the longest length string will oscillate $N$ times. The length of each successive shorter pendulum is adjusted such that it executes one additional oscillation in this same time $\Gamma$.
$\Gamma$ will be defined as $60 seconds$, as this number is arbitrary, and 60 seconds is a good, even duration after which the dance will reset and repeat. (The dance will repeat infinitely, assuming that air resistance is negligible. In reality, all pendulums will come to a stop after some time due to air resistance, but 60 seconds should be a short enough time that air resistance will have little effect on the pendulums over this $\Gamma$.)
Other pendulum waves have found $N=51$ to be a good number, used in conjunction with $\Gamma=60 seconds$. Changing $N$ would make the "dance" quicker or slower, and others have found 51 oscillations over 60 seconds (for the longest pendulum) to provide a good viewing speed, so we will use $N=51$ as well.
Therefore, if 12 pendulums are used (another arbitrary number that has worked well in creating other pendulum wave machines), the 1st pendulum (longest) undergoes 51 oscillations in $\Gamma$ seconds, while the 12th pendulum (shortest) undergoes 62 oscillations (51 oscilations + 11 pendulums from the first) in this same $\Gamma=60 seconds$.
Since $T = \frac{seconds}{oscillation}$, the period of the longest pendulum is $T = \frac{\Gamma}{N}$
Because other pendulums undergo $N$ + (pendulums from the first) oscillations, their period can be written as a function of pendulums from the first: $T(n) = \frac{\Gamma}{N + n}$
Knowing that $T = 2\pi \sqrt{\frac{l}{g}}$, $T(n)$ can be substituted for $T$, yielding $\frac{\Gamma}{N + n} = 2\pi \sqrt{\frac{l(n)}{g}}$
$l$ can then be solved for:
For our pendulum wave, consisting of twelve pendulums, Python 3 code was written to solve for the required $l$ values (with $n$ ranging from 0 to 11 for the longest to shortest pendulums, respectively. This code and its output can be seen below:
In [1]:
import math
from math import pi
lengthsM = []
danceDuration = 60
mostOscils = 51
# Where n is the index of the pendulum (starting at 0)
length = lambda n: 9.81 * (danceDuration / (2*pi*(mostOscils + n)))**2
for n in range(12): # 12 pendulums, indexed 0-11
lengthsM.append(length(n))
# Convert lengths to inches for ease
lengthsIn = []
for length in lengthsM:
lengthsIn.append(length*39.3701)
lengthsMRounded = []
for length in lengthsM:
lengthsMRounded.append(round(length, 3))
lengthsInRounded = []
for length in lengthsIn:
lengthsInRounded.append(round(length, 3))
print("Lengths in meters: {}".format(lengthsMRounded))
print()
print("Lengths in inches: {}".format(lengthsInRounded))
From this point, the pendulum was built. Note that the lengths listed above are from the pivot point of the pendulums to the centers of mass of the hanging golf balls, so the actual strings will be shorter than the numbers given above.
The pendulums were spaced so that there was 0.5in between the golf balls, meaning that strings were hung about 2.2in, or 5.6cm, apart.
Below, a visualization of the device can be seen (from above). This simulates the position the center of mass of each golf ball, as if the pendulum was started with the balls all 10º from their equilibrium position. A dashed line has been drawn in to show the approximate pattern of the pendulums.
In [2]:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation
%matplotlib inline
location = np.linspace(0, 0.056*11, 12)
funcTheta = lambda theta_i, n, t: theta_i * math.cos((2 * pi * t) / (60 / (51 + n)))
funcDispX = lambda l, theta: l * math.sin(theta)
funcDispY = lambda l, theta: -1 * l * math.cos(theta)
# Set up the figure, the axis, and the plot we want to animate
fig, ax = plt.subplots()
ax.set_xlim([0, location[11]])
ax.set_ylim([-0.06, 0.06])
line, = ax.plot([], [], linestyle='--', marker='o')
plt.xlabel("Position along the machine, meters")
plt.ylabel("Horizonal displacement from equilibrium, meters")
fig.suptitle("Top View at t = 0 seconds", fontsize=12)
ax.spines['left'].set_position('zero')
ax.spines['right'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['top'].set_color('none')
ax.xaxis.labelpad = 100
def init1():
line.set_data([], [])
return (line,)
def animate1(t):
displacementArr = []
for n in range(12):
displacementArr.append(funcDispX(lengthsM[n], funcTheta(math.radians(10), n, t)))
fig.suptitle("Top View at t = {:.3f} seconds".format(round(t, 3)), fontsize=12)
line.set_data(location, np.array(displacementArr))
return (line,)
fps = 60
anim = animation.FuncAnimation(fig, animate1, init_func=init1, frames=np.linspace(0, 60, 60*fps//2), blit=True)
anim.save('animTopDown.mp4', writer='ffmpeg', fps=str(fps), extra_args=['-vcodec', 'libx264'])
plt.close()
In [3]:
# Set up the figure, the axis, and the plot we want to animate
fig, ax = plt.subplots()
ax.set_xlim([-0.15, 0.15])
ax.set_ylim([-0.4, 0])
line, = ax.plot([], [], linestyle='', marker='o')
plt.xlabel("Horizonal displacement from equilibrium, meters")
plt.ylabel("Vertical displacement from equilibrium, meters")
fig.suptitle("Top View at t = 0 seconds", fontsize=12)
ax.spines['left'].set_position('zero')
ax.spines['right'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['top'].set_color('none')
ax.xaxis.labelpad = 220
ax.yaxis.labelpad = 150
def init2():
line.set_data([], [])
return (line,)
def animate2(t):
xPos = []
yPos = []
for n in range(12):
xPos.append(funcDispX(lengthsM[n], funcTheta(math.radians(10), n, t)))
yPos.append(funcDispY(lengthsM[n], funcTheta(math.radians(10), n, t)))
fig.suptitle("Head-On View at t = {:.3f} seconds".format(round(t, 3)), fontsize=12)
line.set_data(np.array(xPos), np.array(yPos))
return (line,)
fps = 60
anim = animation.FuncAnimation(fig, animate2, init_func=init2, frames=np.linspace(0, 60, 60*fps//2), blit=True)
anim.save('animHeadOn.mp4', writer='ffmpeg', fps=str(fps), extra_args=['-vcodec', 'libx264'])
plt.close()