derek-thomas HF staff commited on
Commit
aa651cf
1 Parent(s): 3fcff6f

Init commit

Browse files
.gitignore ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
130
+
131
+ .idea/
132
+
133
+ *.ipynb_checkpoints/
app/Disc Golf Simulator.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from logging import getLogger
2
+ from pathlib import Path
3
+ proj_dir = Path(__file__).parents[1]
4
+ import sys
5
+ sys.path.append(str(proj_dir))
6
+
7
+ import numpy as np
8
+ import streamlit as st
9
+
10
+ from shotshaper.projectile import DiscGolfDisc
11
+ from visualize import get_plot, get_stl, get_subplots, visualize_disc
12
+
13
+
14
+ # Define the default values
15
+ default_U = 24.2
16
+ default_omega = 116.8
17
+ default_z0 = 1.3
18
+ default_pitch = 15.5
19
+ default_nose = 0.0
20
+ default_roll = 14.7
21
+
22
+
23
+ def main():
24
+ tab1, tab2 = st.tabs(['Simulator', 'FAQ'])
25
+ with tab1:
26
+ disc_names = {
27
+ 'Innova Wraith': 'dd2',
28
+ 'Innova Firebird': 'cd1',
29
+ 'Innova Roadrunner': 'cd5',
30
+ 'Innova Fairway Driver': 'fd2',
31
+ }
32
+ disc_selected = st.sidebar.selectbox("Disc Selection", disc_names.keys())
33
+ disc_name = disc_names[disc_selected]
34
+
35
+ # Create the sliders with the default values
36
+ U = st.sidebar.slider("Throwing Velocity (m/s)", min_value=0.0, max_value=40.0, value=default_U, step=0.1,
37
+ help='Fastest Throw on record is ~40m/s by Simon Lizotte')
38
+ omega = st.sidebar.slider("Omega", min_value=0.0, max_value=200.0, value=default_omega, step=0.1)
39
+ z0 = st.sidebar.slider("Release Height (m)", min_value=0.0, max_value=2.0, value=default_z0, step=0.1)
40
+ pitch = st.sidebar.slider("Pitch Angle (deg) | Release angle", min_value=0.0, max_value=90.0, value=default_pitch,
41
+ step=0.1)
42
+ nose = st.sidebar.slider("Nose Angle (deg) | Up/Down", min_value=0.0, max_value=90.0, value=default_nose,
43
+ step=0.1)
44
+ roll = st.sidebar.slider("Roll Angle (deg) | Tilt Left/Right", min_value=-90.0, max_value=90.0, value=default_roll,
45
+ step=0.1)
46
+
47
+ pos = np.array((0, 0, default_z0))
48
+ disc_dict = DiscGolfDisc(disc_name)
49
+
50
+ stl_mesh = get_stl(proj_dir / 'shotshaper' / 'discs' / (disc_name + '.stl'))
51
+ fig = visualize_disc(stl_mesh, nose=nose, roll=roll)
52
+
53
+ st.markdown("""## Disc orientation""")
54
+ st.plotly_chart(fig)
55
+ st.markdown("""## Flight Path""")
56
+ shot = disc_dict.shoot(speed=U, omega=omega, pitch=pitch,
57
+ position=pos, nose_angle=nose, roll_angle=roll)
58
+
59
+ # Plot trajectory
60
+ x, y, z = shot.position
61
+ x_new, y_new = -1 * y, x
62
+
63
+ # Reversed x and y to mimic a throw
64
+ fig = get_plot(x_new, y_new, z)
65
+ st.plotly_chart(fig, True)
66
+
67
+ st.markdown(
68
+ f"""
69
+ **Arrows in Blue** show you where your *s-turn* is.
70
+
71
+ **Arrows in Red** show you your *max height* and *lateral deviance*.
72
+
73
+ Hit Play to watch your animated throw.
74
+
75
+ | Metric | Value |
76
+ |--------------|--------|
77
+ | Drift Left | {round(min(x_new), 2)} |
78
+ | Drift Right | {round(max(x_new), 2)} |
79
+ | Max Height | {round(max(z), 2)} |
80
+ | Distance | {round(max(y_new), 2)} |
81
+
82
+ """
83
+ )
84
+
85
+ arc, alphas, betas, lifts, drags, moms, rolls = disc_dict.post_process(shot, omega)
86
+ fig = get_subplots(arc, alphas, lifts, drags, moms, rolls, shot.velocity)
87
+ st.plotly_chart(fig, True)
88
+
89
+ with tab2:
90
+ st.markdown("""
91
+ # Motivation
92
+ I saw some great work by [kegiljarhus](https://github.com/kegiljarhus) [repo](https://github.com/kegiljarhus/shotshaper)
93
+ and wanted to make this available as an app so people could learn more about disc golf. I really want to commend
94
+ the amazing idea of writing a [scientific article](https://link.springer.com/article/10.1007/s12283-022-00390-5)
95
+ AND releasing code, and actually executing it well. This is what gets people excited about STEM.
96
+
97
+ I originally saw this
98
+ [reddit post](https://www.reddit.com/r/discgolf/comments/yyhbcj/wrote_a_scientific_article_on_disc_golf_flight/)
99
+ which really piqued my interest.
100
+
101
+ # Questions
102
+ - I imagine some of you will want to add your disc here, if you can convert your disc into an `.stl` then I will
103
+ add it to the database. If this gets common enough I will add an option to upload your own.
104
+ - I imagine there will be a barrier to entry to do this.
105
+ - If you have any ideas, just let me know in a discussion or in a pull request
106
+ """)
107
+
108
+
109
+ if __name__ == "__main__":
110
+ # Setting up Logger and proj_dir
111
+ logger = getLogger(__name__)
112
+ proj_dir = Path(__file__).parents[1]
113
+
114
+ st.title("Disc Golf Simulator")
115
+
116
+ # initialize_state()
117
+ main()
app/extrema.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from scipy.signal import argrelextrema
3
+
4
+ def find_extrema(x, y):
5
+ # Find the indices of the local maxima and minima
6
+ maxima_idx = argrelextrema(x, np.greater)[0]
7
+ minima_idx = argrelextrema(x, np.less)[0]
8
+
9
+ # Get the x and y values at the extrema
10
+ maxima_x = x[maxima_idx]
11
+ maxima_y = y[maxima_idx]
12
+ minima_x = x[minima_idx]
13
+ minima_y = y[minima_idx]
14
+
15
+ # Combine the maxima and minima into a single array
16
+ extrema_x = np.concatenate((maxima_x, minima_x))
17
+ extrema_y = np.concatenate((maxima_y, minima_y))
18
+
19
+ # Determine whether each extrema is a maximum or minimum
20
+ is_maxima = np.zeros(len(extrema_x), dtype=bool)
21
+ is_maxima[:len(maxima_x)] = True
22
+
23
+ # Sort the extrema by x-value
24
+ idx = np.argsort(extrema_x)
25
+ extrema_x = extrema_x[idx]
26
+ extrema_y = extrema_y[idx]
27
+ is_maxima = is_maxima[idx]
28
+
29
+ # Convert the boolean array to a list of strings
30
+ extrema_type = ["arrow-left" if is_max else "arrow-right" for is_max in is_maxima]
31
+
32
+ return extrema_x, extrema_y, extrema_type
app/get_disc.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+
3
+ headers = {
4
+ 'authority': 'alldiscs.com',
5
+ 'accept': 'application/json, text/javascript, */*; q=0.01',
6
+ 'accept-language': 'en-US,en;q=0.6',
7
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
8
+ 'origin': 'https://alldiscs.com',
9
+ 'referer': 'https://alldiscs.com/',
10
+ 'sec-fetch-dest': 'empty',
11
+ 'sec-fetch-mode': 'cors',
12
+ 'sec-fetch-site': 'same-origin',
13
+ 'sec-gpc': '1',
14
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36',
15
+ 'x-requested-with': 'XMLHttpRequest',
16
+ }
17
+
18
+ params = {
19
+ 'action': 'get_wdtable',
20
+ 'table_id': '5',
21
+ }
22
+
23
+ data = {
24
+ 'draw': '4',
25
+ 'columns[0][data]': '0',
26
+ 'columns[0][name]': 'wdt_ID',
27
+ 'columns[0][searchable]': 'true',
28
+ 'columns[0][orderable]': 'true',
29
+ 'columns[0][search][value]': '',
30
+ 'columns[0][search][regex]': 'false',
31
+ 'columns[1][data]': '1',
32
+ 'columns[1][name]': 'brand',
33
+ 'columns[1][searchable]': 'true',
34
+ 'columns[1][orderable]': 'true',
35
+ 'columns[1][search][value]': '',
36
+ 'columns[1][search][regex]': 'false',
37
+ 'columns[2][data]': '2',
38
+ 'columns[2][name]': 'mold',
39
+ 'columns[2][searchable]': 'true',
40
+ 'columns[2][orderable]': 'true',
41
+ 'columns[2][search][value]': '',
42
+ 'columns[2][search][regex]': 'false',
43
+ 'columns[3][data]': '3',
44
+ 'columns[3][name]': 'type',
45
+ 'columns[3][searchable]': 'true',
46
+ 'columns[3][orderable]': 'true',
47
+ 'columns[3][search][value]': 'Distance|Fairway|Midrange|Putter',
48
+ 'columns[3][search][regex]': 'false',
49
+ 'columns[4][data]': '4',
50
+ 'columns[4][name]': 'speed',
51
+ 'columns[4][searchable]': 'true',
52
+ 'columns[4][orderable]': 'true',
53
+ 'columns[4][search][value]': '1|15',
54
+ 'columns[4][search][regex]': 'false',
55
+ 'columns[5][data]': '5',
56
+ 'columns[5][name]': 'glide',
57
+ 'columns[5][searchable]': 'true',
58
+ 'columns[5][orderable]': 'true',
59
+ 'columns[5][search][value]': '1|7',
60
+ 'columns[5][search][regex]': 'false',
61
+ 'columns[6][data]': '6',
62
+ 'columns[6][name]': 'turn',
63
+ 'columns[6][searchable]': 'true',
64
+ 'columns[6][orderable]': 'true',
65
+ 'columns[6][search][value]': '-5|1',
66
+ 'columns[6][search][regex]': 'false',
67
+ 'columns[7][data]': '7',
68
+ 'columns[7][name]': 'fade',
69
+ 'columns[7][searchable]': 'true',
70
+ 'columns[7][orderable]': 'true',
71
+ 'columns[7][search][value]': '0|5',
72
+ 'columns[7][search][regex]': 'false',
73
+ 'columns[8][data]': '8',
74
+ 'columns[8][name]': 'inproduction',
75
+ 'columns[8][searchable]': 'true',
76
+ 'columns[8][orderable]': 'true',
77
+ 'columns[8][search][value]': 'Coming Soon|Yes',
78
+ 'columns[8][search][regex]': 'false',
79
+ 'columns[9][data]': '9',
80
+ 'columns[9][name]': 'dateapproved',
81
+ 'columns[9][searchable]': 'true',
82
+ 'columns[9][orderable]': 'true',
83
+ 'columns[9][search][value]': '|',
84
+ 'columns[9][search][regex]': 'false',
85
+ 'columns[10][data]': '10',
86
+ 'columns[10][name]': 'link',
87
+ 'columns[10][searchable]': 'true',
88
+ 'columns[10][orderable]': 'true',
89
+ 'columns[10][search][value]': '',
90
+ 'columns[10][search][regex]': 'false',
91
+ 'order[0][column]': '0',
92
+ 'order[0][dir]': 'asc',
93
+ 'start': '0',
94
+ 'length': '10',
95
+ 'search[value]': 'wraith',
96
+ 'search[regex]': 'false',
97
+ 'wdtNonce': '511bd3400c',
98
+ 'sRangeSeparator': '|',
99
+ }
100
+
101
+ response = requests.post('https://alldiscs.com/wp-admin/admin-ajax.php', params=params, headers=headers, data=data)
app/visualize.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ import numpy as np
4
+ import plotly.graph_objects as go
5
+ from plotly.colors import sequential
6
+ from stl.mesh import Mesh
7
+
8
+ from extrema import find_extrema
9
+
10
+
11
+ def get_stl(stl_file):
12
+ """
13
+ Taken from https://community.plotly.com/t/view-3d-cad-data/16920/9
14
+ """
15
+ stl_mesh = Mesh.from_file(stl_file)
16
+ return stl_mesh
17
+
18
+
19
+ def visualize_disc(stl_mesh, nose, roll):
20
+ """
21
+ Taken from https://community.plotly.com/t/view-3d-cad-data/16920/9
22
+ """
23
+ stl_mesh.rotate([1, 0, 0], math.radians(-1*nose))
24
+ stl_mesh.rotate([0, 1, 0], math.radians(roll))
25
+ # stl_mesh.rotate([0, 0, 1], math.radians(z_angle))
26
+
27
+ p, q, r = stl_mesh.vectors.shape # (p, 3, 3)
28
+ # the array stl_mesh.vectors.reshape(p*q, r) can contain multiple copies of the same vertex;
29
+ # extract unique vertices from all mesh triangles
30
+ vertices, ixr = np.unique(stl_mesh.vectors.reshape(p * q, r), return_inverse=True, axis=0)
31
+ I = np.take(ixr, [3 * k for k in range(p)])
32
+ J = np.take(ixr, [3 * k + 1 for k in range(p)])
33
+ K = np.take(ixr, [3 * k + 2 for k in range(p)])
34
+
35
+ x, y, z = vertices.T
36
+ trace = go.Mesh3d(x=x, y=y, z=z, i=I, j=J, k=K)
37
+ # optional parameters to make it look nicer
38
+ trace.update(flatshading=True, lighting_facenormalsepsilon=0, lighting_ambient=0.7)
39
+
40
+ fig = go.Figure(trace)
41
+
42
+ # Add camera controls to the plot
43
+ camera = dict(
44
+ eye=dict(x=0, y=-3, z=0)
45
+ )
46
+ fig.update_layout(scene_camera=camera)
47
+
48
+ fig.update_layout(
49
+ scene=dict(
50
+ xaxis=dict(nticks=4, range=[-0.11, 0.11], ),
51
+ yaxis=dict(nticks=4, range=[-0.11, 0.11], ),
52
+ zaxis=dict(nticks=4, range=[-0.11, 0.11], ), )
53
+ )
54
+ return fig
55
+
56
+
57
+ import plotly.subplots as sp
58
+
59
+
60
+ def get_plot(x, y, z):
61
+ xm = np.min(x) - 1.5
62
+ xM = np.max(x) + 1.5
63
+ ym = -5
64
+ yM = np.max(y) + 1.5
65
+ zm = np.min(z)
66
+ zM = np.max(z)
67
+ N = len(x)
68
+ category = 'Height'
69
+ x_extrema, y_extrema, extrema_type = find_extrema(x, y)
70
+
71
+ xM_abs = max(abs(xm), abs(xM))
72
+ xm_abs = -1 * xM_abs
73
+
74
+ carats_v = go.Scatter(
75
+ x=[category, category],
76
+ y=[min(z), max(z)],
77
+ mode='markers',
78
+ showlegend=False,
79
+ marker=dict(symbol=['arrow-up', 'arrow-down'], size=20, color=['red', 'red']),
80
+ name='Carets',
81
+ )
82
+ carats_h = go.Scatter(
83
+ x=[min(x), max(x)],
84
+ y=['', ''],
85
+ mode='markers',
86
+ showlegend=False,
87
+ marker=dict(symbol=['arrow-right', 'arrow-left'], size=20, color=['red', 'red']),
88
+ name='Carets',
89
+ )
90
+
91
+ extrema = go.Scatter(
92
+ x=x_extrema,
93
+ y=y_extrema,
94
+ mode='markers',
95
+ showlegend=False,
96
+ marker=dict(symbol=extrema_type, size=20, color='blue'),
97
+ name='Extrema',
98
+ )
99
+
100
+ # Create figure with subplots
101
+ fig = sp.make_subplots(rows=2, cols=2, subplot_titles=("Flight Path", "Height", "Lateral Deviance"),
102
+ specs=[[{"rowspan": 2}, {}], [None, {}]], row_heights=[0.5, 0.5])
103
+
104
+ # Add traces to the main plot
105
+ fig.add_trace(
106
+ go.Scatter(x=x, y=y,
107
+ mode="lines",
108
+ showlegend=False,
109
+ line=dict(width=1, color='black')),
110
+ row=1, col=1
111
+ )
112
+
113
+ fig.add_trace(
114
+ go.Scatter(x=x, y=y,
115
+ showlegend=False,
116
+ mode="markers", marker_colorscale=sequential.Peach,
117
+ marker=dict(color=z, size=3, showscale=True)),
118
+ row=1, col=1
119
+ )
120
+
121
+ fig.add_trace(
122
+ extrema,
123
+ row=1, col=1
124
+ )
125
+
126
+ # Add trace for the subplot
127
+ fig.add_trace(carats_v,
128
+ row=1, col=2
129
+ )
130
+ fig.add_trace(
131
+ go.Bar(
132
+ x=[],
133
+ y=[],
134
+ showlegend=False,
135
+ ),
136
+ row=1, col=2
137
+ )
138
+ # Add trace for the subplot
139
+ fig.add_trace(carats_h,
140
+ row=2, col=2
141
+ )
142
+ fig.add_trace(
143
+ go.Bar(
144
+ x=[],
145
+ y=[],
146
+ showlegend=False,
147
+ orientation='h',
148
+ ),
149
+ row=2, col=2
150
+ )
151
+
152
+ # Update layout
153
+ fig.update_layout(
154
+ xaxis=dict(range=[xm, xM], autorange=False, zeroline=False),
155
+ yaxis=dict(range=[ym, yM], autorange=False, zeroline=False),
156
+ title_text="Flight Path",
157
+ hovermode="closest",
158
+ updatemenus=[
159
+ dict(
160
+ type="buttons",
161
+ buttons=[
162
+ dict(
163
+ label="Play",
164
+ method="animate",
165
+ args=[None, {"frame": {"duration": 30, "redraw": True}, "fromcurrent": True}]
166
+ ),
167
+ dict(
168
+ label="Pause",
169
+ method="animate",
170
+ args=[[None], {"frame": {"duration": 0, "redraw": False}, "mode": "immediate",
171
+ "transition": {"duration": 0}}]
172
+ )
173
+ ],
174
+ showactive=False,
175
+ x=0.05,
176
+ y=0.05
177
+ )
178
+ ],
179
+ )
180
+
181
+ # Create frames for the main plot and subplot
182
+ all_frames = [
183
+ go.Frame(data=[
184
+ go.Scatter(
185
+ x=[x[k]],
186
+ y=[y[k]],
187
+ mode="markers",
188
+ showlegend=False,
189
+ marker=dict(color="red", size=10)),
190
+ go.Scatter(x=x, y=y,
191
+ mode="markers", marker_colorscale=sequential.Peach,
192
+ marker=dict(color=z, size=3, showscale=True)),
193
+ extrema,
194
+ carats_v,
195
+ go.Bar(
196
+ x=['Height'],
197
+ y=[z[k]],
198
+ showlegend=False,
199
+ name='Value',
200
+ marker=dict(color='orange', line=dict(width=1))
201
+ # Set color of value bar to orange and line width to 1
202
+ ),
203
+ carats_h,
204
+ go.Bar(
205
+ x=[x[k]],
206
+ y=[''],
207
+ showlegend=False,
208
+ name='Value',
209
+ orientation='h',
210
+ marker=dict(color='orange', line=dict(width=1))
211
+ # Set color of value bar to orange and line width to 1
212
+ )
213
+ ])
214
+ for k in range(N)
215
+ ]
216
+
217
+ # Combine frames for the main plot and subplot
218
+ fig.frames = all_frames
219
+ fig.update_yaxes(scaleanchor="x", scaleratio=1, row=1, col=1)
220
+ fig.update_yaxes(range=[zm - 2, zM + 2], fixedrange=True, row=1, col=2)
221
+ fig.update_xaxes(range=[xm_abs, xM_abs], fixedrange=True, row=2, col=2)
222
+
223
+ # # Add green rectangle at the bottom of the plot
224
+ fig.update_layout(
225
+ shapes=[dict(type="rect", xref="x", yref="y",
226
+ x0=-1, y0=0, x1=1, y1=-4, fillcolor="gray",
227
+ opacity=1, layer="below")],
228
+ plot_bgcolor="green",
229
+ )
230
+
231
+ return fig
232
+
233
+
234
+ import plotly.graph_objs as go
235
+ from plotly.subplots import make_subplots
236
+
237
+
238
+ def get_subplots(arc, alphas, lifts, drags, moms, rolls, velocity):
239
+ fig = make_subplots(rows=2, cols=3, specs=[[{}, {}, {}], [{}, {}, {}]],
240
+ subplot_titles=("Lift force (N)", "Drag force (N)", "Moment (Nm)",
241
+ "Angle of attack (deg)", "Velocities (m/s)", "Roll rate (rad/s)"),
242
+ shared_xaxes=True)
243
+
244
+ fig.add_trace(go.Scatter(x=arc, y=lifts, name="Lift force (N)"), row=1, col=1)
245
+ fig.update_xaxes(title_text="Distance (m)", row=1, col=1)
246
+ fig.update_yaxes(title_text="Lift force (N)", row=1, col=1)
247
+
248
+ fig.add_trace(go.Scatter(x=arc, y=drags, name="Drag force (N)"), row=1, col=2)
249
+ fig.update_xaxes(title_text="Distance (m)", row=1, col=2)
250
+ fig.update_yaxes(title_text="Drag force (N)", row=1, col=2)
251
+
252
+ fig.add_trace(go.Scatter(x=arc, y=moms, name="Moment (Nm)"), row=1, col=3)
253
+ fig.update_xaxes(title_text="Distance (m)", row=1, col=3)
254
+ fig.update_yaxes(title_text="Moment (Nm)", row=1, col=3)
255
+
256
+ fig.add_trace(go.Scatter(x=arc, y=alphas, name="Angle of attack (deg)"), row=2, col=1)
257
+ fig.update_xaxes(title_text="Distance (m)", row=2, col=1)
258
+ fig.update_yaxes(title_text="Angle of attack (deg)", row=2, col=1)
259
+
260
+ fig.add_trace(go.Scatter(x=arc, y=velocity[0, :], name="u"), row=2, col=2)
261
+ fig.add_trace(go.Scatter(x=arc, y=velocity[1, :], name="v"), row=2, col=2)
262
+ fig.add_trace(go.Scatter(x=arc, y=velocity[2, :], name="w"), row=2, col=2)
263
+ fig.update_xaxes(title_text="Distance (m)", row=2, col=2)
264
+ fig.update_yaxes(title_text="Velocities (m/s)", row=2, col=2)
265
+ fig.update_traces(mode='lines', row=2, col=2)
266
+
267
+ fig.add_trace(go.Scatter(x=arc, y=rolls, name="Roll rate (rad/s)"), row=2, col=3)
268
+ fig.update_xaxes(title_text="Distance (m)", row=2, col=3)
269
+ fig.update_yaxes(title_text="Roll rate (rad/s)", row=2, col=3)
270
+
271
+ fig.update_layout(height=600, width=1000, title_text="Plotly Subplots", hovermode='x')
272
+ return fig
bootstrap.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import streamlit.web.bootstrap
3
+ from streamlit import config as _config
4
+
5
+ proj_dir = Path(__file__).parent
6
+ filename = proj_dir / "app" / "app.py"
7
+
8
+ _config.set_option("server.headless", True)
9
+ args = []
10
+
11
+ # streamlit.cli.main_run(filename, args)
12
+ streamlit.web.bootstrap.run(str(filename), "", args, "")
examples/disc_experimental_trajectory_comparison.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import sys
4
+ from shotshaper.projectile import DiscGolfDisc
5
+ import matplotlib.pyplot as pl
6
+ import numpy as np
7
+ import shotshaper.environment as env
8
+ from shotshaper.transforms import T_12
9
+ from random import uniform
10
+
11
+
12
+ def rotz(pos,betad):
13
+ beta = np.radians(betad)
14
+ TZ = np.array([[np.cos(beta), -np.sin(beta), 0],
15
+ [np.sin(beta), np.cos(beta), 0],
16
+ [0, 0, 1]])
17
+
18
+ return np.matmul(TZ,pos)
19
+
20
+ throws=[1,6,15]
21
+
22
+ nthrow = len(throws)
23
+ # pitch, roll, nose,speed,spin, yaw, wind, length
24
+ params = [[15.5, 21.8, 0.0, 24.7, 138, -31.6, 4.8, 89.7],
25
+ [12.3, 14.7, 0.8, 24.2, 128.5, -9.60, 4.8, 87.0],
26
+ [5.20,-0.70, 0.0, 24.5, 147.7, 8.00, 4.8, 106.6]]
27
+
28
+
29
+ d = DiscGolfDisc('dd2')
30
+ fig1, ax1 = pl.subplots( )
31
+
32
+ fig1.set_figheight(4)
33
+ fig1.set_figwidth(6)
34
+
35
+ for i in range(nthrow):
36
+ p = params[i]
37
+ t = throws[i]
38
+ U = p[3]
39
+ omega = p[4]
40
+ z0 = 1.5
41
+ pos = np.array((0,0,z0))
42
+ pitch = p[0]
43
+ yaw = p[5]
44
+ nose = p[2]
45
+ roll = p[1]
46
+
47
+ env.Uref = p[6]
48
+ env.winddir = np.array((1,0,0))
49
+
50
+ # Currently handle yaw by rotating the position after the throw, hence
51
+ # also need to rotate the wind vector accordingly
52
+ env.winddir = rotz(env.winddir, -yaw)
53
+
54
+ s = d.shoot(speed=U, omega=omega, pitch=pitch, position=pos, nose_angle=nose, roll_angle=roll)
55
+
56
+ pos = s.position
57
+ for j in range(len(pos[0,:])):
58
+ pos[:,j] = rotz(pos[:,j], yaw)
59
+ x,y,z = pos
60
+ arc,alphas,betas,lifts,drags,moms,rolls = d.post_process(s, omega)
61
+
62
+ # Plot trajectory
63
+ ax1.plot(x,y,f'C{i}-')
64
+
65
+ # Experiment
66
+ te,xe,ye,ve = np.loadtxt(f'data/throw{t}',skiprows=2,unpack=True)
67
+ ax1.plot(xe,ye,f'C{i}--')
68
+
69
+ ax1.set_xlabel('Distance (m)')
70
+ ax1.set_ylabel('Drift (m)')
71
+ ax1.axis('equal')
72
+
73
+ # Plot other parameters
74
+
75
+ # axes[0,0].plot(arc, lifts)
76
+ # axes[0,0].set_xlabel('Distance (m)')
77
+ # axes[0,0].set_ylabel('Lift force (N)')
78
+
79
+ # axes[0,1].plot(arc, drags)
80
+ # axes[0,1].set_xlabel('Distance (m)')
81
+ # axes[0,1].set_ylabel('Drag force (N)')
82
+
83
+ # axes[0,2].plot(arc, moms)
84
+ # axes[0,2].set_xlabel('Distance (m)')
85
+ # axes[0,2].set_ylabel('Moment (Nm)')
86
+
87
+ # axes[1,0].plot(arc, alphas)
88
+ # axes[1,0].set_xlabel('Distance (m)')
89
+ # axes[1,0].set_ylabel('Angle of attack (deg)')
90
+
91
+ # axes[1,1].plot(arc, s.velocity[0,:])
92
+ # axes[1,1].plot(arc, s.velocity[1,:])
93
+ # axes[1,1].plot(arc, s.velocity[2,:])
94
+ # axes[1,1].set_xlabel('Distance (m)')
95
+ # axes[1,1].set_ylabel('Velocities (m/s)')
96
+
97
+ # axes[1,2].plot(arc, rolls)
98
+ # axes[1,2].set_xlabel('Distance (m)')
99
+ # axes[1,2].set_ylabel('Roll rate (rad/s)')
100
+
101
+ pl.tight_layout()
102
+ pl.show()
103
+
104
+
105
+
106
+
examples/disc_golf_throw.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Example showing a single disc throw.
4
+ """
5
+
6
+ from shotshaper.projectile import DiscGolfDisc
7
+ import matplotlib.pyplot as pl
8
+ import numpy as np
9
+ import plotly.express as px
10
+
11
+ d = DiscGolfDisc('dd2')
12
+ U = 24.2
13
+ omega = 116.8
14
+ z0 = 1.3
15
+ pos = np.array((0,0,z0))
16
+ pitch = 15.5
17
+ nose = 0.0
18
+ roll = 14.7
19
+
20
+ shot = d.shoot(speed=U, omega=omega, pitch=pitch,
21
+ position=pos, nose_angle=nose, roll_angle=roll)
22
+
23
+ # Plot trajectory
24
+ pl.figure(1)
25
+ x,y,z = shot.position
26
+ pl.plot(x,y)
27
+ fig = px.scatter(x,y)
28
+ fig.show()
29
+
30
+ pl.xlabel('Distance (m)')
31
+ pl.ylabel('Drift (m)')
32
+ pl.axis('equal')
33
+
34
+ # Plot other parameters
35
+ arc,alphas,betas,lifts,drags,moms,rolls = d.post_process(shot, omega)
36
+ fig, axes = pl.subplots(nrows=2, ncols=3, dpi=80,figsize=(13,5))
37
+
38
+ axes[0,0].plot(arc, lifts)
39
+ axes[0,0].set_xlabel('Distance (m)')
40
+ axes[0,0].set_ylabel('Lift force (N)')
41
+
42
+ axes[0,1].plot(arc, drags)
43
+ axes[0,1].set_xlabel('Distance (m)')
44
+ axes[0,1].set_ylabel('Drag force (N)')
45
+
46
+ axes[0,2].plot(arc, moms)
47
+ axes[0,2].set_xlabel('Distance (m)')
48
+ axes[0,2].set_ylabel('Moment (Nm)')
49
+
50
+ axes[1,0].plot(arc, alphas)
51
+ axes[1,0].set_xlabel('Distance (m)')
52
+ axes[1,0].set_ylabel('Angle of attack (deg)')
53
+
54
+ axes[1,1].plot(arc, shot.velocity[0,:])
55
+ axes[1,1].plot(arc, shot.velocity[1,:])
56
+ axes[1,1].plot(arc, shot.velocity[2,:])
57
+ axes[1,1].set_xlabel('Distance (m)')
58
+ axes[1,1].set_ylabel('Velocities (m/s)')
59
+ axes[1,1].legend(('u','v','w'))
60
+
61
+ axes[1,2].plot(arc, rolls)
62
+ axes[1,2].set_xlabel('Distance (m)')
63
+ axes[1,2].set_ylabel('Roll rate (rad/s)')
64
+ pl.tight_layout()
65
+
66
+ pl.show()
examples/disc_gui2d.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Graphical user interface to explore the influence
4
+ of disc throw parameters on the trajectory
5
+ """
6
+
7
+ import numpy as np
8
+ import matplotlib.pyplot as pl
9
+ from matplotlib.widgets import Slider, TextBox
10
+ from shotshaper.projectile import DiscGolfDisc
11
+
12
+ name = 'dd2'
13
+ mass = 0.175
14
+
15
+ d = DiscGolfDisc(name, mass=mass)
16
+ speed = 24
17
+ omega = d.empirical_spin(speed)
18
+ z0 = 1.3
19
+ pos = np.array((0,0,z0))
20
+ pitch = 10
21
+ nose = 0.0
22
+ roll = 15.0
23
+ yaw = 0
24
+ adjust_axes = False
25
+
26
+ s = d.shoot(speed=speed, omega=omega, pitch=pitch, position=pos, nose_angle=nose, roll_angle=roll,yaw=yaw)
27
+
28
+ x,y,z = s.position
29
+
30
+ # Creating figure
31
+ fig = pl.figure(1,figsize=(13, 6), dpi=80)
32
+ ax1 = pl.subplot(2,3,2)
33
+ ax2 = pl.subplot(2,3,5)
34
+ ax3 = pl.subplot(1,3,3)
35
+ fac = 1.6
36
+ ylim = fac*max(abs(min(y)),abs(max(y)))
37
+ ax1.axis((min(x),fac*max(x),-ylim,ylim))
38
+ ax2.axis((min(x),fac*max(x),min(z),fac*max(z)))
39
+ ax3.axis((-ylim,ylim,min(z),fac*max(z)))
40
+
41
+ ax3.invert_xaxis()
42
+
43
+ l1, = ax1.plot(x,y,lw=2)
44
+ ax1.set_xlabel('Distance (m)')
45
+ ax1.set_ylabel('Drift (m)')
46
+ l2, = ax2.plot(x,z,lw=2)
47
+ ax2.set_xlabel('Distance (m)')
48
+ ax2.set_ylabel('Height (m)')
49
+
50
+ l3, = ax3.plot(y,z,lw=2)
51
+ ax3.set_xlabel('Drift (m)')
52
+ ax3.set_ylabel('Height (m)')
53
+
54
+ xax = 0.07
55
+ ax4 = pl.axes([xax, 0.80, 0.25, 0.03], facecolor='lightgrey')
56
+ ax5 = pl.axes([xax, 0.75, 0.25, 0.03], facecolor='lightgrey')
57
+ ax6 = pl.axes([xax, 0.70, 0.25, 0.03], facecolor='lightgrey')
58
+ #ax7 = pl.axes([xax, 0.65, 0.25, 0.03], facecolor='lightgrey')
59
+ ax8 = pl.axes([xax, 0.65, 0.25, 0.03], facecolor='lightgrey')
60
+ ax9 = pl.axes([xax, 0.60, 0.25, 0.03], facecolor='lightgrey')
61
+ ax11 = pl.axes([xax, 0.55, 0.25, 0.03], facecolor='lightgrey')
62
+
63
+ s1 = Slider(ax=ax4, label='Speed (m/s)', valmin=15, valmax=35, valinit=speed)
64
+ s2 = Slider(ax=ax5, label='Roll (deg)', valmin=-110, valmax=110, valinit=roll)
65
+ s3 = Slider(ax=ax6, label='Pitch (deg)', valmin=-10, valmax=50, valinit=pitch)
66
+ s5 = Slider(ax=ax8, label='Nose (deg)', valmin=-5, valmax=5, valinit=nose)
67
+ s7 = Slider(ax=ax9, label='Mass (kg)', valmin=0.140, valmax=0.200, valinit=mass)
68
+ s6 = Slider(ax=ax11, label='Spin (-)', valmin=0, valmax=2, valinit=1.0)
69
+
70
+ def update(x):
71
+ speed = s1.val
72
+ roll = s2.val
73
+ pitch = s3.val
74
+ nose = s5.val
75
+ spin = s6.val
76
+ mass = s7.val
77
+ d = DiscGolfDisc(name,mass=mass)
78
+
79
+ omega = spin*d.empirical_spin(speed)
80
+ s = d.shoot(speed=speed, omega=omega, pitch=pitch, position=pos, nose_angle=nose, roll_angle=roll)
81
+ x,y,z = s.position
82
+
83
+ l1.set_xdata(x)
84
+ l1.set_ydata(y)
85
+ l2.set_xdata(x)
86
+ l2.set_ydata(z)
87
+ l3.set_xdata(y)
88
+ l3.set_ydata(z)
89
+
90
+ if adjust_axes:
91
+ ax1.axis((min(x),max(x),min(y),max(y)))
92
+ ax2.axis((min(x),max(x),min(z),max(z)))
93
+ ax3.axis((min(y),max(y),min(z),max(z)))
94
+
95
+ fig.canvas.draw_idle()
96
+
97
+ s1.on_changed(update)
98
+ s2.on_changed(update)
99
+ s3.on_changed(update)
100
+ s5.on_changed(update)
101
+ s6.on_changed(update)
102
+ s7.on_changed(update)
103
+
104
+ pl.show()
105
+
examples/driver.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Simple example comparing different projectiles.
4
+ """
5
+
6
+ from shotshaper.projectile import _Particle, ShotPutBall, SoccerBall
7
+ import matplotlib.pyplot as pl
8
+ import numpy as np
9
+
10
+ U = 10.0
11
+ angle = 20.0
12
+
13
+ p = _Particle()
14
+ shot = p.shoot(speed=U, pitch=angle)
15
+ pl.plot(shot.position[0,:],shot.position[2,:])
16
+
17
+ p = ShotPutBall('M')
18
+ shot = p.shoot(speed=U, pitch=angle)
19
+ pl.plot(shot.position[0,:],shot.position[2,:])
20
+
21
+ p = SoccerBall()
22
+
23
+ spin = np.array((0,0,0))
24
+ shot = p.shoot(speed=U, pitch=angle, spin=spin)
25
+ pl.plot(shot.position[0,:],shot.position[2,:])
26
+
27
+ spin = np.array((0,-10,0))
28
+ shot = p.shoot(speed=U, pitch=angle, spin=spin)
29
+ pl.plot(shot.position[0,:],shot.position[2,:])
30
+
31
+ pl.legend(('vacuum','air', 'no spin','spin'))
32
+
33
+ pl.show()
34
+
35
+
examples/validation_balls.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ This program calculates trajectories that were experimentally conducted in
4
+
5
+ Mencke, J. E., Salewski, M., Trinhammer, O. L., & Adler, A. T. (2020).
6
+ Flight and bounce of spinning sports balls. American Journal of Physics,
7
+ 88(11), 934-947.
8
+
9
+ Specifically, this includes data for:
10
+ - a shot put throw, representing a heavy projectile where the
11
+ gravitational forces dominate over the aerodynamic forces
12
+ - a soccer ball, with slightly more influence of air drag
13
+ - a table tennis ball, significantly impacted by air drag due
14
+ to low weight
15
+ """
16
+
17
+ from shotshaper.projectile import SoccerBall, ShotPutBall, TableTennisBall
18
+ import matplotlib.pyplot as pl
19
+ import numpy as np
20
+
21
+ cases = ('Shot','Soccer ball','Table tennis ball')
22
+ projectiles = (ShotPutBall('M'), SoccerBall(), TableTennisBall())
23
+ ux = (9.0, 11.0, 11.8)
24
+ uy = (7.1, 10.2, 12.0)
25
+ z0 = (2.4, 0.0, 1.0)
26
+ spin = (0,0,0)
27
+
28
+ f, ax = pl.subplots(1, 1, figsize=(6,4))
29
+ for i,c in enumerate(cases):
30
+ speed = np.sqrt(ux[i]**2 + uy[i]**2)
31
+ pitch = np.degrees(np.arctan2(uy[i],ux[i]))
32
+ position = (0, 0, z0[i])
33
+
34
+ s = projectiles[i].shoot(speed=speed,pitch=pitch,position=position,spin=spin)
35
+
36
+ x,y,z = s.position
37
+
38
+ ax.plot(x,z,'C0-')
39
+ ax.text(x[0]-0.5, z[0], c, fontsize=9, ha='right')
40
+
41
+ x,z = np.loadtxt(f'data/{c}_trajectory.dat', unpack=True, delimiter=';')
42
+ ax.plot(x,z,'C1--')
43
+
44
+ ax.legend(('Simulation','Experiment'))
45
+ ax.axis((-10,25,-1,6))
46
+
47
+ pl.xlabel('Length (m)')
48
+ pl.ylabel('Height (m)')
49
+
50
+ pl.show()
examples/validation_spinning_ball.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ This program calculates the trajectory of a spinning soccer ball
4
+ that was experimentally investigated in
5
+
6
+ Mencke, J. E., Salewski, M., Trinhammer, O. L., & Adler, A. T. (2020).
7
+ Flight and bounce of spinning sports balls. American Journal of Physics,
8
+ 88(11), 934-947.
9
+
10
+ """
11
+
12
+ from shotshaper.projectile import SoccerBall
13
+ import matplotlib.pyplot as pl
14
+ import numpy as np
15
+ from scipy.linalg import norm
16
+
17
+ ball = SoccerBall()
18
+ u = np.array([20.0, 2.5, 4.9])
19
+ # Note that spin is here negative along the z-axis, as opposed
20
+ # to positive around the y-axis, since we define z as upwards,
21
+ # while Mencke et al. define y as upwards
22
+ spin = (0,0,-46)
23
+ speed = norm(u)
24
+ pitch = np.degrees(np.arctan2(u[2],u[0]))
25
+ yaw = -np.degrees(np.arctan2(u[1],u[0]))
26
+
27
+ s = ball.shoot(speed=speed,pitch=pitch,yaw=yaw,spin=spin)
28
+
29
+ x,y,z = s.position
30
+
31
+ f, (ax1, ax2) = pl.subplots(2, 1, figsize=(5,7))
32
+
33
+ ax1.plot(x,z,'C0-')
34
+ ax2.plot(x,y,'C0-')
35
+ x,z = np.loadtxt('data/Spin_z_trajectory.dat', unpack=True, delimiter=';')
36
+ ax1.plot(x,z,'C1--')
37
+ x,y = np.loadtxt('data/Spin_y_trajectory.dat', unpack=True, delimiter=';')
38
+ ax2.plot(x,y,'C1--')
39
+
40
+ ax1.legend(('Simulation','Experiment'))
41
+ ax2.legend(('Simulation','Experiment'))
42
+
43
+ ax1.set_xlabel('Length (m)')
44
+ ax1.set_ylabel('Height (m)')
45
+ ax2.set_xlabel('Length (m)')
46
+ ax2.set_ylabel('Drift (m)')
47
+
48
+ pl.show()
notebooks/animation.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
notebooks/disc_plot.ipynb ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1006c45576f250234568583529a2f594a1d6fd5400aeb877b2dbee7dd770cde9
3
+ size 11383386
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ numpy>=1.18.1
2
+ scipy>=1.4.1
3
+ PyYAML==6.0
4
+ numpy_stl==3.0.1
5
+ plotly==5.13.1
setup.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from setuptools import setup, find_packages
2
+ setup(
3
+ name="shotshaper",
4
+ version="0.1.0",
5
+ packages=find_packages(),
6
+ include_package_data=True
7
+ )
shotshaper/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+
shotshaper/discs/.gitattributes ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ cd1.stl filter=lfs diff=lfs merge=lfs -text
2
+ cd5.stl filter=lfs diff=lfs merge=lfs -text
3
+ dd2.stl filter=lfs diff=lfs merge=lfs -text
4
+ fd2.stl filter=lfs diff=lfs merge=lfs -text
shotshaper/discs/cd1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:750a9ec26e520f1d662453952e44a20c2b02eeb4d7f52a1d702c7a5dff335ad0
3
+ size 3869884
shotshaper/discs/cd1.yaml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Overstable control driver, modelled after an Innova Firebird
2
+ # Data taken from CFD simulations
3
+
4
+ diameter: 0.211
5
+ J_xy: 3.759e-03
6
+ J_z: 7.485e-03
7
+ alpha: [-90,-80,-70,-60,-50,-40,-30,-20,-15,
8
+ -10,-8,-6,-4,-2,0,2,4,6,8,10,12,14,16,18,20,
9
+ 25,30,40,50,60,70,80,90]
10
+ Cd: [ 0.9641927, 1.040903, 1.014881, 0.8892227, 0.79079, 0.674083, 0.5343097, 0.272792, 0.1632757,
11
+
12
+ 0.0813, 0.0663, 0.0554, 0.0474, 0.0474, 0.0458, 0.057, 0.0668, 0.0846, 0.109, 0.137, 0.1729, 0.2182, 0.2672, 0.3268, 0.3924,
13
+
14
+ .5411498, 0.7411681, 1.111394, 1.115411, 1.080624, 1.186235, 1.023045, 1.029478 ]
15
+
16
+ Cl: [ 0.0001273178, -0.1869159, -0.3665411, -0.5029187, -0.6435312, -0.7736677, -0.8636631, -0.6659618, -0.4805856,
17
+
18
+ -0.29, -0.219, -0.1522, -0.0802, -0.0026, 0.0674, 0.1646, 0.2486, 0.3467, 0.4523, 0.5633, 0.6728, 0.7904, 0.8948, 1.0084, 1.0936,
19
+ 1.199855, 1.309407, 1.322162, 0.9143433, 0.6057466, 0.4181427, 0.1679042, -0.001581172 ]
20
+
21
+ Cm: [ 0.0003381886, -0.03136248, -0.04697123, -0.06378013, -0.07869955, -0.08915614, -0.100739, -0.07532892, -0.08049738,
22
+ -0.075, -0.0617, -0.0484, -0.0346, -0.0238, -0.0099, 0.002, 0.0112, 0.0211, 0.0323, 0.0461, 0.0601, 0.0749, 0.0929, 0.1087, 0.1159,
23
+
24
+ 0.1514894, 0.1528616, 0.135503, 0.09038871, 0.06759896, 0.07489785, 0.04788332, -0.002841088 ]
shotshaper/discs/cd5.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ffdbc36b511ef0a98ab99ab247c32353f0e15475d2ca2ef8fbf87f5d6e3c188d
3
+ size 6317884
shotshaper/discs/cd5.yaml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Understable control driver, modelled after an Innova Roadrunner
2
+ # Data taken from CFD simulations
3
+
4
+ diameter: 0.211
5
+ J_xy: 3.829e-03
6
+ J_z: 7.616e-03
7
+ alpha: [-90,-80,-70,-60,-50,-40,-30,-20,-15,
8
+ -10,-8,-6,-4,-2,0,2,4,6,8,10,12,14,16,18,20,
9
+ 25,30,40,50,60,70,80,90]
10
+ Cd: [ 0.9179461, 0.9709634, 0.9750627, 0.8941224, 0.7740944, 0.611287, 0.4877146, 0.2566395, 0.1423688,
11
+
12
+ 0.0773, 0.0606, 0.0514, 0.0468, 0.0439, 0.0528, 0.0631, 0.0763, 0.0968, 0.1171, 0.1417, 0.1776, 0.2136, 0.2587, 0.2986, 0.3612,
13
+ 0.5332736, 0.7454067, 1.100109, 1.231819, 1.080511, 1.06314, 1.025655, 1.000069 ]
14
+
15
+ Cl: [ -0.003535795, -0.1650353, -0.3380664, -0.4879364, -0.6047266, -0.6677451, -0.7633718, -0.6095486, -0.4310734,
16
+ #
17
+ -0.2642, -0.1891, -0.1239, -0.0469, 0.0208, 0.1081, 0.1917, 0.2859, 0.4015, 0.4846, 0.5882, 0.6957, 0.7988, 0.9093, 1.0012, 1.1181,
18
+ 1.246332, 1.367988, 1.333375, 1.038364, 0.6176756, 0.4386948, 0.1819935, 0.0001378738 ]
19
+
20
+ Cm: [ -0.004521051, -0.03329007, -0.05428122, -0.072994, -0.09090575, -0.09778813, -0.1002716, -0.08264331, -0.09134467,
21
+ -0.0783, -0.0634, -0.0539, -0.0436, -0.0304, -0.0229, -0.012, -0.0013, 0.005, 0.019, 0.0302, 0.042, 0.0559, 0.0692, 0.0901, 0.1032,
22
+
23
+ 0.1538899, 0.1695394, 0.1369221, 0.1079746, 0.06659547, 0.07765108, 0.04283614, -2.493789e-05 ]
shotshaper/discs/dd2.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0d9226b43dba147376cb204532fa9547c6729d61d086a51803a5769e56016d67
3
+ size 3673484
shotshaper/discs/dd2.yaml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diameter: 0.211
2
+ J_xy: 3.755e-03
3
+ J_z: 7.465e-03
4
+
5
+ alpha: [-90,-80,-70,-60,-50,-40,-30,-20,-15,-10,-8,-6,-4,-2,0,2,4,6,8,10,12,14,
6
+ 16,18,20,25,30,40,50,60,70,80,90]
7
+
8
+ Cd: [1.0156, 1.0413, 0.995, 0.8904, 0.7415, 0.6219, 0.4648, 0.2499, 0.1556,
9
+ 0.0769, 0.0617, 0.0509, 0.0432, 0.0429, 0.0417, 0.0578, 0.0664, 0.0825,
10
+ 0.1058, 0.1281, 0.1624, 0.2015, 0.2462, 0.2962, 0.3591, 0.561, 0.8141,
11
+ 1.2255, 1.2382, 1.0793, 1.1797, 1.199, 1.1867]
12
+
13
+ Cl: [0.0001, -0.2141, -0.4086, -0.5726, -0.6814, -0.7987, -0.8534, -0.694,
14
+ -0.4917, -0.2137, -0.1607, -0.0952, -0.0401, 0.0347, 0.1091, 0.2013,
15
+ 0.2761, 0.3666, 0.477, 0.5604, 0.6684, 0.7773, 0.888, 1.0013, 1.1157,
16
+ 1.67, 1.8447, 1.8161, 1.2563, 0.7495, 0.5236, 0.2646, 0.0033]
17
+
18
+ Cm: [-0.0007, -0.0326, -0.0544, -0.0712, -0.085, -0.0985, -0.0953, -0.0728,
19
+ -0.0742, -0.0966, -0.076, -0.0621, -0.0455, -0.0342, -0.0175, -0.0122,
20
+ 0.0013, 0.0128, 0.0217, 0.0364, 0.0486, 0.0618, 0.0758, 0.0925, 0.1081,
21
+ 0.1568, 0.1712, 0.1419, 0.0995, 0.0731, 0.0692, 0.0421, 0.0016]
shotshaper/discs/fd2.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3f4a12f3fc29c8ddf1661c92ddd750e49ce2fb5f056fe3508b919fb39f9172cc
3
+ size 4492684
shotshaper/discs/fd2.yaml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stable fairway driver, modelled after an Innova Teebird
2
+ # Data taken from CFD simulations (-90 to -15, and 25 to 90 copied from cd1)
3
+
4
+ diameter: 0.211
5
+ J_xy: 3.809e-03
6
+ J_z: 7.577e-03
7
+ alpha: [-90,-80,-70,-60,-50,-40,-30,-20,-15,
8
+ -10,-8,-6,-4,-2,0,2,4,6,8,10,12,14,16,18,20,
9
+ 25,30,40,50,60,70,80,90]
10
+ Cd: [ 0.9641927, 1.040903, 1.014881, 0.8892227, 0.79079, 0.674083, 0.5343097, 0.272792, 0.1632757,
11
+
12
+ #0.0913, 0.0644, 0.0543, 0.0502, 0.0458, 0.0436, 0.0594, 0.0671, 0.0875, 0.1111, 0.1415, 0.1749, 0.2152, 0.2613, 0.3132, 0.3829,
13
+ 0.08, 0.0674, 0.0555, 0.0496, 0.046, 0.0454, 0.0488, 0.0701, 0.0872, 0.1096, 0.1336, 0.1688, 0.212, 0.2604, 0.3172, 0.387,
14
+ .5411498, 0.7411681, 1.111394, 1.115411, 1.080624, 1.186235, 1.023045, 1.029478 ]
15
+
16
+ Cl: [ 0.0001273178, -0.1869159, -0.3665411, -0.5029187, -0.6435312, -0.7736677, -0.8636631, -0.6659618, -0.4805856,
17
+
18
+ #-0.2833, -0.1839, -0.1311, -0.0441, 0.0169, 0.0734, 0.1942, 0.2687, 0.3722, 0.478, 0.5899, 0.6983, 0.8132, 0.9264, 1.0354, 1.1698,
19
+ -0.2624, -0.1941, -0.1464, -0.0673, -0.0005, 0.0672, 0.1408, 0.2622, 0.3575, 0.4587, 0.5593, 0.673, 0.7957, 0.9149, 1.0373, 1.148,
20
+
21
+ 1.199855, 1.309407, 1.322162, 0.9143433, 0.6057466, 0.4181427, 0.1679042, -0.001581172 ]
22
+
23
+ Cm: [ 0.0003381886, -0.03136248, -0.04697123, -0.06378013, -0.07869955, -0.08915614, -0.100739, -0.07532892, -0.08049738,
24
+
25
+ # -0.0763, -0.0653, -0.0475, -0.0432, -0.027, -0.0128, -0.0112, 0.0016, 0.0095, 0.019, 0.0292, 0.042, 0.055, 0.0701, 0.0888, 0.1095,
26
+ -0.0694, -0.0567, -0.0429, -0.0361, -0.0233, -0.0112, 0.0006, 0.0026, 0.0125, 0.0224, 0.0344, 0.0452, 0.0566, 0.0703, 0.0886, 0.1093,
27
+ 0.1514894, 0.1528616, 0.135503, 0.09038871, 0.06759896, 0.07489785, 0.04788332, -0.002841088 ]
shotshaper/environment.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ """
4
+ import numpy as np
5
+
6
+ g = -9.81
7
+ # Air
8
+ rho = 1.225
9
+ mu = 1.81e-5
10
+
11
+ winddir = np.array((1,0,0))
12
+ z0 = 0.1
13
+ Uref = 0.0
14
+ zref = 1.5
15
+ kappa = 0.41
16
+
17
+ def wind_abl(z):
18
+ # For a constant wind:
19
+ # return Uref*winddir
20
+
21
+ if z < 0.0:
22
+ z = 0.0
23
+
24
+ ustar = Uref*kappa/(np.log((zref+z0)/z0))
25
+ u = ustar/kappa*np.log((z+z0)/z0)
26
+
27
+ return u*winddir
shotshaper/projectile.py ADDED
@@ -0,0 +1,607 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Tue Oct 19 18:29:10 2021
4
+
5
+ @author: 2913452
6
+
7
+ TODO:
8
+ - height of athlete, optimal angle shot put
9
+ - note that biomech can influence
10
+ how much force an athlete can emit for each angle
11
+ -
12
+ """
13
+
14
+ from abc import ABC, abstractmethod
15
+ from scipy.integrate import solve_ivp
16
+ from scipy.interpolate import interp1d
17
+ from .transforms import T_12, T_23, T_34, T_14, T_41, T_31
18
+ import matplotlib.pyplot as pl
19
+ from numpy import exp,matmul,pi,sqrt,arctan2,radians,degrees,sin,cos,array,concatenate,linspace,zeros_like,cross,zeros,argmin
20
+ from numpy.linalg import norm
21
+ from . import environment
22
+ import os
23
+ import yaml
24
+
25
+ T_END = 60
26
+ N_STEP = 200
27
+
28
+ def hit_ground(t, y, *args):
29
+ return y[2]
30
+
31
+ def stopped(t, y, *args):
32
+ U = norm(y[3:6])
33
+ return U - 1e-4
34
+
35
+ class Shot:
36
+ def __init__(self,t,x,v,att=None):
37
+ self.time = t
38
+ self.position = x
39
+ self.velocity = v
40
+ if att is not None:
41
+ self.attitude = att
42
+
43
+
44
+ class _Projectile(ABC):
45
+ def __init__(self):
46
+ pass
47
+
48
+ def _shoot(self, advance_function, y0, *args):
49
+ hit_ground.terminal = True
50
+ hit_ground.direction = -1
51
+ stopped.terminal = True
52
+ stopped.direction = -1
53
+
54
+ sol = solve_ivp(advance_function,[0,T_END],y0,
55
+ dense_output=True,args=args,
56
+ method='RK45',
57
+ events=(hit_ground,stopped))
58
+
59
+ t = linspace(0,sol.t[-1],N_STEP)
60
+
61
+ f = sol.sol(t)
62
+ pos = array([f[0],f[1],f[2]])
63
+ vel = array([f[3],f[4],f[5]])
64
+
65
+ if len(f) <= 6:
66
+ shot = Shot(t, pos, vel)
67
+ else:
68
+ att = array([f[6],f[7],f[8]])
69
+ shot = Shot(t, pos, vel, att)
70
+
71
+ return shot
72
+
73
+ @abstractmethod
74
+ def advance(self,t,vec,*args):
75
+ """
76
+ :param float T: Thrust
77
+ :param float Q: Torque
78
+ :param float P: Power
79
+ :return: Right hand side of kinematic equations for a projectile
80
+ :rtype: array
81
+ """
82
+
83
+ class _Particle(_Projectile):
84
+ def __init__(self):
85
+ super().__init__()
86
+
87
+ self.g = environment.g
88
+
89
+ def initialize_shot(self, **kwargs):
90
+ kwargs.setdefault('yaw', 0.0)
91
+
92
+ pitch = radians(kwargs["pitch"])
93
+ yaw = radians(kwargs["yaw"])
94
+ U = kwargs["speed"]
95
+ xy = cos(pitch)
96
+ u = U*xy*cos(yaw)
97
+ w = U*sin(pitch)
98
+ v = U*xy*sin(-yaw)
99
+ if "position" in kwargs:
100
+ x,y,z = kwargs["position"]
101
+ else:
102
+ x = 0.
103
+ y = 0.
104
+ z = 0.
105
+
106
+ y0 = array((x,y,z,u,v,w))
107
+ return y0
108
+
109
+ def shoot(self, **kwargs):
110
+
111
+ y0 = self.initialize_shot(**kwargs)
112
+ shot = self._shoot(self.advance, y0)
113
+
114
+ return shot
115
+
116
+ def gravity_force(self, x=None):
117
+ if x is None:
118
+ return array((0,0,environment.g))
119
+ else:
120
+ # Messy way to return g array...
121
+ f = zeros_like(x)
122
+ f[0,:] = 0
123
+ f[1,:] = 0
124
+ f[2,:] = environment.g
125
+ return f
126
+
127
+ def advance(self, t, vec, *args):
128
+ # x, y, z, u, v, w = vec
129
+ x = vec[0:3]
130
+ u = vec[3:6]
131
+
132
+ f = self.gravity_force()
133
+
134
+ return concatenate((u,f))
135
+
136
+
137
+ class _SphericalParticleAirResistance(_Particle):
138
+ def __init__(self, mass, diameter):
139
+ super().__init__()
140
+
141
+ self.mass = mass
142
+ self.diameter = diameter
143
+ self.radius = 0.5*diameter
144
+ self.area = 0.25*pi*diameter**2
145
+ self.volume = 4./3.*pi*self.radius**3
146
+
147
+
148
+ def air_resistance_force(self, U, Cd):
149
+
150
+ f = -0.5*environment.rho*self.area*Cd*norm(U)*U/self.mass
151
+ #f = -0.5*environment.rho*self.area*Cd*Umag*U/self.mass
152
+
153
+ return f
154
+
155
+ def advance(self, t, vec, *args):
156
+ x = vec[0:3]
157
+ u = vec[3:6]
158
+
159
+ Cd = self.drag_coefficient(norm(u))
160
+
161
+ f = self.air_resistance_force(u, Cd) \
162
+ + self.gravity_force()
163
+
164
+ return concatenate((u,f))
165
+
166
+
167
+ def reynolds_number(self, velocity):
168
+ """
169
+ Reynolds number, non-dimensional number giving the
170
+ ratio of inertial forces to viscous forces. Used
171
+ for calculating the drag coefficient.
172
+
173
+ :param float velocity: Velocity seen by particle
174
+ :return: Reynolds number
175
+ :rtype: float
176
+
177
+ """
178
+ return environment.rho*velocity*self.diameter/environment.mu
179
+
180
+ def drag_coefficient(self, velocity):
181
+ """
182
+ Drag coefficient for sphere, empirical curve fit
183
+ taken from:
184
+
185
+ F. A. Morrison, An Introduction to Fluid Mechanics, (Cambridge
186
+ University Press, New York, 2013). This correlation appears in
187
+ Figure 8.13 on page 625.
188
+
189
+ The full formula is:
190
+
191
+ .. math::
192
+ F = \\frac{2}{\\pi}\\cos^{-1}e^{-f} \\\\
193
+ f = \\frac{B}{2}\\frac{R-r}{r\\sin\\phi}
194
+
195
+
196
+ :param float velocity: Velocity seen by particle
197
+ :return: Drag coefficient
198
+ :rtype: float
199
+ """
200
+
201
+ Re = self.reynolds_number(velocity)
202
+
203
+ if Re <= 0:
204
+ return 1e30
205
+
206
+ tmp1 = Re/5.0
207
+ tmp2 = Re/2.63e5
208
+ tmp3 = Re/1e6
209
+
210
+ Cd = 24.0/Re \
211
+ + 2.6*tmp1/(1 + tmp1**1.52) \
212
+ + 0.411*tmp2**-7.94/(1 + tmp2**-8) \
213
+ + 0.25*tmp3/(1 + tmp3)
214
+
215
+ return Cd
216
+
217
+
218
+ class _SphericalParticleAirResistanceSpin(_SphericalParticleAirResistance):
219
+ def __init__(self, mass, diameter):
220
+ super().__init__(mass, diameter)
221
+
222
+
223
+ def lift_coefficient(self, Umag, omega):
224
+ # TODO - complex dependency on Re. For now,
225
+ # assume constant
226
+ return 0.9
227
+
228
+ def shoot(self, **kwargs):
229
+ y0 = self.initialize_shot(**kwargs)
230
+ spin = array((kwargs["spin"]))
231
+
232
+ shot = self._shoot(self.advance, y0, spin)
233
+
234
+ return shot
235
+
236
+ def spin_force(self,U,spin):
237
+
238
+ Umag = norm(U)
239
+ omega = norm(spin)
240
+
241
+ Cl = self.lift_coefficient(Umag, omega)
242
+
243
+ if U.ndim == 1:
244
+ f = Cl*pi*self.radius**3*environment.rho*cross(spin, U)/self.mass
245
+ else:
246
+ # Messy way to return spin array for post-processing
247
+ f = zeros_like(U)
248
+ for i in range(U.shape[1]):
249
+ f[:,i] = Cl*pi*self.radius**3*environment.rho*cross(spin, U[:,i])/self.mass
250
+
251
+ return f
252
+
253
+ def advance(self, t, vec, spin):
254
+ x = vec[0:3]
255
+ u = vec[3:6]
256
+
257
+ Cd = self.drag_coefficient(norm(u), norm(spin))
258
+
259
+ f = self.air_resistance_force(u, Cd) \
260
+ + self.gravity_force() \
261
+ + self.spin_force(u,spin)
262
+
263
+ return concatenate((u,f))
264
+
265
+
266
+ class ShotPutBall(_SphericalParticleAirResistance):
267
+ """
268
+ Note that diameter can vary 110 mm to 130mm
269
+ and 95 mm to 110 mm
270
+ """
271
+ def __init__(self, weight_class):
272
+
273
+ if weight_class == 'M':
274
+ mass = 7.26
275
+ diameter = 0.11
276
+ elif weight_class == 'F':
277
+ mass = 4.0
278
+ diameter = 0.095
279
+
280
+ super().__init__(mass, diameter)
281
+
282
+
283
+ class SoccerBall(_SphericalParticleAirResistanceSpin):
284
+ """
285
+ Note that diameter can vary 110 mm to 130mm
286
+ and 95 mm to 110 mm
287
+ """
288
+ def __init__(self, mass=0.430, diameter=0.22):
289
+
290
+ super(SoccerBall, self).__init__(mass, diameter)
291
+
292
+ def drag_coefficient(self, velocity, omega):
293
+ # Texture, sewing pattern and spin will alter
294
+ # the drag coefficient.
295
+ # Here, use correlation from
296
+
297
+ # Goff, J. E., & Carré, M. J. (2010). Soccer ball lift
298
+ # coefficients via trajectory analysis.
299
+ # European Journal of Physics, 31(4), 775.
300
+
301
+
302
+ vc = 12.19
303
+ vs = 1.309
304
+
305
+ S = omega*self.radius/velocity;
306
+ if S > 0.05 and velocity > vc:
307
+ Cd = 0.4127*S**0.3056;
308
+ else:
309
+ Cd = 0.155 + 0.346 / (1 + exp((velocity - vc)/vs))
310
+
311
+ return Cd
312
+
313
+ def lift_coefficient(self, Umag, omega):
314
+ # TODO - complex dependency on Re and spin, skin texture etc
315
+ return 0.9
316
+
317
+ class TableTennisBall(SoccerBall):
318
+ """
319
+
320
+ """
321
+ def __init__(self):
322
+
323
+ mass = 2.7e-3
324
+ diameter = 40e-3
325
+
326
+ super(TableTennisBall, self).__init__(mass, diameter)
327
+
328
+
329
+ class DiscGolfDisc(_Projectile):
330
+ def __init__(self, name, mass=0.175):
331
+ this_dir = os.path.dirname(os.path.abspath(__file__))
332
+ path = os.path.join(this_dir, 'discs', name + '.yaml')
333
+
334
+ self.name = name
335
+
336
+ with open(path, 'r') as f:
337
+ data = yaml.load(f, Loader=yaml.FullLoader)
338
+
339
+ self.diameter = data['diameter']
340
+ self.mass = mass
341
+ self.weight = environment.g*mass
342
+ self.area = pi*self.diameter**2/4.0
343
+ self.I_xy = mass*data['J_xy']
344
+ self.I_z = mass*data['J_z']
345
+
346
+ a = array(data['alpha'])
347
+ cl = array(data['Cl'])
348
+ cd = array(data['Cd'])
349
+ cm = array(data['Cm'])
350
+
351
+
352
+ self._alpha,self._Cl,self._Cd,self._Cm = self._flip(a,cl,cd,cm)
353
+ kind = 'linear'
354
+ self.Cl_func = interp1d(self._alpha, self._Cl, kind=kind)
355
+ self.Cd_func = interp1d(self._alpha, self._Cd, kind=kind)
356
+ self.Cm_func = interp1d(self._alpha, self._Cm, kind=kind)
357
+
358
+ def _flip(self,a,cl,cd,cm):
359
+ """
360
+ Data given from -90 deg to 90 deg.
361
+ Expand to -180 to 180 using symmetry considerations.
362
+ """
363
+ n = len(a)
364
+
365
+ idx = argmin(abs(a))
366
+ a2 = zeros(2*n)
367
+ cl2 = zeros(2*n)
368
+ cd2 = zeros(2*n)
369
+ cm2 = zeros(2*n)
370
+
371
+ a2[idx:idx+n] = a[:]
372
+ cl2[idx:idx+n] = cl[:]
373
+ cd2[idx:idx+n] = cd[:]
374
+ cm2[idx:idx+n] = cm[:]
375
+ for i in range(idx):
376
+ a2[i] = -(180 + a[idx-i])
377
+ cl2[i] = -cl[idx-i]
378
+ cd2[i] = cd[idx-i]
379
+ cm2[i] = -cm[idx-i]
380
+
381
+ for i in range(idx+n,2*n):
382
+ a2[i] = 180 - a[idx+n-i-2]
383
+ cl2[i] = -cl[idx+n-i-2]
384
+ cd2[i] = cd[idx+n-i-2]
385
+ cm2[i] = -cm[idx+n-i-2]
386
+
387
+ return a2,cl2,cd2,cm2
388
+
389
+ def _normalize_angle(self, alpha):
390
+ """
391
+ Ensure that the angle fulfils :math:`-\\pi < \\alpha < \\pi`
392
+
393
+ :param float alpha: Angle in radians
394
+ :return: Normalized angle
395
+ :rtype: float
396
+ """
397
+
398
+ return arctan2(sin(alpha), cos(alpha))
399
+
400
+ def Cd(self, alpha):
401
+ """
402
+ Provide drag coefficent for a given angle of attack.
403
+
404
+ :param float alpha: Angle in radians
405
+ :return: Drag coefficient
406
+ :rtype: float
407
+ """
408
+
409
+ # NB! The stored data uses degrees for the angle
410
+ return self.Cd_func(degrees(self._normalize_angle(alpha)))
411
+
412
+ def Cl(self, alpha):
413
+ """
414
+ Provide drag coefficent for a given angle of attack.
415
+
416
+ :param float alpha: Angle in radians
417
+ :return: Drag coefficient
418
+ :rtype: float
419
+ """
420
+
421
+ # NB! The stored data uses degrees for the angle
422
+ return self.Cl_func(degrees(self._normalize_angle(alpha)))
423
+
424
+ def Cm(self, alpha):
425
+ """
426
+ Provide coefficent of moment for a given angle of attack.
427
+
428
+ :param float alpha: Angle in radians
429
+ :return: Coefficient of moment
430
+ :rtype: float
431
+ """
432
+
433
+ # NB! The stored data uses degrees for the angle
434
+ return self.Cm_func(degrees(self._normalize_angle(alpha)))
435
+
436
+
437
+ def plot_coeffs(self, color='k'):
438
+ """
439
+ Utility function to quickly explore disc coefficients.
440
+
441
+ :param string color: Matplotlib color key. Default value is k, i.e. black.
442
+ """
443
+ pl.plot(self._alpha, self._Cl, 'C0-o',label='$C_L$')
444
+ pl.plot(self._alpha, self._Cd, 'C1-o',label='$C_D$')
445
+ pl.plot(self._alpha, 3*self._Cm, 'C2-o',label='$C_M$')
446
+
447
+ a = linspace(-pi,pi,200)
448
+ #pl.plot(degrees(a), self.Cl(a), 'C0-',label='$C_L$')
449
+ #pl.plot(degrees(a), self.Cd(a), 'C1-',label='$C_D$')
450
+ #pl.plot(degrees(a), 3*self.Cm(a), 'C2-',label='$C_M$')
451
+
452
+ pl.xlabel('Angle of attack ($^\circ$)')
453
+ pl.ylabel('Aerodynamic coefficients (-)')
454
+ pl.legend(loc='upper left')
455
+ ax = pl.gca()
456
+ ax2 = pl.gca().twinx()
457
+ ax2.set_ylabel("Aerodynamic efficiency, $C_L/C_D$")
458
+ pl.plot(self._alpha, self._Cl/self._Cd, 'C3-.',label='$C_L/C_D$')
459
+ ax2.legend(loc='upper right')
460
+
461
+ return ax,ax2
462
+
463
+
464
+ def empirical_spin(self, speed):
465
+ # Simple empirical formula for spin rate, based on curve-fitting
466
+ # data from:
467
+ # https://www.dgcoursereview.com/dgr/forums/viewtopic.php?f=2&t=7097
468
+ #omega = -0.257*speed**2 + 15.338*speed
469
+
470
+ # Alternatively, experiments indicate a linear relationship,
471
+ omega = 5.2*speed
472
+
473
+ return omega
474
+
475
+
476
+
477
+ def initialize_shot(self, **kwargs):
478
+ U = kwargs["speed"]
479
+
480
+ kwargs.setdefault('yaw', 0.0)
481
+ #kwargs.setdefault('omega', self.empirical_spin(U))
482
+
483
+ pitch = radians(kwargs["pitch"])
484
+ yaw = radians(kwargs["yaw"])
485
+ omega = kwargs["omega"]
486
+
487
+ # phi, theta
488
+ roll_angle = radians(kwargs["roll_angle"]) # phi
489
+ nose_angle = radians(kwargs["nose_angle"]) # theta
490
+ # psi, rotation around z irrelevant for starting position
491
+ # since the disc is symmetric
492
+
493
+ # Initialize position
494
+ if "position" in kwargs:
495
+ x,y,z = kwargs["position"]
496
+ else:
497
+ x = 0.
498
+ y = 0.
499
+ z = 0.
500
+
501
+ # Initialize velocity
502
+ xy = cos(pitch)
503
+ u = U*xy*cos(yaw)
504
+ v = U*xy*sin(-yaw)
505
+ w = U*sin(pitch)
506
+
507
+ # Initialize angles
508
+ attitude = array([roll_angle, nose_angle, 0])
509
+ # The initial orientation of the disc must also account for the
510
+ # angle of the throw itself, i.e. the launch angle.
511
+ attitude += matmul(T_12(attitude), array((0, pitch, 0)))
512
+
513
+ #attitude = matmul(T_23(yaw),attitude)
514
+ #attitude += matmul(T_12(attitude), array((0, pitch, 0)))
515
+ phi, theta, psi = attitude
516
+ y0 = array((x,y,z,u,v,w,phi,theta,psi))
517
+ return y0, omega
518
+
519
+ def shoot(self, **kwargs):
520
+
521
+ y0, omega = self.initialize_shot(**kwargs)
522
+
523
+ shot = self._shoot(self.advance, y0, omega)
524
+
525
+ return shot
526
+
527
+ def post_process(self, s, omega):
528
+ n = len(s.time)
529
+ alphas = zeros(n)
530
+ betas = zeros(n)
531
+ lifts = zeros(n)
532
+ drags = zeros(n)
533
+ moms = zeros(n)
534
+ rolls = zeros(n)
535
+ for i in range(n):
536
+ x = s.position[:,i]
537
+ u = s.velocity[:,i]
538
+ a = s.attitude[:,i]
539
+
540
+ alpha, beta, Fd, Fl, M, g4 = self.forces(x, u, a, omega)
541
+
542
+ alphas[i] = alpha
543
+ betas[i] = beta
544
+ lifts[i] = Fl
545
+ drags[i] = Fd
546
+ moms[i] = M
547
+ rolls[i] = -M/(omega*(self.I_xy - self.I_z))
548
+
549
+ arc_length = norm(s.position, axis=0)
550
+ return arc_length,degrees(alphas),degrees(betas),lifts,drags,moms,degrees(rolls)
551
+
552
+ def forces(self, x, u, a, omega):
553
+ # Velocity in body axes
554
+ urel = u - environment.wind_abl(x[2])
555
+ u2 = matmul(T_12(a), urel)
556
+ # Side slip angle is the angle between the x and y velocity
557
+ beta = -arctan2(u2[1], u2[0])
558
+ # Velocity in zero side slip axes
559
+ u3 = matmul(T_23(beta), u2)
560
+ # Angle of attack is the angle between
561
+ # vertical and horizontal velocity
562
+ alpha = -arctan2(u3[2], u3[0])
563
+ # Velocity in wind system, where forces are to be calculated
564
+ u4 = matmul(T_34(alpha), u3)
565
+
566
+ # Convert gravitational force from Earth to Wind axes
567
+ g = array((0, 0, self.mass*environment.g))
568
+ g4 = T_14(g, a, beta, alpha)
569
+
570
+ # Aerodynamic forces
571
+ q = 0.5*environment.rho*u4[0]**2
572
+ S = self.area
573
+ D = self.diameter
574
+
575
+ Fd = q*S*self.Cd(alpha)
576
+ Fl = q*S*self.Cl(alpha)
577
+ M = q*S*D*self.Cm(alpha)
578
+
579
+ return alpha, beta, Fd, Fl, M, g4
580
+
581
+ def advance(self, t, vec, omega):
582
+ x = vec[0:3]
583
+ u = vec[3:6]
584
+ a = vec[6:9]
585
+
586
+ alpha, beta, Fd, Fl, M, g4 = self.forces(x, u, a, omega)
587
+
588
+ m = self.mass
589
+ # Calculate accelerations
590
+ dudt = (-Fd + g4[0])/m
591
+ dvdt = g4[1]/m
592
+ dwdt = ( Fl + g4[2])/m
593
+ acc4 = array((dudt,dvdt,dwdt))
594
+ # Roll rate acts around x-axis (in axes 3: zero side slip axes)
595
+ dphidt = -M/(omega*(self.I_xy - self.I_z))
596
+ # Other angular rotations are ignored, assume zero wobble
597
+ angvel3 = array((dphidt, 0, 0))
598
+
599
+ acc1 = T_41(acc4, a, beta, alpha)
600
+ angvel1 = T_31(angvel3, a, beta)
601
+
602
+ return concatenate((u,acc1,angvel1))
603
+
604
+
605
+
606
+
607
+
shotshaper/transforms.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+
4
+ The axes are denoted:
5
+ 1: Earth
6
+ 2: Body
7
+ 3: Zero side slip - rotation around z, to apply roll
8
+ 4: Wind axes - to replicate the CFD condition with wind coming straight onto the body
9
+
10
+ In Crowther & Potts, z-axis is defined pointing downwards. Here, it points upwards
11
+ leading to some different signs.
12
+ """
13
+ import numpy as np
14
+ from numpy import cos,sin,matmul
15
+
16
+ def T_12(attitude):
17
+ """
18
+ Transform from Earth axes to Body axes
19
+ """
20
+ phi, theta, psi = attitude
21
+
22
+ return np.array([[cos(theta)*cos(psi), sin(phi)*sin(theta)*cos(psi) - cos(phi)*sin(psi), cos(phi)*sin(theta)*cos(psi) + sin(phi)*sin(psi)],
23
+ [cos(theta)*sin(psi), sin(phi)*sin(theta)*sin(psi) + cos(phi)*cos(psi), cos(phi)*sin(theta)*sin(psi) - sin(phi)*cos(psi)],
24
+ [-sin(theta), sin(phi)*cos(theta), cos(phi)*cos(theta) ]])
25
+
26
+ def T_23(beta):
27
+ """
28
+ Transform from Body axes to Zero side slip axes,
29
+ Rotation around z-axis by the side-slip angle
30
+ """
31
+ return np.array([[cos(beta), -sin(beta), 0],
32
+ [sin(beta), cos(beta), 0],
33
+ [0, 0, 1]])
34
+
35
+
36
+ def T_34(alpha):
37
+ """
38
+ Transform from Zero side slip axes to Wind axes
39
+ Rotation around y-axis by the angle of attack.
40
+ """
41
+ return np.array([[cos(alpha), 0, -sin(alpha)],
42
+ [0, 1, 0 ],
43
+ [sin(alpha), 0, cos(alpha)]])
44
+
45
+ def T_14(vec, attitude, beta, alpha):
46
+ return matmul(T_34(alpha), matmul(T_23(beta), matmul(T_12(attitude), vec)))
47
+
48
+ def T_21(attitude):
49
+ """
50
+ Transform from Body axes to Earth axes.
51
+ Done by transposing the opposite
52
+ """
53
+ return np.transpose(T_12(attitude))
54
+
55
+ def T_32(beta):
56
+ return np.transpose(T_23(beta))
57
+
58
+ def T_43(alpha):
59
+ return np.transpose(T_34(alpha))
60
+
61
+ def T_41(vec, attitude, beta, alpha):
62
+ return matmul(T_21(attitude), matmul(T_32(beta), matmul(T_43(alpha), vec)))
63
+
64
+ def T_31(vec, attitude, beta):
65
+ return matmul(T_21(attitude), matmul(T_32(beta), vec))
66
+
67
+
68
+
utils/disc_geometric_properties.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Calculate moments of inertia for a disc from STL file.
4
+ """
5
+
6
+ import numpy as np
7
+ import trimesh
8
+ import sys
9
+ import os
10
+
11
+ path = os.path.dirname(os.path.realpath(__file__))
12
+ # attach to logger so trimesh messages will be printed to console
13
+ #trimesh.util.attach_to_log()
14
+
15
+ name = sys.argv[-1]
16
+
17
+ m = trimesh.load(os.path.join(path, 'discs', name + '.stl'))
18
+ trimesh.repair.fix_inversion(m)
19
+ trimesh.repair.fix_normals(m)
20
+ trimesh.repair.fix_winding(m)
21
+
22
+ if m.is_watertight and m.is_winding_consistent and m.is_volume:
23
+ V = m.volume
24
+ J = m.principal_inertia_components/V
25
+ print('Volume: ', V)
26
+ print('J_xy: %4.3e' % J[0])
27
+ print('J_z: %4.3e' % J[2])
28
+