Coupled Harmonic Oscillator#
Teng-Jui Lin
Content adapted from Mr. Ben Trey’s coupled harmonic oscillator internet. The application of scipy.integrate.solve_ivp()
and phase portrait is related to UW AMATH 301, Beginning Scientific Computing.
Phase portraits
Coupled harmonic oscillator
Coupled harmonic oscillator internet
Coupled harmonic oscillator#
Problem Statement. Consider a coupled harmonic oscillator with two blocks
Suppose
Masses:
Spring constants:
Equilibrium length of springs:
Damping constants:
Deliverables:
Generate a plot of
and over timeGenerate an animation of the system
Solution. From the above specifications, we can get some additional information. Since we have no external forces, so we have
Now, we analyze the forces on each block. The net vertical force is zero because gravitational force and the normal force cancels. Friction can be neglected because of smooth surface. We then look at the horizontal forces for
For
which form the ODE
For
which form the ODE
The above two ODEs are coupled system of linear second-order ODEs. They can be written into a system of
that can be solved by scipy.integrate.solve_ivp()
.
Solve the system#
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import scipy
from scipy import integrate
# model params
m1 = 1 # kg
m2 = 1 # kg
k1 = 100 # N/m
k2 = 100 # N/m
k3 = 100 # N/m
c1 = 3 # kg/s
c2 = 3 # kg/s
l1 = 1 # m
l2 = 1 # m
l3 = 1 # m
# time array
t_initial = 0
t_final = 2
t = np.linspace(t_initial, t_final, 1000)
t_len = len(t)
# initial values (x is relative to eqm length)
x1_init = -0.2
v1_init = 0
x2_init = 0.2
v2_init = 0
initial_values = [x1_init, v1_init, x2_init, v2_init]
# external forces
F1 = lambda t : 0
F2 = lambda t : 0
# def F1(t):
# '''External force on m1.'''
# omega = 100
# A = np.sqrt(k1 + k2)+1000
# phi = 1
# return A*np.sin(omega*t + phi)
# def F1(t):
# if(int(t/3)%2 == 0):
# omega = 1e4
# else:
# omega = np.sqrt(k1 + k2)
# A = 20
# phi = 1
# return A*np.sin(omega*t + phi)
## equivalent, simpler form for coupled ode systems
## note: this form not convenient for phase portrait
def coupled_ode_syst(t, Y):
# t -> independent variable
# Y -> functions evaluated at independent variable
x1, v1, x2, v2 = Y
derivatives = np.array([
v1,
(-k1*x1 - k2*(x1 - x2) - c1*v1 + F1(t))/m1,
v2,
(-k2*(x2 - x1) - k3*x2 - c2*v2 + F2(t))/m2,
])
return derivatives
ode_soln = scipy.integrate.solve_ivp(coupled_ode_syst, [t_initial, t_final], initial_values, t_eval=t).y
x1, v1, x2, v2 = ode_soln
Position plot#
def custom_plot_settings():
%config InlineBackend.figure_format = 'retina'
%matplotlib inline
plt.rcParams.update({
'font.family': 'Arial', # Times New Roman, Calibri
'font.weight': 'normal',
'mathtext.fontset': 'cm',
'font.size': 18,
'lines.linewidth': 2,
'axes.linewidth': 2,
'axes.spines.top': False,
'axes.spines.right': False,
'axes.titleweight': 'bold',
'axes.titlesize': 18,
'axes.labelweight': 'bold',
'xtick.major.size': 8,
'xtick.major.width': 2,
'ytick.major.size': 8,
'ytick.major.width': 2,
'figure.dpi': 80,
'savefig.dpi': 300,
'legend.framealpha': 1,
'legend.edgecolor': 'black',
'legend.fancybox': False,
'legend.fontsize': 14,
'animation.html': 'html5',
})
custom_plot_settings()
fig, axs = plt.subplots(2, 1, figsize=(5, 6), sharex=True)
axs[0].plot(t, x1, label='$x_1(t)$')
axs[0].plot(t, x2, label='$x_2(t)$')
axs[0].set_ylabel('$x(t)$')
# axs[0].set_ylim(0.5, 2.5)
axs[1].plot(t, v1, label='$v_1(t)$')
axs[1].plot(t, v2, label='$v_2(t)$')
axs[1].set_xlabel('$t$')
axs[1].set_ylabel('$v(t)$')
# axs[1].set_ylim(-3.5, 3.5)
for i in range(2):
axs[i].set_xlim(t_initial, t_final)
axs[i].plot([t_initial, t_final], [0, 0], '--', color='grey', lw=0.5, zorder=0) # zero ref

