diff --git a/lib/suspendedobject.py b/lib/suspendedobject.py
new file mode 100644
index 0000000..be60226
--- /dev/null
+++ b/lib/suspendedobject.py
@@ -0,0 +1,559 @@
+import numpy as np
+from operator import add
+
+from ipywidgets import interact, interactive, fixed, interact_manual
+from ipywidgets import HBox, VBox, Label, Layout
+import ipywidgets as widgets
+
+import matplotlib.pyplot as plt
+plt.style.use('seaborn-whitegrid') # global style for plotting
+
+from IPython.display import IFrame
+from IPython.display import set_matplotlib_formats, display, Math, Markdown, Latex, HTML
+set_matplotlib_formats('svg')
+
+from bokeh.io import push_notebook, show, output_notebook, curdoc
+from bokeh.plotting import figure
+from bokeh.models import Legend, ColumnDataSource, Slider, Span, LegendItem
+from bokeh.models import Arrow, OpenHead, NormalHead, VeeHead, LabelSet
+from bokeh.models.glyphs import Wedge, Bezier
+from bokeh.layouts import gridplot, row, column
+
+output_notebook(hide_banner=True)
+
+class SuspendedObjectLab:
+ """
+ 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.5, 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)
+ '''
+
+ ###--- Static 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
+
+
+ # Parameters for drawing forces
+ self.gravity = 9.81
+ self.force_scaling = .01
+ self.forces_nb = 4
+
+ # parameters for sliders
+ self.m_counterweight_min = 0.0
+ self.m_counterweight_max = 100.0
+ self.m_counterweight = self.m_counterweight_min # initial mass of the counterweight (0 by default, no counterweight at the beginning)
+
+ # parameter to draw the angle
+ self.radius=0.3
+
+
+ def launch(self):
+
+ ###--- Elements of the ihm:
+ # IHM input elements
+ self.m_counterweight_label = Label('Mass of the counterweight ($kg$):', layout=Layout(margin='0px 5px 0px 0px'))
+ self.m_counterweight_widget = widgets.FloatSlider(min=self.m_counterweight_min,max=self.m_counterweight_max,step=0.5,value=self.m_counterweight, layout=Layout(margin='0px'))
+ self.m_counterweight_note = Label('[Note: once you have clicked on the slider (the circle becomes blue), you can use the arrows from your keyboard to make it move.]', layout=Layout(margin='0px 0px 15px 0px'))
+
+ self.m_counterweight_input = VBox([HBox([self.m_counterweight_label, self.m_counterweight_widget], layout=Layout(margin='0px')), self.m_counterweight_note])
+
+ # Linking widgets to handlers
+ self.m_counterweight_widget.observe(self.m_counterweight_event_handler, names='value')
+
+ # IHM output element for moodle quiz
+ self.quiz_output = widgets.Output()
+
+
+ ###--- Compute variables dependent with the counterweight selected by the user
+ alpha = self.get_angle(self.m_counterweight)
+ alpha_degrees = radians_to_degrees(alpha)
+ alpha_text = '⍺ = {:.2f} °'.format(alpha_degrees)
+
+ coord_object = self.get_object_coords(alpha)
+ height_text = 'h = {:.2f} m'.format(coord_object[1])
+
+
+ ###--- Create the figure ---###
+ # LIMITATIONS of Bokeh (BokehJS 1.4.0)
+ # - labels: impossible to use LaTeX formatting in labels
+ # - arc: impossible to take into account figure ratio when dynamic rescaling is activated
+ # - forces/vectors: impossible to adjust the line_color and line_dash of OpenHead according to datasource
+
+ ###--- First display the clothesline
+ # Fix graph to problem boundaries
+ ymargin = .05
+ xmargin = .2
+ fig_object = figure(title='Suspended object ({} kg)'.format(self.m_object), #plot_width=600, plot_height=400, sizing_mode='stretch_height',
+ y_range=(self.y_origin-ymargin,self.y_origin+self.height+ymargin), x_range=(self.x_origin-xmargin,self.x_origin+self.distance+xmargin),
+ background_fill_color='#ffffff', toolbar_location=None)
+ fig_object.title.align = "center"
+ fig_object.yaxis.axis_label = 'Height (m)'
+
+ # Customize graph style so that it doesn't look too much like a graph
+ fig_object.ygrid.visible = False
+ fig_object.xgrid.visible = False
+ fig_object.outline_line_color = None
+
+ # trick to keep the three graphs aligned, while having auto scaling
+ fig_object.xaxis.axis_label = "Distance (m) "
+ #fig_object.xaxis.axis_line_color = "white"
+ #fig_object.xaxis.axis_label_text_color = "white"
+ #fig_object.xaxis.major_label_text_color = "white"
+ #fig_object.xaxis.major_tick_line_color = "white"
+ #fig_object.xaxis.minor_tick_line_color = "white"
+
+ # Draw the horizon line
+ fig_object.add_layout(Span(location=self.height, dimension='width', line_color='gray', line_dash='dashed', line_width=1))
+
+ # Draw the poles
+ fig_object.line([self.x_origin, self.x_origin], [self.y_origin, self.y_origin+self.height], color="black", line_width=8, line_cap="round")
+ fig_object.line([self.x_origin+self.distance, self.x_origin+self.distance], [self.y_origin, self.y_origin+self.height], color="black", line_width=8, line_cap="round")
+
+ # Draw the ground
+ fig_object.add_layout(Span(location=self.x_origin, dimension='width', line_color='black', line_width=1))
+ fig_object.hbar(y=self.y_origin-ymargin, height=ymargin*2, left=self.x_origin-xmargin, right=self.x_origin+self.distance+xmargin, color="white", line_color="white", hatch_pattern="/", hatch_color="gray")
+
+
+ # --DYN-- Draw the point at which the object is suspended (this data source also used for the other graphs)
+ self.object_source = ColumnDataSource(data=dict(
+ x=[coord_object[0]],
+ y=[coord_object[1]],
+ m_counterweight=[self.m_counterweight],
+ alpha_degrees=[alpha_degrees],
+ height_text=[height_text],
+ alpha_text=[alpha_text]
+ ))
+ fig_object.circle(source=self.object_source, x='x', y='y', size=8, fill_color="black", line_color='black', line_width=2)
+ fig_object.add_layout(LabelSet(source=self.object_source, x='x', y='y', text='height_text', level='glyph', x_offset=8, y_offset=-20))
+
+ # --DYN-- Draw the hanging cable
+ self.cable_source = ColumnDataSource(data=dict(
+ x=[self.x_origin, coord_object[0], self.x_origin+self.distance],
+ y=[self.y_origin+self.height, coord_object[1], self.y_origin+self.height]
+ ))
+ fig_object.line(source=self.cable_source, x='x', y='y', color="black", line_width=2, line_cap="round")
+
+
+ # --DYN-- Draw the angle between the hanging cable and horizonline
+ # Trick here: we use a straight line because the Arc glyph doesn't support dynamic figure ratio
+ ratio=1.5
+ x0=self.x_origin+ratio*self.radius
+ y0=self.y_origin+self.height
+ x1=(self.x_origin+self.radius*np.cos(alpha))
+ y1=(self.y_origin+self.height-self.radius*np.sin(alpha))
+ self.alpha_arc = fig_object.line([x0, x1], [y0, y1], color="gray", line_width=1, line_dash=[2,2])
+ fig_object.add_layout(LabelSet(source=self.object_source, x=self.x_origin, y=self.y_origin+self.height, text='alpha_text', level='glyph', x_offset=50, y_offset=-20))
+
+
+
+ # --DYN-- Draw the force vectors
+ # Weight
+ Fy = self.m_object*self.gravity*self.force_scaling
+ # Tension
+ Tx = ((self.m_object*self.gravity) / (2*np.tan(alpha)))*self.force_scaling
+ Ty = .5*self.m_object*self.gravity*self.force_scaling
+
+ self.forces_x_start = [coord_object[0]]*self.forces_nb
+ self.forces_y_start = [coord_object[1]]*self.forces_nb
+ self.forces_x_mag = [0, Tx, 0, -Tx]
+ self.forces_y_mag = [-Fy, Ty, Fy, Ty]
+
+ self.forces_source = ColumnDataSource(data=dict(
+ x_start=self.forces_x_start,
+ y_start=self.forces_y_start,
+ x_end=list(map(add, self.forces_x_start, self.forces_x_mag)),
+ y_end=list(map(add, self.forces_y_start, self.forces_y_mag)),
+ name=["F", "T", "Tr", "T"],
+ color=["blue", "red", "gray", "red"],
+ dash=["solid", "solid", [2,2], "solid"],
+ x_offset=[8, 25, 8, -35],
+ y_offset=[-45, 6, 45, 6]
+ ))
+
+ # Draw the arrows
+ forces_arrows = Arrow(source=self.forces_source, x_start='x_start', y_start='y_start', x_end='x_end', y_end='y_end',
+ line_color='color', line_width=2, end=OpenHead(line_width=2, size=12, line_color='black'))
+ fig_object.add_layout(forces_arrows)
+
+ # Add the labels
+ forces_labels = LabelSet(source=self.forces_source, x='x_start', y='y_start', text='name', text_color='color', level='glyph',
+ x_offset='x_offset', y_offset='y_offset', render_mode='canvas')
+ fig_object.add_layout(forces_labels)
+
+
+ # --DYN-- Draw the tension projection lines
+ self.proj_source = ColumnDataSource(data=dict(
+ x=self.forces_source.data["x_end"][1:4],
+ y=self.forces_source.data["y_end"][1:4]
+ ))
+ fig_object.line(source=self.proj_source, x='x', y='y', color="gray", line_width=1, line_dash="dashed")
+
+
+ ###--- Then display the angle and the height as functions from the mass of the counterweight
+ # Create all possible values of the mass of the counterweight
+ m_cw_list = np.linspace(self.m_counterweight_min, self.m_counterweight_max, 100)
+
+ # Compute the angle (in degrees) and height for all these values
+ angle_list = []
+ height_list = []
+ for m in m_cw_list:
+ a = self.get_angle(m)
+ angle_list.append(radians_to_degrees(a))
+
+ c = self.get_object_coords(a)
+ height_list.append(c[1])
+
+
+ ### Create the graph for height
+ fig_height = figure(title='Height (m)', #plot_height=400, plot_width=200,
+ y_range=(-ymargin,self.height+ymargin), x_range=(0,102),
+ background_fill_color='#ffffff', toolbar_location=None)
+ fig_height.title.align = "center"
+ fig_height.xaxis.axis_label = 'Mass of the counterweight (kg)'
+ fig_height.grid.grid_line_color = 'lightgray'
+ fig_height.grid.minor_grid_line_color = 'gainsboro'
+
+ # Draw the curve of the function
+ fig_height.line(m_cw_list, height_list, color="green", line_width=1)
+
+ # Draw the horizon line
+ fig_height.add_layout(Span(location=self.height, dimension='width', line_color='gray', line_dash='dashed', line_width=1))
+
+ # --DYN-- Add the current height from the counterweight selected by the user
+ fig_height.circle(source=self.object_source, x='m_counterweight', y='y', size=8, fill_color="black", line_color='black', line_width=2, legend_field="height_text")
+ fig_height.legend.location = "bottom_right"
+ fig_height.legend.label_text_font_size = '12pt'
+
+
+ ### Create the graph for angle
+ fig_alpha = figure(title='Angle ⍺ (°)', #plot_height=400, plot_width=400,
+ y_range=(-1,angle_list[0]+1), x_range=(0,102),
+ background_fill_color='#ffffff', toolbar_location=None)
+ fig_alpha.title.align = "center"
+ fig_alpha.xaxis.axis_label = 'Mass of the counterweight (kg)'
+ fig_alpha.grid.grid_line_color = 'lightgray'
+ fig_alpha.grid.minor_grid_line_color = 'gainsboro'
+
+ # Draw the curve of the function
+ c = fig_alpha.line(m_cw_list, angle_list, color="green", line_width=1)
+
+ # Draw the horizon line
+ fig_alpha.add_layout(Span(location=0, dimension='width', line_color='gray', line_dash='dashed', line_width=1))
+
+ # --DYN-- Add the current angle from the counterweight selected by the user
+ fig_alpha.circle(source=self.object_source, x='m_counterweight', y='alpha_degrees', size=8, fill_color="black", line_color='black', line_width=2, legend_field="alpha_text")
+ fig_alpha.legend.location = "top_right"
+ fig_alpha.legend.label_text_font_size = '12pt'
+
+
+ ###--- Add a quiz from Moodle
+ iframe_quiz = '''
+
+
+ '''
+ widget_quiz = widgets.HTML(value=iframe_quiz)
+
+
+ ###--- Display the whole interface
+ show(row(column(children=[fig_object], sizing_mode='stretch_height'), fig_alpha, fig_height, sizing_mode='scale_both'), notebook_handle=True)
+ #display(VBox([self.m_counterweight_input, widget_quiz]))
+ display(VBox([self.m_counterweight_input]))
+
+
+ # Event handlers
+ def m_counterweight_event_handler(self, change):
+ # get new value of counterweight mass
+ self.m_counterweight = change.new
+
+ # compute all problem values
+ alpha = self.get_angle(self.m_counterweight)
+ alpha_degrees = radians_to_degrees(alpha)
+ alpha_text = '⍺ = {:.2f} °'.format(alpha_degrees)
+
+ coord_object = self.get_object_coords(alpha)
+ height_text = 'h = {:.2f} m'.format(coord_object[1])
+
+ self.forces_y_start = [coord_object[1]]*self.forces_nb
+ Tx = ((self.m_object*self.gravity) / (2*np.tan(alpha)))*self.force_scaling
+ self.forces_x_mag = [0, Tx, 0, -Tx]
+
+ # update the object representation on all graphs (coordinates+labels)
+ self.object_source.data = dict(
+ x=[coord_object[0]],
+ y=[coord_object[1]],
+ m_counterweight=[self.m_counterweight],
+ alpha_degrees=[alpha_degrees],
+ height_text=[height_text],
+ alpha_text=[alpha_text]
+ )
+
+ # update line representing the angle alpha
+ self.alpha_arc.data_source.patch({
+ 'x' : [(1, self.x_origin+self.radius*np.cos(alpha))],
+ 'y' : [(1, self.y_origin+self.height-self.radius*np.sin(alpha))]
+ })
+
+ # update the point where the object is attached on cable
+ self.cable_source.patch({
+ 'x' : [(1, coord_object[0])],
+ 'y' : [(1, coord_object[1])]
+ })
+
+ # update point of start for all forces, update Tx and Ty for T
+ self.forces_source.patch({
+ 'y_start' : [(slice(self.forces_nb), self.forces_y_start)],
+ 'x_end' : [(slice(self.forces_nb), list(map(add, self.forces_x_start, self.forces_x_mag)))],
+ 'y_end' : [(slice(self.forces_nb), list(map(add, self.forces_y_start, self.forces_y_mag)))],
+ })
+
+ # update the tension projection lines
+ self.proj_source.patch({
+ 'x' : [(slice(3), self.forces_source.data["x_end"][1:4])],
+ 'y' : [(slice(3), self.forces_source.data["y_end"][1:4])]
+ })
+
+ push_notebook()
+
+
+
+ def visualize_counterweight(self, m_counterweight = 0):
+
+ ### first let's validate the counterweight
+ # it cannot be negative
+ if m_counterweight < 0:
+ print("\033[1m\x1b[91m The mass of the counterweight cannot be negative. \x1b[0m\033[0m")
+ return
+
+ # update attribute with new value
+ self.m_counterweight = m_counterweight
+
+ # compute the angle given by this counterweight
+ alpha = self.get_angle(self.m_counterweight)
+ alpha_degrees = radians_to_degrees(alpha)
+
+ # visualize the situation
+ self.visualize_angle(alpha_degrees)
+
+
+
+ def visualize_angle(self, angle_degrees):
+
+ ### first let's validate the angle
+ # it cannot be null (i.e. cable horizontal) or negative
+ if angle_degrees <= 0:
+ print("\033[1m\x1b[91m The angle cannot be null or negative. \x1b[0m\033[0m")
+ return
+
+ # it cannot be more than the default angle given the parameters of the situation
+ alpha_default = np.arctan(self.height / (self.distance / 2))
+ alpha_default_degrees = radians_to_degrees(alpha_default)
+ if angle_degrees > alpha_default_degrees:
+ print(f"\033[1m\x1b[91m The angle cannot be greater than {alpha_default_degrees:.2f} degrees given the parameters of this situation (poles of {self.height} meters, distant by {self.distance} meters). \x1b[0m\033[0m")
+ angle_degrees = alpha_default_degrees
+
+
+ # compute variables dependent with the angle selected by the user
+ alpha_degrees = angle_degrees
+ alpha = degrees_to_radians(alpha_degrees)
+ alpha_text = '⍺ = {:.2f} °'.format(alpha_degrees)
+
+ coord_object = self.get_object_coords(alpha)
+ height_text = 'h = {:.2f} m'.format(coord_object[1])
+
+
+ ###--- Create the figure ---###
+ # LIMITATIONS of Bokeh (BokehJS 1.4.0)
+ # - labels: impossible to use LaTeX formatting in labels
+ # - arc: impossible to take into account figure ratio when dynamic rescaling is activated
+ # - forces/vectors: impossible to adjust the line_color and line_dash of OpenHead according to datasource
+
+ ###--- First display the clothesline
+ # Fix graph to problem boundaries
+ ymargin = .05
+ xmargin = .2
+ fig_object = figure(title='Suspended object ({} kg)'.format(self.m_object), plot_width=800, plot_height=400, #sizing_mode='scale_both',
+ y_range=(self.y_origin-ymargin,self.y_origin+self.height+ymargin), x_range=(self.x_origin-xmargin,self.x_origin+self.distance+xmargin),
+ background_fill_color='#ffffff', toolbar_location=None)
+ fig_object.title.align = "center"
+ fig_object.yaxis.axis_label = 'Height (m)'
+ fig_object.xaxis.axis_label = "Distance (m)"
+
+ # Customize graph style so that it doesn't look too much like a graph
+ fig_object.ygrid.visible = False
+ fig_object.xgrid.visible = False
+ fig_object.outline_line_color = None
+
+
+ # Draw the horizon line
+ fig_object.add_layout(Span(location=self.height, dimension='width', line_color='gray', line_dash='dashed', line_width=1))
+
+ # Draw the poles
+ fig_object.line([self.x_origin, self.x_origin], [self.y_origin, self.y_origin+self.height], color="black", line_width=8, line_cap="round")
+ fig_object.line([self.x_origin+self.distance, self.x_origin+self.distance], [self.y_origin, self.y_origin+self.height], color="black", line_width=8, line_cap="round")
+
+ # Draw the ground
+ fig_object.add_layout(Span(location=self.x_origin, dimension='width', line_color='black', line_width=1))
+ fig_object.hbar(y=self.y_origin-ymargin, height=ymargin*2, left=self.x_origin-xmargin, right=self.x_origin+self.distance+xmargin, color="white", line_color="white", hatch_pattern="/", hatch_color="gray")
+
+
+ # --DYN-- Draw the point at which the object is suspended (this data source also used for the other graphs)
+ self.object_source = ColumnDataSource(data=dict(
+ x=[coord_object[0]],
+ y=[coord_object[1]],
+ m_counterweight=[self.m_counterweight],
+ alpha_degrees=[alpha_degrees],
+ height_text=[height_text],
+ alpha_text=[alpha_text]
+ ))
+ fig_object.circle(source=self.object_source, x='x', y='y', size=8, fill_color="black", line_color='black', line_width=2)
+ fig_object.add_layout(LabelSet(source=self.object_source, x='x', y='y', text='height_text', level='glyph', x_offset=8, y_offset=-20))
+
+ # --DYN-- Draw the hanging cable
+ self.cable_source = ColumnDataSource(data=dict(
+ x=[self.x_origin, coord_object[0], self.x_origin+self.distance],
+ y=[self.y_origin+self.height, coord_object[1], self.y_origin+self.height]
+ ))
+ fig_object.line(source=self.cable_source, x='x', y='y', color="black", line_width=2, line_cap="round")
+
+
+ # --DYN-- Draw the angle between the hanging cable and horizonline
+ # Trick here: we use a straight line because the Arc glyph doesn't support dynamic figure ratio
+ ratio=1.5
+ x0=self.x_origin+ratio*self.radius
+ y0=self.y_origin+self.height
+ x1=(self.x_origin+self.radius*np.cos(alpha))
+ y1=(self.y_origin+self.height-self.radius*np.sin(alpha))
+ self.alpha_arc = fig_object.line([x0, x1], [y0, y1], color="gray", line_width=1, line_dash=[2,2])
+ fig_object.add_layout(LabelSet(source=self.object_source, x=self.x_origin, y=self.y_origin+self.height, text='alpha_text', level='glyph', x_offset=50, y_offset=-20))
+
+
+ # --DYN-- Draw the force vectors
+ # Weight
+ Fy = self.m_object*self.gravity*self.force_scaling
+ # Tension
+ Tx = ((self.m_object*self.gravity) / (2*np.tan(alpha)))*self.force_scaling
+ Ty = .5*self.m_object*self.gravity*self.force_scaling
+
+ self.forces_x_start = [coord_object[0]]*self.forces_nb
+ self.forces_y_start = [coord_object[1]]*self.forces_nb
+ self.forces_x_mag = [0, Tx, 0, -Tx]
+ self.forces_y_mag = [-Fy, Ty, Fy, Ty]
+
+ self.forces_source = ColumnDataSource(data=dict(
+ x_start=self.forces_x_start,
+ y_start=self.forces_y_start,
+ x_end=list(map(add, self.forces_x_start, self.forces_x_mag)),
+ y_end=list(map(add, self.forces_y_start, self.forces_y_mag)),
+ name=["F", "T", "Tr", "T"],
+ color=["blue", "red", "gray", "red"],
+ dash=["solid", "solid", [2,2], "solid"],
+ x_offset=[8, 25, 8, -35],
+ y_offset=[-45, 6, 45, 6]
+ ))
+
+ # Draw the arrows
+ ### Bokeh issue here: with a datasource, it is not possible to specify the color of the openhead so it remains black
+ forces_arrows = Arrow(source=self.forces_source, x_start='x_start', y_start='y_start', x_end='x_end', y_end='y_end',
+ line_color='color', line_width=2, end=OpenHead(line_width=2, size=12))
+ fig_object.add_layout(forces_arrows)
+
+
+ # Add the labels
+ forces_labels = LabelSet(source=self.forces_source, x='x_start', y='y_start', text='name', text_color='color', level='glyph',
+ x_offset='x_offset', y_offset='y_offset', render_mode='canvas')
+ fig_object.add_layout(forces_labels)
+
+
+ # --DYN-- Draw the tension projection lines
+ self.proj_source = ColumnDataSource(data=dict(
+ x=self.forces_source.data["x_end"][1:4],
+ y=self.forces_source.data["y_end"][1:4]
+ ))
+ fig_object.line(source=self.proj_source, x='x', y='y', color="gray", line_width=1, line_dash="dashed")
+
+
+ ###--- Display the whole interface
+ show(row(children=[fig_object]), notebook_handle=True) #, sizing_mode="scale_both"
+
+
+ # 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 = 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 considered on the ground for all values of the angle which give a delta height higher than the height of the poles
+
+ :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]
+
+
+
+def degrees_to_radians(angle_degrees):
+ return angle_degrees * np.pi / 180
+
+def radians_to_degrees(angle_radians):
+ return angle_radians * 180 / np.pi
+
+
+# EOF