Spaces:
Sleeping
Sleeping
Commit
•
aa651cf
1
Parent(s):
3fcff6f
Init commit
Browse files- .gitignore +133 -0
- app/Disc Golf Simulator.py +117 -0
- app/extrema.py +32 -0
- app/get_disc.py +101 -0
- app/visualize.py +272 -0
- bootstrap.py +12 -0
- examples/disc_experimental_trajectory_comparison.py +106 -0
- examples/disc_golf_throw.py +66 -0
- examples/disc_gui2d.py +105 -0
- examples/driver.py +35 -0
- examples/validation_balls.py +50 -0
- examples/validation_spinning_ball.py +48 -0
- notebooks/animation.ipynb +0 -0
- notebooks/disc_plot.ipynb +3 -0
- requirements.txt +5 -0
- setup.py +7 -0
- shotshaper/__init__.py +3 -0
- shotshaper/discs/.gitattributes +4 -0
- shotshaper/discs/cd1.stl +3 -0
- shotshaper/discs/cd1.yaml +24 -0
- shotshaper/discs/cd5.stl +3 -0
- shotshaper/discs/cd5.yaml +23 -0
- shotshaper/discs/dd2.stl +3 -0
- shotshaper/discs/dd2.yaml +21 -0
- shotshaper/discs/fd2.stl +3 -0
- shotshaper/discs/fd2.yaml +27 -0
- shotshaper/environment.py +27 -0
- shotshaper/projectile.py +607 -0
- shotshaper/transforms.py +68 -0
- utils/disc_geometric_properties.py +28 -0
.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 |
+
|