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