Animation of the system#
Note on animation: for local reproducible results, download ffmpeg and add to path variable. For reproducible results online, use Google Colab and run the command below.
# Run the command in Google Colab for reproducible results online
# !apt install ffmpeg
def spring_xy(left_bound, right_bound, vertical_shift=0.25, harmonic=15, amplitude=0.1):
'''
Returns array of x and y coordinates of spring animation object
as a standing wave with wave function
y = A sin(2pi(x-x0)/lambda) + b
where
lambda = 2L/n
'''
interval_len = right_bound - left_bound
wave_len = 2*(interval_len)/harmonic
spring_x = np.linspace(left_bound, right_bound, 100)
spring_y = amplitude*np.sin(2*np.pi*(spring_x - left_bound)/wave_len) + vertical_shift
return np.array([spring_x, spring_y])
# animation object params
block_width = 0
block_height = 0.5
block_center_offset = block_width / 2
wall1_x = 0
block1_eqm_x = wall1_x + l1
block2_eqm_x = block1_eqm_x + l2
wall2_x = block2_eqm_x + l3
# ## interactive plot
# ## uncomment for testing
# ## used for checking time series before making animation
# ## can be used to test all animation below
# # plot settings
# custom_plot_settings()
# %matplotlib qt
# # plot static portion
# fig, ax = plt.subplots(figsize=(5, 2))
# ax.set_xlim(wall1_x, wall2_x)
# ax.set_ylim(0, 1)
# ax.spines['right'].set_visible(True)
# plt.tight_layout()
# # plot empty framework
# block1 = ax.add_patch(patches.Rectangle((block1_eqm_x+block_center_offset, 0), block_width, block_height, fill=False, color='black', zorder=10, lw=2))
# block2 = ax.add_patch(patches.Rectangle((block2_eqm_x+block_center_offset, 0), block_width, block_height, fill=False, color='black', zorder=10, lw=2))
# spring1, = ax.plot([], [], color='black')
# spring2, = ax.plot([], [], color='purple')
# spring3, = ax.plot([], [], color='black')
# title = ax.set_title('')
# # animation parameters
# t_range = t_len # manually set t range, default t_len
# anim_time = 10 # s
# fps = 60
# frame_num = int(fps * anim_time)
# # update changes each frame
# for n in range(frame_num):
# time_points = round(t_range/frame_num)
# frame_final_time = min(time_points*n+time_points, t_range-1) # avoid index out of range
# title.set_text(f't = {t[frame_final_time] :.2f}')
# # spring 1
# left_bound = wall1_x
# right_bound = block1_eqm_x + x1[frame_final_time] - block_center_offset
# spring1.set_data(*spring_xy(left_bound, right_bound))
# # spring 2
# left_bound = block1_eqm_x + x1[frame_final_time] + block_center_offset
# right_bound = block2_eqm_x + x2[frame_final_time] - block_center_offset
# spring2.set_data(*spring_xy(left_bound, right_bound))
# # spring 3
# left_bound = block2_eqm_x + x2[frame_final_time] + block_center_offset
# right_bound = wall2_x
# spring3.set_data(*spring_xy(left_bound, right_bound))
# # blocks
# block1.set_xy([block1_eqm_x + x1[frame_final_time] - block_center_offset, 0])
# block2.set_xy([block2_eqm_x + x2[frame_final_time] - block_center_offset, 0])
# plt.pause(0.0001)
def make_animation(t_range=t_len, anim_time=4, fps=60):
'''
This function is notebook-specific and not meant to generalize to other settings.
Makes animation of coupled harmonic oscillator with damping.
Warning: Many parameters are taken from the global namespace. They need to be defined before use.
'''
# back to static plot and animations
custom_plot_settings()
# plot static portion
fig, ax = plt.subplots(figsize=(5, 2))
ax.set_xlim(wall1_x, wall2_x)
ax.set_ylim(0, 1)
ax.spines['right'].set_visible(True)
plt.tight_layout()
# plot empty framework
block1 = ax.add_patch(patches.Rectangle((block1_eqm_x+block_center_offset, 0), block_width, block_height, fill=False, color='black', zorder=10, lw=2))
block2 = ax.add_patch(patches.Rectangle((block2_eqm_x+block_center_offset, 0), block_width, block_height, fill=False, color='black', zorder=10, lw=2))
spring1, = ax.plot([], [], color='black')
spring2, = ax.plot([], [], color='purple')
spring3, = ax.plot([], [], color='black')
title = ax.set_title('')
def draw_frame(n):
'''
Commands to update parameters.
Here, the spring and block objects and the title.
'''
time_points = round(t_range/frame_num)
frame_final_time = min(time_points*n+time_points, t_range-1) # avoid index out of range
title.set_text(f't = {t[frame_final_time] :.2f}')
# spring 1
left_bound = wall1_x
right_bound = block1_eqm_x + x1[frame_final_time] - block_center_offset
spring1.set_data(*spring_xy(left_bound, right_bound))
# spring 2
left_bound = block1_eqm_x + x1[frame_final_time] + block_center_offset
right_bound = block2_eqm_x + x2[frame_final_time] - block_center_offset
spring2.set_data(*spring_xy(left_bound, right_bound))
# spring 3
left_bound = block2_eqm_x + x2[frame_final_time] + block_center_offset
right_bound = wall2_x
spring3.set_data(*spring_xy(left_bound, right_bound))
# blocks
block1.set_xy([block1_eqm_x + x1[frame_final_time] - block_center_offset, 0])
block2.set_xy([block2_eqm_x + x2[frame_final_time] - block_center_offset, 0])
return fig,
# create animation of given time length
# note here we fit all the data points into the given animation time
from matplotlib import animation
frame_num = int(fps * anim_time)
anim = animation.FuncAnimation(fig, draw_frame, frames=frame_num, interval=1000/fps, blit=True)
plt.close() # disable showing initial frame
return anim
# convert animation to video (time-limiting step)
from IPython.display import HTML
anim = make_animation() # uses custom function above
HTML(anim.to_html5_video() + '<style>video{width: 400px !important; height: auto;}</style>')
Internet#
message='Mr. Trey'
messageBinary=[0]*len(message)
#turning string into ascii
for n in range(len(message)):
messageBinary[n]=bin(ord(message[n]))
#removing ob
for n in range(len(message)):
messageBinary[n]=messageBinary[n][2:]
if len(messageBinary[n])<7:
messageBinary[n]='0'+messageBinary[n]
messageToSend=''.join(messageBinary)
print('Message in binary sent '+messageToSend+'\n')
Message in binary sent 10011011110010010111001000001010100111001011001011111001
# model params
m1 = 1 # kg
m2 = 1 # kg
k1 = 100 # N/m
k2 = 100 # N/m
k3 = 100 # N/m
c1 = 3 # kg/s
c2 = 3 # kg/s
l1 = 1 # m
l2 = 1 # m
l3 = 1 # m
# time array
t_initial = 0
t_final = 3*len(messageToSend) - 1
t = np.linspace(t_initial, t_final, 1000)
t_len = len(t)
# initial values (x is relative to eqm length)
x1_init = 0
v1_init = 0
x2_init = 0
v2_init = 0
initial_values = [x1_init, v1_init, x2_init, v2_init]
def F1(t):
if(messageToSend[int(t/3)] == '0'):
omega = 1e4
else:
omega = np.sqrt(k1 + k2)
A = 20
phi = 1
return A*np.sin(omega*t + phi)
def coupled_ode_syst(t, Y):
# youtube
# t -> independent variable
# Y -> functions evaluated at independent variable
x1, v1, x2, v2 = Y
derivatives = np.array([
v1,
(-k1*x1 - k2*(x1 - x2) - c1*v1 + F1(t))/m1,
v2,
(-k2*(x2 - x1) - k3*x2 - c2*v2)/m2,
])
return derivatives
ode_soln = scipy.integrate.solve_ivp(coupled_ode_syst, [t_initial, t_final], initial_values, t_eval=t).y
x1, v1, x2, v2 = ode_soln
fig, axs = plt.subplots(2, 1, figsize=(10, 4), sharex=True)
axs[0].plot(t, x1, label='$x_1(t)$', alpha=1)
axs[0].plot(t, x2, label='$x_2(t)$', alpha=0.5)
axs[0].set_ylabel('$x(t)$')
# axs[0].set_ylim(0.5, 2.5)
axs[1].plot(t, v1, label='$v_1(t)$', alpha=1)
axs[1].plot(t, v2, label='$v_2(t)$', alpha=0.5)
axs[1].set_xlabel('$t$')
axs[1].set_ylabel('$v(t)$')
# axs[1].set_ylim(-3.5, 3.5)
for i in range(2):
axs[i].set_xlim(t_initial, t_final)
axs[i].plot([t_initial, t_final], [0, 0], '--', color='grey', lw=0.5, zorder=0) # zero ref

# convert animation to video (time-limiting step)
from IPython.display import HTML
anim = make_animation() # uses custom function above
HTML(anim.to_html5_video() + '<style>video{width: 400px !important; height: auto;}</style>')