Page MenuHomec4science

suspendedobjects.py
No OneTemporary

File Metadata

Created
Sun, Apr 28, 11:11

suspendedobjects.py

import numpy as np
from ipywidgets import interact, interactive, fixed, interact_manual
from ipywidgets import HBox, VBox, Label, Layout
import ipywidgets as widgets
from IPython.display import set_matplotlib_formats
set_matplotlib_formats('svg')
import matplotlib.pyplot as plt
import matplotlib.patches as pat
plt.style.use('seaborn-whitegrid') # global style for plotting
###--- Functions depending on the problem parameters
def get_angle_from_masses(m_object, m_counterweight, distance, height):
"""
Computes the angle that the cable makes with the horizon depending on the counterweight chosen:
- if the counterweight is sufficient: angle = arcsin(1/2 * m_object / m_counterweight)
- else (object on the ground): alpha = np.arctan(height / (distance / 2))
:m_object: mass of the object
:m_counterweight: mass of the counterweight (has to be > 0)
:distance: horizontal distance between the two poles
:height: height of the poles (same height for both)
:returns: angle that the cable makes with the horizon (in rad)
"""
# Default alpha value i.e. object is on the ground
alpha_default = np.arctan(height / (distance / 2))
alpha = alpha_default
# Let's check that there is actually a counterweight
if m_counterweight > 0:
# Then we compute the ratio of masses
ratio = 0.5 * m_object / m_counterweight
# Check that the ratio of masses is in the domain of validity of arcsin ([-1;1])
if abs(ratio) < 1:
alpha = np.arcsin(ratio)
return min(alpha_default, alpha)
def get_object_coordinates(angle, x_origin, y_origin, distance, height):
"""
Computes the position of the object on the cable taking into account the angle determined by the counterweight and the dimensions of the hanging system.
By default:
- the object is supposed to be suspended exactly in the middle of the cable
- the object is on considered the ground for all values of the angle
:angle: angle that the cable makes with the horizon
:x_origin: x coordinate of the bottom of the left pole (origin of the coordinate system)
:y_origin: y coordinate of the bottom of the left pole (origin of the coordinate system)
:distance: horizontal distance between the two poles
:height: height of the poles (same height for both)
:returns: coordinates of the point at which the object are hanged
"""
# the jean is midway between the poles
x_object = x_origin + 0.5 * distance
# default y value: the jean is on the ground
y_object = y_origin
# we check that the angle is comprised between horizontal (greater than 0) and vertical (smaller than pi/2)
if angle > 0 and angle < (np.pi / 2):
# we compute the delta between the horizon and the point given by the angle
delta = (0.5 * distance * np.tan(angle))
# we check that the delta is smaller than the height of the poles (otherwise it just means the jean is on the ground)
if delta <= height:
y_object = y_origin + height - delta
return [x_object, y_object]
class SuspendedObjectsLab:
"""
This class embeds all the necessary code to create a virtual lab to study the static equilibrium of an object suspended on a clothesline with a counterweight.
"""
def __init__(self, m_object = 3, distance = 5, height = 1, x_origin = 0, y_origin = 0,
get_angle_from_masses = get_angle_from_masses, get_object_coordinates = get_object_coordinates):
'''
Initiates and displays the virtual lab on suspended objects.
:m_object: mass of the suspended object
:distance: horizontal distance between the two poles
:height: height of the poles (same height for both)
:x_origin: x coordinate of the bottom of the left pole (origin of the coordinate system)
:y_origin: y coordinate of the bottom of the left pole (origin of the coordinate system)
:get_angle_from_masses: function to compute the angle that the cable makes with the horizon, as a function of the respective masses of the object and the counterweight -- angle(m_object, m_counterweight, distance, height)
:get_object_coordinates: function to compute the coordinates of the point at which the object is suspended on the cable -- coord(angle, x_origin, y_origin, distance, height)
'''
###--- Parameters of the situation
self.m_object = m_object # mass of the wet object, in kg
self.distance = distance # distance between the poles, in m
self.height = height # height of the poles, in m
self.x_origin = x_origin # x coordinate of point of origin of the figure = x position of the left pole, in m
self.y_origin = y_origin # y coordinate of point of origin of the figure = y position of the lower point (ground), in m
# Functions to compute equations
self.get_angle_from_masses = get_angle_from_masses
self.get_object_coordinates = get_object_coordinates
###--- Then we define the elements of the ihm:
# parameters for sliders
self.m_counterweight_min = 0
self.m_counterweight_max = 100
self.m_counterweight = self.m_counterweight_min # initial mass of the counterweight (0 by default, no counterweight at the beginning)
# IHM input elements
input_layout=Layout(margin='5px 10px')
self.m_counterweight_label = Label('Mass of the counterweight ($kg$):', layout=input_layout)
self.m_counterweight_widget = widgets.FloatSlider(min=self.m_counterweight_min,max=self.m_counterweight_max,step=1,value=self.m_counterweight, layout=input_layout)
self.m_counterweight_input = HBox([self.m_counterweight_label, self.m_counterweight_widget])
# IHM output elements
self.y_object_output = widgets.Output(layout=input_layout)
self.graph_output = widgets.Output(layout=input_layout)
# Linking widgets to handlers
self.m_counterweight_widget.observe(self.m_counterweight_event_handler, names='value')
# Organize layout
self.ihm = VBox([self.m_counterweight_input, self. y_object_output, self.graph_output])
###--- Finally, we display the whole interface and we update it right away so that it plots the graph with current values
display(self.ihm);
self.update_lab()
# Event handlers
def m_counterweight_event_handler(self, change):
self.m_counterweight = change.new
self.update_lab()
# Create visualisation
def update_lab(self):
# Clear outputs
self.graph_output.clear_output(wait=True)
self.y_object_output.clear_output(wait=True)
alpha = self.get_angle_from_masses(self.m_object, self.m_counterweight, self.distance, self.height)
alpha_degrees = alpha*180/np.pi
coord_object = self.get_object_coordinates(alpha, self.x_origin, self.y_origin, self.distance, self.height)
# Create the figure
fig = plt.figure(figsize=(14, 4))
ax1 = plt.subplot(131)
ax2 = plt.subplot(132)
ax3 = plt.subplot(133, sharex = ax2, sharey = ax1)
###--- First display the clothesline
ax1.set_title('Clothesline')
# Fix graph to problem boundaries
ax1.set_ylim(bottom = self.y_origin) # limit bottom of y axis to ground
ax1.set_ylim(top = self.y_origin + self.height + .1) # limit top of y axis to values just above height
# Customize graph style so that it doesn't look like a graph
ax1.get_xaxis().set_visible(False) # hide the x axis
ax1.set_ylabel("Height ($m$)") # add a label on the y axis
ax1.grid(False) # hide the grid
ax1.spines['top'].set_visible(False) # hide the frame except bottom line
ax1.spines['right'].set_visible(False)
ax1.spines['left'].set_visible(False)
# Draw poles
x_pole1 = np.array([self.x_origin, self.x_origin])
y_pole1 = np.array([self.y_origin, self.y_origin+self.height])
ax1.plot(x_pole1, y_pole1, "k-", linewidth=7)
x_pole2 = np.array([self.x_origin+self.distance, self.x_origin+self.distance])
y_pole2 = np.array([self.y_origin, self.y_origin+self.height])
ax1.plot(x_pole2, y_pole2, "k-", linewidth=7)
# Draw the hanging cable
x = np.array([self.x_origin, coord_object[0], self.x_origin+self.distance])
y = np.array([self.y_origin+self.height, coord_object[1], self.y_origin+self.height])
ax1.plot(x, y, linewidth=3, linestyle = "-", color="green")
# Draw the horizon line
ax1.axhline(y=self.y_origin+self.height, color='gray', linestyle='-.', linewidth=1, zorder=1)
# Draw the angle between the hanging cable and horizonline
ellipse_radius = 0.2
fig_ratio = self.height / self.distance
ax1.add_patch(pat.Arc(xy = (self.x_origin, self.y_origin+self.height), width = ellipse_radius/fig_ratio, height = ellipse_radius, theta1 = -1*alpha_degrees, theta2 = 0, color="gray", linestyle='-.'))
ax1.annotate(r'$\alpha$', xy=(self.x_origin+(.15/fig_ratio), self.y_origin+self.height-.05))
# Draw the point at which the object is suspended
ax1.scatter(coord_object[0], coord_object[1], s=80, c="r", zorder=15)
ax1.annotate('m = {} kg'.format(self.m_object), xy=(coord_object[0], coord_object[1]), xytext=(coord_object[0]+0.1, coord_object[1]-0.1))
###--- Then display the angle and the height as functions from the mass of the counterweight
ax2.set_title(r'Angle $\alpha$ of the cable vs. horizon ($^\circ$)')
ax3.set_title(r'Height of the suspension point ($m$)')
# Create all possible values of the mass of the counterweight
m_cw = np.linspace(self.m_counterweight_min, self.m_counterweight_max, 100)
# Compute the angle (in degrees) and height for all these values
angle = []
height = []
for m in m_cw:
a = self.get_angle_from_masses(self.m_object, m, self.distance, self.height)
angle.append(a*180/np.pi)
c = self.get_object_coordinates(a, self.x_origin, self.y_origin, self.distance, self.height)
height.append(c[1])
# Display the functions on the graphs
ax2.set_xlabel('Mass of the counterweight (kg)')
ax2.plot(m_cw, angle, "b")
ax3.set_xlabel('Mass of the counterweight (kg)')
ax3.plot(m_cw, height, "b")
# Draw the horizon lines
ax2.axhline(y=self.y_origin, color='gray', linestyle='-.', linewidth=1, zorder=1)
ax3.axhline(y=self.y_origin+self.height, color='gray', linestyle='-.', linewidth=1, zorder=1)
# Add the current angle from the counterweight selected by the user
ax2.scatter(self.m_counterweight, alpha_degrees, s=80, c="r", zorder=15)
# Add the current height from the counterweight selected by the user
ax3.scatter(self.m_counterweight, coord_object[1], s=80, c="r", zorder=15)
# Display height of object selected
with self.y_object_output:
print("Height of the point at which the object is hanged: {:.4f} m".format(coord_object[1]))
# Display graph
with self.graph_output:
plt.show();
# EOF

Event Timeline