simian-examples 3.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,429 @@
1
+ """Ball Thrower example."""
2
+
3
+ import logging
4
+ import math
5
+ import os
6
+
7
+ from simian.examples.ballthrower_engine import BallThrower
8
+ from simian.gui import Form, component, component_properties, utils
9
+
10
+ # Prepare often used settings.
11
+ COLLAPSE_PANEL = {"collapsible": True}
12
+ LABEL_LEFT = {"labelPosition": "left-left"}
13
+ INLINE = {"customClass": "form-check-inline"}
14
+ SHOW_LABEL = {"hideLabel": False}
15
+
16
+
17
+ if __name__ == "__main__":
18
+ # Run as a script
19
+ from simian.entrypoint import assert_simian_gui_imports
20
+
21
+ assert_simian_gui_imports()
22
+ from simian.local import run
23
+
24
+ run("simian.examples.ballthrower", window_title="Ball Thrower example", debug=False)
25
+
26
+
27
+ def gui_event(meta_data: dict, payload: dict) -> dict:
28
+ """Event handling of the application.
29
+
30
+ Args:
31
+ meta_data: Form meta data.
32
+ payload: Current status of the Form's contents.
33
+
34
+ Returns:
35
+ payload: Updated Form contents.
36
+ """
37
+ # Register the events with their callback functions.
38
+ Form.eventHandler(
39
+ ThrowBall=throwing,
40
+ ClearHistory=clear_history,
41
+ CauseError=cause_error,
42
+ )
43
+ try:
44
+ caller = utils.getEventFunction(meta_data, payload)
45
+ payload = caller(meta_data, payload)
46
+
47
+ except ImportError as exc:
48
+ # No method with matching name found in the current namespace. May be used to have custom
49
+ # event routing. Here we only log the occurrence.
50
+ logging.debug(exc.msg)
51
+
52
+ return payload
53
+
54
+
55
+ def throwing(_meta_data: dict, payload: dict) -> dict:
56
+ """Simulate throwing a ball.
57
+
58
+ Get the settings from the payload and multiply with the enabled states to switch off parts of
59
+ the simulation.
60
+ """
61
+ enable_drag, _ = utils.getSubmissionData(payload, key="enableDrag", parent="options")
62
+ enable_wind, _ = utils.getSubmissionData(payload, key="enableWind", parent="options")
63
+ throw_speed, _ = utils.getSubmissionData(payload, key="throwSpeed")
64
+ throw_angle, _ = utils.getSubmissionData(payload, key="throwAngle")
65
+ drag_coeff = utils.getSubmissionData(payload, key="dragCoefficient")[0] * enable_drag
66
+ radius_ball = utils.getSubmissionData(payload, key="ballRadius")[0] * enable_drag
67
+ rho_air = utils.getSubmissionData(payload, key="airDensity")[0] * enable_drag
68
+ gravity = utils.getSubmissionData(payload, key="gravity")[0] * -1 # Point downward.
69
+ mass_ball, _ = utils.getSubmissionData(payload, key="ballMass") # May not be zero.
70
+ wind_speed = utils.getSubmissionData(payload, key="windSpeed")[0] * enable_drag * enable_wind
71
+
72
+ # Convert the speed and angle to the speed components.
73
+ ver_speed = throw_speed * math.sin(throw_angle / 180 * math.pi)
74
+ hor_speed = throw_speed * math.cos(throw_angle / 180 * math.pi)
75
+
76
+ _t, x, y, _u, _v = BallThrower.throw_ball(
77
+ u0=hor_speed,
78
+ v0=ver_speed,
79
+ Cd=drag_coeff,
80
+ r=radius_ball,
81
+ rho=rho_air,
82
+ g=gravity,
83
+ m=mass_ball,
84
+ w=wind_speed,
85
+ )
86
+
87
+ # Create a Plotly object from the information in the payload.
88
+ plot_obj, _ = utils.getSubmissionData(payload, key="plot")
89
+
90
+ # Plot the new newly calculated x and y values, append the legend and put the plotly
91
+ # object in the submission data.
92
+ nr = len(plot_obj.figure.data) + 1
93
+ plot_obj.figure.add_scatter(x=x, y=y, name=f"Attempt {nr}", mode="lines")
94
+ utils.setSubmissionData(payload, key="plot", data=plot_obj)
95
+
96
+ # Update the table with the settings used for the throws.
97
+ new_row = [
98
+ nr,
99
+ nr,
100
+ round(hor_speed, 3),
101
+ round(ver_speed, 3),
102
+ wind_speed,
103
+ mass_ball,
104
+ radius_ball,
105
+ drag_coeff,
106
+ gravity,
107
+ rho_air,
108
+ ]
109
+
110
+ if nr == 1:
111
+ # First throw. Only put the new row in the table.
112
+ new_table_values = [new_row]
113
+ else:
114
+ # Extra row. Append the row to the DataFrame from the submission data.
115
+ new_table_values, _ = utils.getSubmissionData(payload, key="summary")
116
+ new_table_values.append(dict(zip(new_table_values[0].keys(), new_row)))
117
+
118
+ utils.setSubmissionData(payload, key="summary", data=new_table_values)
119
+
120
+ # Accept all changes to settings and lower the changes flag.
121
+ payload["pristine"] = True
122
+
123
+ return payload
124
+
125
+
126
+ def clear_history(_meta_data: dict, payload: dict) -> dict:
127
+ """Clears the data from the plot and table."""
128
+ # Clear the data from the plot.
129
+ plot_obj, _ = utils.getSubmissionData(payload, "plot")
130
+ plot_obj.figure.data = []
131
+ utils.setSubmissionData(payload, key="plot", data=plot_obj)
132
+
133
+ # Clear the values in the table.
134
+ utils.setSubmissionData(payload, key="summary", data=[])
135
+
136
+ payload = utils.addAlert(payload, "History of throws cleared.", "info")
137
+
138
+ return payload
139
+
140
+
141
+ def cause_error(_meta_data: dict, payload: dict) -> dict:
142
+ """Throws an error so that the user can see the error handling in action."""
143
+ raise RuntimeError("Error thrown for testing the error handling mechanism.")
144
+
145
+
146
+ def gui_init(*_args) -> dict:
147
+ """Initializes the form.
148
+
149
+ Initialization of the form and the components therein.
150
+
151
+ Returns:
152
+ payload: Form definition.
153
+ """
154
+ # Create a form object as the base of the application.
155
+ form_obj = Form()
156
+
157
+ # Create tabs in the form.
158
+ tabs = component.Tabs(key="tabs", parent=form_obj)
159
+ tab_inputs, tab_settings = tabs.setContent(labels=["Inputs", "Used settings"])
160
+
161
+ # Create a Columns object and fill it with two empty columns.
162
+ col = component.Columns(key="inputColumns", parent=tab_inputs)
163
+ left_column, right_column = col.setContent(components=[], widths=[6, 6])
164
+
165
+ # Options Panel
166
+ wind_options = {"logic": _wind_options(), **INLINE}
167
+
168
+ # Turn off black formatter for readability of build tables.
169
+ utils.addComponentsFromTable(
170
+ parent=left_column,
171
+ build_table=[
172
+ # key class level options default label tooltip
173
+ ["options", "Container", 1, SHOW_LABEL, None, "Options:", None],
174
+ ["enableDrag", "Checkbox", 2, INLINE, False, "Enable Drag", "Enable the simulation of the effect of drag on the thrown ball."],
175
+ ["enableWind", "Checkbox", 2, wind_options, False, "Enable Wind", "Enable the simulation of the effect of wind on the thrown ball. Requires drag to be enabled."]
176
+ ],
177
+ ) # fmt: skip
178
+
179
+ # Add Throw speed and angle controls.
180
+ add_throw_speed_controls(left_column)
181
+
182
+ # Add the Throw, Ball, Constants and Wind panels to the left column.
183
+ _add_options_panels(left_column)
184
+
185
+ # Fill the right column.
186
+ fill_right_column(right_column)
187
+
188
+ # Fill the Settings tab.
189
+ _fill_settings_tab(tab_settings)
190
+
191
+ # Define the logo.
192
+ image_file = os.path.join(os.path.dirname(__file__), "logo.png")
193
+ logo = utils.encodeImage(image_file)
194
+
195
+ # Put the form in the outputs.
196
+ payload = {
197
+ "form": form_obj,
198
+ "navbar": {
199
+ "logo": logo,
200
+ "title": "Ball Thrower",
201
+ "subtitle": "<small>Simian Demo</small>",
202
+ },
203
+ "showChanged": True, # Flag changes to the settings in the UI.
204
+ }
205
+
206
+ return payload
207
+
208
+
209
+ def add_throw_speed_controls(parent) -> dict:
210
+ """Create and add Throw speed and angle controls."""
211
+ # Allow speed values between 0 and 20 m/s.
212
+
213
+ speed_options = {
214
+ "validate": {
215
+ "min": 0,
216
+ "max": 20 * math.sqrt(2),
217
+ "required": True,
218
+ },
219
+ **LABEL_LEFT,
220
+ }
221
+
222
+ angle_options = {
223
+ "validate": {
224
+ "min": 0,
225
+ "max": 90,
226
+ "required": True,
227
+ },
228
+ **LABEL_LEFT,
229
+ }
230
+
231
+ # Throw Panel
232
+ return utils.addComponentsFromTable(
233
+ parent=parent,
234
+ build_table=[
235
+ # key class level options default label tooltip
236
+ ["throwPanel", "Panel", 1, COLLAPSE_PANEL, None, "Throw settings", None],
237
+ ["throwSpeed", "Number", 2, speed_options, 10, "Throw speed [m/s]:", "Speed with which the ball is thrown."],
238
+ ["throwAngle", "Number", 2, angle_options, 45, "Throw angle [degrees]:", "Angle at which the ball is thrown."]
239
+ ],
240
+ ) # fmt: skip
241
+
242
+
243
+ def _add_options_panels(left_column):
244
+ # Ball properties
245
+ # Hide ball panel when drag is not enabled.
246
+ show_drag_enabled = {"customConditional": "show=data.options.enableDrag"}
247
+ ball_panel_opts = {
248
+ **show_drag_enabled,
249
+ **COLLAPSE_PANEL,
250
+ }
251
+
252
+ # Ball mass number, allow values larger than 0.
253
+ ball_mass_opts = {
254
+ "validate": {
255
+ "min": 0.000001,
256
+ "required": True,
257
+ },
258
+ **LABEL_LEFT,
259
+ }
260
+
261
+ # Ball radius number, allow values larger than 0.
262
+ ball_radius_opts = {
263
+ "validate": {
264
+ "min": 0,
265
+ "required": True,
266
+ },
267
+ **LABEL_LEFT,
268
+ }
269
+
270
+ # Drag coefficient number, allow values between 0 and 10.
271
+ drag_coeff_opts = {
272
+ "validate": {
273
+ "min": 0,
274
+ "max": 10,
275
+ "required": True,
276
+ },
277
+ **LABEL_LEFT,
278
+ }
279
+
280
+ utils.addComponentsFromTable(
281
+ parent=left_column,
282
+ build_table=[
283
+ # key class level options default label tooltip
284
+ ["ballPanel", "Panel", 1, ball_panel_opts, None, "Ball settings", None],
285
+ ["ballMass", "Number", 2, ball_mass_opts, 0.05, "Mass [kg]:", "Mass of the ball."],
286
+ ["ballRadius", "Number", 2, ball_radius_opts, 0.1, "Radius [m]:", "Radius of the ball."],
287
+ ["dragCoefficient", "Number", 2, drag_coeff_opts, 0.4, "Drag coefficient [-]:", "Drag coefficient of the ball."]
288
+ ],
289
+ ) # fmt: skip
290
+
291
+ # Constants Panel
292
+ # Allow gravity values between 0.1 and 20 m/s^2.
293
+ gravity_opts = {
294
+ "validate": {
295
+ "min": 0.1,
296
+ "max": 20,
297
+ "required": True,
298
+ },
299
+ **LABEL_LEFT,
300
+ }
301
+
302
+ # Air density number, allow values between 0 and 10 kg/m^3.
303
+ air_dens_opts = {
304
+ **show_drag_enabled,
305
+ "validate": {
306
+ "min": 0,
307
+ "max": 10,
308
+ "required": True,
309
+ },
310
+ **LABEL_LEFT,
311
+ }
312
+
313
+ utils.addComponentsFromTable(
314
+ parent=left_column,
315
+ build_table=[
316
+ # key class level options default label tooltip
317
+ ["constantsPanel", "Panel", 1, COLLAPSE_PANEL, None, "Constants", None],
318
+ ["gravity", "Number", 2, gravity_opts, 9.81, "Gravity [m/s<sup>2</sup>]:", "Gravitational accelleration acting on the ball."],
319
+ ["airDensity", "Number", 2, air_dens_opts, 1.29, "Air density [kg/m<sup>3</sup>]:", "Density of the air through which the ball moves."]
320
+ ],
321
+ ) # fmt: skip
322
+
323
+ # Wind Panel
324
+ # Hide wind panel when wind is not enabled.
325
+ wind_panel_options = {
326
+ "conditional": {
327
+ "when": "enableWind",
328
+ "eq": False,
329
+ },
330
+ **COLLAPSE_PANEL,
331
+ }
332
+
333
+ # Allow wind speed number values between -10 and 10 m/s.
334
+ wind_options = {
335
+ "validate": {
336
+ "min": -10,
337
+ "max": 10,
338
+ "required": True,
339
+ },
340
+ **LABEL_LEFT,
341
+ }
342
+
343
+ utils.addComponentsFromTable(
344
+ parent=left_column,
345
+ build_table=[
346
+ # key class level options default label tooltip
347
+ ["windPanel", "Panel", 1, wind_panel_options, None, "Wind settings", None],
348
+ ["windSpeed", "Number", 2, wind_options, -1, "Wind speed [m/s]:", "Wind speed. Positive values correspond with tailwind. Negative values correspond with headwind."]
349
+ ],
350
+ ) # fmt: skip
351
+
352
+
353
+ def fill_right_column(right_column) -> None:
354
+ """Fill right column of the app."""
355
+ # Actions Panel
356
+ throw_options = {"disableOnInvalid": True, **INLINE}
357
+ error_options = {"theme": "danger", **INLINE}
358
+
359
+ buttons_dict = utils.addComponentsFromTable(
360
+ parent=right_column,
361
+ build_table=[
362
+ # key class level options label tooltip
363
+ ["ButtonPanel", "Container", 1, SHOW_LABEL, "Actions:", None],
364
+ ["throwButton", "Button", 2, throw_options, "Throw", "Click to simulate a throw with the chosen settings."],
365
+ ["clearButton", "Button", 2, INLINE, "Clear results", "Click to remove all of the current results."],
366
+ ["errorButton", "Button", 2, error_options, "Cause error", "Click to cause an error and see the error handling in action."]
367
+ ],
368
+ column_names=["key", "class", "level", "options", "label", "tooltip"],
369
+ ) # fmt: skip
370
+
371
+ # Add the events to the buttons.
372
+ buttons_dict["throwButton"].setEvent(event_name="ThrowBall")
373
+ buttons_dict["clearButton"].setEvent(event_name="ClearHistory")
374
+ buttons_dict["errorButton"].setEvent(event_name="CauseError")
375
+
376
+ # Plot
377
+ # Create a plot window to plot the ball trajectories in.
378
+ my_plot = component.Plotly(key="plot", parent=right_column)
379
+ my_plot.defaultValue["data"] = []
380
+ my_plot.defaultValue["layout"] = {
381
+ "title": {"text": "Balls thrown"},
382
+ "xaxis": {"title": "Distance [m]"},
383
+ "yaxis": {"title": "Height [m]"},
384
+ "margin": {"t": 40, "b": 30, "l": 50},
385
+ }
386
+ my_plot.defaultValue["config"] = {"displaylogo": False}
387
+
388
+
389
+ def _fill_settings_tab(tab_settings):
390
+ """Fill Settings tab."""
391
+ # Create a table to store the throw settings in.
392
+ table_out = component.DataTables(key="summary", parent=tab_settings)
393
+ table_out.label = "Used settings per throw"
394
+ table_out.setFeatures(searching=False, ordering=False, paging=False)
395
+ table_out.setColumns(
396
+ *[
397
+ list(x)
398
+ for x in zip(
399
+ ["Id", "id"],
400
+ ["Attempt", "attempt"],
401
+ ["Horizontal speed", "hor_speed"],
402
+ ["Vertical speed", "ver_speed"],
403
+ ["Wind speed", "wind_speed"],
404
+ ["Ball mass", "ball_mass"],
405
+ ["Radius", "radius"],
406
+ ["Drag Coeff.", "drag"],
407
+ ["Gravity", "gravity"],
408
+ ["Air density", "air_density"],
409
+ )
410
+ ],
411
+ visible=[False] + [True] * 9,
412
+ )
413
+
414
+
415
+ def _wind_options() -> list:
416
+ """Define drag enabled/disabled triggers."""
417
+ # Disable the Wind checkbox when the Drag checkbox is unticked.
418
+ wind_disable = component_properties.create_disable_logic(
419
+ trigger_type="simple", trigger_value=["enableDrag", False]
420
+ )
421
+
422
+ # Ensure the Wind checkbox is unticked when Drag is unticked.
423
+ wind_disable.actions.append(
424
+ {
425
+ "type": "value",
426
+ "value": "value = false",
427
+ }
428
+ )
429
+ return wind_disable
@@ -0,0 +1,102 @@
1
+ """Ballthrower class. Implements the functional model.
2
+
3
+ Copyright 2020-2023 MonkeyProof Solutions BV.
4
+ """
5
+
6
+ import numpy as np
7
+ from scipy.integrate import solve_ivp
8
+
9
+
10
+ class BallThrower:
11
+ """Ballthrower class. Implements the functional model."""
12
+
13
+ @staticmethod
14
+ def throw_ball(
15
+ x0: float = 0,
16
+ y0: float = 0,
17
+ u0: float = 10,
18
+ v0: float = 10,
19
+ Cd: float = 0.4,
20
+ r: float = 0.05,
21
+ rho: float = 1.239,
22
+ g: float = -9.81,
23
+ m: float = 0.1,
24
+ w: float = 0,
25
+ ) -> list:
26
+ """Ball thrower solver.
27
+
28
+ Args:
29
+ x0: Initial horizontal location [m]. Defaults to 0.
30
+ y0: Initial vertical location [m]. Defaults to 0.
31
+ u0: Initial horizontal speed [m/s]. Defaults to 10.
32
+ v0: Initial vertical speed [m/s]. Defaults to 10.
33
+ Cd: Drag coefficient of the ball [-]. Defaults to 0.4.
34
+ r: Radius of the ball [m]. Defaults to 0.05.
35
+ rho: Air density [kg/m3]. Defaults to 1.239.
36
+ g: Gravitational pull [m/s2]. Defaults to -9.81.
37
+ m: Mass of the ball [kg]. Defaults to 0.1.
38
+ w: Wind speed [m/s]. Defaults to 0.
39
+
40
+ Returns:
41
+ t: Time stamps (in seconds).
42
+ x: Horizontal distance travelled over time (in meters).
43
+ y: Vertical distance travelled over time (in meters).
44
+ u: Horizontal speed over time (in meters per second).
45
+ v: Vertical speed over time (in meters per second).
46
+ """
47
+
48
+ # Define the zero crossing function that should abort the simulation.
49
+ def _zero_crossing(_t, y, *_args):
50
+ return y[2]
51
+
52
+ # Ensure that gravity is pointing downwards.
53
+ g = abs(g) * -1
54
+
55
+ _zero_crossing.terminal = True
56
+ _zero_crossing.direction = -1
57
+
58
+ # Solve the differential equation.
59
+ outputs = solve_ivp(
60
+ fun=BallThrower._drag_ball,
61
+ t_span=[0, np.inf],
62
+ y0=[x0, u0, y0, v0],
63
+ events=_zero_crossing,
64
+ args=(Cd, r, rho, g, m, w),
65
+ rtol=1e-8,
66
+ atol=1e-8,
67
+ max_step=0.1,
68
+ )
69
+
70
+ t = outputs.t
71
+ y = outputs.y
72
+
73
+ # Return the time stamps, location and speed components.
74
+ return t, y[0], y[2], y[1], y[3]
75
+
76
+ @staticmethod
77
+ def _drag_ball(
78
+ _t: float, y: list, Cd: float, r: float, rho: float, g: float, m: float, w: float
79
+ ) -> list:
80
+ """Calculates the location and speed of the ball at time step t.
81
+
82
+ Args:
83
+ t: Time stamp.
84
+ y: The location and speed of the ball at time stamp t - 1.
85
+ Cd: Drag coefficient of the ball [-].
86
+ r: Radius of the ball [m].
87
+ rho: Air density [kg/m3].
88
+ g: Gravitational pull [m/s2].
89
+ m: Mass of the ball [kg].
90
+ w: Wind speed [m/s].
91
+
92
+ Returns:
93
+ y: List of dx/dt, du/dt, dy/dt and dv/dt values.
94
+ """
95
+ A = np.pi * r**2
96
+
97
+ return [
98
+ y[1],
99
+ -0.5 * rho / m * (y[1] - w) ** 2 * Cd * A * np.sign(y[1] - w),
100
+ y[3],
101
+ -0.5 * rho / m * y[3] ** 2 * Cd * A * np.sign(y[3]) + g,
102
+ ]
@@ -0,0 +1,76 @@
1
+ function fillTreemap(data, year) {
2
+ console.log("Year: " + year);
3
+
4
+ var idx = data.years.map((y, i) => y == year ? i : false).filter(Boolean);
5
+
6
+ var countries = idx.map((i) => data.countries[i]);
7
+ var parents = idx.map((i) => data.parents[i]);
8
+ var ids = idx.map((i) => data.ids[i]);
9
+ var lifeExp = idx.map((i) => data.lifeExp[i]);
10
+ var pop = idx.map((i) => data.pop[i]);
11
+
12
+ var layout = {
13
+ coloraxis: {
14
+ colorbar: {
15
+ title: {
16
+ "text": "lifeExp"
17
+ }
18
+ },
19
+ colorscale: getColorScale(),
20
+ cmid: lifeExp.slice(-1)[0]
21
+ },
22
+ legend: {
23
+ tracegroupgap: 0
24
+ },
25
+ margin: {
26
+ t: 50,
27
+ l: 25,
28
+ r: 25,
29
+ b: 25
30
+ }
31
+ };
32
+
33
+ var trace = {
34
+ branchvalues: "total",
35
+ domain: {
36
+ x: [0.0, 1.0],
37
+ y: [0.0, 1.0]
38
+ },
39
+ hovertemplate: "Country: %{label}<br>Population: %{value}<br>Continent: %{parent}<br>Life expectancy: %{color}",
40
+ ids: ids,
41
+ labels: countries,
42
+ marker: {
43
+ coloraxis: "coloraxis",
44
+ colors: lifeExp
45
+ },
46
+ name: "",
47
+ parents: parents,
48
+ values: pop,
49
+ type: "treemap"
50
+ };
51
+
52
+ return {
53
+ layout: layout,
54
+ data: [trace]
55
+ };
56
+ }
57
+
58
+ function getColorScale() {
59
+ var cmap = [
60
+ [103, 0, 31],
61
+ [178, 24, 43],
62
+ [214, 96, 77],
63
+ [244, 165, 130],
64
+ [253, 219, 199],
65
+ [247, 247, 247],
66
+ [209, 229, 240],
67
+ [146, 197, 222],
68
+ [67, 147, 195],
69
+ [33, 102, 172],
70
+ [5, 48, 97],
71
+ ];
72
+
73
+ var nCmap = cmap.length - 1;
74
+
75
+ return cmap.map((c, i) => [1.0 * i / nCmap, 'rgb(' + c[0] + ', ' + c[1] + ', ' + c[2] + ')']);
76
+ }
Binary file