diff --git a/Banner/Noto.ipynb b/Banner/Noto.ipynb new file mode 100644 index 0000000..097c9a4 --- /dev/null +++ b/Banner/Noto.ipynb @@ -0,0 +1,164 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# The physics of suspended objects" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import math\n", + "from IPython.display import IFrame\n", + "IFrame('https://h5p.org/h5p/embed/583522', 800, 440)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The forces applied on the jeans are:\n", + "* the weight: $\\vec F_j = m_j \\vec g$ \n", + "* the force exerted by the cable on each side of the jeans: assuming the jeans are suspended at the exact center of the cable, then the tension applied on each of the two sides is is equally distributed $\\vec T$, which combine into a vertical resulting tention $\\vec T_r = 2.\\vec T$\n", + "\n", + "\n", + "\n", + "From Newton's second law in a static equilibrium we can write: $\\sum \\vec F_j = \\vec 0$ \n", + "With the forces on the jeans we get: $\\vec F_j + \\vec T_r = 0$ \n", + "Using the fact that the tension is equal on both sides of the jeans we get: $\\vec F_j + 2.\\vec T = 0$ \n", + "[...] \n", + "With equations $(1)$ and $(2)$ we obtain: $\\left\\{\\begin{matrix}T = \\frac{m_j.g}{2.sin(\\alpha)} \\\\ T = m_{cw}.g\\end{matrix}\\right.$\n", + "\n", + "This allow us to find the mass of the counterweight as a function of the *mass of the jeans* and of the *angle that the line makes with the horizon*: \n", + "\n", + "$\n", + "\\begin{align}\n", + "m_{cw} = \\frac{m_j}{2.sin(\\alpha)}\n", + "\\end{align}\n", + "$\n", + "\n", + "Here is the equivalent Python function:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def counterweight_mass(jean_mass, alpha):\n", + " return jean_mass / (2 * math.sin(alpha))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To pull the cable taut to an angle of $\\frac{\\pi}{120}=1.5^{\\circ}$ with jeans of 3 kg suspended on it, the counterweight necessary is:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "57.302 kg\n" + ] + } + ], + "source": [ + "mcw = counterweight_mass(3, math.pi/120)\n", + "print('{:.03f} kg'.format(mcw))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The virtual lab below allows to vary the mass of the counterweight and visualize the cable as well as the angle $\\alpha$ it makes with the horizong and the height at which the jeans get suspended:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "47941ad57f34473a9bec9895ae2525cd", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(Output(), HBox(children=(Label(value='Mass of the counterweight ($kg$):'), FloatSlider(value=12…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "from lib.suspendedobjects import *\n", + "SuspendedObjectsLab();" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.8" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/Banner/figs/jean-solution.png b/Banner/figs/jean-solution.png new file mode 100644 index 0000000..7d8c742 Binary files /dev/null and b/Banner/figs/jean-solution.png differ diff --git a/Banner/lib/suspendedobjects.py b/Banner/lib/suspendedobjects.py new file mode 100644 index 0000000..8c231a2 --- /dev/null +++ b/Banner/lib/suspendedobjects.py @@ -0,0 +1,242 @@ +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, display, Math, Markdown, Latex +set_matplotlib_formats('svg') + +import matplotlib.pyplot as plt +import matplotlib.patches as pat +plt.style.use('seaborn-whitegrid') # global style for plotting + + +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): + ''' + 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) + ''' + + ###--- 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 + + + ###--- Then we define the elements of the ihm: + # parameters for sliders + self.m_counterweight_min = 0 + self.m_counterweight_max = 100 + self.m_counterweight = 12 # 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.title_output = widgets.Output()#layout=input_layout + with self.title_output: + display(Markdown(r'Mass of the suspended object: $m = {} kg$'.format(self.m_object))) + 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.graph_output, self.m_counterweight_input]) + + + ###--- 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() + + + # Utility functions + def get_angle(self, m_counterweight): + """ + 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_counterweight: mass of the chosen counterweight + + :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(self.height / (self.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 * self.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_coords(self, angle): + """ + 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 + + :returns: coordinates of the point at which the object are hanged + """ + # the jean is midway between the poles + x_object = self.x_origin + 0.5 * self.distance + + # default y value: the jean is on the ground + y_object = self.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 * self.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 <= self.height: + y_object = self.y_origin + self.height - delta + + return [x_object, y_object] + + + # Create visualisation + def update_lab(self): + # Clear outputs + self.graph_output.clear_output(wait=True) + + # Compute new values with the counterweight selected by the user + alpha = self.get_angle(self.m_counterweight) + alpha_degrees = alpha*180/np.pi + alpha_text = r'$\alpha$ = {:.2f} $^\circ$'.format(alpha_degrees) + + coord_object = self.get_object_coords(alpha) + height_text = r'h = {:.2f} $m$'.format(coord_object[1]) + + + # Create the figure + fig = plt.figure(figsize=(12, 4)) + ax1 = plt.subplot(131) + ax2 = plt.subplot(132) + ax3 = plt.subplot(133, sharex = ax2, sharey = ax1) + + + ###--- First display the clothesline + ax1.set_title('Suspended object ({} kg)'.format(self.m_object)) + + # 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.grid(False) # hide the grid + ax1.set_ylabel("Height ($m$)") # add a label on the y axis + ax1.set_xlabel("Distance ($m$)") # add a label on the x axis + 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(alpha_text, xy=(self.x_origin, self.y_origin+self.height), xytext=(30, -15), textcoords='offset points', bbox=dict(boxstyle="round", facecolor = "white", edgecolor = "white", alpha = 0.8)) + + # 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(height_text, xy=(coord_object[0], coord_object[1]), xytext=(10, -10), textcoords='offset points', bbox=dict(boxstyle="round", facecolor = "white", edgecolor = "white", alpha = 0.8)) + + + + ###--- Then display the angle and the height as functions from the mass of the counterweight + ax2.set_title(r'Angle $\alpha$ ($^\circ$)') + ax3.set_title(r'Height ($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(m) + #a = self.get_angle_from_masses(self.m_object, m, self.distance, self.height) + angle.append(a*180/np.pi) + c = self.get_object_coords(a) + #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) + ax2.annotate(alpha_text, xy=(self.m_counterweight, alpha_degrees), xytext=(10, 5), textcoords='offset points', bbox=dict(boxstyle="round", facecolor = "white", edgecolor = "white", alpha = 0.8)) + + # 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) + ax3.annotate(height_text, xy=(self.m_counterweight, coord_object[1]), xytext=(10, -10), textcoords='offset points', bbox=dict(boxstyle="round", facecolor = "white", edgecolor = "white", alpha = 0.8)) + + + # Display graph + with self.graph_output: + plt.show(); + + + +# EOF