derek-thomas
		
	commited on
		
		
					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 | 
            +
             | 
