├── .checkignore ├── .gitignore ├── .travis.yml ├── COPYING ├── README.rst ├── data ├── cessna172.inp └── datcom.out ├── docs ├── Makefile ├── make.bat └── source │ ├── api.rst │ ├── conf.py │ ├── index.rst │ └── logo │ ├── logo.svg │ ├── logo_24.png │ ├── logo_300.png │ └── logo_48.png ├── environment.yml ├── examples └── How it works.ipynb ├── readthedocs.yml ├── setup.py └── src └── pyfme ├── __init__.py ├── aero └── tests │ └── test_aero.py ├── aircrafts ├── __init__.py ├── aircraft.py ├── ball.py ├── cessna_172.py ├── cessna_310.py └── tests │ ├── test_ball.py │ ├── test_cessna_172.py │ └── test_cessna_310.py ├── environment ├── __init__.py ├── atmosphere.py ├── environment.py ├── gravity.py ├── tests │ └── test_isa.py └── wind.py ├── models ├── __init__.py ├── constants.py ├── dynamic_system.py ├── euler_flat_earth.py ├── state │ ├── __init__.py │ ├── acceleration.py │ ├── aircraft_state.py │ ├── angular_acceleration.py │ ├── angular_velocity.py │ ├── attitude.py │ ├── position.py │ └── velocity.py └── tests │ └── test_euler_flat_earth.py ├── prop └── tests │ └── test_prop.py ├── simulator.py ├── tests └── test_pyfme.py └── utils ├── __init__.py ├── altimetry.py ├── anemometry.py ├── change_euler_quaternion.py ├── coordinates.py ├── input_generator.py ├── tests ├── test_altimetry.py ├── test_anemometry.py ├── test_anemometry_stagnationpressure.py ├── test_anemometry_tascaseas.py ├── test_coordinates.py ├── test_euler_quaternion.py ├── test_inputs_generator.py └── test_trimmer.py └── trimmer.py /.checkignore: -------------------------------------------------------------------------------- 1 | # Ignore tests 2 | **/tests/* 3 | **/test_*.py 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .cache 3 | .idea 4 | build 5 | 6 | *.py[cod~] 7 | 8 | *.egg-info 9 | 10 | .spyder* 11 | 12 | examples/.ipynb_checkpoints 13 | examples/examples-notebook/.ipynb_checkpoints/ 14 | 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.6" 5 | 6 | branches: 7 | only: 8 | - master 9 | - 0.1.x 10 | 11 | before_install: 12 | # http://conda.pydata.org/docs/travis.html 13 | - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh 14 | - bash miniconda.sh -b -p $HOME/miniconda 15 | - export PATH="$HOME/miniconda/bin:$PATH" 16 | - hash -r 17 | - conda config --set always_yes yes --set changeps1 no 18 | - conda update -q conda 19 | # Useful for debugging any issues with conda 20 | - conda info -a 21 | - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION 22 | - source activate test-environment 23 | - conda install numpy scipy matplotlib pandas tqdm 24 | - pip install coverage pytest pytest-cov codecov 25 | 26 | install: 27 | - pip install --no-deps . # Test installation correctness 28 | 29 | script: 30 | - py.test -vv # Test against installed code 31 | 32 | after_success: 33 | # Uninstall to test coverage against sources 34 | - pip uninstall pyfme -y 35 | - pip install --no-deps -e . 36 | - py.test --cov 37 | - codecov 38 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2015 AeroPython Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | PyFME 2 | ===== 3 | 4 | .. image:: http://unmaintained.tech/badge.svg 5 | :target: http://unmaintained.tech 6 | :alt: No Maintenance Intended 7 | 8 | *This project is not maintaned anymore by its original authors. 9 | If you want to maintain it, feel free to fork this repository and continue pushing there. 10 | If there is one fork that is sufficiently maintained, we might consider 11 | transferring this project to a different GitHub organization and giving write access to others. 12 | Also, take a look at* `FlightMechanics.jl `_ 13 | *for a new alternative implementation in Julia.* 14 | 15 | :Name: PyFME 16 | :Description: Python Flight Mechanics Engine 17 | :Website: https://github.com/AeroPython/PyFME 18 | :Author: AeroPython Team 19 | :Version: 0.2.dev0 20 | 21 | .. |travisci| image:: https://img.shields.io/travis/AeroPython/PyFME/master.svg?style=flat-square 22 | :target: https://travis-ci.org/AeroPython/PyFME 23 | 24 | .. |codecov| image:: https://img.shields.io/codecov/c/github/AeroPython/PyFME.svg?style=flat-square 25 | :target: https://codecov.io/gh/AeroPython/PyFME?branch=master 26 | 27 | .. |docs| image:: https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat-square 28 | :target: http://pyfme.readthedocs.io/en/latest/?badge=latest 29 | 30 | .. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square 31 | :target: https://github.com/AeroPython/PyFME/raw/master/COPYING 32 | 33 | .. |mailing| image:: https://img.shields.io/badge/mailing%20list-groups.io-8cbcd1.svg?style=flat-square 34 | :target: https://groups.io/g/aeropython 35 | 36 | .. |mybinder| image:: https://mybinder.org/badge.svg 37 | :target: https://mybinder.org/v2/gh/AeroPython/PyFME/master?urlpath=notebooks%2Fexamples%2FHow%2520it%2520works.ipynb 38 | 39 | |travisci| |codecov| |mybinder| |docs| |license| |mailing| 40 | 41 | .. image:: http://pyfme.readthedocs.io/en/latest/_images/logo_300.png 42 | :target: https://github.com/AeroPython/PyFME 43 | :alt: PyFME 44 | :width: 300px 45 | :align: center 46 | 47 | **If you want to know how PyFME works, how to collaborate or get our contact information, 48 | please visit our** `wiki`_ 49 | 50 | .. _`wiki`: https://github.com/AeroPython/PyFME/wiki 51 | 52 | Example Notebook 53 | ---------------- 54 | 55 | See how it works: visit the our example Notebook!: 56 | https://mybinder.org/v2/gh/AeroPython/PyFME/master?urlpath=notebooks%2Fexamples%2FHow%2520it%2520works.ipynb 57 | 58 | How to install 59 | -------------- 60 | 61 | PyFME is not yet in PyPI, so you can install directly from the source code:: 62 | 63 | $ pip install https://github.com/AeroPython/PyFME/archive/0.1.x.zip 64 | 65 | If you have git installed, you can also try:: 66 | 67 | $ pip install git+https://github.com/AeroPython/PyFME.git 68 | 69 | If you get any installation or compilation errors, make sure you have the latest pip and setuptools:: 70 | 71 | $ pip install --upgrade pip setuptools 72 | 73 | How to run the tests 74 | -------------------- 75 | 76 | Install in editable mode and call `py.test`:: 77 | 78 | $ pip install -e . 79 | $ py.test 80 | -------------------------------------------------------------------------------- /data/cessna172.inp: -------------------------------------------------------------------------------- 1 | * 2 | * Cessna 172 3 | * Input file for Digital Datcom 4 | * 5 | * PyFME @ Aeropython 6 | * 7 | CASEID Cessna172 : Fuselage + Wing + Aileron 8 | DIM M 9 | DERIV RAD 10 | * Flight condition 11 | $FLTCON LOOP=3.0, 12 | NMACH=1.0, MACH(1)=0.16, 13 | NALT=1.0, ALT(1)=1500.0, 14 | NALPHA=12.0, ALSCHD(1)=-7.5,-5.0,-2.5,0.0,2.5,5.,7.5,10.,15.,17.,18.,19.5$ 15 | * Location of CG, wing, vertical and horizontal tails 16 | $SYNTHS 17 | XCG=2.22, ZCG=0.99, 18 | XW=1.84, ZW=2.07, ALIW=1.5, 19 | XH=5.95, ZH=1.16, ALIH=0.0, 20 | XV=6.18, ZV=1.29, 21 | SCALE=1.0, VERTUP=.TRUE.$ 22 | * Body geometry 23 | $BODY NX=20.0, ITYPE=2.0, METHOD=2.0, 24 | X(1)=0.10,0.26,0.55,0.67,1.16,1.53,1.75,1.88,2.59,3.09,3.52, 25 | 3.95,4.39,4.85,5.41,5.87,6.33,6.79,7.22,7.57, 26 | S(1)=0.71,1.9,2.02,2.13,2.26,2.6,2.93,3.21,3.26,2.99, 27 | 2.45,2.17,1.85,1.63,1.39,1.13,0.93,0.65,0.45,0.06, 28 | P(1)=5.36,5.72,5.63,5.71,6.24,6.63,6.95,6.98,6.77,6.09, 29 | 6.09,5.5,5.36,5.21,5.05,4.97,4.93,4.89,4.87, 30 | R(1)=0.17,0.49,0.55,0.58,0.64,0.67,0.71,0.74,0.75,0.69,0.63,0.57, 31 | 0.52,0.44,0.38,0.31,0.25,0.17,0.12,0.01, 32 | ZU(1)=1.47,1.60,1.62,1.63,1.63,1.89,2.04,2.15,2.12,2.04,1.76, 33 | 1.57,1.51,1.50,1.46,1.42,1.38,1.35,1.32,1.29, 34 | ZL(1)=1.14,0.88,0.72,0.70,0.61,0.59,0.59,0.62,0.66,0.71,0.75,0.78, 35 | 0.83,0.85,0.88,0.92,0.97,0.01,1.09,1.14$ 36 | NACA-W-4-2412 37 | * Wing airfoil and planform 38 | $WGPLNF 39 | CHRDR=1.626, CHRDBP=1.626, CHRDTP=1.13, 40 | SSPN=5.49, SSPNE=4.75, SSPNOP=2.941, 41 | SAVSI=0.0, SAVSO=3.0, CHSTAT=0.0, 42 | TWISTA=-3.0, DHDADI=1.7333, DHDADO=1.7333, TYPE=1.0$ 43 | * Aileron geometry 44 | $ASYFLP 45 | NDELTA=9.0,STYPE=4.0, 46 | DELTAL(1)=-15.,-10.,-5.,-2.5,0.,5.,10.,15.,20., 47 | DELTAR(1)=20.,15.,10.,5.,0.,-2.5,-5.,-10.,-15., 48 | CHRDFI=0.312, CHRDFO=0.312, 49 | SPANFI=2.44, SPANFO=5.15$ 50 | SAVE 51 | NEXT CASE 52 | CASEID Cessna172 : Vertical and Horizontal Stabilizer + Elevator 53 | DAMP 54 | NACA-V-4-0009 55 | * Vertical tail planform 56 | $VTPLNF 57 | CHRDR=1.39, CHRDTP=0.65, 58 | SSPN=1.36, SSPNE=1.30, 59 | SAVSI=35.0, CHSTAT=0.25, TYPE=1.0$ 60 | NACA-H-4-0012 61 | * Horizontal tail planform 62 | $HTPLNF 63 | CHRDR=1.39, CHRDTP=0.77, 64 | SSPN=1.73, SSPNE=1.50, 65 | SAVSI=9.0, CHSTAT=0.0, TWISTA=0.0, 66 | DHDADI=0.0, TYPE=1.0$ 67 | * Elevator geometry 68 | $SYMFLP FTYPE=1.0, 69 | SPANFI=0.23, SPANFO=1.727, 70 | CHRDFI=0.56, CHRDFO=0.27, 71 | NTYPE=1.0, NDELTA=9.0, 72 | DELTA(1)=-26.0,-20.0,-10.0,-5.0,0.0,7.5,15.0,22.5,28.0$ 73 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PyFME.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PyFME.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PyFME" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PyFME" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PyFME.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PyFME.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. automodule:: pyfme.simulator 5 | :members: 6 | 7 | `pyfme.environment` package 8 | --------------------------- 9 | 10 | .. automodule:: pyfme.environment 11 | 12 | .. autoclass:: pyfme.environment.environment.Environment 13 | :members: 14 | 15 | .. autoclass:: pyfme.environment.atmosphere.Atmosphere 16 | :members: 17 | 18 | .. autoclass:: pyfme.environment.atmosphere.ISA1976 19 | :members: 20 | 21 | .. automodule:: pyfme.environment.gravity 22 | :members: 23 | 24 | .. automodule:: pyfme.environment.wind 25 | :members: 26 | 27 | 28 | `pyfme.models` package 29 | ---------------------- 30 | 31 | .. automodule:: pyfme.models 32 | 33 | .. automodule:: pyfme.models.euler_flat_earth 34 | :members: 35 | 36 | .. automodule:: pyfme.models.systems 37 | :members: 38 | 39 | `pyfme.utils` package 40 | --------------------- 41 | 42 | .. automodule:: pyfme.utils 43 | 44 | .. automodule:: pyfme.utils.coordinates 45 | :members: 46 | 47 | .. automodule:: pyfme.utils.anemometry 48 | :members: 49 | 50 | .. automodule:: pyfme.utils.input_generator 51 | :members: 52 | 53 | .. automodule:: pyfme.utils.trimmer 54 | :members: 55 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # PyFME documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Jan 16 17:07:20 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import shlex 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | #sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.mathjax', 36 | 'sphinx.ext.napoleon', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix(es) of source filenames. 43 | # You can specify multiple suffix as a list of string: 44 | # source_suffix = ['.rst', '.md'] 45 | source_suffix = '.rst' 46 | 47 | # The encoding of source files. 48 | #source_encoding = 'utf-8-sig' 49 | 50 | # The master toctree document. 51 | master_doc = 'index' 52 | 53 | # General information about the project. 54 | project = 'PyFME' 55 | copyright = '2016, AeroPython Team' 56 | author = 'AeroPython Team' 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = '0.2' 64 | # The full version, including alpha/beta/rc tags. 65 | release = '0.2.dev0' 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = None 73 | 74 | # There are two options for replacing |today|: either, you set today to some 75 | # non-false value, then it is used: 76 | #today = '' 77 | # Else, today_fmt is used as the format for a strftime call. 78 | #today_fmt = '%B %d, %Y' 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | exclude_patterns = [] 83 | 84 | # The reST default role (used for this markup: `text`) to use for all 85 | # documents. 86 | #default_role = None 87 | 88 | # If true, '()' will be appended to :func: etc. cross-reference text. 89 | #add_function_parentheses = True 90 | 91 | # If true, the current module name will be prepended to all description 92 | # unit titles (such as .. function::). 93 | #add_module_names = True 94 | 95 | # If true, sectionauthor and moduleauthor directives will be shown in the 96 | # output. They are ignored by default. 97 | #show_authors = False 98 | 99 | # The name of the Pygments (syntax highlighting) style to use. 100 | pygments_style = 'sphinx' 101 | 102 | # A list of ignored prefixes for module index sorting. 103 | #modindex_common_prefix = [] 104 | 105 | # If true, keep warnings as "system message" paragraphs in the built documents. 106 | #keep_warnings = False 107 | 108 | # If true, `todo` and `todoList` produce output, else they produce nothing. 109 | todo_include_todos = False 110 | 111 | 112 | # -- Options for HTML output ---------------------------------------------- 113 | 114 | # The theme to use for HTML and HTML Help pages. See the documentation for 115 | # a list of builtin themes. 116 | import sphinx_rtd_theme 117 | html_theme = "sphinx_rtd_theme" 118 | 119 | # Theme options are theme-specific and customize the look and feel of a theme 120 | # further. For a list of options available for each theme, see the 121 | # documentation. 122 | #html_theme_options = {} 123 | 124 | # Add any paths that contain custom themes here, relative to this directory. 125 | #html_theme_path = [] 126 | 127 | # The name for this set of Sphinx documents. If None, it defaults to 128 | # " v documentation". 129 | #html_title = None 130 | 131 | # A shorter title for the navigation bar. Default is the same as html_title. 132 | #html_short_title = None 133 | 134 | # The name of an image file (relative to this directory) to place at the top 135 | # of the sidebar. 136 | #html_logo = None 137 | 138 | # The name of an image file (within the static path) to use as favicon of the 139 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 140 | # pixels large. 141 | #html_favicon = None 142 | 143 | # Add any paths that contain custom static files (such as style sheets) here, 144 | # relative to this directory. They are copied after the builtin static files, 145 | # so a file named "default.css" will overwrite the builtin "default.css". 146 | html_static_path = ['_static'] 147 | 148 | # Add any extra paths that contain custom files (such as robots.txt or 149 | # .htaccess) here, relative to this directory. These files are copied 150 | # directly to the root of the documentation. 151 | #html_extra_path = [] 152 | 153 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 154 | # using the given strftime format. 155 | #html_last_updated_fmt = '%b %d, %Y' 156 | 157 | # If true, SmartyPants will be used to convert quotes and dashes to 158 | # typographically correct entities. 159 | #html_use_smartypants = True 160 | 161 | # Custom sidebar templates, maps document names to template names. 162 | #html_sidebars = {} 163 | 164 | # Additional templates that should be rendered to pages, maps page names to 165 | # template names. 166 | #html_additional_pages = {} 167 | 168 | # If false, no module index is generated. 169 | #html_domain_indices = True 170 | 171 | # If false, no index is generated. 172 | #html_use_index = True 173 | 174 | # If true, the index is split into individual pages for each letter. 175 | #html_split_index = False 176 | 177 | # If true, links to the reST sources are added to the pages. 178 | #html_show_sourcelink = True 179 | 180 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 181 | #html_show_sphinx = True 182 | 183 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 184 | #html_show_copyright = True 185 | 186 | # If true, an OpenSearch description file will be output, and all pages will 187 | # contain a tag referring to it. The value of this option must be the 188 | # base URL from which the finished HTML is served. 189 | #html_use_opensearch = '' 190 | 191 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 192 | #html_file_suffix = None 193 | 194 | # Language to be used for generating the HTML full-text search index. 195 | # Sphinx supports the following languages: 196 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 197 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 198 | #html_search_language = 'en' 199 | 200 | # A dictionary with options for the search language support, empty by default. 201 | # Now only 'ja' uses this config value 202 | #html_search_options = {'type': 'default'} 203 | 204 | # The name of a javascript file (relative to the configuration directory) that 205 | # implements a search results scorer. If empty, the default will be used. 206 | #html_search_scorer = 'scorer.js' 207 | 208 | # Output file base name for HTML help builder. 209 | htmlhelp_basename = 'PyFMEdoc' 210 | 211 | # -- Options for LaTeX output --------------------------------------------- 212 | 213 | latex_elements = { 214 | # The paper size ('letterpaper' or 'a4paper'). 215 | #'papersize': 'letterpaper', 216 | 217 | # The font size ('10pt', '11pt' or '12pt'). 218 | #'pointsize': '10pt', 219 | 220 | # Additional stuff for the LaTeX preamble. 221 | #'preamble': '', 222 | 223 | # Latex figure (float) alignment 224 | #'figure_align': 'htbp', 225 | } 226 | 227 | # Grouping the document tree into LaTeX files. List of tuples 228 | # (source start file, target name, title, 229 | # author, documentclass [howto, manual, or own class]). 230 | latex_documents = [ 231 | (master_doc, 'PyFME.tex', 'PyFME Documentation', 232 | 'AeroPython Team', 'manual'), 233 | ] 234 | 235 | # The name of an image file (relative to this directory) to place at the top of 236 | # the title page. 237 | #latex_logo = None 238 | 239 | # For "manual" documents, if this is true, then toplevel headings are parts, 240 | # not chapters. 241 | #latex_use_parts = False 242 | 243 | # If true, show page references after internal links. 244 | #latex_show_pagerefs = False 245 | 246 | # If true, show URL addresses after external links. 247 | #latex_show_urls = False 248 | 249 | # Documents to append as an appendix to all manuals. 250 | #latex_appendices = [] 251 | 252 | # If false, no module index is generated. 253 | #latex_domain_indices = True 254 | 255 | 256 | # -- Options for manual page output --------------------------------------- 257 | 258 | # One entry per manual page. List of tuples 259 | # (source start file, name, description, authors, manual section). 260 | man_pages = [ 261 | (master_doc, 'pyfme', 'PyFME Documentation', 262 | [author], 1) 263 | ] 264 | 265 | # If true, show URL addresses after external links. 266 | #man_show_urls = False 267 | 268 | 269 | # -- Options for Texinfo output ------------------------------------------- 270 | 271 | # Grouping the document tree into Texinfo files. List of tuples 272 | # (source start file, target name, title, author, 273 | # dir menu entry, description, category) 274 | texinfo_documents = [ 275 | (master_doc, 'PyFME', 'PyFME Documentation', 276 | author, 'PyFME', 'One line description of project.', 277 | 'Miscellaneous'), 278 | ] 279 | 280 | # Documents to append as an appendix to all manuals. 281 | #texinfo_appendices = [] 282 | 283 | # If false, no module index is generated. 284 | #texinfo_domain_indices = True 285 | 286 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 287 | #texinfo_show_urls = 'footnote' 288 | 289 | # If true, do not generate a @detailmenu in the "Top" node's menu. 290 | #texinfo_no_detailmenu = False 291 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to PyFME's documentation! 2 | ================================= 3 | 4 | Python Flight Mechanics Engine. 5 | 6 | .. image:: logo/logo_300.png 7 | :width: 300px 8 | :align: center 9 | 10 | The main idea behind a Flight Dynamics Engine is to model the physics involved in aircraft's flight. Models for the 11 | aircraft, atmosphere, dynamics, aerodynamics... implemented here allow us to calculate the aircraft response to the 12 | pilot's inputs. 13 | 14 | Contents 15 | -------- 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | 20 | api 21 | 22 | 23 | Indices and tables 24 | ================== 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | 30 | -------------------------------------------------------------------------------- /docs/source/logo/logo_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AeroPython/PyFME/156fa9f1db097f107c20ad7354c71b1eaee4cbb1/docs/source/logo/logo_24.png -------------------------------------------------------------------------------- /docs/source/logo/logo_300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AeroPython/PyFME/156fa9f1db097f107c20ad7354c71b1eaee4cbb1/docs/source/logo/logo_300.png -------------------------------------------------------------------------------- /docs/source/logo/logo_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AeroPython/PyFME/156fa9f1db097f107c20ad7354c71b1eaee4cbb1/docs/source/logo/logo_48.png -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: pyfme 2 | dependencies: 3 | - numpy >=1.7 4 | - scipy >=0.17.0 5 | - matplotlib >= 1.5 6 | - pytest >= 2.9 7 | - pandas >= 0.20 8 | - tqdm 9 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | python: 2 | setup_py_install: true 3 | conda: 4 | file: environment.yml 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # http://stackoverflow.com/a/10975371/554319 5 | import io 6 | from setuptools import setup, find_packages 7 | 8 | 9 | # http://blog.ionelmc.ro/2014/05/25/python-packaging/ 10 | setup( 11 | name="PyFME", 12 | version="0.2.dev0", 13 | description="Python Flight Mechanics Engine", 14 | author="AeroPython Team", 15 | author_email="aeropython@groups.io", 16 | url="http://pyfme.readthedocs.io/en/latest/", 17 | download_url="https://github.com/AeroPython/PyFME/", 18 | license="MIT", 19 | keywords=[ 20 | "aero", "aerospace", "engineering", 21 | "flight mechanics", "standard atmosphere", "simulation", 22 | ], 23 | python_requires=">=3.5", 24 | install_requires=[ 25 | "numpy", 26 | "scipy", 27 | "matplotlib", 28 | "pandas" 29 | ], 30 | tests_require=[ 31 | "pytest" 32 | ], 33 | packages=find_packages('src'), 34 | package_dir={'': 'src'}, 35 | classifiers=[ 36 | "Development Status :: 2 - Pre-Alpha", 37 | "Intended Audience :: Education", 38 | "Intended Audience :: Science/Research", 39 | "License :: OSI Approved :: MIT License", 40 | "Operating System :: OS Independent", 41 | "Programming Language :: Python", 42 | "Programming Language :: Python :: 3", 43 | "Programming Language :: Python :: 3.5", 44 | "Programming Language :: Python :: Implementation :: CPython", 45 | "Topic :: Scientific/Engineering", 46 | "Topic :: Scientific/Engineering :: Physics", 47 | ], 48 | long_description=io.open('README.rst', encoding='utf-8').read(), 49 | include_package_data=True, 50 | zip_safe=False, 51 | ) 52 | -------------------------------------------------------------------------------- /src/pyfme/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Flight Mechanics Engine (PyFME). 3 | Copyright (c) AeroPython Development Team. 4 | Distributed under the terms of the MIT License. 5 | """ 6 | -------------------------------------------------------------------------------- /src/pyfme/aero/tests/test_aero.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AeroPython/PyFME/156fa9f1db097f107c20ad7354c71b1eaee4cbb1/src/pyfme/aero/tests/test_aero.py -------------------------------------------------------------------------------- /src/pyfme/aircrafts/__init__.py: -------------------------------------------------------------------------------- 1 | from .cessna_310 import Cessna310 2 | from .cessna_172 import Cessna172 3 | -------------------------------------------------------------------------------- /src/pyfme/aircrafts/aircraft.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Flight Mechanics Engine (PyFME). 3 | Copyright (c) AeroPython Development Team. 4 | Distributed under the terms of the MIT License. 5 | 6 | Generic Aircraft 7 | ---------------- 8 | 9 | """ 10 | from abc import abstractmethod 11 | 12 | import numpy as np 13 | 14 | from pyfme.utils.anemometry import tas2cas, tas2eas, calculate_alpha_beta_TAS 15 | 16 | 17 | class Aircraft(object): 18 | 19 | def __init__(self): 20 | # Mass & Inertia 21 | self.mass = 0 # kg 22 | self.inertia = np.zeros((3, 3)) # kg·m² 23 | 24 | # Geometry 25 | self.Sw = 0 # m2 26 | self.chord = 0 # m 27 | self.span = 0 # m 28 | 29 | # Controls 30 | self.controls = {} 31 | self.control_limits = {} 32 | 33 | # Coefficients 34 | # Aero 35 | self.CL, self.CD, self.Cm = 0, 0, 0 36 | self.CY, self.Cl, self.Cn = 0, 0, 0 37 | 38 | # Thrust 39 | self.Ct = 0 40 | 41 | # Forces & moments 42 | self.total_forces = np.zeros(3) 43 | self.total_moments = np.zeros(3) 44 | 45 | # Velocities 46 | self.TAS = 0 # True Air Speed. 47 | self.CAS = 0 # Calibrated Air Speed. 48 | self.EAS = 0 # Equivalent Air Speed. 49 | self.Mach = 0 # Mach number 50 | self.q_inf = 0 # Dynamic pressure at infty (Pa) 51 | 52 | # Angles 53 | self.alpha = 0 # Angle of attack (AOA). 54 | self.beta = 0 # Angle of sideslip (AOS). 55 | self.alpha_dot = 0 # Rate of change of AOA. 56 | 57 | @property 58 | def Ixx(self): 59 | return self.inertia[0, 0] 60 | 61 | @property 62 | def Iyy(self): 63 | return self.inertia[1, 1] 64 | 65 | @property 66 | def Izz(self): 67 | return self.inertia[2, 2] 68 | 69 | @property 70 | def Fx(self): 71 | return self.total_forces[0] 72 | 73 | @property 74 | def Fy(self): 75 | return self.total_forces[1] 76 | 77 | @property 78 | def Fz(self): 79 | return self.total_forces[2] 80 | 81 | @property 82 | def Mx(self): 83 | return self.total_moments[0] 84 | 85 | @property 86 | def My(self): 87 | return self.total_moments[1] 88 | 89 | @property 90 | def Mz(self): 91 | return self.total_moments[2] 92 | 93 | def _set_current_controls(self, controls): 94 | 95 | # If a control is not given, the previous value is assigned. 96 | for control_name, control_value in controls.items(): 97 | limits = self.control_limits[control_name] 98 | if limits[0] <= control_value <= limits[1]: 99 | self.controls[control_name] = control_value 100 | else: 101 | # TODO: maybe raise a warning and assign max deflection 102 | msg = ( 103 | f"Control {control_name} out of range ({control_value} " 104 | f"when min={limits[0]} and max={limits[1]})" 105 | ) 106 | raise ValueError(msg) 107 | 108 | def _calculate_aerodynamics(self, state, environment): 109 | 110 | # Velocity relative to air: aerodynamic velocity. 111 | aero_vel = state.velocity.vel_body - environment.body_wind 112 | 113 | self.alpha, self.beta, self.TAS = calculate_alpha_beta_TAS( 114 | u=aero_vel[0], v=aero_vel[1], w=aero_vel[2] 115 | ) 116 | 117 | # Setting velocities & dynamic pressure 118 | self.CAS = tas2cas(self.TAS, environment.p, environment.rho) 119 | self.EAS = tas2eas(self.TAS, environment.rho) 120 | self.Mach = self.TAS / environment.a 121 | self.q_inf = 0.5 * environment.rho * self.TAS ** 2 122 | 123 | def _calculate_aerodynamics_2(self, TAS, alpha, beta, environment): 124 | 125 | self.alpha, self.beta, self.TAS = alpha, beta, TAS 126 | 127 | # Setting velocities & dynamic pressure 128 | self.CAS = tas2cas(self.TAS, environment.p, environment.rho) 129 | self.EAS = tas2eas(self.TAS, environment.rho) 130 | self.Mach = self.TAS / environment.a 131 | self.q_inf = 0.5 * environment.rho * self.TAS ** 2 132 | 133 | @abstractmethod 134 | def calculate_forces_and_moments(self, state, environment, controls): 135 | 136 | self._set_current_controls(controls) 137 | self._calculate_aerodynamics(state, environment) 138 | -------------------------------------------------------------------------------- /src/pyfme/aircrafts/ball.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Ball 8 | ---- 9 | 10 | Created on Fri Jan 8 18:53:17 2016 11 | 12 | @author: JuanMatSa 13 | """ 14 | 15 | import numpy as np 16 | from pyfme.utils.coordinates import wind2body 17 | 18 | # Constants 19 | mu = 1.983e-5 # kg/m/s 20 | # Data tables 21 | Re_list = np.array([38000, 100000, 160000, 200000, 250000, 300000, 330000, 22 | 350000, 375000, 400000, 500000, 800000, 200000, 4000000]) 23 | CD_full_list = np.array([0.49, 0.50, 0.51, 0.51, 0.49, 0.46, 0.39, 0.20, 0.09, 24 | 0.07, 0.07, 0.10, 0.15, 0.18]) 25 | Sn_list = np.array([0.00, 0.04, 0.10, 0.20, 0.40]) 26 | C_magnus_list = np.array([0.00, 0.10, 0.16, 0.23, 0.33]) 27 | 28 | 29 | def geometric_data(r=0.111): 30 | 31 | """Provides the value of some geometric data. 32 | 33 | Parameters 34 | ---------- 35 | r : float 36 | radius(m) 37 | 38 | Returns 39 | ------- 40 | r : float 41 | radius(m) 42 | S_circle : float 43 | Surface (m2) 44 | S_sphere : float 45 | Surface (m2) 46 | Vol : float 47 | Volume (m3) 48 | """ 49 | 50 | S_circle = np.pi * r ** 2 51 | S_sphere = 4 * np.pi * r ** 2 52 | Vol = 4 / 3. * np.pi * r ** 3 53 | 54 | return r, S_circle, S_sphere, Vol 55 | 56 | 57 | def mass_and_inertial_data(r, mass=0.440): 58 | 59 | """Provides the value of some mass and inertial data. 60 | 61 | Parameters 62 | ---------- 63 | r : float 64 | radius (m) 65 | mass : float 66 | mass (kg) 67 | 68 | Returns 69 | ------- 70 | inertia : float array 71 | diagonal array (3x3) wich elements are Ixx_b, Iyy_b, Izz_b: 72 | Ixx_b : float 73 | Moment of Inertia x-axis (Kg * m2) 74 | Iyy_b : float 75 | Moment of Inertia y-axis (Kg * m2) 76 | Izz_b : float 77 | Moment of Inertia z-axis (Kg * m2) 78 | """ 79 | 80 | Ixx_b = 2 * mass * (r ** 2) / 3. 81 | Iyy_b = Ixx_b 82 | Izz_b = Ixx_b 83 | 84 | inertia = np.diag([Ixx_b, Iyy_b, Izz_b]) 85 | 86 | return inertia 87 | 88 | 89 | def check_reynolds_number(Re): 90 | """Reynolds number must be between 38e3 and 4e6 91 | Parameters 92 | ---------- 93 | Re : float 94 | Reynolds number 95 | 96 | Raises 97 | ------ 98 | ValueError 99 | If the value of the Reynolds number is outside the defined layers. 100 | """ 101 | if not (Re_list[0] <= Re <= Re_list[-1]): 102 | raise ValueError('Reynolds number is not inside correct range') 103 | 104 | 105 | def check_sn(Sn): 106 | """Effective spin number must be between 0.00 and 0.40 107 | Parameters 108 | ---------- 109 | Sn : float 110 | Effective spin number 111 | 112 | See Also 113 | -------- 114 | get_magnus_effect_forces function 115 | 116 | Raises 117 | ------ 118 | ValueError 119 | If the value of the effective spin number is outside the defined 120 | layers. 121 | 122 | """ 123 | if not (Sn_list[0] <= Sn <= Sn_list[-1]): 124 | raise ValueError('Effective spin number is not inside correct range') 125 | 126 | 127 | def get_aerodynamic_forces(lin_vel, ang_vel, TAS, rho, alpha, beta, 128 | magnus_effect=True): 129 | """Given the velocity vectors vel and ang_vel (body axes) it provides the 130 | forces (aerodynamic drag and Magnus effect if it exists) (body axes). 131 | Data for a soccer ball (smooth sphere). 132 | 133 | Parameters 134 | ---------- 135 | lin_vel : float array 136 | [u, v, w] air linear velocity body-axes (m/s). 137 | ang_vel : float array 138 | [p, q, r] air angular velocity body-axes (m/s). 139 | TAS : float 140 | true air speed (m/s) 141 | rho : float 142 | air density (kg/m3). 143 | alpha : float 144 | angle of attack (rad). 145 | beta : float 146 | sideslip angle (rad). 147 | magnus_effect : boolean 148 | True if magnus effect is under consideration. 149 | 150 | Returns 151 | ------- 152 | Total_aerodynamic_forces_body : float array 153 | Drag (+ magnus effect if exists) Forces vector (1x3) (body axes) (N). 154 | 155 | See Also 156 | -------- 157 | [2] 158 | 159 | Notes 160 | ----- 161 | Smooth ball selected. see[1] 162 | 163 | Reynolds vs CD_full table: 164 | 165 | +------------+------------+ 166 | | Re |CD_full_list| 167 | +============+============+ 168 | | 3.8e4 | 0.49 | 169 | +------------+------------+ 170 | |1.e5 | 0.51 | 171 | +------------+------------+ 172 | |100000 | 0.50 | 173 | +------------+------------+ 174 | |160000 | 0.51 | 175 | +------------+------------+ 176 | |200000 | 0.51 | 177 | +------------+------------+ 178 | |250000 | 0.49 | 179 | +------------+------------+ 180 | |300000 | 0.46 | 181 | +------------+------------+ 182 | |330000 | 0.39 | 183 | +------------+------------+ 184 | |350000 | 0.20 | 185 | +------------+------------+ 186 | |375000 | 0.09 | 187 | +------------+------------+ 188 | |400000 | 0.07 | 189 | +------------+------------+ 190 | |500000 | 0.07 | 191 | +------------+------------+ 192 | |800000 | 0.10 | 193 | +------------+------------+ 194 | |2000000 | 0.15 | 195 | +------------+------------+ 196 | |4000000 | 0.18 | 197 | +------------+------------+ 198 | 199 | Re : float 200 | Re = rho * TAS * radius / mu # Reynolds number. values between 38e3 201 | and 4e6. 202 | 203 | References 204 | ---------- 205 | .. [1] "Aerodynamics of Sports Balls" Bruce D. Jothmann, January 2007 206 | .. [2] "Aerodynamics of Sports Balls" Annual Review of Fluid Mechanics, 207 | 1875.17:15 Mehta, R.D. 208 | """ 209 | u, v, w = lin_vel # components of the linear velocity 210 | p, q, r = ang_vel # components of the angular velocity 211 | 212 | radius, A_front, _, _ = geometric_data() 213 | Re = rho * TAS * radius / mu # Reynolds number 214 | check_reynolds_number(Re) 215 | 216 | # %Obtaining of Drag coefficient and Drag force 217 | CD_full = np.interp(Re, Re_list, CD_full_list) 218 | 219 | D = 0.5 * rho * TAS ** 2 * A_front * CD_full 220 | D_vector_body = wind2body(([-D, 0, 0]), alpha, beta) 221 | # %It adds or not the magnus effect, depending on the variable 222 | # % magnus_effect 223 | if magnus_effect: 224 | F_magnus_vector_body = get_magnus_effect_forces(lin_vel, ang_vel, TAS, 225 | rho, radius, A_front, 226 | alpha, beta) 227 | 228 | total_aerodynamic_forces_body = D_vector_body + F_magnus_vector_body 229 | return total_aerodynamic_forces_body 230 | else: 231 | total_aerodynamic_forces_body = D_vector_body 232 | return total_aerodynamic_forces_body 233 | 234 | 235 | def get_magnus_effect_forces(lin_vel, ang_vel, TAS, rho, radius, A_front, 236 | alpha, beta): 237 | """Given the velocity vectors vel and ang_vel (body axes) it provides the 238 | forces (aerodynamic drag and Magnus effect if it exists) (body axes). 239 | Data for a soccer ball (smooth sphere). 240 | 241 | Parameters 242 | ---------- 243 | lin_vel : float array 244 | [u, v, w] air velocity body-axes (m/s). 245 | ang_vel : float array 246 | [p, q, r] air velocity body-axes (m/s). 247 | TAS : float 248 | true air speed (m/s) 249 | rho : float 250 | Air density (ISA) (kg/m3) 251 | radius : float 252 | sphere radius (m) 253 | A_front : float 254 | Area of a frontal section of the sphere (m) 255 | alpha : float 256 | angle of attack (rad). 257 | beta : float 258 | sideslip angle (rad). 259 | 260 | Returns 261 | ------- 262 | F_magnus : float 263 | magnus force (N). 264 | F_magnus_vector_body : float array 265 | magnus Forces vector (1x3) (body axes) (N). 266 | 267 | Notes 268 | ----- 269 | Smooth ball selected. see [1] 270 | 271 | Sn vs C_magnus table: [1] 272 | 273 | +------------+------------+ 274 | | Sn | C_magnus | 275 | +============+============+ 276 | | 0 | 0 | 277 | +------------+------------+ 278 | | 0.04 | 0.1 | 279 | +------------+------------+ 280 | | 0.10 | 0.16 | 281 | +------------+------------+ 282 | | 0.20 | 0.23 | 283 | +------------+------------+ 284 | | 0.40 | 0.33 | 285 | +------------+------------+ 286 | 287 | Sn : float 288 | Sn = wn * radius / V # Sn is the effective spin number and must take 289 | values between 0 and 0.4 [1] 290 | wn : float array 291 | normal projection of the angular velocity vector over the linear 292 | velocity vector [1] 293 | 294 | References 295 | ---------- 296 | .. [1] "Aerodynamics of Sports Balls" Bruce D. Jothmann, January 2007 297 | .. [2] "Aerodynamics of Sports Balls" Annual Review of Fluid Mechanics, 298 | 1875.17:15 Mehta, R.D. 299 | """ 300 | u, v, w = lin_vel # components of the linear velocity 301 | p, q, r = ang_vel # components of the angular velocity 302 | 303 | # %Obtaining of Magnus force coefficient and Magnus force 304 | wn = np.sqrt((v * r - w * q) ** 2 + (w * p - u * r) ** 2 + 305 | (u * q - v * p) ** 2) / np.sqrt(u ** 2 + v ** 2 + w ** 2) 306 | # normal projection of the angular velocity vector over the linear velocity 307 | # vector 308 | 309 | Sn = wn * radius / TAS # Sn is the effective sping number and must take 310 | # values between 0 and 0.4 [1] 311 | check_sn(Sn) 312 | 313 | C_magnus = np.interp(Sn, Sn_list, C_magnus_list) 314 | 315 | check_magnus = np.isclose((v * r - w * q, w * p - u * r, u * q - v * p), 316 | (0, 0, 0)) 317 | if check_magnus.all(): 318 | F_magnus = 0 319 | F_magnus_vector_body = np.array([0, 0, 0]) 320 | else: 321 | F_magnus = 0.5 * rho * TAS ** 2 * A_front * C_magnus 322 | dir_F_magnus = - np.array([v * r - w * q, w * p - u * r, 323 | u * q - v * p]) / np.sqrt( 324 | (v * r - w * q) ** 2 + (w * p - u * r) ** 2 + 325 | (u * q - v * p) ** 2) 326 | 327 | F_magnus_vector_body = dir_F_magnus * F_magnus 328 | 329 | return F_magnus_vector_body 330 | -------------------------------------------------------------------------------- /src/pyfme/aircrafts/cessna_310.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Cessna 310 8 | ---------- 9 | 10 | CY_b is the side force stability derivative with respect to the 11 | angle of sideslip 12 | CY_da is the side force stability derivative with respect to the 13 | aileron deflection 14 | CY_dr is the side force stability derivative with respect to the 15 | rudder deflection 16 | 17 | Cl_b is the rolling moment stability derivative with respect to 18 | angle of sideslip 19 | Cl_da is the rolling moment stability derivative with respect to 20 | the aileron deflection 21 | Cl_dr is the rolling moment stability derivative with respect to 22 | the rudder deflection 23 | 24 | Cn_b is the yawing moment stability derivative with respect to the 25 | angle of sideslip 26 | Cn_da is the yawing moment stability derivative with respect to the 27 | aileron deflection 28 | Cn_dr is the yawing moment stability derivative with respect to the 29 | rudder deflection 30 | 31 | CL_0 is the lift coefficient evaluated at the initial condition 32 | CL_a is the lift stability derivative with respect to the angle of attack 33 | CL_de is the lift stability derivative with respect to the elevator 34 | deflection 35 | CL_dih is the lift stability derivative with respect to the stabilator 36 | deflection 37 | CD_0 is the drag coefficient evaluated at the initial condition 38 | CD_a is the drag stability derivative with respect to the angle of attack 39 | Cm_0 is the pitching moment coefficient evaluated at the condition 40 | (alpha0 = deltaE = deltaih = 0º) 41 | Cm_a is the pitching moment stability derivative with respect to the angle 42 | of attack 43 | Cm_de is the pitching moment stability derivative with respect to the 44 | elevator deflection 45 | Cm_dih is the pitching moment stability derivative with respect to the 46 | stabilator deflection 47 | 48 | """ 49 | import numpy as np 50 | 51 | from pyfme.aircrafts.aircraft import Aircraft 52 | from pyfme.models.constants import ft2m, slugft2_2_kgm2, lbs2kg 53 | 54 | 55 | class Cessna310(Aircraft): 56 | """ 57 | Cessna 310 58 | 59 | The Cessna 310 is an American six-seat, low-wing, twin-engined monoplane 60 | that was produced by Cessna between 1954 and 1980. It was the first 61 | twin-engined aircraft that Cessna put into production after World War II. 62 | 63 | References 64 | ---------- 65 | AIRCRAFT DYNAMICS From modelling to simulation (Marcello R. Napolitano) 66 | page 589 67 | """ 68 | 69 | def __init__(self): 70 | 71 | # Mass & Inertia 72 | self.mass = 4600 * lbs2kg # kg 73 | self.inertia = np.diag([8884, 1939, 11001]) * slugft2_2_kgm2 # kg·m² 74 | # Ixz_b = 0 * slugft2_2_kgm2 # Kg * m2 75 | 76 | # Geometry 77 | self.Sw = 175 * ft2m**2 # m2 78 | self.chord = 4.79 * ft2m # m 79 | self.span = 36.9 * ft2m # m 80 | 81 | # Aerodynamic Data (Linearized) 82 | # |CL| |CL_0 CL_a CL_de CL_dih| | 1 | 83 | # |CD| = |CD_0 CD_a 0 0 | | alpha | 84 | # |Cm| |Cm_0 Cm_a Cm_de Cm_dih| |delta_e | 85 | # |delta_ih| 86 | # 87 | # |CY| |CY_b CY_da CY_dr| |beta | 88 | # |Cl| = |Cl_b Cl_da Cl_dr| |delta_a| 89 | # |Cn| |Cn_b Cn_da Cn_dr| |delta_r| 90 | self._long_coef_matrix = np.array([[0.288, 4.58, 0.81, 0], 91 | [0.029, 0.160, 0, 0], 92 | [ 0.07, -0.137, -2.26, 0]]) 93 | 94 | self._lat_coef_matrix = np.array([[-0.698 , 0, 0.230], 95 | [-0.1096, 0.172, 0.0192], 96 | [ 0.1444, -0.0168, -0.1152]]) 97 | self._long_inputs = np.zeros(4) 98 | self._long_inputs[0] = 1.0 99 | 100 | self._lat_inputs = np.zeros(3) 101 | 102 | # CONTROLS 103 | self.controls = {'delta_elevator': 0, 104 | 'hor_tail_incidence': 0, 105 | 'delta_aileron': 0, 106 | 'delta_rudder': 0, 107 | 'delta_t': 0} 108 | 109 | # FIXME: these limits are not real 110 | self.control_limits = {'delta_elevator': (-1, 1), # rad 111 | 'hor_tail_incidence': (-1, 1), # rad 112 | 'delta_aileron': (-1, 1), # rad 113 | 'delta_rudder': (-1, 1), # rad 114 | 'delta_t': (0, 1)} # non dimensional 115 | 116 | # Coefficients 117 | # Aero 118 | self.CL, self.CD, self.Cm = 0, 0, 0 119 | self.CY, self.Cl, self.Cn = 0, 0, 0 120 | # Thrust 121 | self.Ct = 0 122 | 123 | self.gravity_force = np.zeros(3) 124 | self.total_forces = np.zeros(3) 125 | self.total_moments = np.zeros(3) 126 | self.load_factor = 0 127 | 128 | # Velocities 129 | self.TAS = 0 # True Air Speed. 130 | self.CAS = 0 # Calibrated Air Speed. 131 | self.EAS = 0 # Equivalent Air Speed. 132 | self.Mach = 0 # Mach number 133 | 134 | self.q_inf = 0 # Dynamic pressure at infty (Pa) 135 | 136 | # Angles 137 | self.alpha = 0 # Angle of attack (AOA). 138 | self.beta = 0 # Angle of sideslip (AOS). 139 | 140 | @property 141 | def delta_elevator(self): 142 | return self.controls['delta_elevator'] 143 | 144 | @property 145 | def delta_rudder(self): 146 | return self.controls['delta_rudder'] 147 | 148 | @property 149 | def delta_aileron(self): 150 | return self.controls['delta_aileron'] 151 | 152 | @property 153 | def delta_t(self): 154 | return self.controls['delta_t'] 155 | 156 | def _calculate_aero_lon_forces_moments_coeffs(self): 157 | 158 | self._long_inputs[1] = self.alpha 159 | self._long_inputs[2] = self.controls['delta_elevator'] 160 | self._long_inputs[3] = self.controls['hor_tail_incidence'] 161 | 162 | self.CL, self.CD, self.Cm = self._long_coef_matrix @ self._long_inputs 163 | 164 | def _calculate_aero_lat_forces_moments_coeffs(self): 165 | self._lat_inputs[0] = self.beta 166 | self._lat_inputs[1] = self.controls['delta_aileron'] 167 | self._lat_inputs[2] = self.controls['delta_rudder'] 168 | 169 | self.CY, self.Cl, self.Cn = self._lat_coef_matrix @ self._lat_inputs 170 | 171 | def _calculate_aero_forces_moments(self): 172 | q = self.q_inf 173 | Sw = self.Sw 174 | c = self.chord 175 | b = self.span 176 | self._calculate_aero_lon_forces_moments_coeffs() 177 | self._calculate_aero_lat_forces_moments_coeffs() 178 | L = q * Sw * self.CL 179 | D = q * Sw * self.CD 180 | Y = q * Sw * self.CY 181 | l = q * Sw * b * self.Cl 182 | m = q * Sw * c * self.Cm 183 | n = q * Sw * b * self.Cn 184 | return L, D, Y, l, m , n 185 | 186 | def _calculate_thrust_forces_moments(self): 187 | q = self.q_inf 188 | Sw = self.Sw 189 | self.Ct = 0.031 * self.controls['delta_t'] 190 | Ft = np.array([q * Sw * self.Ct, 0, 0]) 191 | return Ft 192 | 193 | def calculate_forces_and_moments(self, state, environment, controls): 194 | 195 | # Update controls and aerodynamics 196 | super().calculate_forces_and_moments(state, environment, controls) 197 | 198 | Ft = self._calculate_thrust_forces_moments() 199 | L, D, Y, l, m, n = self._calculate_aero_forces_moments() 200 | 201 | Fg = environment.gravity_vector * self.mass 202 | # FIXME: is it necessary to use wind2body conversion? 203 | Fa = np.array([-D, Y, -L]) 204 | 205 | self.total_forces = 10 * Ft + Fg + Fa 206 | self.total_moments = np.array([l, m, n]) 207 | return self.total_forces, self.total_moments 208 | -------------------------------------------------------------------------------- /src/pyfme/aircrafts/tests/test_ball.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Frames of Reference orientation test functions 8 | ---------------------------------------------- 9 | """ 10 | import pytest 11 | 12 | import numpy as np 13 | from numpy.testing import (assert_array_almost_equal, assert_almost_equal) 14 | 15 | 16 | from pyfme.aircrafts.ball import (geometric_data, mass_and_inertial_data, 17 | get_aerodynamic_forces, 18 | check_reynolds_number, check_sn, 19 | get_magnus_effect_forces) 20 | 21 | 22 | def test_geometric_data(): 23 | 24 | # Test with default r=0.111 25 | r_expected = 0.111 26 | S_circle_expected = 0.03870756308 27 | S_sphere_expected = 0.1548302523 28 | Vol_expected = 0.005728719337 29 | 30 | r, S_circle, S_sphere, Vol = geometric_data(0.111) 31 | 32 | assert_almost_equal(r, r_expected) 33 | assert_almost_equal(S_circle, S_circle_expected) 34 | assert_almost_equal(S_sphere, S_sphere_expected) 35 | assert_almost_equal(Vol, Vol_expected) 36 | 37 | 38 | def test_mass_and_inertial_data(): 39 | 40 | # Test with default r=0.111 (m) and mass = 0.440 (kg) 41 | inertia_expected = np.diag([1.0, 1.0, 1.0]) 42 | inertia_expected *= 0.00361416 43 | 44 | inertia = mass_and_inertial_data(0.111, 0.440) 45 | 46 | assert_array_almost_equal(inertia, inertia_expected) 47 | 48 | 49 | def test_check_reynolds_number(): 50 | 51 | wrong_values = (0, 1e10) 52 | 53 | for value in wrong_values: 54 | Re = value 55 | with pytest.raises(ValueError): 56 | check_reynolds_number(Re) 57 | 58 | 59 | def test_check_sn(): 60 | 61 | wrong_values = (-0.5, 0.5) 62 | 63 | for value in wrong_values: 64 | Sn = value 65 | with pytest.raises(ValueError): 66 | check_sn(Sn) 67 | 68 | 69 | def test_get_aerodynamic_forces(): 70 | 71 | # Test with magnus effect 72 | lin_vel = np.array([30, 0, 0]) 73 | ang_vel = np.array([0, 1, 1]) 74 | TAS = 30 75 | rho = 1.225000018124288 76 | alpha = 0 77 | beta = 0 78 | magnus_effect = True 79 | forces = get_aerodynamic_forces(lin_vel, ang_vel, TAS, rho, alpha, beta, 80 | magnus_effect) 81 | 82 | Total_aerodynamic_forces_body = np.array([-10.83340379, 0.1973722863, 83 | -0.1973722863]) 84 | 85 | assert_array_almost_equal(forces, Total_aerodynamic_forces_body) 86 | 87 | # Test without magnus effect 88 | lin_vel = np.array([30, 0, 0]) 89 | ang_vel = np.array([0, 1, 1]) 90 | TAS = 30 91 | rho = 1.225000018124288 92 | alpha = 0 93 | beta = 0 94 | magnus_effect = False 95 | forces = get_aerodynamic_forces(lin_vel, ang_vel, TAS, rho, alpha, beta, 96 | magnus_effect) 97 | Total_aerodynamic_forces_body = np.array([-10.83340379, 0, 98 | 0]) 99 | 100 | assert_array_almost_equal(forces, Total_aerodynamic_forces_body) 101 | 102 | 103 | def test_get_magnus_effect_forces(): 104 | 105 | lin_vel = np.array([30, 0, 0]) 106 | ang_vel = np.array([0, 1, 1]) 107 | TAS = 30 108 | rho = 1.225000018124288 109 | radius = 0.111 110 | A_front = 0.03870756308 111 | alpha = 0 112 | beta = 0 113 | 114 | F_magnus_vector_body_expected = np.array([0, 0.1973722863, -0.1973722863]) 115 | forces = get_magnus_effect_forces(lin_vel, ang_vel, TAS, rho, radius, 116 | A_front, alpha, beta) 117 | assert_array_almost_equal(forces, F_magnus_vector_body_expected) 118 | -------------------------------------------------------------------------------- /src/pyfme/aircrafts/tests/test_cessna_172.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | """ 7 | import pytest 8 | 9 | import numpy as np 10 | from numpy.testing import assert_array_almost_equal 11 | 12 | from pyfme.aircrafts.cessna_172 import Cessna172 13 | 14 | 15 | @pytest.mark.skip(reason="aircraft class is to be refactored") 16 | def test_calculate_aero_forces_moments(): 17 | aircraft = Cessna172() 18 | aircraft.q_inf = 0.5 * 1.225 * 45 ** 2 19 | aircraft.TAS = 45 20 | aircraft.alpha = np.deg2rad(5.0) 21 | 22 | aircraft.controls = {'delta_elevator': 0, 23 | 'hor_tail_incidence': 0, 24 | 'delta_aileron': 0, 25 | 'delta_rudder': 0, 26 | 'delta_t': 0} 27 | 28 | L, D, Y, l, m, n = aircraft._calculate_aero_forces_moments() 29 | 30 | assert_array_almost_equal([L, D, Y], 31 | [13060.49063, 964.4670, 0.], 32 | decimal=4) 33 | 34 | assert_array_almost_equal([l, m, n], 35 | [0, -2046.6386, 0], 36 | decimal=2) 37 | -------------------------------------------------------------------------------- /src/pyfme/aircrafts/tests/test_cessna_310.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Created on Sat Jan 9 23:56:51 2016 8 | 9 | @author:olrosales@gmail.com 10 | 11 | @AeroPython 12 | """ 13 | 14 | from numpy.testing import assert_array_almost_equal 15 | 16 | from pyfme.aircrafts.cessna_310 import Cessna310 17 | 18 | 19 | def test_calculate_aero_forces_moments_alpha_beta_zero(): 20 | aircraft = Cessna310() 21 | aircraft.q_inf = 0.5 * 1.225 * 100 ** 2 22 | aircraft.alpha = 0 23 | aircraft.beta = 0 24 | 25 | aircraft.controls = {'delta_elevator': 0, 26 | 'hor_tail_incidence': 0, 27 | 'delta_aileron': 0, 28 | 'delta_rudder': 0, 29 | 'delta_t': 0} 30 | 31 | L, D, Y, l, m, n = aircraft._calculate_aero_forces_moments() 32 | assert_array_almost_equal([L, D, Y], 33 | [28679.16845, 2887.832934, 0.], 34 | decimal=4) 35 | assert_array_almost_equal([l, m, n], 36 | [0, 10177.065816, 0], 37 | decimal=4) 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/pyfme/environment/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Environment 8 | ----------- 9 | 10 | """ 11 | from .environment import Environment 12 | -------------------------------------------------------------------------------- /src/pyfme/environment/atmosphere.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Atmosphere 8 | ---------- 9 | 10 | """ 11 | 12 | from math import exp, sqrt 13 | from abc import abstractmethod 14 | 15 | from pyfme.models.constants import GAMMA_AIR, R_AIR, GRAVITY 16 | from pyfme.utils.altimetry import geometric2geopotential 17 | 18 | 19 | class Atmosphere(object): 20 | 21 | def __init__(self): 22 | 23 | self._geopotential_alt = None # Current geopotential height (m). 24 | self._pressure_altitude = None # Current pressure altitude (m). 25 | self._T = None # Temperature (K). 26 | self._p = None # Pressure (atm). 27 | self._rho = None # Density (kg/m³). 28 | self._a = None # Speed of sound (m/s). 29 | 30 | def update(self, state): 31 | """Update atmosphere state for the given system state. 32 | 33 | Parameters 34 | ---------- 35 | state : System object 36 | System object with attribute alt_geop (geopotential 37 | altitude. 38 | 39 | Returns 40 | ------- 41 | T : float 42 | Temperature (K). 43 | p : float 44 | Pressure (Pa). 45 | rho : float 46 | Density (kg/m³) 47 | a : float 48 | Sound speed at flight level (m/s) 49 | 50 | Raises 51 | ------ 52 | ValueError 53 | If the value of the altitude is outside the defined layers. 54 | 55 | Notes 56 | ----- 57 | Check layers and reference values in [2]. 58 | 59 | References 60 | ---------- 61 | .. [1] U.S. Standard Atmosphere, 1976, U.S. Government Printing Office, 62 | Washington, D.C., 1976 63 | .. [2] https://en.wikipedia.org/wiki/U.S._Standard_Atmosphere 64 | 65 | """ 66 | # Geopotential altitude 67 | self._geopotential_alt = geometric2geopotential(state.position.height) 68 | 69 | T, p, rho, a = self.__call__(self._geopotential_alt) 70 | 71 | self._T = T 72 | self._p = p 73 | self._rho = rho 74 | self._a = a 75 | 76 | @property 77 | def T(self): 78 | return self._T 79 | 80 | @property 81 | def p(self): 82 | return self._p 83 | 84 | @property 85 | def rho(self): 86 | return self._rho 87 | 88 | @property 89 | def a(self): 90 | return self._a 91 | 92 | @abstractmethod 93 | def __call__(self, h): 94 | pass 95 | 96 | 97 | class ISA1976(Atmosphere): 98 | """ 99 | International Standard Atmosphere 1976 100 | -------------------------------------- 101 | Implementation based on: 102 | .. [1] U.S. Standard Atmosphere, 1976, U.S. Government Printing Office, 103 | Washington, D.C., 1976 104 | 105 | From: https://en.wikipedia.org/wiki/U.S._Standard_Atmosphere 106 | 107 | ========= ============ ========= =========== ============= 108 | Subscript Geopotential Static Standard Temperature 109 | altitude Pressure Temperature Lapse Rate 110 | above MSL (pascals) (K) (K/m) 111 | (m) 112 | ========= ============ ========= =========== ============= 113 | 0 0 101325 288.15 -0.0065 114 | 1 11000 22632.1 216.65 0 115 | 2 20000 5474.89 216.65 0.001 116 | 3 32000 868.019 228.65 0.0028 117 | 4 47000 110.906 270.65 0 118 | 5 51000 66.9389 270.65 -0.0028 119 | 6 71000 3.95642 214.65 -0.002 120 | ========= ============ ========= =========== ============= 121 | """ 122 | 123 | def __init__(self): 124 | super().__init__() 125 | self._gamma = GAMMA_AIR # Adiabatic index or ratio of specific heats 126 | self._R_g = R_AIR # Gas constant J/(Kg·K) 127 | self._g0 = GRAVITY # Gravity m/s^2 128 | # Layer constants 129 | self._h0 = (0, 11000, 20000, 32000, 47000, 51000, 71000, 84500) # m 130 | self._T0_layers = (288.15, 216.65, 216.65, 228.65, 270.65, 270.65, 131 | 214.65) # K 132 | self._p0_layers = (101325.0, 22632.1, 5474.89, 868.019, 110.906, 133 | 66.9389, 3.95642) # Pa 134 | self._alpha_layers = (-0.0065, 0, 0.001, 0.0028, 0, -0.0028, 135 | -0.002) # K / m 136 | 137 | # Initialize at h=0 138 | self.h = 0 # Current height (m). 139 | self._T = self._T0_layers[0] # Temperature (K). 140 | self._p = self._p0_layers[0] # Pressure (atm). 141 | self._rho = self.p / (self._R_g * self.T) 142 | self._a = sqrt(self._gamma * self._R_g * self.T) 143 | 144 | def __call__(self, h): 145 | """ISA 1976 Standard atmosphere temperature, pressure and density. 146 | 147 | Parameters 148 | ---------- 149 | h : float 150 | Geopotential altitude (m). h values must range from 0 to 84500 m. 151 | 152 | Returns 153 | ------- 154 | T : float 155 | Temperature (K). 156 | p : float 157 | Pressure (Pa). 158 | rho : float 159 | Density (kg/m³) 160 | a : float 161 | Sound speed at flight level (m/s) 162 | 163 | Raises 164 | ------ 165 | ValueError 166 | If the value of the altitude is outside the defined layers. 167 | 168 | Notes 169 | ----- 170 | Note that this method will calculate the atmosphere `T, p, rho, 171 | a` values corresponding to the given geopotential altitude, but the 172 | atmosphere object will not be updated. Use update instead to 173 | update the atmosphere. 174 | 175 | Check layers and reference values in [2]. 176 | 177 | References 178 | ---------- 179 | .. [1] U.S. Standard Atmosphere, 1976, U.S. Government Printing Office, 180 | Washington, D.C., 1976 181 | .. [2] https://en.wikipedia.org/wiki/U.S._Standard_Atmosphere 182 | 183 | """ 184 | g0 = self._g0 185 | R_a = self._R_g 186 | gamma = self._gamma 187 | 188 | if h < 0.0: 189 | raise ValueError("Altitude cannot be less than 0 m.") 190 | 191 | elif self._h0[0] <= h < self._h0[1]: # Troposphere 192 | T0 = self._T0_layers[0] 193 | p0 = self._p0_layers[0] 194 | alpha = self._alpha_layers[0] 195 | 196 | T = T0 + alpha * h 197 | p = p0 * (T0 / (T0 + alpha * h)) ** (g0 / (R_a * alpha)) 198 | 199 | elif self._h0[1] <= h < self._h0[2]: # Tropopause 200 | T0 = self._T0_layers[1] 201 | p0 = self._p0_layers[1] 202 | # alpha = self._alpha_layers[1] 203 | h_diff = h - self._h0[1] 204 | 205 | T = T0 206 | p = p0 * exp(-g0 * h_diff / (R_a * T0)) 207 | 208 | elif self._h0[2] <= h < self._h0[3]: # Stratosphere 1 209 | T0 = self._T0_layers[2] 210 | p0 = self._p0_layers[2] 211 | alpha = self._alpha_layers[2] 212 | h_diff = h - self._h0[2] 213 | 214 | T = T0 + alpha * h_diff 215 | p = p0 * (T0 / (T0 + alpha * h_diff)) ** (g0 / (R_a * alpha)) 216 | 217 | elif self._h0[3] <= h < self._h0[4]: # Stratosphere 2 218 | T0 = self._T0_layers[3] 219 | p0 = self._p0_layers[3] 220 | alpha = self._alpha_layers[3] 221 | h_diff = h - self._h0[3] 222 | 223 | T = T0 + alpha * h_diff 224 | p = p0 * (T0 / (T0 + alpha * h_diff)) ** (g0 / (R_a * alpha)) 225 | 226 | elif self._h0[4] <= h < self._h0[5]: # Stratopause 227 | T0 = self._T0_layers[4] 228 | p0 = self._p0_layers[4] 229 | # alpha = self._alpha_layers[4] 230 | h_diff = h - self._h0[4] 231 | 232 | T = T0 233 | p = p0 * exp(-g0 * h_diff / (R_a * T0)) 234 | 235 | elif self._h0[5] <= h < self._h0[6]: # Mesosphere 1 236 | T0 = self._T0_layers[5] 237 | p0 = self._p0_layers[5] 238 | alpha = self._alpha_layers[5] 239 | h_diff = h - self._h0[5] 240 | 241 | T = T0 + alpha * h_diff 242 | p = p0 * (T0 / (T0 + alpha * h_diff)) ** (g0 / (R_a * alpha)) 243 | 244 | elif self._h0[6] <= h <= self._h0[7]: # Mesosphere 2 245 | T0 = self._T0_layers[6] 246 | p0 = self._p0_layers[6] 247 | alpha = self._alpha_layers[6] 248 | h_diff = h - self._h0[6] 249 | 250 | T = T0 + alpha * h_diff 251 | p = p0 * (T0 / (T0 + alpha * h_diff)) ** (g0 / (R_a * alpha)) 252 | 253 | else: 254 | raise ValueError( 255 | "Altitude cannot be greater than {} m.".format(self._h0[7])) 256 | 257 | rho = p / (R_a * T) 258 | a = sqrt(gamma * R_a * T) 259 | 260 | return T, p, rho, a 261 | -------------------------------------------------------------------------------- /src/pyfme/environment/environment.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Flight Mechanics Engine (PyFME). 3 | Copyright (c) AeroPython Development Team. 4 | Distributed under the terms of the MIT License. 5 | """ 6 | 7 | 8 | class Environment(object): 9 | """ 10 | Stores all the environment info: atmosphere, gravity and wind. 11 | """ 12 | 13 | def __init__(self, atmosphere, gravity, wind): 14 | """ 15 | Parameters 16 | ---------- 17 | atmosphere : Atmosphere 18 | Atmospheric model. 19 | gravity : Gravity 20 | Gravity model. 21 | wind : Wind 22 | Wind or gust model. 23 | """ 24 | self.atmosphere = atmosphere 25 | self.gravity = gravity 26 | self.wind = wind 27 | 28 | @property 29 | def T(self): 30 | return self.atmosphere.T 31 | 32 | @property 33 | def p(self): 34 | return self.atmosphere.p 35 | 36 | @property 37 | def rho(self): 38 | return self.atmosphere.rho 39 | 40 | @property 41 | def a(self): 42 | return self.atmosphere.a 43 | 44 | @property 45 | def gravity_magnitude(self): 46 | return self.gravity.magnitude 47 | 48 | @property 49 | def gravity_vector(self): 50 | return self.gravity.vector 51 | 52 | @property 53 | def body_wind(self): 54 | return self.wind.body 55 | 56 | @property 57 | def horizon_wind(self): 58 | return self.wind.horizon 59 | 60 | def update(self, state): 61 | self.atmosphere.update(state) 62 | self.gravity.update(state) 63 | self.wind.update(state) 64 | -------------------------------------------------------------------------------- /src/pyfme/environment/gravity.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Flight Mechanics Engine (PyFME). 3 | Copyright (c) AeroPython Development Team. 4 | Distributed under the terms of the MIT License. 5 | 6 | Gravity Models 7 | -------------- 8 | 9 | """ 10 | from abc import abstractmethod 11 | 12 | import numpy as np 13 | 14 | from pyfme.models.constants import GRAVITY, STD_GRAVITATIONAL_PARAMETER 15 | from pyfme.utils.coordinates import hor2body 16 | 17 | 18 | class Gravity(object): 19 | """Generic gravity model""" 20 | 21 | def __init__(self): 22 | self._magnitude = None 23 | self._versor = np.zeros([3]) # Body axis 24 | self._vector = np.zeros([3]) # Body axis 25 | 26 | @property 27 | def magnitude(self): 28 | return self._magnitude 29 | 30 | @property 31 | def versor(self): 32 | return self._versor 33 | 34 | @property 35 | def vector(self): 36 | return self._vector 37 | 38 | @abstractmethod 39 | def update(self, system): 40 | pass 41 | 42 | 43 | class VerticalConstant(Gravity): 44 | """Vertical constant gravity model. 45 | """ 46 | 47 | def __init__(self): 48 | self._magnitude = GRAVITY 49 | self._z_horizon = np.array([0, 0, 1], dtype=float) 50 | 51 | def update(self, state): 52 | self._versor = hor2body(self._z_horizon, 53 | theta=state.attitude.theta, 54 | phi=state.attitude.phi, 55 | psi=state.attitude.psi 56 | ) 57 | 58 | self._vector = self.magnitude * self.versor 59 | 60 | 61 | class VerticalNewton(Gravity): 62 | """Vertical gravity model with magnitude varying according to Newton's 63 | universal law of gravitation. 64 | """ 65 | 66 | def __init__(self): 67 | self._z_horizon = np.array([0, 0, 1], dtype=float) 68 | 69 | def update(self, state): 70 | r_squared = (state.position.coord_geocentric @ 71 | state.position.coord_geocentric) 72 | self._magnitude = STD_GRAVITATIONAL_PARAMETER / r_squared 73 | self._versor = hor2body(self._z_horizon, 74 | theta=state.attittude.theta, 75 | phi=state.attittude.phi, 76 | psi=state.attitude.psi 77 | ) 78 | self._vector = self.magnitude * self._vector 79 | 80 | 81 | class LatitudeModel(Gravity): 82 | # TODO: https://en.wikipedia.org/wiki/Gravity_of_Earth#Latitude_model 83 | 84 | def __init__(self): 85 | super().__init__() 86 | raise NotImplementedError 87 | 88 | def update(self, system): 89 | raise NotImplementedError 90 | -------------------------------------------------------------------------------- /src/pyfme/environment/tests/test_isa.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Tests of the ISA functions 8 | -------------------------- 9 | All numerical results are validated against the `COESA`_ standard. 10 | 11 | .. _`COESA`: http://hdl.handle.net/2060/19770009539 12 | 13 | Based on scikit-aero (c) 2012 scikit-aero authors. 14 | """ 15 | import numpy as np 16 | from numpy.testing import (assert_equal, assert_almost_equal) 17 | 18 | import pytest 19 | 20 | from pyfme.environment.atmosphere import ISA1976 21 | 22 | atmosphere = ISA1976() 23 | 24 | 25 | def test_sea_level(): 26 | h = 0.0 # m 27 | expected_T = 288.15 # K 28 | expected_p = 101325.0 # Pa 29 | expected_rho = 1.2250 # kg / m3 30 | expected_a = 340.293990543 # m / s 31 | 32 | T, p, rho, a = atmosphere(h) 33 | 34 | # Reads: "Assert if T equals expected_T" 35 | assert_equal(T, expected_T) 36 | assert_equal(p, expected_p) 37 | assert_almost_equal(rho, expected_rho, decimal=4) 38 | assert_almost_equal(a, expected_a, decimal=4) 39 | 40 | 41 | def test_altitude_is_out_of_range(): 42 | wrong_h = (-1.0, 84501) # m 43 | 44 | for h in wrong_h: 45 | with pytest.raises(ValueError): 46 | atmosphere(h) 47 | 48 | 49 | def test_results_under_11km(): 50 | h = np.array([0.0, 51 | 50.0, 52 | 550.0, 53 | 6500.0, 54 | 10000.0, 55 | 11000.0 56 | ]) # m 57 | expected_T = np.array([288.150, 58 | 287.825, 59 | 284.575, 60 | 245.900, 61 | 223.150, 62 | 216.650 63 | ]) # K 64 | 65 | expected_rho = np.array([1.2250, 66 | 1.2191, 67 | 1.1616, 68 | 0.62384, 69 | 0.41271, 70 | 0.36392 71 | ]) # kg / m3 72 | 73 | expected_a = np.array([340.29, 74 | 340.10, 75 | 338.18, 76 | 314.36, 77 | 299.46, 78 | 295.07 79 | ]) # m / s 80 | 81 | for ii, h_ in enumerate(h): 82 | T, p, rho, a = atmosphere(h_) 83 | assert_almost_equal(T, expected_T[ii], decimal=3) 84 | assert_almost_equal(rho, expected_rho[ii], decimal=4) 85 | assert_almost_equal(a, expected_a[ii], decimal=2) 86 | 87 | 88 | def test_results_under_20km(): 89 | h = np.array([12000, 90 | 14200, 91 | 17500, 92 | 20000 93 | ]) # m 94 | expected_T = np.array([216.650, 95 | 216.650, 96 | 216.650, 97 | 216.650, 98 | ]) # K 99 | 100 | expected_rho = np.array([0.31083, 101 | 0.21971, 102 | 0.13058, 103 | 0.088035 104 | ]) # kg / m3 105 | 106 | expected_a = np.array([295.07, 107 | 295.07, 108 | 295.07, 109 | 295.07, 110 | ]) 111 | 112 | for ii, h_ in enumerate(h): 113 | T, p, rho, a = atmosphere(h_) 114 | assert_almost_equal(T, expected_T[ii], decimal=3) 115 | assert_almost_equal(rho, expected_rho[ii], decimal=5) 116 | assert_almost_equal(a, expected_a[ii], decimal=2) 117 | 118 | 119 | def test_results_under_32km(): 120 | h = np.array([22100, 121 | 24000, 122 | 28800, 123 | 32000 124 | ]) # m 125 | expected_T = np.array([218.750, 126 | 220.650, 127 | 225.450, 128 | 228.650 129 | ]) # K 130 | 131 | expected_rho = np.array([0.062711, 132 | 0.046267, 133 | 0.021708, 134 | 0.013225 135 | ]) # kg / m3 136 | 137 | expected_a = np.array([296.50, 138 | 297.78, 139 | 301.00, 140 | 303.13 141 | ]) # m/s 142 | 143 | for ii, h_ in enumerate(h): 144 | T, p, rho, a = atmosphere(h_) 145 | assert_almost_equal(T, expected_T[ii], decimal=3) 146 | assert_almost_equal(rho, expected_rho[ii], decimal=6) 147 | assert_almost_equal(a, expected_a[ii], decimal=2) 148 | 149 | 150 | def test_results_under_47km(): 151 | h = np.array([32200, 152 | 36000, 153 | 42000, 154 | 47000 155 | ]) # m 156 | expected_T = np.array([229.210, 157 | 239.850, 158 | 256.650, 159 | 270.650 160 | ]) # K 161 | 162 | expected_rho = np.array([0.012805, 163 | 0.0070344, 164 | 0.0028780, 165 | 0.0014275 166 | ]) # kg / m3 167 | 168 | expected_a = np.array([303.50, 169 | 310.47, 170 | 321.16, 171 | 329.80 172 | ]) # m / s 173 | 174 | for ii, h_ in enumerate(h): 175 | T, p, rho, a = atmosphere(h_) 176 | assert_almost_equal(T, expected_T[ii], decimal=3) 177 | assert_almost_equal(rho, expected_rho[ii], decimal=6) 178 | assert_almost_equal(a, expected_a[ii], decimal=2) 179 | 180 | 181 | def test_results_under_51km(): 182 | h = np.array([47200, 183 | 49000, 184 | 51000 185 | ]) # m 186 | expected_T = np.array([270.650, 187 | 270.650, 188 | 270.650 189 | ]) # K 190 | 191 | expected_rho = np.array([0.0013919, 192 | 0.0011090, 193 | 0.00086160 194 | ]) # kg / m3 195 | 196 | expected_a = np.array([329.80, 197 | 329.80, 198 | 329.80 199 | ]) # m / s 200 | 201 | for ii, h_ in enumerate(h): 202 | T, p, rho, a = atmosphere(h_) 203 | assert_almost_equal(T, expected_T[ii], decimal=3) 204 | assert_almost_equal(rho, expected_rho[ii], decimal=7) 205 | assert_almost_equal(a, expected_a[ii], decimal=2) 206 | 207 | 208 | def test_results_under_71km(): 209 | h = np.array([51500, 210 | 60000, 211 | 71000 212 | ]) # m 213 | expected_T = np.array([269.250, 214 | 245.450, 215 | 214.650 216 | ]) # K 217 | 218 | expected_rho = np.array([0.00081298, 219 | 2.8832e-4, 220 | 6.4211e-5 221 | ]) # kg / m3 222 | 223 | expected_a = np.array([328.94, 224 | 314.07, 225 | 293.70 226 | ]) # m / s 227 | 228 | for ii, h_ in enumerate(h): 229 | T, p, rho, a = atmosphere(h_) 230 | assert_almost_equal(T, expected_T[ii], decimal=3) 231 | assert_almost_equal(rho, expected_rho[ii], decimal=8) 232 | assert_almost_equal(a, expected_a[ii], decimal=2) 233 | 234 | 235 | def test_results_under_84km(): 236 | h = np.array([52000, 237 | 60000, 238 | 84500 239 | ]) # m 240 | expected_T = np.array([267.850, 241 | 245.450, 242 | 187.650 243 | ]) # K 244 | 245 | expected_rho = np.array([7.6687e-4, 246 | 2.8832e-4, 247 | 7.3914e-6 248 | ]) # kg / m3 249 | 250 | expected_a = np.array([328.09, 251 | 314.07, 252 | 274.61, 253 | ]) # m/s 254 | 255 | for ii, h_ in enumerate(h): 256 | T, p, rho, a = atmosphere(h_) 257 | assert_almost_equal(T, expected_T[ii], decimal=3) 258 | assert_almost_equal(rho, expected_rho[ii], decimal=8) 259 | assert_almost_equal(a, expected_a[ii], decimal=2) 260 | -------------------------------------------------------------------------------- /src/pyfme/environment/wind.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Flight Mechanics Engine (PyFME). 3 | Copyright (c) AeroPython Development Team. 4 | Distributed under the terms of the MIT License. 5 | 6 | Wind Models 7 | ----------- 8 | 9 | """ 10 | import numpy as np 11 | 12 | 13 | class NoWind(object): 14 | 15 | def __init__(self): 16 | # Wind velocity: FROM North to South, FROM East to West, 17 | # Wind velocity in the UPSIDE direction 18 | self.horizon = np.zeros([3], dtype=float) 19 | self.body = np.zeros([3], dtype=float) 20 | 21 | def update(self, state): 22 | pass 23 | -------------------------------------------------------------------------------- /src/pyfme/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .euler_flat_earth import EulerFlatEarth 2 | -------------------------------------------------------------------------------- /src/pyfme/models/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Constant variables 8 | ------------------ 9 | Sources: 10 | 11 | [1] - COESA standard - U.S. Standard Atmosphere, 1976, U.S. Government Printing 12 | Office, Washington, D.C., 1976: 13 | http://hdl.handle.net/2060/19770009539 14 | 15 | [2] - "Introducción a la Ingenería Aeroespacial". Sebastián Franchini, 16 | Óscar López García. UPM 17 | 18 | """ 19 | 20 | # AIR CONSTANTS 21 | # Adiabatic index or ratio of specific heats (dry air at 20º C) - [1] 22 | GAMMA_AIR = 1.4 23 | # Specific gas constant for dry air (J/(Kg·K)) 24 | R_AIR = 287.05287 25 | 26 | # Air at sea level conditions h=0 (m) 27 | RHO_0 = 1.225 # Density at sea level (kg/m3) - [1] 28 | P_0 = 101325 # Pressure at sea level (Pa) - [1] 29 | T_0 = 288.15 # Temperature at sea level (K) - [1] 30 | SOUND_VEL_0 = 340.293990543 # Sound speed at sea level (m/s) 31 | 32 | 33 | # EARTH CONSTANTS 34 | GRAVITY = 9.80665 # Gravity of Ethe Earth (m/s^2) - [1] 35 | # Standard Gravitational Parameter 36 | # product of the gravitational constant G and the mass M of the body (m³/s²) 37 | STD_GRAVITATIONAL_PARAMETER = 3.986004418e14 38 | EARTH_MASS = 5.9722e24 # Mass of the Earth (kg) 39 | GRAVITATIONAL_CONSTANT = 6.67384e11 # Gravitational constant (N·m²/kg²) 40 | EARTH_MEAN_RADIUS = 6371000 # Mean radius of the Earth (m) - [2] 41 | 42 | 43 | # CONVERSIONS 44 | lbs2kg = 0.453592 # Pounds (lb) to kilograms (kg) 45 | ft2m = 0.3048 # Feet (ft) to meters (m) 46 | slug2kg = 14.5939 # Slug to kilograms (kg) 47 | slugft2_2_kgm2 = 1.35581795 # Slug*feet^2 to kilograms*meters^2 (kg*m^2) 48 | -------------------------------------------------------------------------------- /src/pyfme/models/dynamic_system.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Dynamic System & Aircraft Dynamic System 8 | ---------------------------------------- 9 | 10 | Dynamic system class to integrate initial value problems numerically serves 11 | as base for implementation of dynamic systems. 12 | 13 | The Aircraft Dynamic Systems extends the Dynamic System taking into account 14 | the Aircraft State. 15 | """ 16 | from abc import abstractmethod 17 | 18 | import numpy as np 19 | from scipy.integrate import solve_ivp 20 | 21 | 22 | class DynamicSystem: 23 | """Dynamic system class to integrate initial value problems numerically. 24 | Serves as base for implementation of dynamic systems. 25 | 26 | Attributes 27 | ---------- 28 | state_vector : ndarray 29 | State vector. 30 | time : float 31 | Current integration time. 32 | 33 | Methods 34 | ------- 35 | integrate(t_end, t_eval=None, dense_output=True) 36 | Integrate the system from current time to t_end. 37 | fun(t, x) 38 | Dynamic system equations 39 | """ 40 | 41 | def __init__(self, t0, x0, method='RK45', options=None): 42 | """ Dynamic system 43 | 44 | Parameters 45 | ---------- 46 | t0 : float 47 | Initial time. 48 | x0 : array_like 49 | Initial state vector. 50 | method : str, opt 51 | Integration method. Accepts any method implemented in 52 | scipy.integrate.solve_ivp. 53 | options : dict 54 | Options for the selected method. 55 | """ 56 | 57 | if options is None: 58 | options = {} 59 | self._state_vector = x0 60 | self._state_vector_dot = np.zeros_like(x0) 61 | self._time = t0 62 | 63 | self._method = method 64 | self._options = options 65 | 66 | @property 67 | def state_vector(self): 68 | return self._state_vector 69 | 70 | @property 71 | def state_vector_dot(self): 72 | return self._state_vector_dot 73 | 74 | @property 75 | def time(self): 76 | return self._time 77 | 78 | def integrate(self, t_end, t_eval=None, dense_output=True): 79 | """Integrate the system from current time to t_end. 80 | 81 | Parameters 82 | ---------- 83 | t_end : float 84 | Final time. 85 | t_eval : array_like, opt 86 | Times at which to store the computed solution, must be sorted 87 | and lie within current time and t_end. If None (default), use 88 | points selected by a solver. 89 | dense_output: bool, opt 90 | Whether to compute a continuous solution. Default is True. 91 | 92 | Returns 93 | ------- 94 | t : ndarray, shape (n_points,) 95 | Time points. 96 | y : ndarray, shape (n, n_points) 97 | Solution values at t. 98 | sol : Bunch object with the following fields defined: 99 | t : ndarray, shape (n_points,) 100 | Time points. 101 | y : ndarray, shape (n, n_points) 102 | Solution values at t. 103 | sol : OdeSolution or None 104 | Found solution as OdeSolution instance, None if dense_output 105 | was set to False. 106 | t_events : list of ndarray or None 107 | Contains arrays with times at each a corresponding event was 108 | detected, the length of the list equals to the number of 109 | events. None if events was None. 110 | nfev : int 111 | Number of the system rhs evaluations. 112 | njev : int 113 | Number of the Jacobian evaluations. 114 | nlu : int 115 | Number of LU decompositions. 116 | status : int 117 | Reason for algorithm termination: 118 | -1: Integration step failed. 119 | 0: The solver successfully reached the interval end. 120 | 1: A termination event occurred. 121 | message : string 122 | Verbal description of the termination reason. 123 | success : bool 124 | True if the solver reached the interval end or a termination event 125 | occurred (status >= 0). 126 | """ 127 | # TODO: this mehtod is intended to return the whole integration history 128 | # meanwhile, only time_step is called 129 | # How dos it update the full system? 130 | x0 = self.state_vector 131 | t_ini = self.time 132 | 133 | t_span = (t_ini, t_end) 134 | method = self._method 135 | 136 | # TODO: prepare to use jacobian in case it is defined 137 | sol = solve_ivp(self.fun, t_span, x0, method=method, t_eval=t_eval, 138 | dense_output=dense_output, **self._options) 139 | 140 | self._time = sol.t[-1] 141 | self._state_vector = sol.y[:, -1] 142 | 143 | return sol.t, sol.y, sol 144 | 145 | def time_step(self, dt): 146 | """Integrate the system from current time to t_end. 147 | 148 | Parameters 149 | ---------- 150 | dt : float 151 | Time step. 152 | 153 | Returns 154 | ------- 155 | y : ndarray, shape (n) 156 | Solution values at t_end. 157 | """ 158 | 159 | x0 = self.state_vector 160 | t_ini = self.time 161 | 162 | t_span = (t_ini, t_ini + dt) 163 | method = self._method 164 | 165 | # TODO: prepare to use jacobian in case it is defined 166 | sol = solve_ivp(self.fun_wrapped, t_span, x0, method=method, 167 | **self._options) 168 | 169 | if sol.status == -1: 170 | raise RuntimeError(f"Integration did not converge at t={t_ini}") 171 | 172 | self._time = sol.t[-1] 173 | self._state_vector = sol.y[:, -1] 174 | 175 | return self._state_vector 176 | 177 | @abstractmethod 178 | def fun(self, t, x): 179 | """ Right-hand side of the system (dy / dt = f(t, y)). The calling 180 | signature is fun(t, x). Here t is a scalar and there are two options 181 | for ndarray y. 182 | It can either have shape (n,), then fun must return array_like with 183 | shape (n,). Or alternatively it can have shape (n, k), then fun must 184 | return array_like with shape (n, k), i.e. each column corresponds to a 185 | single column in y. The choice between the two options is determined 186 | by vectorized argument (see below). The vectorized implementation 187 | allows faster approximation of the Jacobian by finite differences 188 | (required for stiff solvers). 189 | """ 190 | raise NotImplementedError 191 | 192 | def fun_wrapped(self, t, x): 193 | # First way that comes to my mind in order to store the derivatives 194 | # that are useful for full_state calculation 195 | state_dot = self.fun(t, x) 196 | self._state_vector_dot = state_dot 197 | return state_dot 198 | 199 | 200 | class AircraftDynamicSystem(DynamicSystem): 201 | """Aircraft's Dynamic System 202 | 203 | Attributes 204 | ---------- 205 | full_state : AircraftState 206 | Current aircraft state. 207 | update_simulation : fun 208 | Function that updates environment and aircraft in order to get 209 | proper forces, moments and mass properties for integration steps. 210 | 211 | Methods 212 | ------- 213 | steady_state_trim_fun 214 | time_step 215 | """ 216 | def __init__(self, t0, full_state, method='RK45', options=None): 217 | """Aircraft Dynamic system initialization. 218 | 219 | Parameters 220 | ---------- 221 | t0 : float 222 | Initial time (s). 223 | full_state : AircraftState 224 | Initial aircraft's state. 225 | method : str, opt 226 | Integration method. Any method included in 227 | scipy.integrate.solve_ivp can be used. 228 | options : dict, opt 229 | Options accepted by the integration method. 230 | """ 231 | x0 = self._get_state_vector_from_full_state(full_state) 232 | self.full_state = self._adapt_full_state_to_dynamic_system(full_state) 233 | 234 | super().__init__(t0, x0, method=method, options=options) 235 | 236 | self.update_simulation = None 237 | 238 | @abstractmethod 239 | def _adapt_full_state_to_dynamic_system(self, full_state): 240 | """Transforms the given state to the one used in the 241 | AircraftDynamicSystem in order to initialize dynamic's system 242 | initial state. 243 | For example, the full state given may be using quaternions for 244 | attitude representation, but the Aircraft dynamic system may 245 | propagate Euler angles. 246 | 247 | Parameters 248 | ---------- 249 | full_state : AircraftState 250 | 251 | """ 252 | raise NotImplementedError 253 | 254 | @abstractmethod 255 | def _update_full_system_state_from_state(self, state, state_dot): 256 | """Updates full system's state (AircraftState) based on the 257 | implemented dynamic's system state vector and derivative of system's 258 | state vector (output of system's equations dx/dt=f(x, t)) 259 | 260 | Parameters 261 | ---------- 262 | state : array_like 263 | State vector. 264 | state_dot : array_like 265 | Derivative of state vector. 266 | 267 | """ 268 | raise NotImplementedError 269 | 270 | @abstractmethod 271 | def _get_state_vector_from_full_state(self, full_state): 272 | """Gets the state vector given the full state. 273 | 274 | Parameters 275 | ---------- 276 | full_state : AircraftState 277 | Aircraft's full state 278 | 279 | Returns 280 | ------- 281 | state_vector : ndarray 282 | State vector. 283 | """ 284 | raise NotImplementedError 285 | 286 | @abstractmethod 287 | def steady_state_trim_fun(self, full_state, environment, aircraft, 288 | controls): 289 | """Output from linear and angular momentum conservation equations: 290 | ax, ay, az, p, q, r, which must be zero after the steady state 291 | trimming process 292 | 293 | Parameters 294 | ---------- 295 | full_state : AircraftState 296 | Full aircraft state. 297 | environment : Environment 298 | Environment in which the aircraft is being trimmed. 299 | aircraft : Aircraft 300 | Aircraft being trimmed. 301 | controls : dict 302 | Controls of the aircraft being trimmed. 303 | 304 | Returns 305 | ------- 306 | rv : ndarray 307 | Output from linear and angular momentum conservation equations: 308 | ax, ay, az, p, q, r. 309 | """ 310 | raise NotImplementedError 311 | 312 | def time_step(self, dt): 313 | """Perform an integration time step 314 | 315 | Parameters 316 | ---------- 317 | dt : float 318 | Time step for integration 319 | 320 | Returns 321 | ------- 322 | full_state : AircraftState 323 | Aircraft's state after integration time step 324 | """ 325 | super().time_step(dt) 326 | # Now self.state_vector and state_vector_dot are updated 327 | self._update_full_system_state_from_state(self.state_vector, 328 | self.state_vector_dot) 329 | 330 | return self.full_state 331 | -------------------------------------------------------------------------------- /src/pyfme/models/euler_flat_earth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Euler Flat Earth 8 | ---------------- 9 | 10 | Classical aircraft motion equations assuming no Earth rotation 11 | inertial effects, representing attitude with Euler angles (not valid for 12 | all-attitude flight) and integrating aircraft position in Earth axis (Flat 13 | Earth). 14 | """ 15 | 16 | import numpy as np 17 | from numpy import sin, cos 18 | 19 | from pyfme.models.dynamic_system import AircraftDynamicSystem 20 | from pyfme.models.state import ( 21 | AircraftState, EarthPosition, EulerAttitude, BodyVelocity, 22 | BodyAngularVelocity, BodyAcceleration, BodyAngularAcceleration 23 | ) 24 | 25 | 26 | class EulerFlatEarth(AircraftDynamicSystem): 27 | """Euler Flat Earth Dynamic System. 28 | 29 | Classical aircraft motion equations assuming no Earth rotation, no Earth 30 | curvature and modelling attitude with Euler angles. Aircraft position is 31 | performed on Earth axis. 32 | """ 33 | 34 | def fun(self, t, x): 35 | 36 | self._update_full_system_state_from_state(x, self.state_vector_dot) 37 | updated_simulation = self.update_simulation(t, self.full_state) 38 | 39 | mass = updated_simulation.aircraft.mass 40 | inertia = updated_simulation.aircraft.inertia 41 | forces = updated_simulation.aircraft.total_forces 42 | moments = updated_simulation.aircraft.total_moments 43 | 44 | rv = _system_equations(t, x, mass, inertia, forces, moments) 45 | 46 | return rv 47 | 48 | def steady_state_trim_fun(self, full_state, environment, aircraft, 49 | controls): 50 | 51 | environment.update(full_state) 52 | aircraft.calculate_forces_and_moments(full_state, environment, 53 | controls) 54 | 55 | mass = aircraft.mass 56 | inertia = aircraft.inertia 57 | forces = aircraft.total_forces 58 | moments = aircraft.total_moments 59 | 60 | t0 = 0 61 | x0 = self._get_state_vector_from_full_state(full_state) 62 | 63 | rv = _system_equations(t0, x0, mass, inertia, forces, moments) 64 | return rv[:6] 65 | 66 | def _update_full_system_state_from_state(self, state, state_dot): 67 | 68 | self.full_state.position.update(state[9:12]) 69 | self.full_state.attitude.update(state[6:9]) 70 | att = self.full_state.attitude 71 | self.full_state.velocity.update(state[0:3], att) 72 | self.full_state.angular_vel.update(state[3:6], att) 73 | 74 | self.full_state.acceleration.update(state_dot[0:3], att) 75 | self.full_state.angular_accel.update(state_dot[3:6], att) 76 | 77 | def _adapt_full_state_to_dynamic_system(self, full_state): 78 | 79 | pos = EarthPosition(full_state.position.x_earth, 80 | full_state.position.y_earth, 81 | full_state.position.height, 82 | full_state.position.lat, 83 | full_state.position.lon) 84 | 85 | att = EulerAttitude(full_state.attitude.theta, 86 | full_state.attitude.phi, 87 | full_state.attitude.psi) 88 | 89 | vel = BodyVelocity(full_state.velocity.u, 90 | full_state.velocity.v, 91 | full_state.velocity.w, 92 | att) 93 | 94 | ang_vel = BodyAngularVelocity(full_state.angular_vel.p, 95 | full_state.angular_vel.q, 96 | full_state.angular_vel.r, 97 | att) 98 | 99 | accel = BodyAcceleration(full_state.acceleration.u_dot, 100 | full_state.acceleration.v_dot, 101 | full_state.acceleration.w_dot, 102 | att) 103 | 104 | ang_accel = BodyAngularAcceleration(full_state.angular_accel.p_dot, 105 | full_state.angular_accel.q_dot, 106 | full_state.angular_accel.r_dot, 107 | att) 108 | 109 | full_state = AircraftState(pos, att, vel, ang_vel, accel, ang_accel) 110 | return full_state 111 | 112 | def _get_state_vector_from_full_state(self, full_state): 113 | 114 | x0 = np.array( 115 | [ 116 | full_state.velocity.u, 117 | full_state.velocity.v, 118 | full_state.velocity.w, 119 | full_state.angular_vel.p, 120 | full_state.angular_vel.q, 121 | full_state.angular_vel.r, 122 | full_state.attitude.theta, 123 | full_state.attitude.phi, 124 | full_state.attitude.psi, 125 | full_state.position.x_earth, 126 | full_state.position.y_earth, 127 | full_state.position.z_earth 128 | ] 129 | ) 130 | return x0 131 | 132 | 133 | # TODO: numba jit 134 | def _system_equations(time, state_vector, mass, inertia, forces, moments): 135 | """Euler flat earth equations: linear momentum equations, angular momentum 136 | equations, angular kinematic equations, linear kinematic 137 | equations. 138 | 139 | Parameters 140 | ---------- 141 | time : float 142 | Current time (s). 143 | state_vector : array_like, shape(9) 144 | Current value of absolute velocity and angular velocity, both 145 | expressed in body axes, euler angles and position in Earth axis. 146 | (u, v, w, p, q, r, theta, phi, psi, x, y, z) 147 | (m/s, m/s, m/s, rad/s, rad/s rad/s, rad, rad, rad, m, m ,m). 148 | mass : float 149 | Current mass of the aircraft (kg). 150 | inertia : array_like, shape(3, 3) 151 | 3x3 tensor of inertia of the aircraft (kg * m2) 152 | Current equations assume that the aircraft has a symmetry plane 153 | (x_b - z_b), thus J_xy and J_yz must be null. 154 | forces : array_like, shape(3) 155 | 3 dimensional vector containing the total total_forces (including 156 | gravity) in x_b, y_b, z_b axes (N). 157 | moments : array_like, shape(3) 158 | 3 dimensional vector containing the total total_moments in x_b, 159 | y_b, z_b axes (N·m). 160 | 161 | Returns 162 | ------- 163 | dstate_dt : array_like, shape(9) 164 | Derivative with respect to time of the state vector. 165 | Current value of absolute acceleration and angular acceleration, 166 | both expressed in body axes, Euler angles derivatives and velocity 167 | with respect to Earth Axis. 168 | (du_dt, dv_dt, dw_dt, dp_dt, dq_dt, dr_dt, dtheta_dt, dphi_dt, 169 | dpsi_dt, dx_dt, dy_dt, dz_dt) 170 | (m/s² , m/s², m/s², rad/s², rad/s², rad/s², rad/s, rad/s, rad/s, 171 | m/s, m/s, m/s). 172 | 173 | References 174 | ---------- 175 | .. [1] B. Etkin, "Dynamics of Atmospheric Flight", Courier Corporation, 176 | p. 149 (5.8 The Flat-Earth Approximation), 2012. 177 | 178 | .. [2] M. A. Gómez Tierno y M. Pérez Cortés, "Mecánica del Vuelo", 179 | Garceta Grupo Editorial, pp.18-25 (Tema 2: Ecuaciones Generales del 180 | Moviemiento), 2012. 181 | 182 | """ 183 | # Note definition of total_moments of inertia p.21 Gomez Tierno, et al 184 | # Mecánica de vuelo 185 | Ix = inertia[0, 0] 186 | Iy = inertia[1, 1] 187 | Iz = inertia[2, 2] 188 | Jxz = - inertia[0, 2] 189 | 190 | Fx, Fy, Fz = forces 191 | L, M, N = moments 192 | 193 | u, v, w = state_vector[0:3] 194 | p, q, r = state_vector[3:6] 195 | theta, phi, psi = state_vector[6:9] 196 | 197 | # Linear momentum equations 198 | du_dt = Fx / mass + r * v - q * w 199 | dv_dt = Fy / mass - r * u + p * w 200 | dw_dt = Fz / mass + q * u - p * v 201 | 202 | # Angular momentum equations 203 | dp_dt = (L * Iz + N * Jxz - q * r * (Iz ** 2 - Iz * Iy + Jxz ** 2) + 204 | p * q * Jxz * (Ix + Iz - Iy)) / (Ix * Iz - Jxz ** 2) 205 | dq_dt = (M + (Iz - Ix) * p * r - Jxz * (p ** 2 - r ** 2)) / Iy 206 | dr_dt = (L * Jxz + N * Ix + p * q * (Ix ** 2 - Ix * Iy + Jxz ** 2) - 207 | q * r * Jxz * (Iz + Ix - Iy)) / (Ix * Iz - Jxz ** 2) 208 | 209 | # Angular Kinematic equations 210 | dtheta_dt = q * cos(phi) - r * sin(phi) 211 | dphi_dt = p + (q * sin(phi) + r * cos(phi)) * np.tan(theta) 212 | dpsi_dt = (q * sin(phi) + r * cos(phi)) / cos(theta) 213 | 214 | # Linear kinematic equations 215 | dx_dt = (cos(theta) * cos(psi) * u + 216 | (sin(phi) * sin(theta) * cos(psi) - cos(phi) * sin(psi)) * v + 217 | (cos(phi) * sin(theta) * cos(psi) + sin(phi) * sin(psi)) * w) 218 | dy_dt = (cos(theta) * sin(psi) * u + 219 | (sin(phi) * sin(theta) * sin(psi) + cos(phi) * cos(psi)) * v + 220 | (cos(phi) * sin(theta) * sin(psi) - sin(phi) * cos(psi)) * w) 221 | dz_dt = -u * sin(theta) + v * sin(phi) * cos(theta) + w * cos( 222 | phi) * cos(theta) 223 | 224 | return np.array([du_dt, dv_dt, dw_dt, dp_dt, dq_dt, dr_dt, dtheta_dt, 225 | dphi_dt, dpsi_dt, dx_dt, dy_dt, dz_dt]) 226 | -------------------------------------------------------------------------------- /src/pyfme/models/state/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Title 8 | ----- 9 | 10 | """ 11 | from .aircraft_state import AircraftState 12 | 13 | from .position import EarthPosition, GeodeticPosition 14 | from .attitude import EulerAttitude, QuaternionAttitude 15 | from .velocity import BodyVelocity, NEDVelocity 16 | from .acceleration import BodyAcceleration, NEDAcceleration 17 | from .angular_velocity import BodyAngularVelocity, EulerAngularRates 18 | from .angular_acceleration import (BodyAngularAcceleration, 19 | EulerAngularAcceleration) 20 | -------------------------------------------------------------------------------- /src/pyfme/models/state/acceleration.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Flight Mechanics Engine (PyFME). 3 | Copyright (c) AeroPython Development Team. 4 | Distributed under the terms of the MIT License. 5 | 6 | Acceleration 7 | ------------ 8 | 9 | """ 10 | from abc import abstractmethod 11 | 12 | import numpy as np 13 | 14 | from pyfme.utils.coordinates import body2hor, hor2body 15 | 16 | 17 | class Acceleration: 18 | """Acceleration 19 | 20 | Attributes 21 | ---------- 22 | accel_body : ndarray, shape(3) 23 | (u_dot [m/s²], v_dot [m/s²], w_dot [m/s²]) 24 | u_dot 25 | v_dot 26 | w_dot 27 | accel_NED : ndarray, shape(3) 28 | (VN_dot [m/s²], VE_dot [m/s²], VD_dot [m/s²]) 29 | VN_dot 30 | VE_dot 31 | VD_dot 32 | """ 33 | 34 | def __init__(self): 35 | # Body axis 36 | self._accel_body = np.zeros(3) # m/s² 37 | # Local horizon (NED) 38 | self._accel_NED = np.zeros(3) # m/s² 39 | 40 | @abstractmethod 41 | def update(self, coords, attitude): 42 | raise NotImplementedError 43 | 44 | @property 45 | def accel_body(self): 46 | return self._accel_body 47 | 48 | @property 49 | def u_dot(self): 50 | return self._accel_body[0] 51 | 52 | @property 53 | def v_dot(self): 54 | return self._accel_body[1] 55 | 56 | @property 57 | def w_dot(self): 58 | return self._accel_body[2] 59 | 60 | @property 61 | def accel_NED(self): 62 | return self._accel_NED 63 | 64 | @property 65 | def v_north_dot(self): 66 | return self._accel_NED[0] 67 | 68 | @property 69 | def v_east_dot(self): 70 | return self._accel_NED[1] 71 | 72 | @property 73 | def v_down_dot(self): 74 | return self._accel_NED[2] 75 | 76 | @property 77 | def value(self): 78 | """Only for testing purposes""" 79 | return np.hstack((self.accel_body, self.accel_NED)) 80 | 81 | 82 | class BodyAcceleration(Acceleration): 83 | 84 | def __init__(self, u_dot, v_dot, w_dot, attitude): 85 | super().__init__() 86 | self.update(np.array([u_dot, v_dot, w_dot]), attitude) 87 | 88 | def update(self, coords, attitude): 89 | self._accel_body[:] = coords 90 | self._accel_NED = body2hor(coords, 91 | attitude.theta, 92 | attitude.phi, 93 | attitude.psi) 94 | 95 | def __repr__(self): 96 | rv = (f"u_dot: {self.u_dot:.2f} m/s², v_dot: {self.v_dot:.2f} m/s², " 97 | f"w_dot: {self.u_dot:.2f} m/s²") 98 | return rv 99 | 100 | 101 | class NEDAcceleration(Acceleration): 102 | 103 | def __init__(self, vn_dot, ve_dot, vd_dot, attitude): 104 | super().__init__() 105 | self.update(np.array([vn_dot, ve_dot, vd_dot]), attitude) 106 | 107 | def update(self, coords, attitude): 108 | self._accel_NED[:] = coords 109 | self._accel_body = hor2body(coords, 110 | attitude.theta, 111 | attitude.phi, 112 | attitude.psi) 113 | 114 | def __repr__(self): 115 | rv = (f"V_north_dot: {self.v_north_dot:.2f} m/s², " 116 | f"V_east_dot: {self.v_east_dot:.2f} m/s², " 117 | f"V_down_dot: {self.v_down_dot:.2f} m/s²") 118 | return rv 119 | -------------------------------------------------------------------------------- /src/pyfme/models/state/aircraft_state.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Aircraft State 8 | -------------- 9 | 10 | """ 11 | 12 | import numpy as np 13 | 14 | from .angular_velocity import BodyAngularVelocity 15 | from .acceleration import BodyAcceleration 16 | from .angular_acceleration import BodyAngularAcceleration 17 | 18 | 19 | class AircraftState: 20 | def __init__(self, position, attitude, velocity, angular_vel=None, 21 | acceleration=None, angular_accel=None): 22 | 23 | self.position = position 24 | self.attitude = attitude 25 | self.velocity = velocity 26 | 27 | if angular_vel is None: 28 | angular_vel = BodyAngularVelocity(0, 0, 0, attitude) 29 | if acceleration is None: 30 | acceleration = BodyAcceleration(0, 0, 0, attitude) 31 | if angular_accel is None: 32 | angular_accel = BodyAngularAcceleration(0, 0, 0, attitude) 33 | 34 | self.angular_vel = angular_vel 35 | self.acceleration = acceleration 36 | self.angular_accel = angular_accel 37 | 38 | @property 39 | def value(self): 40 | """Only for testing purposes""" 41 | return np.hstack((self.position.value, self.attitude.value, 42 | self.velocity.value, self.angular_vel.value, 43 | self.acceleration.value, self.angular_accel.value)) 44 | 45 | def __repr__(self): 46 | rv = ( 47 | "Aircraft State \n" 48 | f"{self.position} \n" 49 | f"{self.attitude} \n" 50 | f"{self.velocity} \n" 51 | f"{self.angular_vel} \n" 52 | f"{self.acceleration} \n" 53 | f"{self.angular_accel} \n" 54 | ) 55 | return rv 56 | -------------------------------------------------------------------------------- /src/pyfme/models/state/angular_acceleration.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Flight Mechanics Engine (PyFME). 3 | Copyright (c) AeroPython Development Team. 4 | Distributed under the terms of the MIT License. 5 | 6 | Angular Acceleration 7 | -------------------- 8 | 9 | """ 10 | from abc import abstractmethod 11 | 12 | import numpy as np 13 | 14 | 15 | class AngularAcceleration: 16 | """Angular Accelerations 17 | 18 | Attributes 19 | ---------- 20 | accel_ang : ndarray, shape(3) 21 | (p_dot [rad/s²], q_dot [rad/s²], r_dot [rad/s²]) 22 | p_dot 23 | q_dot 24 | r_dot 25 | euler_ang_acc : ndarray, shape(3) 26 | (theta_2dot [rad/s²], phi_2dot [rad/s²], psi_2dot [rad/s²]) 27 | theta_2dot 28 | phi_2dot 29 | psi_2dot 30 | """ 31 | 32 | def __init__(self): 33 | # ANGULAR VELOCITY: (p_dot, q_dot, r_dot) 34 | self._acc_ang_body = np.zeros(3) # rad/s 35 | # EULER ANGLE RATES (theta_dot2, phi_dot2, psi_dot2) 36 | self._euler_ang_acc = np.zeros(3) # rad/s 37 | 38 | @abstractmethod 39 | def update(self, coords, attitude): 40 | raise ValueError 41 | 42 | @property 43 | def acc_ang_body(self): 44 | return self._acc_ang_body 45 | 46 | @property 47 | def p_dot(self): 48 | return self._acc_ang_body[0] 49 | 50 | @property 51 | def q_dot(self): 52 | return self._acc_ang_body[1] 53 | 54 | @property 55 | def r_dot(self): 56 | return self._acc_ang_body[2] 57 | 58 | @property 59 | def euler_ang_acc(self): 60 | return self._euler_ang_acc 61 | 62 | @property 63 | def theta_2dot(self): 64 | return self._euler_ang_acc[0] 65 | 66 | @property 67 | def phi_2dot(self): 68 | return self._euler_ang_acc[1] 69 | 70 | @property 71 | def psi_2dot(self): 72 | return self._euler_ang_acc[2] 73 | 74 | @property 75 | def value(self): 76 | """Only for testing purposes""" 77 | return np.hstack((self.acc_ang_body, self.euler_ang_acc)) 78 | 79 | 80 | class BodyAngularAcceleration(AngularAcceleration): 81 | 82 | def __init__(self, p_dot, q_dot, r_dot, attitude): 83 | super().__init__() 84 | self.update(np.array([p_dot, q_dot, r_dot]), attitude) 85 | 86 | def update(self, coords, attitude): 87 | self._acc_ang_body[:] = coords 88 | # TODO: transform angular acc in body axis to euler angles 89 | # acc 90 | self._euler_ang_acc = np.zeros(3) # rad/s 91 | 92 | def __repr__(self): 93 | rv = (f"P_dot: {self.p_dot:.2f} rad/s², " 94 | f"Q_dot: {self.q_dot:.2f} rad/s², " 95 | f"R_dot: {self.r_dot:.2f} rad/s²") 96 | return rv 97 | 98 | 99 | class EulerAngularAcceleration(AngularAcceleration): 100 | 101 | def __init__(self, theta_dot, phi_dot, psi_dot, attitude): 102 | super().__init__() 103 | self.update(np.array([theta_dot, phi_dot, psi_dot]), 104 | attitude) 105 | 106 | def update(self, coords, attitude): 107 | self._euler_ang_acc[:] = coords 108 | # TODO: transform euler angles acc to angular acceleration in body 109 | # axis 110 | self._acc_ang_body[:] = np.zeros(3) # rad/s 111 | -------------------------------------------------------------------------------- /src/pyfme/models/state/angular_velocity.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Flight Mechanics Engine (PyFME). 3 | Copyright (c) AeroPython Development Team. 4 | Distributed under the terms of the MIT License. 5 | 6 | Angular Velocity 7 | ---------------- 8 | 9 | """ 10 | from abc import abstractmethod 11 | 12 | import numpy as np 13 | 14 | 15 | class AngularVelocity: 16 | """Angular velocity 17 | 18 | vel_ang : ndarray, shape(3) 19 | (p [rad/s], q [rad/s], r [rad/s]) 20 | p 21 | q 22 | r 23 | euler_ang_rates : ndarray, shape(3) 24 | (theta_dot [rad/s], phi_dot [rad/s], psi_dot [rad/s]) 25 | theta 26 | phi 27 | psi 28 | """ 29 | 30 | def __init__(self): 31 | # ANGULAR VELOCITY: (p, q, r) 32 | self._vel_ang_body = np.zeros(3) # rad/s 33 | # EULER ANGLE RATES (theta_dot, phi_dot, psi_dot) 34 | self._euler_ang_rate = np.zeros(3) # rad/s 35 | 36 | @abstractmethod 37 | def update(self, coords, attitude): 38 | raise NotImplementedError 39 | 40 | @property 41 | def vel_ang_body(self): 42 | return self._vel_ang_body 43 | 44 | @property 45 | def p(self): 46 | return self._vel_ang_body[0] 47 | 48 | @property 49 | def q(self): 50 | return self._vel_ang_body[1] 51 | 52 | @property 53 | def r(self): 54 | return self._vel_ang_body[2] 55 | 56 | @property 57 | def euler_ang_rate(self): 58 | return self._euler_ang_rate 59 | 60 | @property 61 | def theta_dot(self): 62 | return self._euler_ang_rate[0] 63 | 64 | @property 65 | def phi_dot(self): 66 | return self._euler_ang_rate[1] 67 | 68 | @property 69 | def psi_dot(self): 70 | return self._euler_ang_rate[2] 71 | 72 | @property 73 | def value(self): 74 | """Only for testing purposes""" 75 | return np.hstack((self.vel_ang_body, self.euler_ang_rate)) 76 | 77 | 78 | class BodyAngularVelocity(AngularVelocity): 79 | 80 | def __init__(self, p, q, r, attitude): 81 | # TODO: docstring 82 | super().__init__() 83 | self.update(np.array([p, q, r]), attitude) 84 | 85 | def update(self, coords, attitude): 86 | self._vel_ang_body[:] = coords 87 | # TODO: transform angular velocity in body axis to euler angles 88 | # rates 89 | self._euler_ang_rate = np.zeros(3) # rad/s 90 | 91 | def __repr__(self): 92 | return (f"P: {self.p:.2f} rad/s, " 93 | f"Q: {self.q:.2f} rad/s, " 94 | f"R: {self.r:.2f} rad/s") 95 | 96 | 97 | class EulerAngularRates(AngularVelocity): 98 | 99 | def __init__(self, theta_dot, phi_dot, psi_dot, attitude): 100 | # TODO: docstring 101 | super().__init__() 102 | self.update(np.array([theta_dot, phi_dot, psi_dot]), 103 | attitude) 104 | 105 | def update(self, coords, attitude): 106 | self._euler_ang_rate[:] = coords 107 | # TODO: transform euler angles rates to angular velocity in body 108 | # axis 109 | self._vel_ang_body[:] = np.zeros(3) # rad/s 110 | -------------------------------------------------------------------------------- /src/pyfme/models/state/attitude.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Flight Mechanics Engine (PyFME). 3 | Copyright (c) AeroPython Development Team. 4 | Distributed under the terms of the MIT License. 5 | 6 | Attitude 7 | -------- 8 | Aircraft attitude representations prepared to store the aircraft orientation in 9 | Euler angles and quaternions independently of the dynamic system used. 10 | """ 11 | from abc import abstractmethod 12 | 13 | import numpy as np 14 | 15 | 16 | class Attitude: 17 | """Attitude 18 | 19 | Attributes 20 | ---------- 21 | 22 | euler_angles : ndarray, shape(3) 23 | (theta [rad], phi [rad], psi [rad]) 24 | theta 25 | phi 26 | psi 27 | quaternions : ndarray, shape(4) 28 | (q0, q1, q2, q3) 29 | q0 30 | q1 31 | q2 32 | q3 33 | """ 34 | 35 | def __init__(self): 36 | # Euler angles (psi, theta, phi) 37 | self._euler_angles = np.zeros(3) # rad 38 | # Quaternions (q0, q1, q2, q3) 39 | self._quaternions = np.zeros(4) 40 | 41 | @abstractmethod 42 | def update(self, value): 43 | raise NotImplementedError 44 | 45 | @property 46 | def euler_angles(self): 47 | return self._euler_angles 48 | 49 | @property 50 | def psi(self): 51 | return self._euler_angles[2] 52 | 53 | @property 54 | def theta(self): 55 | return self._euler_angles[0] 56 | 57 | @property 58 | def phi(self): 59 | return self._euler_angles[1] 60 | 61 | @property 62 | def quaternions(self): 63 | return self._quaternions 64 | 65 | @property 66 | def q0(self): 67 | return self._quaternions[0] 68 | 69 | @property 70 | def q1(self): 71 | return self._quaternions[1] 72 | 73 | @property 74 | def q2(self): 75 | return self._quaternions[2] 76 | 77 | @property 78 | def q3(self): 79 | return self._quaternions[3] 80 | 81 | @property 82 | def value(self): 83 | """Only for testing purposes""" 84 | return np.hstack((self.euler_angles, self.quaternions)) 85 | 86 | 87 | class EulerAttitude(Attitude): 88 | 89 | def __init__(self, theta, phi, psi): 90 | # TODO: docstring 91 | super().__init__() 92 | self.update(np.array([theta, phi, psi])) 93 | 94 | def update(self, value): 95 | self._euler_angles[:] = value 96 | # TODO: transform quaternions to Euler angles 97 | self._quaternions = np.zeros(4) 98 | 99 | def __repr__(self): 100 | rv = (f"theta: {self.theta:.3f} rad, phi: {self.phi:.3f} rad, " 101 | f"psi: {self.psi:.3f} rad") 102 | return rv 103 | 104 | 105 | class QuaternionAttitude(Attitude): 106 | def __init__(self, q0, q1, q2, q3): 107 | # TODO: docstring 108 | super().__init__() 109 | self.update(np.array([q0, q1, q2, q3])) 110 | 111 | def update(self, value): 112 | self._quaternions[:] = value 113 | # TODO: transform quaternions to Euler angles 114 | self._euler_angles = np.zeros(3) 115 | -------------------------------------------------------------------------------- /src/pyfme/models/state/position.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Flight Mechanics Engine (PyFME). 3 | Copyright (c) AeroPython Development Team. 4 | Distributed under the terms of the MIT License. 5 | 6 | Position 7 | -------- 8 | Aircraft position class prepared to represent the aircraft position in Earth 9 | axis, geodetic coordinates and geocentric coordinates independently of the 10 | dynamic system used 11 | """ 12 | from abc import abstractmethod 13 | 14 | import numpy as np 15 | 16 | from pyfme.models.constants import EARTH_MEAN_RADIUS 17 | 18 | 19 | class Position: 20 | """Position 21 | 22 | Attributes 23 | ---------- 24 | 25 | geodetic_coordinates : ndarray, shape(3) 26 | (lat [rad], lon [rad], height [m]) 27 | lat 28 | lon 29 | height 30 | geocentric_coordinates : ndarray, shape(3) 31 | (x_geo [m], y_geo [m], z_geo [m]) 32 | x_geo 33 | y_geo 34 | z_geo 35 | earth_coordinates : ndarray, shape(3) 36 | (x_earth [m], y_earth [m], z_earth [m]) 37 | x_earth 38 | y_earth 39 | z_earth 40 | """ 41 | 42 | def __init__(self, geodetic, geocentric, earth): 43 | # Geodetic coordinates: (geodetic lat, lon, height above ellipsoid) 44 | self._geodetic_coordinates = np.asarray(geodetic, dtype=float) # rad 45 | # Geocentric coordinates (rotating with Earth): (x_geo, y_geo, z_geo) 46 | self._geocentric_coordinates = np.asarray(geocentric, dtype=float) # m 47 | # Earth coordinates (x_earth, y_earth, z_earth) 48 | self._earth_coordinates = np.asarray(earth, dtype=float) # m 49 | 50 | @abstractmethod 51 | def update(self, coords): 52 | raise NotImplementedError 53 | 54 | @property 55 | def geocentric_coordinates(self): 56 | return self._geocentric_coordinates 57 | 58 | @property 59 | def x_geo(self): 60 | return self._geocentric_coordinates[0] 61 | 62 | @property 63 | def y_geo(self): 64 | return self._geocentric_coordinates[1] 65 | 66 | @property 67 | def z_geo(self): 68 | return self._geocentric_coordinates[2] 69 | 70 | @property 71 | def geodetic_coordinates(self): 72 | return self._geodetic_coordinates 73 | 74 | @property 75 | def lat(self): 76 | return self._geodetic_coordinates[0] 77 | 78 | @property 79 | def lon(self): 80 | return self._geodetic_coordinates[1] 81 | 82 | @property 83 | def height(self): 84 | return self._geodetic_coordinates[2] 85 | 86 | @property 87 | def earth_coordinates(self): 88 | return self._earth_coordinates 89 | 90 | @property 91 | def x_earth(self): 92 | return self._earth_coordinates[0] 93 | 94 | @property 95 | def y_earth(self): 96 | return self._earth_coordinates[1] 97 | 98 | @property 99 | def z_earth(self): 100 | return self._earth_coordinates[2] 101 | 102 | @property 103 | def value(self): 104 | """Only for testing purposes""" 105 | return np.hstack((self.earth_coordinates, self.geodetic_coordinates, 106 | self.geodetic_coordinates)) 107 | 108 | 109 | class EarthPosition(Position): 110 | 111 | def __init__(self, x, y, height, lat=0, lon=0): 112 | # TODO: docstring 113 | earth = np.array([x, y, -height]) 114 | # TODO: Assuming round earth use changes in x & y to calculate 115 | # new lat and lon. z_earth is -height: 116 | geodetic = np.array([lat, lon, height]) # m 117 | # TODO: make transformation from geodetic to geocentric: 118 | geocentric = np.zeros(3) # m 119 | super().__init__(geodetic, geocentric, earth) 120 | 121 | def update(self, value): 122 | # Assuming round earth use changes in x & y to calculate 123 | # new lat and lon. z_earth is -height: 124 | delta_x, delta_y, _ = value - self.earth_coordinates 125 | delta_lat = delta_x / EARTH_MEAN_RADIUS 126 | delta_lon = delta_y / EARTH_MEAN_RADIUS 127 | self._geodetic_coordinates = \ 128 | np.array([self.lat + delta_lat, self.lon + delta_lon, -value[2]]) 129 | 130 | # TODO: make transformation from geodetic to geocentric: 131 | self._geocentric_coordinates = np.zeros(3) # m 132 | 133 | # Update Earth coordinates with value 134 | self._earth_coordinates[:] = value 135 | 136 | def __repr__(self): 137 | rv = (f"x_e: {self.x_earth:.2f} m, y_e: {self.y_earth:.2f} m, " 138 | f"z_e: {self.z_earth:.2f} m") 139 | return rv 140 | 141 | 142 | class GeodeticPosition(Position): 143 | 144 | def __init__(self, lat, lon, height, x_earth=0, y_earth=0): 145 | # TODO: docstring 146 | earth = np.array([x_earth, y_earth, -height]) 147 | # TODO: Assuming round earth use changes in x & y to calculate 148 | # new lat and lon. z_earth is -height: 149 | geodetic = np.array([lat, lon, height]) # m 150 | # TODO: make transformation from geodetic to geocentric: 151 | geocentric = np.zeros(3) # m 152 | super().__init__(geodetic, geocentric, earth) 153 | 154 | def update(self, value): 155 | # Assuming round earth use changes in x & y to calculate 156 | # new x, y from lat and lon. z_earth is -height 157 | delta_lat, delta_lon, _ = self.geodetic_coordinates - value 158 | dx_e = EARTH_MEAN_RADIUS * delta_lat 159 | dy_e = EARTH_MEAN_RADIUS * delta_lon 160 | self._earth_coordinates[:] = \ 161 | np.array([self.x_earth + dx_e, self.y_earth + dy_e, -value[2]]) 162 | 163 | # TODO: make transformation from geodetic to geocentric: 164 | self._geocentric_coordinates = np.zeros(3) # m 165 | 166 | # Update geodetic coordinates with value 167 | self._geodetic_coordinates[:] = value 168 | 169 | 170 | # class GeocentricPosition(Position): 171 | # TODO: 172 | # raise NotImplementedError 173 | -------------------------------------------------------------------------------- /src/pyfme/models/state/velocity.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Flight Mechanics Engine (PyFME). 3 | Copyright (c) AeroPython Development Team. 4 | Distributed under the terms of the MIT License. 5 | 6 | Velocity 7 | -------- 8 | 9 | The aircraft state has 10 | """ 11 | from abc import abstractmethod 12 | 13 | import numpy as np 14 | 15 | 16 | # TODO: think about generic changes from body to horizon that could be used for 17 | # velocity, accelerations... 18 | # If also changes from attitude of elements in the body (such as sensors) to 19 | # body and horizon coordinates are implemented it would be useful! 20 | from pyfme.utils.coordinates import body2hor, hor2body 21 | 22 | 23 | class Velocity: 24 | """Velocity 25 | 26 | Attributes 27 | ---------- 28 | 29 | vel_body : ndarray, shape(3) 30 | (u [m/s], v [m/s], w [m/s]) 31 | u 32 | v 33 | w 34 | vel_NED : ndarray, shape(3) 35 | (v_north [m/s], v_east [m/s], v_down [m/s]) 36 | v_north 37 | v_east 38 | v_down 39 | """ 40 | 41 | def __init__(self): 42 | # Body axis 43 | self._vel_body = np.zeros(3) # m/s 44 | # Local horizon (NED) 45 | self._vel_NED = np.zeros(3) # m/s 46 | 47 | @abstractmethod 48 | def update(self, coords, attitude): 49 | raise NotImplementedError 50 | 51 | @property 52 | def vel_body(self): 53 | return self._vel_body 54 | 55 | @property 56 | def u(self): 57 | return self.vel_body[0] 58 | 59 | @property 60 | def v(self): 61 | return self.vel_body[1] 62 | 63 | @property 64 | def w(self): 65 | return self.vel_body[2] 66 | 67 | @property 68 | def vel_NED(self): 69 | return self._vel_NED 70 | 71 | @property 72 | def v_north(self): 73 | return self._vel_NED[0] 74 | 75 | @property 76 | def v_east(self): 77 | return self._vel_NED[1] 78 | 79 | @property 80 | def v_down(self): 81 | return self._vel_NED[2] 82 | 83 | @property 84 | def value(self): 85 | """Only for testing purposes""" 86 | return np.hstack((self.vel_body, self.vel_NED)) 87 | 88 | 89 | class BodyVelocity(Velocity): 90 | 91 | def __init__(self, u, v, w, attitude): 92 | # TODO: docstring 93 | super().__init__() 94 | self.update(np.array([u, v, w]), attitude) 95 | 96 | def update(self, value, attitude): 97 | self._vel_body[:] = value 98 | self._vel_NED = body2hor(value, 99 | attitude.theta, 100 | attitude.phi, 101 | attitude.psi) # m/s 102 | 103 | def __repr__(self): 104 | return f"u: {self.u:.2f} m/s, v: {self.v:.2f} m/s, w: {self.w:.2f} m/s" 105 | 106 | 107 | class NEDVelocity(Velocity): 108 | def __init__(self, vn, ve, vd, attitude): 109 | # TODO: docstring 110 | super().__init__() 111 | self.update(np.array([vn, ve, vd]), attitude) 112 | 113 | def update(self, value, attitude): 114 | self._vel_NED[:] = value 115 | self._vel_body = hor2body(value, 116 | attitude.theta, 117 | attitude.phi, 118 | attitude.psi) # m/s 119 | 120 | def __repr__(self): 121 | return (f"V_north: {self.v_north:.2f} m/s," 122 | f"V_east: {self.v_east:.2f} m/s, " 123 | f"V_down: {self.v_down:.2f} m/s") 124 | -------------------------------------------------------------------------------- /src/pyfme/models/tests/test_euler_flat_earth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Tests of equations of euler flat earth model. 4 | """ 5 | 6 | import pytest 7 | import numpy as np 8 | 9 | from pyfme.models.euler_flat_earth import _system_equations, EulerFlatEarth 10 | from pyfme.models.state import (EarthPosition, EulerAttitude, BodyVelocity, 11 | BodyAngularVelocity, BodyAcceleration, 12 | BodyAngularAcceleration, AircraftState) 13 | from pyfme.models.constants import EARTH_MEAN_RADIUS 14 | 15 | 16 | pos = EarthPosition(0, 0, 2000) 17 | att = EulerAttitude(5/180*np.pi, 15/180*np.pi, 45/180*np.pi) 18 | vel = BodyVelocity(50, 2, 3, att) 19 | ang_vel = BodyAngularVelocity(1/180*np.pi, 5/180*np.pi, 5/180*np.pi, att) 20 | accel = BodyAcceleration(0, 0, 0, att) 21 | ang_accel = BodyAngularAcceleration(0, 0, 0, att) 22 | 23 | full_state = AircraftState(pos, att, vel, ang_vel, accel, ang_accel) 24 | 25 | 26 | def test_system_equations(): 27 | time = 0 28 | state_vector = np.array( 29 | [1, 1, 1, 1, 1, 1, 30 | np.pi / 4, np.pi / 4, 0, 31 | 1, 1, 1], 32 | dtype=float 33 | ) 34 | 35 | mass = 10 36 | inertia = np.array([[1000, 0, -100], 37 | [ 0, 100, 0], 38 | [-100, 0, 100]], dtype=float) 39 | 40 | forces = np.array([100., 100., 100.], dtype=float) 41 | moments = np.array([100., 1000., 100], dtype=float) 42 | 43 | exp_sol = np.array( 44 | [10, 10, 10, 11. / 9, 1, 92. / 9, 45 | 0, 1 + 2 ** 0.5, 2, 46 | 1 + (2 ** 0.5) / 2, 0, 1 - (2 ** 0.5) / 2], 47 | dtype=float 48 | ) 49 | sol = _system_equations(time, state_vector, mass, inertia, forces, moments) 50 | np.testing.assert_allclose(sol, exp_sol, rtol=1e-7, atol=1e-15) 51 | 52 | 53 | def test_fun_raises_error_if_no_update_simulation_is_defined(): 54 | system = EulerFlatEarth(t0=0, full_state=full_state) 55 | x = np.zeros_like(system.state_vector) 56 | with pytest.raises(TypeError): 57 | system.fun(t=0, x=x) 58 | 59 | 60 | def test_update_full_system_state_from_state(): 61 | system = EulerFlatEarth(t0=0, full_state=full_state) 62 | 63 | x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], dtype=float) 64 | x_dot = np.array([13, 14, 15, 16, 17, 18]) 65 | 66 | # Lat and lon after update in EarthFlatEarth are calculated from 67 | # delta_x, and delta_y with Earth mean radius, so depends on the 68 | # previous state. 69 | # If this test broke after changing the way lat and lon are updated from 70 | # Earth coordinates (ie. taking into account only the first point and 71 | # current status) you are in the right place 72 | dlat = (x[9] - system.full_state.position.x_earth) / EARTH_MEAN_RADIUS 73 | dlon = (x[10] - system.full_state.position.y_earth) / EARTH_MEAN_RADIUS 74 | 75 | system._update_full_system_state_from_state(x, x_dot) 76 | 77 | exp_pos = EarthPosition(10, 11, -12, lat=dlat, lon=dlon) 78 | exp_att = EulerAttitude(7, 8, 9) 79 | exp_vel = BodyVelocity(1, 2, 3, exp_att) 80 | exp_ang_vel = BodyAngularVelocity(4, 5, 6, exp_att) 81 | exp_accel = BodyAcceleration(13, 14, 15, exp_att) 82 | exp_ang_accel = BodyAngularAcceleration(16, 17, 18, exp_att) 83 | 84 | exp_full_state = AircraftState(exp_pos, exp_att, exp_vel, exp_ang_vel, 85 | exp_accel, exp_ang_accel) 86 | 87 | np.testing.assert_allclose(system.full_state.value, exp_full_state.value) 88 | 89 | 90 | def test_get_state_vector_from_full_state(): 91 | 92 | system = EulerFlatEarth(0, full_state) 93 | 94 | x = np.array([50, 2, 3, 95 | 1/180*np.pi, 5/180*np.pi, 5/180*np.pi, 96 | 5/180*np.pi, 15/180*np.pi, 45/180*np.pi, 97 | 0, 0, -2000]) 98 | 99 | np.testing.assert_allclose(system.state_vector, x) 100 | -------------------------------------------------------------------------------- /src/pyfme/prop/tests/test_prop.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AeroPython/PyFME/156fa9f1db097f107c20ad7354c71b1eaee4cbb1/src/pyfme/prop/tests/test_prop.py -------------------------------------------------------------------------------- /src/pyfme/simulator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Flight Mechanics Engine (PyFME). 3 | Copyright (c) AeroPython Development Team. 4 | Distributed under the terms of the MIT License. 5 | 6 | Simulation class 7 | ---------------- 8 | Select the simulation configuration based on a system (and its dynamic 9 | model), environment and aircraft. 10 | 11 | """ 12 | import copy 13 | import operator 14 | 15 | import pandas as pd 16 | import tqdm 17 | 18 | 19 | class Simulation: 20 | """ 21 | Simulation class stores the simulation configuration, aircraft, system and 22 | environment. It provides methods for simulation running and results 23 | storing. 24 | 25 | Attributes 26 | ---------- 27 | system : System 28 | System object with mathematical model of the dynamic system and 29 | integrator (ie. EulerFlatEarth) 30 | aircraft : Aircraft 31 | Aircraft model, where aerodynamics and forces are calculated 32 | environment : Environment 33 | Environment containing the atmosphere, gravity and wind models. 34 | controls : dict of callable 35 | Dictionary containing the control names as keys and functions of 36 | time as values. 37 | results : dict of lists 38 | Dictionary containing the variables that have been set to be saved 39 | during the simulation. 40 | """ 41 | 42 | _default_save_vars = { 43 | 'time': 'system.time', 44 | # environment 45 | 'temperature': 'environment.T', 46 | 'pressure': 'environment.p', 47 | 'rho': 'environment.rho', 48 | 'a': 'environment.a', 49 | # aircraft 50 | 'Fx': 'aircraft.Fx', 51 | 'Fy': 'aircraft.Fy', 52 | 'Fz': 'aircraft.Fz', 53 | 54 | 'Mx': 'aircraft.Mx', 55 | 'My': 'aircraft.My', 56 | 'Mz': 'aircraft.Mz', 57 | 58 | 'TAS': 'aircraft.TAS', 59 | 'Mach': 'aircraft.Mach', 60 | 'q_inf': 'aircraft.q_inf', 61 | 62 | 'alpha': 'aircraft.alpha', 63 | 'beta': 'aircraft.beta', 64 | 65 | 'rudder': 'aircraft.delta_rudder', 66 | 'aileron': 'aircraft.delta_aileron', 67 | 'elevator': 'aircraft.delta_elevator', 68 | 'thrust': 'aircraft.delta_t', 69 | # system 70 | 'x_earth': 'system.full_state.position.x_earth', 71 | 'y_earth': 'system.full_state.position.y_earth', 72 | 'z_earth': 'system.full_state.position.z_earth', 73 | 74 | 'height': 'system.full_state.position.height', 75 | 76 | 'psi': 'system.full_state.attitude.psi', 77 | 'theta': 'system.full_state.attitude.theta', 78 | 'phi': 'system.full_state.attitude.phi', 79 | 80 | 'u': 'system.full_state.velocity.u', 81 | 'v': 'system.full_state.velocity.v', 82 | 'w': 'system.full_state.velocity.w', 83 | 84 | 'v_north': 'system.full_state.velocity.v_north', 85 | 'v_east': 'system.full_state.velocity.v_east', 86 | 'v_down': 'system.full_state.velocity.v_down', 87 | 88 | 'p': 'system.full_state.angular_vel.p', 89 | 'q': 'system.full_state.angular_vel.q', 90 | 'r': 'system.full_state.angular_vel.r' 91 | } 92 | 93 | def __init__(self, aircraft, system, environment, controls, dt=0.01, 94 | save_vars=None): 95 | """ 96 | Simulation object 97 | 98 | Parameters 99 | ---------- 100 | aircraft : Aircraft 101 | Aircraft model 102 | system : System 103 | System model 104 | environment : Environment 105 | Environment model. 106 | save_vars : dict, opt 107 | Dictionary containing the names of the variables to be saved and 108 | the object and attribute where it is calculated. If not given, the 109 | ones set in `_default_save_vars` are used. 110 | """ 111 | self.system = copy.deepcopy(system) 112 | self.aircraft = copy.deepcopy(aircraft) 113 | self.environment = copy.deepcopy(environment) 114 | 115 | self.system.update_simulation = self.update 116 | 117 | self.controls = controls 118 | 119 | self.dt = dt 120 | 121 | if not save_vars: 122 | self._save_vars = self._default_save_vars 123 | # Initialize results structure 124 | self.results = {name: [] for name in self._save_vars} 125 | 126 | @property 127 | def time(self): 128 | return self.system.time 129 | 130 | def update(self, time, state): 131 | self.environment.update(state) 132 | 133 | controls = self._get_current_controls(time) 134 | 135 | self.aircraft.calculate_forces_and_moments( 136 | state, 137 | self.environment, 138 | controls 139 | ) 140 | return self 141 | 142 | def propagate(self, time): 143 | """Run the simulation by integrating the system until time t. 144 | 145 | Parameters 146 | ---------- 147 | time : float 148 | Final time of the simulation 149 | 150 | Notes 151 | ----- 152 | The propagation relies on the dense output of the integration 153 | method, so that the number and length of the time steps is 154 | automatically chosen. 155 | """ 156 | dt = self.dt 157 | half_dt = self.dt/2 158 | 159 | bar = tqdm.tqdm(total=time, desc='time', initial=self.system.time) 160 | 161 | # To deal with floating point issues we cannot check equality to 162 | # final time to finish propagation 163 | time_plus_half_dt = time + half_dt 164 | while self.system.time + dt < time_plus_half_dt: 165 | t = self.system.time 166 | self.environment.update(self.system.full_state) 167 | controls = self._get_current_controls(t) 168 | self.aircraft.calculate_forces_and_moments(self.system.full_state, 169 | self.environment, 170 | controls) 171 | self.system.time_step(dt) 172 | self._save_time_step() 173 | bar.update(dt) 174 | 175 | bar.close() 176 | 177 | results = pd.DataFrame(self.results) 178 | results.set_index('time', inplace=True) 179 | 180 | return results 181 | 182 | def _save_time_step(self): 183 | """Saves the selected variables for the current system, environment 184 | and aircraft. 185 | """ 186 | for var_name, value_pointer in self._save_vars.items(): 187 | self.results[var_name].append( 188 | operator.attrgetter(value_pointer)(self) 189 | ) 190 | 191 | def _get_current_controls(self, time): 192 | """Get the control values for the current time step for the given 193 | input functions. 194 | 195 | Parameters 196 | ---------- 197 | time : float 198 | Current time value. 199 | 200 | Returns 201 | ------- 202 | controls : dict 203 | Control value for each control 204 | 205 | Notes 206 | ----- 207 | Current controls are only a function of time in this kind of 208 | simulation (predefined inputs). However, if the AP is active, 209 | controls will be also function of the system state and environment. 210 | """ 211 | c = {c_name: c_fun(time) for c_name, c_fun in self.controls.items()} 212 | return c 213 | -------------------------------------------------------------------------------- /src/pyfme/tests/test_pyfme.py: -------------------------------------------------------------------------------- 1 | from pyfme.aircrafts import Cessna172 2 | from pyfme.environment.environment import Environment 3 | from pyfme.environment.atmosphere import ISA1976 4 | from pyfme.environment.gravity import VerticalConstant 5 | from pyfme.environment.wind import NoWind 6 | from pyfme.models.state.position import EarthPosition 7 | from pyfme.utils.trimmer import steady_state_trim 8 | from pyfme.models import EulerFlatEarth 9 | from pyfme.simulator import Simulation 10 | 11 | from pyfme.utils.input_generator import Constant, Doublet 12 | 13 | 14 | def test_simulation(): 15 | 16 | atmosphere = ISA1976() 17 | gravity = VerticalConstant() 18 | wind = NoWind() 19 | 20 | environment = Environment(atmosphere, gravity, wind) 21 | aircraft = Cessna172() 22 | 23 | initial_position = EarthPosition(0, 0, 1000) 24 | 25 | controls_0 = {'delta_elevator': 0.05, 26 | 'delta_aileron': 0, 27 | 'delta_rudder': 0, 28 | 'delta_t': 0.5, 29 | } 30 | 31 | trimmed_state, trimmed_controls = steady_state_trim( 32 | aircraft, environment, initial_position, psi=1, TAS=50, 33 | controls=controls_0 34 | ) 35 | 36 | system = EulerFlatEarth(t0=0, full_state=trimmed_state) 37 | 38 | controls = { 39 | 'delta_elevator': Doublet(2, 1, 0.1, 40 | trimmed_controls['delta_elevator']), 41 | 'delta_aileron': Constant(trimmed_controls['delta_aileron']), 42 | 'delta_rudder': Constant(trimmed_controls['delta_rudder']), 43 | 'delta_t': Constant(trimmed_controls['delta_t']) 44 | } 45 | 46 | simulation = Simulation(aircraft, system, environment, controls) 47 | simulation.propagate(10) 48 | -------------------------------------------------------------------------------- /src/pyfme/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AeroPython/PyFME/156fa9f1db097f107c20ad7354c71b1eaee4cbb1/src/pyfme/utils/__init__.py -------------------------------------------------------------------------------- /src/pyfme/utils/altimetry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Altimetry 8 | --------- 9 | Functions which transform geometric altitude into geopotential altitude, and 10 | vice versa. 11 | """ 12 | 13 | from pyfme.models.constants import EARTH_MEAN_RADIUS 14 | 15 | Re = EARTH_MEAN_RADIUS 16 | 17 | 18 | def geopotential2geometric(h): 19 | """Geometric altitude above MSL (mean sea level) 20 | 21 | This function transforms geopotential altitude into geometric altitude. 22 | 23 | Parameters 24 | ---------- 25 | h : float 26 | Geopotential altitude above MSL (mean sea level) (m) 27 | Returns 28 | ------- 29 | z : float 30 | Geometric altitude above MSL (mean sea level) (m) 31 | 32 | See Also 33 | -------- 34 | geopotential 35 | 36 | References 37 | ---------- 38 | .. [1] International Organization for Standardization, Standard Atmosphere, 39 | ISO 2533:1975, 1975 40 | 41 | """ 42 | 43 | z = Re * h / (Re - h) 44 | return z 45 | 46 | 47 | def geometric2geopotential(z): 48 | """Geopotential altitude above MSL (mean sea level) 49 | 50 | This function transforms geometric altitude into geopotential altitude. 51 | 52 | Parameters 53 | ---------- 54 | z : float 55 | Geometric altitude above MSL (mean sea level) (m) 56 | Returns 57 | ------- 58 | h : float 59 | Geopotential altitude above MSL (mean sea level) (m) 60 | 61 | See Also 62 | -------- 63 | geometric 64 | 65 | References 66 | ---------- 67 | .. [1] International Organization for Standardization, Standard Atmosphere, 68 | ISO 2533:1975, 1975 69 | 70 | """ 71 | 72 | h = Re * z / (Re + z) 73 | return h 74 | -------------------------------------------------------------------------------- /src/pyfme/utils/anemometry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Anemometry related functions (to be completed) 8 | ---------------------------- 9 | Set of functions which allows to obtain the True Airspeed (TAS), the 10 | Equivalent airspeed (EAS) or the Calibrated Airspeed (CAS) known one of the 11 | others. 12 | 13 | True Airspeed (TAS): is the speed of the aircraft relative to the 14 | airmass in which it is flying. 15 | 16 | Equivalent Airspeed (EAS): is the airspeed at sea level in the 17 | International Standard Atmosphere at which the dynamic pressure 18 | is the same as the dynamic pressure at the true airspeed (TAS) and 19 | altitude at which the aircraft is flying. 20 | 21 | Calibrated airspeed (CAS) is the speed shown by a conventional 22 | airspeed indicator after correction for instrument error and 23 | position error. 24 | """ 25 | 26 | from math import asin, atan, sqrt 27 | from pyfme.models.constants import RHO_0, P_0, SOUND_VEL_0, GAMMA_AIR 28 | 29 | 30 | rho_0 = RHO_0 # density at sea level (kg/m3) 31 | p_0 = P_0 # pressure at sea level (Pa) 32 | a_0 = SOUND_VEL_0 # sound speed at sea level (m/s) 33 | gamma = GAMMA_AIR # heat capacity ratio 34 | 35 | 36 | def calculate_alpha_beta_TAS(u, v, w): 37 | """ 38 | Calculate the angle of attack (AOA), angle of sideslip (AOS) and true air 39 | speed from the **aerodynamic velocity** in body coordinates. 40 | 41 | Parameters 42 | ---------- 43 | u : float 44 | x-axis component of aerodynamic velocity. (m/s) 45 | v : float 46 | y-axis component of aerodynamic velocity. (m/s) 47 | w : float 48 | z-axis component of aerodynamic velocity. (m/s) 49 | 50 | Returns 51 | ------- 52 | alpha : float 53 | Angle of attack (rad). 54 | beta : float 55 | Angle of sideslip (rad). 56 | TAS : float 57 | True Air Speed. (m/s) 58 | 59 | Notes 60 | ----- 61 | See [1] or [2] for frame of reference definition. 62 | See [3] for formula derivation. 63 | 64 | $$ TAS = sqrt(u^2 + v^2 + w^2)$$ 65 | 66 | $$ alpha = \atan(w / u) $$ 67 | $$ beta = \asin(v / TAS) $$ 68 | 69 | References 70 | ---------- 71 | .. [1] B. Etkin, "Dynamics of Atmospheric Flight," Courier Corporation, 72 | pp. 104-120, 2012. 73 | .. [2] Gómez Tierno, M.A. et al, "Mecánica del Vuelo," Garceta, pp. 1-12, 74 | 2012. 75 | .. [3] Stevens, BL and Lewis, FL, "Aircraft Control and Simulation", 76 | Wiley-lnterscience, pp. 64, 1992. 77 | """ 78 | 79 | TAS = sqrt(u ** 2 + v ** 2 + w ** 2) 80 | 81 | alpha = atan(w / u) 82 | beta = asin(v / TAS) 83 | 84 | return alpha, beta, TAS 85 | 86 | 87 | def calculate_dynamic_pressure(rho, TAS): 88 | """Calculates the dynamic pressure. 89 | 90 | Parameters 91 | ---------- 92 | rho : float 93 | Air density (kg/m³). 94 | TAS : float 95 | True Air Speed (m/s). 96 | 97 | Returns 98 | ------- 99 | q_inf : float 100 | Dynamic pressure. (Pa) 101 | 102 | Notes 103 | ----- 104 | $$ q_{inf} = 1/2 · rho · TAS² $$ 105 | """ 106 | 107 | return 0.5 * rho * TAS ** 2 108 | 109 | 110 | def calculate_viscosity_Sutherland(T): 111 | """Calculates the viscosity of the air 112 | 113 | Parameters 114 | ----------- 115 | T : float 116 | Temperature (K) 117 | 118 | Returns 119 | ----------- 120 | visc : float 121 | viscosity of the air (kg/(m s)) 122 | 123 | Notes 124 | ----------- 125 | AcCording to [1] the limits for this function are: 126 | 127 | p < p_c =36 Atm (3.65 MPa) 128 | T < 2000 K 129 | 130 | According to [2] the limits for this function are: 131 | 132 | T < 550 K 133 | 134 | 135 | """ 136 | 137 | visc_0 = 1.176*1e-5 # kg(m s) 138 | T_0 = 273.1 # K 139 | b = 0.4042 # non-dimensional 140 | 141 | return visc_0 * (T / T_0)**(3 / 2) * ((1 + b)/((T / T_0) + b)) 142 | 143 | 144 | def tas2eas(tas, rho): 145 | """Given the True Airspeed, this function provides the Equivalent Airspeed. 146 | 147 | True Airspeed (TAS): is the speed of the aircraft relative to the 148 | airmass in which it is flying. 149 | 150 | Equivalent Airspeed (EAS): is the airspeed at sea level in the 151 | International Standard Atmosphere at which the dynamic pressure 152 | is the same as the dynamic pressure at the true airspeed (TAS) and 153 | altitude at which the aircraft is flying. 154 | 155 | Parameters 156 | ---------- 157 | tas : float 158 | True Airspeed (TAS) (m/s) 159 | rho : float 160 | Air density at flight level (kg/m3) 161 | Returns 162 | ------- 163 | eas : float 164 | Equivalent Airspeed (EAS) (m/s) 165 | """ 166 | eas = tas * sqrt(rho / rho_0) 167 | 168 | return eas 169 | 170 | 171 | def eas2tas(eas, rho): 172 | """Given the Equivalent Airspeed, this function provides the True Airspeed. 173 | 174 | True Airspeed (TAS): is the speed of the aircraft relative to the 175 | airmass in which it is flying. 176 | 177 | Equivalent Airspeed (EAS): is the airspeed at sea level in the 178 | International Standard Atmosphere at which the dynamic pressure 179 | is the same as the dynamic pressure at the true airspeed (TAS) and 180 | altitude at which the aircraft is flying. 181 | 182 | Parameters 183 | ---------- 184 | eas : float 185 | Equivalent Airspeed (EAS) (m/s) 186 | rho : float 187 | Air density at flight level (kg/m3) 188 | Returns 189 | ------- 190 | tas : float 191 | True Airspeed (TAS) (m/s) 192 | """ 193 | 194 | tas = eas / sqrt(rho / rho_0) 195 | 196 | return tas 197 | 198 | 199 | def tas2cas(tas, p, rho): 200 | """Given the True Airspeed, this function provides the Calibrated Airspeed. 201 | 202 | True Airspeed (TAS): is the speed of the aircraft relative to the 203 | airmass in which it is flying. 204 | 205 | Calibrated airspeed (CAS) is the speed shown by a conventional 206 | airspeed indicator after correction for instrument error and 207 | position error. 208 | 209 | Parameters 210 | ---------- 211 | tas : float 212 | True Airspeed (TAS) (m/s) 213 | p : float 214 | Air static pressure at flight level (Pa) 215 | rho : float 216 | Air density at flight level (kg/m3) 217 | Returns 218 | ------- 219 | cas : float 220 | Calibrated Airspeed (CAS) (m/s) 221 | """ 222 | 223 | a = sqrt(gamma * p / rho) 224 | var = (gamma - 1) / gamma 225 | 226 | temp = (tas**2 * (gamma - 1) / (2 * a**2) + 1) ** (1/var) 227 | temp = (temp - 1) * (p / p_0) 228 | temp = (temp + 1) ** var - 1 229 | 230 | cas = sqrt(2 * a_0 ** 2 / (gamma - 1) * temp) 231 | 232 | return cas 233 | 234 | 235 | def cas2tas(cas, p, rho): 236 | """Given the Calibrated Airspeed, this function provides the True Airspeed. 237 | 238 | True Airspeed (TAS): is the speed of the aircraft relative to the 239 | airmass in which it is flying. 240 | 241 | Calibrated airspeed (CAS) is the speed shown by a conventional 242 | airspeed indicator after correction for instrument error and 243 | position error. 244 | 245 | Parameters 246 | ---------- 247 | cas : float 248 | Calibrated Airspeed (CAS) (m/s) 249 | p : float 250 | Air static pressure at flight level (Pa) 251 | rho : float 252 | Air density at flight level (kg/m3) 253 | Returns 254 | ------- 255 | tas : float 256 | True Airspeed (TAS) (m/s) 257 | """ 258 | 259 | a = sqrt(gamma * p / rho) 260 | var = (gamma - 1) / gamma 261 | 262 | temp = (cas**2 * (gamma - 1) / (2 * a_0**2) + 1) ** (1/var) 263 | temp = (temp - 1) * (p_0 / p) 264 | temp = (temp + 1) ** var - 1 265 | 266 | tas = sqrt(2 * a ** 2 / (gamma - 1) * temp) 267 | 268 | return tas 269 | 270 | 271 | def cas2eas(cas, p, rho): 272 | """Given the Calibrated Airspeed, this function provides the Equivalent 273 | Airspeed. 274 | 275 | Calibrated airspeed (CAS) is the speed shown by a conventional 276 | airspeed indicator after correction for instrument error and 277 | position error. 278 | 279 | Equivalent Airspeed (EAS): is the airspeed at sea level in the 280 | International Standard Atmosphere at which the dynamic pressure 281 | is the same as the dynamic pressure at the true airspeed (TAS) and 282 | altitude at which the aircraft is flying. 283 | 284 | Parameters 285 | ---------- 286 | cas : float 287 | Calibrated Airspeed (CAS) (m/s) 288 | p : float 289 | Air static pressure at flight level (Pa) 290 | rho : float 291 | Air density at flight level (kg/m3) 292 | Returns 293 | ------- 294 | eas : float 295 | Equivalent Airspeed (EAS) (m/s) 296 | """ 297 | 298 | tas = cas2tas(cas, p, rho) 299 | 300 | eas = tas2eas(tas, rho) 301 | 302 | return eas 303 | 304 | 305 | def eas2cas(eas, p, rho): 306 | """Given the Equivalent Airspeed, this function provides the Calibrated 307 | Airspeed. 308 | 309 | Calibrated airspeed (CAS) is the speed shown by a conventional 310 | airspeed indicator after correction for instrument error and 311 | position error. 312 | 313 | Equivalent Airspeed (EAS): is the airspeed at sea level in the 314 | International Standard Atmosphere at which the dynamic pressure 315 | is the same as the dynamic pressure at the true airspeed (TAS) and 316 | altitude at which the aircraft is flying. 317 | 318 | Parameters 319 | ---------- 320 | eas : float 321 | Equivalent Airspeed (EAS) (m/s) 322 | p : float 323 | Air static pressure at flight level (Pa) 324 | rho : float 325 | Air density at flight level (kg/m3) 326 | Returns 327 | ------- 328 | cas : float 329 | Calibrated Airspeed (CAS) (m/s) 330 | """ 331 | 332 | tas = eas2tas(eas, rho) 333 | 334 | cas = tas2cas(tas, p, rho) 335 | 336 | return cas 337 | 338 | 339 | def stagnation_pressure(p, a, tas): 340 | """Given the static pressure, the sound velocity and the true air speed, 341 | it returns the stagnation pressure for a compressible flow. 342 | 343 | The stagnation pressure is the is the static pressure a fluid retains when 344 | brought to rest isoentropically from Mach number M 345 | 346 | Subsonic case: Bernouilli's equation compressible form. 347 | 348 | Supersonic case: Due to the shock wave Bernouilli's equation is no longer 349 | applicable. Rayleigh Pitot tube formula is used. 350 | 351 | Parameters 352 | ---------- 353 | tas : float 354 | True Airspeed (TAS) (m/s) 355 | p : float 356 | Air static pressure at flight level (Pa) 357 | a : float 358 | sound speed at flight level (m/s) 359 | Returns 360 | ------- 361 | p_stagnation : float 362 | Stagnation pressure at flight level (Pa) 363 | References 364 | ---------- 365 | .. [1] http://www.dept.aoe.vt.edu/~lutze/AOE3104/airspeed.pdf 366 | """ 367 | 368 | var = (gamma - 1) / gamma 369 | M = tas/a 370 | 371 | if M < 1: 372 | p_stagnation = 1 + (gamma-1) * (M**2) / 2 373 | p_stagnation **= (1/var) 374 | p_stagnation *= p 375 | else: 376 | p_stagnation = (gamma+1)**2 * M**2 377 | p_stagnation /= (4*gamma*(M**2) - 2*(gamma-1)) 378 | p_stagnation **= (1/var) 379 | p_stagnation *= (1 - gamma + 2*gamma*(M**2)) / (gamma+1) 380 | p_stagnation *= p 381 | 382 | return p_stagnation 383 | -------------------------------------------------------------------------------- /src/pyfme/utils/change_euler_quaternion.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Tue Jan 12 22:34:43 2016 4 | 5 | @author: Juan 6 | """ 7 | 8 | from math import atan2, asin, cos, sin 9 | import numpy as np 10 | 11 | 12 | def quatern2euler(quaternion): 13 | '''Given a quaternion, the euler_angles vector is returned. 14 | 15 | Parameters 16 | ---------- 17 | quaternion : array_like 18 | 1x4 vector with the four elements of the quaternion: 19 | [q_0, q_1, q_2, q_3] 20 | 21 | Returns 22 | ------- 23 | euler_angles : array_like 24 | 1x3 array with the euler angles: [theta, phi, psi] (rad) 25 | 26 | References 27 | ---------- 28 | .. [1] "Modeling and Simulation of Aerospace Vehicle Dynamics" (Aiaa\ 29 | Education Series) Peter H. Ziepfel 30 | ''' 31 | check_unitnorm(quaternion) 32 | 33 | q_0, q_1, q_2, q_3 = quaternion 34 | 35 | check_unitnorm(quaternion) 36 | 37 | psi = atan2(2 * (q_1 * q_2 + q_0 * q_3), 38 | q_0 ** 2 + q_1 ** 2 - q_2 ** 2 - q_3 ** 2) 39 | 40 | theta = asin(-2 * (q_1 * q_3 - q_0 * q_2)) 41 | 42 | phi = atan2(2 * (q_2 * q_3 + q_0 * q_1), 43 | q_0 ** 2 + q_1 ** 2 - q_2 ** 2 - q_3 ** 2) 44 | 45 | euler_angles = np.array([theta, phi, psi]) 46 | 47 | return euler_angles 48 | 49 | 50 | def euler2quatern(euler_angles): 51 | '''Given the euler_angles vector, the quaternion vector is returned. 52 | 53 | Parameters 54 | ---------- 55 | euler_angles : array_like 56 | 1x3 array with the euler angles: [theta, phi, psi] (rad) 57 | 58 | Returns 59 | ------- 60 | quaternion : array_like 61 | 1x4 vector with the four elements of the quaternion: 62 | [q_0, q_1, q_2, q_3] 63 | 64 | References 65 | ---------- 66 | .. [1] "Modeling and Simulation of Aerospace Vehicle Dynamics" (Aiaa\ 67 | Education Series) Peter H. Ziepfel 68 | ''' 69 | theta, phi, psi = euler_angles 70 | 71 | q_0 = cos(psi / 2.) * cos(theta / 2.) * cos(phi / 2.) +\ 72 | sin(psi / 2) * sin(theta / 2) * sin(phi / 2) 73 | 74 | q_1 = cos(psi / 2.) * cos(theta / 2.) * sin(phi / 2.) -\ 75 | sin(psi / 2.) * sin(theta / 2.) * cos(phi / 2.) 76 | 77 | q_2 = cos(psi / 2) * sin(theta / 2.) * cos(phi / 2.) +\ 78 | sin(psi / 2.) * cos(theta / 2.) * sin(phi / 2.) 79 | 80 | q_3 = sin(psi / 2.) * cos(theta / 2.) * cos(phi / 2.) -\ 81 | cos(psi / 2.) * sin(theta / 2.) * sin(phi / 2.) 82 | 83 | quaternion = np.array([q_0, q_1, q_2, q_3]) 84 | 85 | return quaternion 86 | 87 | 88 | def vel_quaternion(quaternion, ang_vel): 89 | '''Given the angular velocity vector and the quaternion, 90 | the derivative of the quaternion vector is returned. 91 | 92 | Parameters 93 | ---------- 94 | quaternion : array_like 95 | 1x4 vector with the four elements of the quaternion: q_0, q_1, q_2, q_3 96 | 97 | ang_vel : array_like 98 | (p, q, r) Absolute angular velocity of aircraft with respect to 99 | inertial frame of ref in body axes coordinates 100 | 101 | Returns 102 | ------- 103 | d_quaternion : array_like 104 | 1x4 vector with the derivative of the four elements of the quaternion: 105 | [d_q_0, d_q_1, d_q_2, d_q_3] 106 | 107 | References 108 | ---------- 109 | .. [1] "Modeling and Simulation of Aerospace Vehicle Dynamics" (Aiaa\ 110 | Education Series) Peter H. Ziepfel 111 | ''' 112 | p, q, r = ang_vel 113 | 114 | q_0, q_1, q_2, q_3 = quaternion 115 | 116 | d_q_0 = 0.5 * (-p * q_1 - q * q_2 - r * q_3) 117 | 118 | d_q_1 = 0.5 * (p * q_0 + r * q_2 - q * q_3) 119 | 120 | d_q_2 = 0.5 * (q * q_0 - r * q_1 + p * q_3) 121 | 122 | d_q_3 = 0.5 * (r * q_0 + q * q_1 - p * q_2) 123 | 124 | d_quaternion = np.array([d_q_0, d_q_1, d_q_2, d_q_3]) 125 | 126 | return d_quaternion 127 | 128 | 129 | def check_unitnorm(quaternion): 130 | '''Given a quaternion, it checks the modulus (it must be unit). If it is 131 | not unit, it raises an error. 132 | 133 | Parameters 134 | ---------- 135 | quaternion : array_like 136 | 1x4 vector with the four elements of the quaternion: 137 | [q_0, q_1, q_2, q_3] 138 | Raises 139 | ------ 140 | ValueError: 141 | Selected quaternion norm is not unit 142 | ''' 143 | q_0, q_1, q_2, q_3 = quaternion 144 | 145 | check_value = np.isclose([q_0 ** 2 + q_1 ** 2 + q_2 ** 2 + q_3 ** 2], [1]) 146 | 147 | if not check_value: 148 | raise ValueError('Selected quaternion norm is not unit') 149 | -------------------------------------------------------------------------------- /src/pyfme/utils/input_generator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Inputs generator 8 | ---------------- 9 | Provides some typical inputs signals such as: step, doublet, ramp, harmonic. 10 | """ 11 | from abc import abstractmethod 12 | 13 | from numpy import vectorize, float64 14 | from numpy import sin, pi 15 | 16 | 17 | def vectorize_float(method): 18 | vect_method = vectorize(method, otypes=[float64]) 19 | 20 | def wrapper(self, *args, **kwargs): 21 | return vect_method(self, *args, **kwargs) 22 | 23 | return wrapper 24 | 25 | 26 | # TODO: documentation 27 | class Control(object): 28 | 29 | @abstractmethod 30 | def _fun(self, t): 31 | raise NotImplementedError 32 | 33 | def __call__(self, t): 34 | r = self._fun(t) 35 | # NumPy vecotrize returns an numpy.ndarray object with size 1 and no 36 | # shape if the input is scalar, however in this case, a float is 37 | # expected. 38 | # TODO: 39 | # Numba vectorize does return a scalar, however it does not deal 40 | # with function methods. 41 | if r.size == 1: 42 | return float(r) 43 | else: 44 | return r 45 | 46 | def __add__(self, other): 47 | control = Control() 48 | control._fun = lambda t: self(t) + other(t) 49 | control._vec_fun = vectorize(control._fun, otypes=[float64]) 50 | return control 51 | 52 | def __sub__(self, other): 53 | control = Control() 54 | control._fun = lambda t: self(t) - other(t) 55 | control._vec_fun = vectorize(control._fun, otypes=[float64]) 56 | return control 57 | 58 | def __mul__(self, other): 59 | control = Control() 60 | control._fun = lambda t: self(t) * other(t) 61 | control._vec_fun = vectorize(control._fun, otypes=[float64]) 62 | return control 63 | 64 | 65 | class Constant(Control): 66 | 67 | def __init__(self, offset=0): 68 | self.offset = offset 69 | 70 | @vectorize_float 71 | def _fun(self, t): 72 | return self.offset 73 | 74 | 75 | class Step(Control): 76 | 77 | def __init__(self, t_init, T, A, offset=0): 78 | self.t_init = t_init 79 | self.T = T 80 | self.A = A 81 | self.offset = offset 82 | 83 | self.t_fin = self.t_init + self.T 84 | 85 | @vectorize_float 86 | def _fun(self, t): 87 | value = self.offset 88 | if self.t_init <= t <= self.t_fin: 89 | value += self.A 90 | return value 91 | 92 | 93 | class Doublet(Control): 94 | 95 | def __init__(self, t_init, T, A, offset=0): 96 | self.t_init = t_init 97 | self.T = T 98 | self.A = A 99 | self.offset = offset 100 | 101 | self.t_fin1 = self.t_init + self.T / 2 102 | self.t_fin2 = self.t_init + self.T 103 | 104 | @vectorize_float 105 | def _fun(self, t): 106 | value = self.offset 107 | 108 | if self.t_init <= t < self.t_fin1: 109 | value += self.A / 2 110 | elif self.t_fin1 < t <= self.t_fin2: 111 | value -= self.A / 2 112 | return value 113 | 114 | 115 | class Ramp(Control): 116 | 117 | def __init__(self, t_init, T, A, offset=0): 118 | self.t_init = t_init 119 | self.T = T 120 | self.A = A 121 | self.offset = offset 122 | 123 | self.slope = self.A / self.T 124 | self.t_fin = self.t_init + self.T 125 | 126 | @vectorize_float 127 | def _fun(self, t): 128 | value = self.offset 129 | if self.t_init <= t <= self.t_fin: 130 | value += self.slope * (t - self.t_init) 131 | 132 | return value 133 | 134 | 135 | class Harmonic(Control): 136 | 137 | def __init__(self, t_init, T, A, freq, phase, offset=0): 138 | super().__init__() 139 | self.t_init = t_init 140 | self.t_fin = t_init + T 141 | self.A = A 142 | self.freq = freq 143 | self.phase = phase 144 | self.offset = offset 145 | 146 | @vectorize_float 147 | def _fun(self, t): 148 | value = self.offset 149 | 150 | if self.t_init <= t <= self.t_fin: 151 | value += self.A/2 * sin(2 * pi * self.freq * (t - self.t_init) + 152 | self.phase) 153 | 154 | return value 155 | -------------------------------------------------------------------------------- /src/pyfme/utils/tests/test_altimetry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Tests of altimetry 8 | ------------------ 9 | """ 10 | 11 | from numpy.testing import (assert_almost_equal) 12 | 13 | from pyfme.utils.altimetry import (geopotential2geometric, 14 | geometric2geopotential) 15 | 16 | 17 | def test1_geopotential2geometric(): 18 | 19 | h = 11000.0 20 | 21 | z = geopotential2geometric(h) 22 | expected_z = 11019.025157232705 23 | 24 | assert_almost_equal(z, expected_z) 25 | 26 | 27 | def test2_geopotential2geometric(): 28 | 29 | h = 20000.0 30 | 31 | z = geopotential2geometric(h) 32 | expected_z = 20062.982207526373 33 | 34 | assert_almost_equal(z, expected_z) 35 | 36 | 37 | def test1_geometric2geopotential(): 38 | 39 | z = 0.0 40 | 41 | h = geometric2geopotential(z) 42 | expected_h = 0.0 43 | 44 | assert_almost_equal(h, expected_h) 45 | 46 | 47 | def test2_geometric2geopotential(): 48 | 49 | z = 86000.0 50 | 51 | h = geometric2geopotential(z) 52 | expected_h = 84854.57642868205 53 | 54 | assert_almost_equal(h, expected_h) 55 | -------------------------------------------------------------------------------- /src/pyfme/utils/tests/test_anemometry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Anemometry test functions 8 | ------------------------- 9 | """ 10 | 11 | from math import sqrt, atan, asin 12 | from numpy.testing import assert_almost_equal 13 | 14 | from pyfme.utils.anemometry import (calculate_alpha_beta_TAS, 15 | calculate_dynamic_pressure) 16 | 17 | 18 | def test_calculate_alpha_beta_TAS(): 19 | 20 | u, v, w = 10, 0, 10 21 | 22 | expected_TAS = sqrt(200) 23 | expected_alfa = atan(1) 24 | expected_beta = 0 25 | 26 | alfa, beta, TAS = calculate_alpha_beta_TAS(u, v, w) 27 | 28 | assert_almost_equal((expected_alfa, expected_beta, expected_TAS), 29 | (alfa, beta, TAS)) 30 | 31 | u, v, w = 10, 10, 0 32 | 33 | expected_alfa = 0 34 | expected_beta = asin(10 / sqrt(200)) 35 | 36 | alfa, beta, TAS = calculate_alpha_beta_TAS(u, v, w) 37 | 38 | assert_almost_equal((expected_alfa, expected_beta, expected_TAS), 39 | (alfa, beta, TAS)) 40 | 41 | 42 | def test_calculate_dynamic_pressure(): 43 | 44 | rho = 1 45 | TAS = 1 46 | 47 | expected_q_inf = 0.5 48 | 49 | q_inf = calculate_dynamic_pressure(rho, TAS) 50 | 51 | assert_almost_equal(expected_q_inf, q_inf) 52 | -------------------------------------------------------------------------------- /src/pyfme/utils/tests/test_anemometry_stagnationpressure.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Stagnation pressure test function 8 | --------------------------------- 9 | """ 10 | from numpy.testing import (assert_almost_equal) 11 | 12 | from pyfme.utils.anemometry import stagnation_pressure 13 | from pyfme.environment.atmosphere import ISA1976 14 | 15 | atmosphere = ISA1976() 16 | 17 | 18 | def test_stagnation_pressure(): 19 | 20 | # subsonic case 21 | _, p, _, a = atmosphere(11000) 22 | tas = 240 23 | 24 | # This value is hardcoded from the function result with the current 25 | # constants 26 | 27 | p_stagnation_expected = 34962.95478339375 28 | p_stagnation = stagnation_pressure(p, a, tas) 29 | assert_almost_equal(p_stagnation, p_stagnation_expected) 30 | 31 | # This value is hardcoded from the function result with the current 32 | # constants 33 | 34 | # supersonic case 35 | _, p, _, a = atmosphere(11000) 36 | tas = 400 37 | p_stagnation_expected = 65558.23180026768 38 | 39 | p_stagnation = stagnation_pressure(p, a, tas) 40 | assert_almost_equal(p_stagnation, p_stagnation_expected) 41 | -------------------------------------------------------------------------------- /src/pyfme/utils/tests/test_anemometry_tascaseas.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | TAS CAS EAS conversion test functions 8 | ------------------------------------- 9 | """ 10 | from numpy.testing import (assert_almost_equal) 11 | 12 | 13 | from pyfme.utils.anemometry import (tas2eas, eas2tas, cas2eas, eas2cas, 14 | tas2cas, cas2tas) 15 | from pyfme.models.constants import RHO_0, P_0 16 | from pyfme.environment.atmosphere import ISA1976 17 | 18 | 19 | atmosphere = ISA1976() 20 | 21 | 22 | def test_tas2eas(): 23 | 24 | # sea level 25 | tas = 275 26 | eas_expected = 275 27 | 28 | eas = tas2eas(tas, RHO_0) 29 | 30 | assert_almost_equal(eas, eas_expected) 31 | 32 | # Test at 11000m 33 | _, _, rho, _ = atmosphere(11000) 34 | tas = 275 35 | eas_expected = 149.88797172756003 36 | 37 | eas = tas2eas(tas, rho) 38 | 39 | assert_almost_equal(eas, eas_expected) 40 | 41 | 42 | def test_eas2tas(): 43 | 44 | # sea level 45 | eas = 149.88797172756003 46 | tas_expected = 149.88797172756003 47 | 48 | tas = eas2tas(eas, RHO_0) 49 | 50 | assert_almost_equal(tas, tas_expected) 51 | 52 | # Test at 11000m 53 | _, _, rho, _ = atmosphere(11000) 54 | eas = 149.88797172756003 55 | tas_expected = 275 56 | 57 | tas = eas2tas(eas, rho) 58 | 59 | assert_almost_equal(tas, tas_expected) 60 | 61 | 62 | def test_tas2cas(): 63 | 64 | # sea level 65 | tas = 275 66 | cas_expected = 275 67 | 68 | cas = tas2cas(tas, P_0, RHO_0) 69 | 70 | assert_almost_equal(cas, cas_expected) 71 | 72 | # Test at 11000m 73 | _, p, rho, _ = atmosphere(11000) 74 | tas = 275 75 | cas_expected = 162.03569680495048 76 | 77 | cas = tas2cas(tas, p, rho) 78 | 79 | assert_almost_equal(cas, cas_expected) 80 | 81 | 82 | def test_cas2tas(): 83 | 84 | # sea level 85 | cas = 275 86 | tas_expected = 275 87 | 88 | tas = cas2tas(cas, P_0, RHO_0) 89 | 90 | assert_almost_equal(tas, tas_expected) 91 | 92 | # Test at 11000m 93 | _, p, rho, _ = atmosphere(11000) 94 | cas = 162.03569680495048 95 | tas_expected = 275 96 | 97 | tas = cas2tas(cas, p, rho) 98 | 99 | assert_almost_equal(tas, tas_expected) 100 | 101 | 102 | def test_cas2eas(): 103 | 104 | # sea level 105 | cas = 275 106 | eas_expected = 275 107 | 108 | eas = cas2eas(cas, P_0, RHO_0) 109 | 110 | assert_almost_equal(eas, eas_expected) 111 | 112 | # Test at 11000m 113 | _, p, rho, _ = atmosphere(11000) 114 | cas = 162.03569680495048 115 | eas_expected = 149.88797172756003 116 | 117 | eas = cas2eas(cas, p, rho) 118 | 119 | assert_almost_equal(eas, eas_expected) 120 | 121 | 122 | def test_eas2cas(): 123 | 124 | # sea level 125 | eas = 275 126 | cas_expected = 275 127 | 128 | cas = eas2cas(eas, P_0, RHO_0) 129 | 130 | assert_almost_equal(cas, cas_expected) 131 | 132 | # Test at 11000m 133 | _, p, rho, _ = atmosphere(11000) 134 | eas = 149.88797172756003 135 | cas_expected = 162.03569680495048 136 | 137 | cas = eas2cas(eas, p, rho) 138 | 139 | assert_almost_equal(cas, cas_expected) 140 | -------------------------------------------------------------------------------- /src/pyfme/utils/tests/test_coordinates.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Frames of Reference orientation test functions 8 | ---------------------------------------------- 9 | """ 10 | import pytest 11 | 12 | import numpy as np 13 | from numpy.testing import (assert_array_almost_equal) 14 | 15 | 16 | from pyfme.utils.coordinates import (body2hor, hor2body, 17 | check_theta_phi_psi_range, 18 | hor2wind, wind2hor, 19 | check_gamma_mu_chi_range, 20 | body2wind, wind2body, 21 | check_alpha_beta_range) 22 | 23 | 24 | def test_check_theta_range(): 25 | 26 | wrong_values = (3 * np.pi, - 3 * np.pi) 27 | 28 | for value in wrong_values: 29 | # 0 is always a correct value 30 | with pytest.raises(ValueError) as excinfo: 31 | check_theta_phi_psi_range(value, 0, 0) 32 | assert ("ValueError: Theta value is not inside correct range" 33 | in excinfo.exconly()) 34 | 35 | 36 | def test_check_phi_range(): 37 | 38 | wrong_values = (3 * np.pi, - 3 * np.pi) 39 | 40 | for value in wrong_values: 41 | # 0 is always a correct value 42 | with pytest.raises(ValueError) as excinfo: 43 | check_theta_phi_psi_range(0, value, 0) 44 | assert ("ValueError: Phi value is not inside correct range" 45 | in excinfo.exconly()) 46 | 47 | 48 | def test_check_psi_range(): 49 | 50 | wrong_values = (3 * np.pi, - 3 * np.pi) 51 | 52 | for value in wrong_values: 53 | # 0 is always a correct value 54 | with pytest.raises(ValueError) as excinfo: 55 | check_theta_phi_psi_range(0, 0, value) 56 | assert ("ValueError: Psi value is not inside correct range" 57 | in excinfo.exconly()) 58 | 59 | 60 | def test_body2hor(): 61 | 62 | # Test with a pitch rotation 63 | vector_body = np.array([1, 1, 1]) 64 | theta, phi, psi = np.deg2rad(45), 0, 0 65 | 66 | vector_hor = body2hor(vector_body, theta, phi, psi) 67 | 68 | vector_hor_expected = np.array([2 * 0.70710678118654757, 1, 0]) 69 | 70 | assert_array_almost_equal(vector_hor, vector_hor_expected) 71 | 72 | # Test with a roll rotation 73 | vector_body = np.array([1, 1, 1]) 74 | theta, phi, psi = 0, np.deg2rad(45), 0 75 | 76 | vector_hor = body2hor(vector_body, theta, phi, psi) 77 | 78 | vector_hor_expected = np.array([1, 0, 2 * 0.70710678118654757]) 79 | 80 | assert_array_almost_equal(vector_hor, vector_hor_expected) 81 | 82 | # Test with a yaw rotation 83 | vector_body = np.array([1, 1, 1]) 84 | theta, phi, psi = 0, 0, np.deg2rad(45) 85 | 86 | vector_hor = body2hor(vector_body, theta, phi, psi) 87 | 88 | vector_hor_expected = np.array([0, 2 * 0.70710678118654757, 1]) 89 | 90 | assert_array_almost_equal(vector_hor, vector_hor_expected) 91 | 92 | 93 | def test_hor2body(): 94 | 95 | # Test with a pitch rotation 96 | vector_hor = np.array([2 * 0.70710678118654757, 1, 0]) 97 | theta, phi, psi = np.deg2rad(45), 0, 0 98 | 99 | vector_body_expected = np.array([1, 1, 1]) 100 | 101 | vector_body = hor2body(vector_hor, theta, phi, psi) 102 | 103 | assert_array_almost_equal(vector_body, vector_body_expected) 104 | 105 | # Test with a roll rotation 106 | vector_hor = np.array([1, 0, 2 * 0.70710678118654757]) 107 | theta, phi, psi = 0, np.deg2rad(45), 0 108 | 109 | vector_body_expected = np.array([1, 1, 1]) 110 | 111 | vector_body = hor2body(vector_hor, theta, phi, psi) 112 | 113 | assert_array_almost_equal(vector_body, vector_body_expected) 114 | 115 | # Test with a yaw rotation 116 | vector_hor = np.array([0, 2 * 0.70710678118654757, 1]) 117 | theta, phi, psi = 0, 0, np.deg2rad(45) 118 | 119 | vector_body_expected = np.array([1, 1, 1]) 120 | 121 | vector_body = hor2body(vector_hor, theta, phi, psi) 122 | 123 | assert_array_almost_equal(vector_body, vector_body_expected) 124 | 125 | 126 | def test_check_gamma_mu_chi_range(): 127 | 128 | wrong_values = (3 * np.pi, - 3 * np.pi) 129 | 130 | for value in wrong_values: 131 | # 0 is always a correct value 132 | angles = [0, 0, 0] 133 | for ii in range(3): 134 | angles[ii] = value 135 | with pytest.raises(ValueError): 136 | check_gamma_mu_chi_range(*angles) 137 | 138 | 139 | def test_check_gamma_range(): 140 | 141 | wrong_values = (3 * np.pi, - 3 * np.pi) 142 | 143 | for value in wrong_values: 144 | # 0 is always a correct value 145 | with pytest.raises(ValueError) as excinfo: 146 | check_gamma_mu_chi_range(value, 0, 0) 147 | assert ("ValueError: Gamma value is not inside correct range" 148 | in excinfo.exconly()) 149 | 150 | 151 | def test_check_mu_range(): 152 | 153 | wrong_values = (3 * np.pi, - 3 * np.pi) 154 | 155 | for value in wrong_values: 156 | # 0 is always a correct value 157 | with pytest.raises(ValueError) as excinfo: 158 | check_gamma_mu_chi_range(0, value, 0) 159 | assert ("ValueError: Mu value is not inside correct range" 160 | in excinfo.exconly()) 161 | 162 | 163 | def test_check_chi_range(): 164 | 165 | wrong_values = (3 * np.pi, - 3 * np.pi) 166 | 167 | for value in wrong_values: 168 | # 0 is always a correct value 169 | with pytest.raises(ValueError) as excinfo: 170 | check_gamma_mu_chi_range(0, 0, value) 171 | assert ("ValueError: Chi value is not inside correct range" 172 | in excinfo.exconly()) 173 | 174 | 175 | def test_wind2hor(): 176 | 177 | # Test with a pitch rotation 178 | vector_wind = np.array([1, 1, 1]) 179 | gamma, mu, chi = np.deg2rad(45), 0, 0 180 | 181 | vector_hor = wind2hor(vector_wind, gamma, mu, chi) 182 | 183 | vector_hor_expected = np.array([2 * 0.70710678118654757, 1, 0]) 184 | 185 | assert_array_almost_equal(vector_hor, vector_hor_expected) 186 | 187 | # Test with a roll rotation 188 | vector_wind = np.array([1, 1, 1]) 189 | gamma, mu, chi = 0, np.deg2rad(45), 0 190 | 191 | vector_hor = wind2hor(vector_wind, gamma, mu, chi) 192 | 193 | vector_hor_expected = np.array([1, 0, 2 * 0.70710678118654757]) 194 | 195 | assert_array_almost_equal(vector_hor, vector_hor_expected) 196 | 197 | # Test with a yaw rotation 198 | vector_wind = np.array([1, 1, 1]) 199 | gamma, mu, chi = 0, 0, np.deg2rad(45) 200 | 201 | vector_hor = wind2hor(vector_wind, gamma, mu, chi) 202 | 203 | vector_hor_expected = np.array([0, 2 * 0.70710678118654757, 1]) 204 | 205 | assert_array_almost_equal(vector_hor, vector_hor_expected) 206 | 207 | 208 | def test_hor2wind(): 209 | 210 | # Test with a pitch rotation 211 | vector_hor = np.array([2 * 0.70710678118654757, 1, 0]) 212 | gamma, mu, chi = np.deg2rad(45), 0, 0 213 | 214 | vector_wind_expected = np.array([1, 1, 1]) 215 | 216 | vector_wind = hor2wind(vector_hor, gamma, mu, chi) 217 | 218 | assert_array_almost_equal(vector_wind, vector_wind_expected) 219 | 220 | # Test with a roll rotation 221 | vector_hor = np.array([1, 0, 2 * 0.70710678118654757]) 222 | gamma, mu, chi = 0, np.deg2rad(45), 0 223 | 224 | vector_wind_expected = np.array([1, 1, 1]) 225 | 226 | vector_wind = hor2wind(vector_hor, gamma, mu, chi) 227 | 228 | assert_array_almost_equal(vector_wind, vector_wind_expected) 229 | 230 | # Test with a yaw rotation 231 | vector_hor = np.array([0, 2 * 0.70710678118654757, 1]) 232 | gamma, mu, chi = 0, 0, np.deg2rad(45) 233 | 234 | vector_wind_expected = np.array([1, 1, 1]) 235 | 236 | vector_wind = hor2wind(vector_hor, gamma, mu, chi) 237 | 238 | assert_array_almost_equal(vector_wind, vector_wind_expected) 239 | 240 | 241 | def test_check_alpha_beta_range(): 242 | 243 | wrong_values = (3 * np.pi, - 3 * np.pi) 244 | 245 | for value in wrong_values: 246 | # 0 is always a correct value 247 | angles = [0, 0] 248 | for ii in range(2): 249 | angles[ii] = value 250 | with pytest.raises(ValueError): 251 | check_alpha_beta_range(*angles) 252 | 253 | 254 | def test_check_alpha_range(): 255 | 256 | wrong_values = (3 * np.pi, - 3 * np.pi) 257 | 258 | for value in wrong_values: 259 | # 0 is always a correct value 260 | with pytest.raises(ValueError) as excinfo: 261 | check_alpha_beta_range(value, 0) 262 | assert ("ValueError: Alpha value is not inside correct range" 263 | in excinfo.exconly()) 264 | 265 | 266 | def test_check_beta_range(): 267 | 268 | wrong_values = (3 * np.pi, - 3 * np.pi) 269 | 270 | for value in wrong_values: 271 | # 0 is always a correct value 272 | with pytest.raises(ValueError) as excinfo: 273 | check_alpha_beta_range(0, value) 274 | assert ("ValueError: Beta value is not inside correct range" 275 | in excinfo.exconly()) 276 | 277 | 278 | def test_wind2body(): 279 | 280 | # Test with an increment of the angle of attack 281 | vector_wind = np.array([1, 1, 1]) 282 | alpha, beta = np.deg2rad(45), 0 283 | 284 | vector_body = wind2body(vector_wind, alpha, beta) 285 | 286 | vector_body_expected = np.array([0, 1, 2 * 0.70710678118654757]) 287 | 288 | assert_array_almost_equal(vector_body, vector_body_expected) 289 | 290 | # Test with an increment of the sideslip angle 291 | vector_wind = np.array([1, 1, 1]) 292 | alpha, beta = 0, np.deg2rad(45) 293 | 294 | vector_body = wind2body(vector_wind, alpha, beta) 295 | 296 | vector_body_expected = np.array([0, 2 * 0.70710678118654757, 1]) 297 | 298 | assert_array_almost_equal(vector_body, vector_body_expected) 299 | 300 | 301 | def test_body2wind(): 302 | 303 | # Test with an increment of the angle of attack 304 | vector_body = np.array([0, 1, 2 * 0.70710678118654757]) 305 | alpha, beta = np.deg2rad(45), 0 306 | 307 | vector_wind = body2wind(vector_body, alpha, beta) 308 | 309 | vector_wind_expected = np.array([1, 1, 1]) 310 | 311 | assert_array_almost_equal(vector_wind, vector_wind_expected) 312 | 313 | # Test with an increment of the sideslip angle 314 | vector_body = np.array([0, 2 * 0.70710678118654757, 1]) 315 | alpha, beta = 0, np.deg2rad(45) 316 | 317 | vector_wind = body2wind(vector_body, alpha, beta) 318 | 319 | vector_wind_expected = np.array([1, 1, 1]) 320 | 321 | assert_array_almost_equal(vector_wind, vector_wind_expected) 322 | -------------------------------------------------------------------------------- /src/pyfme/utils/tests/test_euler_quaternion.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Sun Jan 17 12:32:11 2016 4 | 5 | @author: Juan 6 | """ 7 | import pytest 8 | 9 | import numpy as np 10 | from numpy.testing import (assert_array_almost_equal) 11 | 12 | 13 | from pyfme.utils.change_euler_quaternion import (quatern2euler, euler2quatern, 14 | check_unitnorm, 15 | vel_quaternion) 16 | 17 | 18 | def test_quatern2euler(): 19 | 20 | quaternion = np.array([0.8660254037844387, 0, 0.5, 0]) 21 | 22 | euler_angles_expected = np.array([1.04719755, 0.0, 0.0]) 23 | 24 | euler_angles = quatern2euler(quaternion) 25 | 26 | assert_array_almost_equal(euler_angles, euler_angles_expected) 27 | 28 | 29 | def test_euler2quatern(): 30 | 31 | euler_angles = np.array([0.0, 1.0471975511965976, 0.0]) 32 | 33 | quaternion_expected = np.array([0.8660254037844387, 0.5, 0, 0]) 34 | 35 | quaternion = euler2quatern(euler_angles) 36 | 37 | assert_array_almost_equal(quaternion, quaternion_expected) 38 | 39 | 40 | def test_vel_quaternion(): 41 | # test for bank velocity p = 1 42 | quaternion = np.array([0.8660254037844387, 0, 0.5, 0]) 43 | 44 | ang_vel = np.array([1, 0, 0]) 45 | 46 | d_quaternion_expected = np.array([0, 0.4330127018922193, 0, -0.25]) 47 | 48 | d_quaternion = vel_quaternion(quaternion, ang_vel) 49 | 50 | assert_array_almost_equal(d_quaternion, d_quaternion_expected) 51 | 52 | # test for pitch velocity q = 1 53 | quaternion = np.array([0.8660254037844387, 0, 0.5, 0]) 54 | 55 | ang_vel = np.array([0, 1, 0]) 56 | 57 | d_quaternion_expected = np.array([-0.25, 0, 0.4330127018922193, 0]) 58 | 59 | d_quaternion = vel_quaternion(quaternion, ang_vel) 60 | 61 | assert_array_almost_equal(d_quaternion, d_quaternion_expected) 62 | 63 | # test for yaw velocity r = 1 64 | quaternion = np.array([0.8660254037844387, 0, 0.5, 0]) 65 | 66 | ang_vel = np.array([0, 0, 1]) 67 | 68 | d_quaternion_expected = np.array([0, 0.25, 0, 0.4330127018922193]) 69 | 70 | d_quaternion = vel_quaternion(quaternion, ang_vel) 71 | 72 | assert_array_almost_equal(d_quaternion, d_quaternion_expected) 73 | 74 | 75 | def test_check_unitnorm(): 76 | 77 | wrong_value = ([1, 0, 0.25, 0.5]) 78 | 79 | with pytest.raises(ValueError): 80 | check_unitnorm(wrong_value) 81 | -------------------------------------------------------------------------------- /src/pyfme/utils/tests/test_inputs_generator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Inputs Generator Tests 8 | ---------------------- 9 | Test functions for input generator module. 10 | 11 | """ 12 | import numpy as np 13 | from numpy.testing import assert_almost_equal 14 | from pyfme.utils.input_generator import (Step, Doublet, Ramp, Harmonic, 15 | Constant) 16 | 17 | 18 | def test_input_scalar_output_scalar(): 19 | control = Constant(1.5) 20 | control_value = control(1.5) 21 | 22 | assert isinstance(control_value, float) 23 | 24 | 25 | def test_step(): 26 | t_init = 0 27 | T = 5 28 | A = 1.5 29 | time = np.linspace(0, 10, 11) 30 | 31 | expected_input = np.zeros([11]) 32 | expected_input[0:6] = A 33 | 34 | step_input = Step(t_init, T, A, offset=0) 35 | real_input = step_input(time) 36 | 37 | assert_almost_equal(real_input, expected_input) 38 | 39 | 40 | def test_step_bounds_not_included(): 41 | t_init = 0.1 42 | T = 4.8 43 | A = 1.5 44 | time = np.linspace(0, 10, 11) 45 | 46 | expected_input = np.zeros([11]) 47 | expected_input[1:5] = A 48 | 49 | step_input = Step(t_init, T, A, offset=0) 50 | real_input = step_input(time) 51 | 52 | assert_almost_equal(real_input, expected_input) 53 | 54 | 55 | def test_step_offset(): 56 | t_init = 0 57 | T = 5 58 | A = 1.5 59 | time = np.linspace(0, 10, 11) 60 | offset = 2.6 61 | 62 | expected_input = np.zeros([11]) + offset 63 | expected_input[0:6] += A 64 | 65 | step_input = Step(t_init, T, A, offset=offset) 66 | real_input = step_input(time) 67 | 68 | assert_almost_equal(real_input, expected_input) 69 | 70 | 71 | def test_doublet(): 72 | t_init = 0 73 | T = 5. 74 | A = 3. 75 | time = np.linspace(0, 10, 11) 76 | 77 | expected_input = np.zeros([11]) 78 | expected_input[0:3] = A/2 79 | expected_input[3:6] = -A/2 80 | 81 | doublet_input = Doublet(t_init, T, A, offset=0) 82 | real_input = doublet_input(time) 83 | 84 | assert_almost_equal(real_input, expected_input) 85 | 86 | 87 | def test_doublet_bounds_not_included(): 88 | t_init = 0.1 89 | T = 4.8 90 | A = 3. 91 | time = np.linspace(0, 10, 11) 92 | 93 | expected_input = np.zeros([11]) 94 | expected_input[1:3] = A/2 95 | expected_input[3:5] = -A/2 96 | 97 | doublet_input = Doublet(t_init, T, A, offset=0) 98 | real_input = doublet_input(time) 99 | 100 | assert_almost_equal(real_input, expected_input) 101 | 102 | 103 | def test_doublet_offset(): 104 | t_init = 0 105 | T = 5 106 | A = 1.5 107 | time = np.linspace(0, 10, 11) 108 | offset = 2.6 109 | 110 | expected_input = np.zeros([11]) + offset 111 | expected_input[0:3] += A/2 112 | expected_input[3:6] += -A/2 113 | 114 | doublet_input = Doublet(t_init, T, A, offset=offset) 115 | real_input = doublet_input(time) 116 | 117 | assert_almost_equal(real_input, expected_input) 118 | 119 | 120 | def test_ramp(): 121 | t_init = 0 122 | T = 4. 123 | A = 3. 124 | time = np.linspace(0, 10, 11) 125 | 126 | expected_input = np.zeros([11]) 127 | expected_input[0:5] = np.array([0, A/4, A/2, 3*A/4, A]) 128 | 129 | ramp_input = Ramp(t_init, T, A, offset=0) 130 | real_input = ramp_input(time) 131 | 132 | assert_almost_equal(real_input, expected_input) 133 | 134 | 135 | def test_ramp_offset(): 136 | t_init = 0 137 | T = 4. 138 | A = 3. 139 | time = np.linspace(0, 10, 11) 140 | offset = 1 141 | 142 | expected_input = np.zeros([11]) + offset 143 | expected_input[0:5] += np.array([0, A/4, A/2, 3*A/4, A]) 144 | 145 | ramp_input = Ramp(t_init, T, A, offset=offset) 146 | real_input = ramp_input(time) 147 | 148 | assert_almost_equal(real_input, expected_input) 149 | 150 | 151 | def test_harmonic(): 152 | t_init = 0 153 | T = 4. 154 | A = 3.0 155 | time = np.linspace(0, 10, 11) 156 | f = 0.25 157 | 158 | expected_input = np.zeros([11]) 159 | expected_input[0:5] = np.array([0, A/2, 0, -A/2, 0]) 160 | 161 | harmonic_input = Harmonic(t_init, T, A, f, phase=0, offset=0) 162 | real_input = harmonic_input(time) 163 | 164 | assert_almost_equal(real_input, expected_input) 165 | 166 | 167 | def test_harmonic_offset(): 168 | t_init = 0 169 | T = 4. 170 | A = 3. 171 | time = np.linspace(0, 10, 11) 172 | offset = 1 173 | f = 0.25 174 | 175 | expected_input = np.zeros([11]) + offset 176 | expected_input[0:5] += np.array([0, A/2, 0, -A/2, 0]) 177 | 178 | harmonic_input = Harmonic(t_init, T, A, f, phase=0, offset=offset) 179 | real_input = harmonic_input(time) 180 | 181 | assert_almost_equal(real_input, expected_input) 182 | 183 | 184 | def test_harmonic_phase(): 185 | t_init = 0 186 | T = 4. 187 | A = 3. 188 | time = np.linspace(0, 10, 11) 189 | phase = np.pi/2 190 | f = 0.25 191 | 192 | expected_input = np.zeros([11]) 193 | expected_input[0:5] += np.array([A/2, 0, -A/2, 0, A/2]) 194 | 195 | harmonic_input = Harmonic(t_init, T, A, f, phase=phase) 196 | real_input = harmonic_input(time) 197 | 198 | assert_almost_equal(real_input, expected_input) 199 | 200 | 201 | def test_constant(): 202 | 203 | offset = 3.5 204 | time = np.linspace(0, 5, 11) 205 | 206 | expected_input = np.full_like(time, offset) 207 | 208 | constant = Constant(offset) 209 | real_input = constant(time) 210 | 211 | assert_almost_equal(real_input, expected_input) 212 | 213 | 214 | def test_add_controls_01(): 215 | 216 | offset1 = 3.5 217 | offset2 = 1.2 218 | 219 | time = np.linspace(0, 5, 11) 220 | 221 | expected_input = np.full_like(time, offset1 + offset2) 222 | 223 | constant1 = Constant(offset1) 224 | constant2 = Constant(offset2) 225 | 226 | constant_input = constant1 + constant2 227 | real_input = constant_input(time) 228 | 229 | assert_almost_equal(real_input, expected_input) 230 | 231 | 232 | def test_add_controls_02(): 233 | 234 | time = np.linspace(0, 10, 11) 235 | 236 | # Define harmonic input 237 | t_init = 0 238 | T = 4. 239 | A = 3. 240 | phase = np.pi / 2 241 | f = 0.25 242 | 243 | expected_harm_input = np.zeros([11]) 244 | expected_harm_input[0:5] += np.array([A / 2, 0, -A / 2, 0, A / 2]) 245 | 246 | harmonic_input = Harmonic(t_init, T, A, f, phase=phase) 247 | 248 | # Define ramp input 249 | t_init = 0 250 | T = 4. 251 | A = 3. 252 | 253 | expected_ramp_input = np.zeros([11]) 254 | expected_ramp_input[0:5] = np.array([0, A / 4, A / 2, 3 * A / 4, A]) 255 | 256 | ramp_input = Ramp(t_init, T, A, offset=0) 257 | 258 | # Add both 259 | composed_input = ramp_input + harmonic_input 260 | real_input = composed_input(time) 261 | 262 | expected_input = expected_ramp_input + expected_harm_input 263 | 264 | assert_almost_equal(real_input, expected_input) 265 | 266 | 267 | def test_subtract_controls_01(): 268 | 269 | time = np.linspace(0, 10, 11) 270 | 271 | # Define harmonic input 272 | t_init = 0 273 | T = 4. 274 | A = 3. 275 | phase = np.pi / 2 276 | f = 0.25 277 | 278 | expected_harm_input = np.zeros([11]) 279 | expected_harm_input[0:5] += np.array([A / 2, 0, -A / 2, 0, A / 2]) 280 | 281 | harmonic_input = Harmonic(t_init, T, A, f, phase=phase) 282 | 283 | # Define ramp input 284 | t_init = 0 285 | T = 4. 286 | A = 3. 287 | 288 | expected_ramp_input = np.zeros([11]) 289 | expected_ramp_input[0:5] = np.array([0, A / 4, A / 2, 3 * A / 4, A]) 290 | 291 | ramp_input = Ramp(t_init, T, A, offset=0) 292 | 293 | # Subtract both 294 | composed_input = ramp_input - harmonic_input 295 | real_input = composed_input(time) 296 | 297 | expected_input = expected_ramp_input - expected_harm_input 298 | 299 | assert_almost_equal(real_input, expected_input) 300 | 301 | 302 | def test_multiply_controls_01(): 303 | 304 | time = np.linspace(0, 10, 11) 305 | 306 | # Define harmonic input 307 | t_init = 0 308 | T = 4. 309 | A = 3. 310 | phase = np.pi / 2 311 | f = 0.25 312 | 313 | expected_harm_input = np.zeros([11]) 314 | expected_harm_input[0:5] += np.array([A / 2, 0, -A / 2, 0, A / 2]) 315 | 316 | harmonic_input = Harmonic(t_init, T, A, f, phase=phase) 317 | 318 | # Define ramp input 319 | t_init = 0 320 | T = 4. 321 | A = 3. 322 | 323 | expected_ramp_input = np.zeros([11]) 324 | expected_ramp_input[0:5] = np.array([0, A / 4, A / 2, 3 * A / 4, A]) 325 | 326 | ramp_input = Ramp(t_init, T, A, offset=0) 327 | 328 | # Multiply both 329 | composed_input = ramp_input * harmonic_input 330 | real_input = composed_input(time) 331 | 332 | expected_input = expected_ramp_input * expected_harm_input 333 | 334 | assert_almost_equal(real_input, expected_input) 335 | 336 | time = np.linspace(0, 10, 11) 337 | 338 | # Define harmonic input 339 | t_init = 0 340 | T = 4. 341 | A = 3. 342 | phase = np.pi / 2 343 | f = 0.25 344 | 345 | expected_harm_input = np.zeros([11]) 346 | expected_harm_input[0:5] += np.array([A / 2, 0, -A / 2, 0, A / 2]) 347 | 348 | harmonic_input = Harmonic(t_init, T, A, f, phase=phase) 349 | 350 | # Define ramp input 351 | t_init = 0 352 | T = 4. 353 | A = 3. 354 | 355 | expected_ramp_input = np.zeros([11]) 356 | expected_ramp_input[0:5] = np.array([0, A / 4, A / 2, 3 * A / 4, A]) 357 | 358 | ramp_input = Ramp(t_init, T, A, offset=0) 359 | 360 | # Add both 361 | composed_input = ramp_input + harmonic_input 362 | real_input = composed_input(time) 363 | 364 | expected_input = expected_harm_input + expected_ramp_input 365 | 366 | assert_almost_equal(real_input, expected_input) 367 | -------------------------------------------------------------------------------- /src/pyfme/utils/tests/test_trimmer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Test functions for trimmer 8 | -------------------------- 9 | 10 | These values are hardcoded from the function results with the current 11 | constants. 12 | 13 | """ 14 | from itertools import product 15 | 16 | import pytest 17 | from numpy.testing import assert_almost_equal 18 | 19 | from pyfme.utils.trimmer import (rate_of_climb_cons, turn_coord_cons, 20 | turn_coord_cons_horizontal_and_small_beta) 21 | 22 | 23 | def test_rate_of_climb_cons(): 24 | 25 | alpha = 0.05 26 | beta = 0.01 27 | phi = 0.3 28 | gamma = 0.2 29 | 30 | expected_theta = 0.25072488304787743 31 | 32 | theta = rate_of_climb_cons(gamma, alpha, beta, phi) 33 | 34 | assert_almost_equal(theta, expected_theta) 35 | 36 | 37 | def test_turn_coord_cons_horizontal_and_small_beta(): 38 | 39 | turn_rate = 0.05 40 | alpha = 0.05 41 | TAS = 100 42 | 43 | expected_phi = 0.4720091827041734 44 | 45 | phi = turn_coord_cons_horizontal_and_small_beta(turn_rate, alpha, TAS) 46 | 47 | assert_almost_equal(phi, expected_phi) 48 | 49 | 50 | def test_turn_coord_cons1_against_2(): 51 | 52 | turn_rate = 0.05 53 | alpha = 0.05 54 | TAS = 100 55 | beta = 0 56 | gamma = 0 57 | 58 | expected_phi = turn_coord_cons_horizontal_and_small_beta(turn_rate, 59 | alpha, 60 | TAS) 61 | phi = turn_coord_cons(turn_rate, alpha, beta, TAS, gamma) 62 | 63 | assert_almost_equal(phi, expected_phi) 64 | 65 | 66 | def test_turn_coord_cons_small_gamma(): 67 | 68 | turn_rate = 0.05 69 | alpha = 0.05 70 | TAS = 100 71 | beta = 0.01 72 | gamma = 0 73 | 74 | expected_phi = 0.472092273171819 75 | 76 | phi = turn_coord_cons(turn_rate, alpha, beta, TAS, gamma) 77 | 78 | assert_almost_equal(phi, expected_phi) 79 | 80 | 81 | def test_turn_coord_cons_big_gamma(): 82 | 83 | turn_rate = 0.05 84 | alpha = 0.05 85 | TAS = 100 86 | beta = 0.01 87 | gamma = 0.2 88 | 89 | expected_phi = 0.4767516242692935 90 | 91 | phi = turn_coord_cons(turn_rate, alpha, beta, TAS, gamma) 92 | 93 | assert_almost_equal(phi, expected_phi) 94 | 95 | 96 | trim_test_data = product( 97 | [30, 45], 98 | [500, 1000, 2000], 99 | [0, 0.005, -0.005], 100 | [0, 0.01, -0.01, 0.05, -0.05] 101 | ) 102 | 103 | 104 | @pytest.mark.parametrize('TAS, h0, turn_rate, gamma', trim_test_data) 105 | def test_stationary_condition_trimming_Cessna172_ISA1972_NoWind_VerticalConstant( 106 | TAS, h0, turn_rate, gamma 107 | ): 108 | import numpy as np 109 | 110 | from pyfme.aircrafts import Cessna172 111 | from pyfme.environment.environment import Environment 112 | from pyfme.environment.atmosphere import ISA1976 113 | from pyfme.environment.gravity import VerticalConstant 114 | from pyfme.environment.wind import NoWind 115 | from pyfme.models.state import EarthPosition 116 | from pyfme.utils.trimmer import steady_state_trim 117 | 118 | aircraft = Cessna172() 119 | atmosphere = ISA1976() 120 | gravity = VerticalConstant() 121 | wind = NoWind() 122 | environment = Environment(atmosphere, gravity, wind) 123 | 124 | # Initial conditions. 125 | psi0 = 1.0 # rad 126 | x0, y0 = 0, 0 # m 127 | 128 | pos0 = EarthPosition(x0, y0, h0) 129 | 130 | controls0 = {'delta_elevator': 0.05, 131 | 'delta_aileron': 0.2 * np.sign(turn_rate), 132 | 'delta_rudder': 0.2 * np.sign(turn_rate), 133 | 'delta_t': 0.5, 134 | } 135 | 136 | trimmed_state, trimmed_controls = steady_state_trim( 137 | aircraft, environment, pos0, psi0, TAS, controls0, gamma, turn_rate, 138 | ) 139 | 140 | # Acceleration 141 | np.testing.assert_almost_equal( 142 | trimmed_state.acceleration.value, 143 | np.zeros_like(trimmed_state.acceleration.value), 144 | decimal=2 145 | ) 146 | # Angular acceleration 147 | np.testing.assert_almost_equal( 148 | trimmed_state.angular_accel.value, 149 | np.zeros_like(trimmed_state.angular_accel.value), 150 | decimal=2 151 | ) 152 | -------------------------------------------------------------------------------- /src/pyfme/utils/trimmer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Flight Mechanics Engine (PyFME). 4 | Copyright (c) AeroPython Development Team. 5 | Distributed under the terms of the MIT License. 6 | 7 | Trimmer 8 | ------- 9 | This module solves the problem of calculating the values of the state and 10 | control vectors that satisfy the state equations of the aircraft at the 11 | given condition. This cannot be done analytically because of the very complex 12 | functional dependence on the aerodynamic data. Instead, it must be done with 13 | a numerical algorithm which iteratively adjusts the independent variables 14 | until some solution criterion is met. 15 | """ 16 | 17 | import copy 18 | from math import sqrt, sin, cos, tan, atan 19 | 20 | import numpy as np 21 | from scipy.optimize import least_squares 22 | 23 | from pyfme.models import EulerFlatEarth 24 | from pyfme.models.state import AircraftState, EulerAttitude, BodyVelocity 25 | from pyfme.utils.coordinates import wind2body 26 | from pyfme.models.constants import GRAVITY 27 | 28 | 29 | def steady_state_trim(aircraft, environment, pos, psi, TAS, controls, gamma=0, 30 | turn_rate=0, exclude=None, verbose=0): 31 | """Finds a combination of values of the state and control variables 32 | that correspond to a steady-state flight condition. 33 | 34 | Steady-state aircraft flight is defined as a condition in which all 35 | of the motion variables are constant or zero. That is, the linear and 36 | angular velocity components are constant (or zero), thus all 37 | acceleration components are zero. 38 | 39 | Parameters 40 | ---------- 41 | aircraft : Aircraft 42 | Aircraft to be trimmed. 43 | environment : Environment 44 | Environment where the aircraft is trimmed including atmosphere, 45 | gravity and wind. 46 | pos : Position 47 | Initial position of the aircraft. 48 | psi : float, opt 49 | Initial yaw angle (rad). 50 | TAS : float 51 | True Air Speed (m/s). 52 | controls : dict 53 | Initial value guess for each control or fixed value if control is 54 | included in exclude. 55 | gamma : float, optional 56 | Flight path angle (rad). 57 | turn_rate : float, optional 58 | Turn rate, d(psi)/dt (rad/s). 59 | exclude : list, optional 60 | List with controls not to be trimmed. If not given, every control 61 | is considered in the trim process. 62 | verbose : {0, 1, 2}, optional 63 | Level of least_squares verbosity: 64 | * 0 (default) : work silently. 65 | * 1 : display a termination report. 66 | * 2 : display progress during iterations (not supported by 'lm' 67 | method). 68 | 69 | Returns 70 | ------- 71 | state : AircraftState 72 | Trimmed aircraft state. 73 | trimmed_controls : dict 74 | Trimmed aircraft controls 75 | 76 | Notes 77 | ----- 78 | See section 3.4 in [1] for the algorithm description. 79 | See section 2.5 in [1] for the definition of steady-state flight 80 | condition. 81 | 82 | References 83 | ---------- 84 | .. [1] Stevens, BL and Lewis, FL, "Aircraft Control and Simulation", 85 | Wiley-lnterscience. 86 | """ 87 | 88 | # Set initial state 89 | att0 = EulerAttitude(theta=0, phi=0, psi=psi) 90 | vel0 = BodyVelocity(u=TAS, v=0, w=0, attitude=att0) 91 | # Full state 92 | state0 = AircraftState(pos, att0, vel0) 93 | 94 | # Environment and aircraft are modified in order not to alter their 95 | # state during trimming process 96 | environment = copy.deepcopy(environment) 97 | aircraft = copy.deepcopy(aircraft) 98 | 99 | # Update environment for the current state 100 | environment.update(state0) 101 | 102 | # Create system: dynamic equations will be used to find the controls and 103 | # state which generate no u_dot, v_dot, w_dot, p_dot. q_dot, r_dot 104 | system = EulerFlatEarth(t0=0, full_state=state0) 105 | 106 | # Initialize alpha and beta 107 | # TODO: improve initialization method 108 | alpha0 = 0.05 109 | beta0 = 0.001 * np.sign(turn_rate) 110 | 111 | # For the current alpha, beta, TAS and env, set the aerodynamics of 112 | # the aircraft (q_inf, CAS, EAS...) 113 | aircraft._calculate_aerodynamics_2(TAS, alpha0, beta0, environment) 114 | 115 | # Initialize controls 116 | for control in aircraft.controls: 117 | if control not in controls: 118 | raise ValueError( 119 | "Control {} not given in initial_controls: {}".format( 120 | control, controls) 121 | ) 122 | else: 123 | aircraft.controls[control] = controls[control] 124 | 125 | if exclude is None: 126 | exclude = [] 127 | 128 | # Select the controls that will be trimmed 129 | controls_to_trim = list(aircraft.controls.keys() - exclude) 130 | 131 | # Set the variables for the optimization 132 | initial_guess = [alpha0, beta0] 133 | for control in controls_to_trim: 134 | initial_guess.append(controls[control]) 135 | 136 | # Set bounds for each variable to be optimized 137 | lower_bounds = [-0.5, -0.25] # Alpha and beta lower bounds. 138 | upper_bounds = [+0.5, +0.25] # Alpha and beta upper bounds. 139 | for ii in controls_to_trim: 140 | lower_bounds.append(aircraft.control_limits[ii][0]) 141 | upper_bounds.append(aircraft.control_limits[ii][1]) 142 | bounds = (lower_bounds, upper_bounds) 143 | 144 | args = (system, aircraft, environment, controls_to_trim, gamma, turn_rate) 145 | 146 | # Trim 147 | results = least_squares(trimming_cost_func, 148 | x0=initial_guess, 149 | args=args, 150 | verbose=verbose, 151 | bounds=bounds) 152 | 153 | # Residuals: last trim_function evaluation 154 | u_dot, v_dot, w_dot, p_dot, q_dot, r_dot = results.fun 155 | 156 | att = system.full_state.attitude 157 | system.full_state.acceleration.update([u_dot, v_dot, w_dot], att) 158 | system.full_state.angular_accel.update([p_dot, q_dot, r_dot], att) 159 | 160 | trimmed_controls = controls 161 | for key, val in zip(controls_to_trim, results.x[2:]): 162 | trimmed_controls[key] = val 163 | 164 | return system.full_state, trimmed_controls 165 | 166 | 167 | def turn_coord_cons(turn_rate, alpha, beta, TAS, gamma=0): 168 | """Calculates phi for coordinated turn. 169 | """ 170 | 171 | g0 = GRAVITY 172 | G = turn_rate * TAS / g0 173 | 174 | if abs(gamma) < 1e-8: 175 | phi = G * cos(beta) / (cos(alpha) - G * sin(alpha) * sin(beta)) 176 | phi = atan(phi) 177 | else: 178 | a = 1 - G * tan(alpha) * sin(beta) 179 | b = sin(gamma) / cos(beta) 180 | c = 1 + G ** 2 * cos(beta) ** 2 181 | 182 | sq = sqrt(c * (1 - b ** 2) + G ** 2 * sin(beta) ** 2) 183 | 184 | num = (a - b ** 2) + b * tan(alpha) * sq 185 | den = a ** 2 - b ** 2 * (1 + c * tan(alpha) ** 2) 186 | 187 | phi = atan(G * cos(beta) / cos(alpha) * num / den) 188 | return phi 189 | 190 | 191 | def turn_coord_cons_horizontal_and_small_beta(turn_rate, alpha, TAS): 192 | """Calculates phi for coordinated turn given that gamma is equal to zero 193 | and beta is small (beta << 1). 194 | """ 195 | 196 | g0 = GRAVITY 197 | G = turn_rate * TAS / g0 198 | phi = G / cos(alpha) 199 | phi = atan(phi) 200 | return phi 201 | 202 | 203 | def rate_of_climb_cons(gamma, alpha, beta, phi): 204 | """Calculates theta for the given ROC, wind angles, and roll angle. 205 | """ 206 | a = cos(alpha) * cos(beta) 207 | b = sin(phi) * sin(beta) + cos(phi) * sin(alpha) * cos(beta) 208 | sq = sqrt(a ** 2 - sin(gamma) ** 2 + b ** 2) 209 | theta = (a * b + sin(gamma) * sq) / (a ** 2 - sin(gamma) ** 2) 210 | theta = atan(theta) 211 | return theta 212 | 213 | 214 | def trimming_cost_func(trimmed_params, system, aircraft, environment, 215 | controls2trim, gamma, turn_rate): 216 | """Function to optimize 217 | """ 218 | alpha = trimmed_params[0] 219 | beta = trimmed_params[1] 220 | 221 | new_controls = {} 222 | for ii, control in enumerate(controls2trim): 223 | new_controls[control] = trimmed_params[ii + 2] 224 | 225 | # Choose coordinated turn constrain equation: 226 | if abs(turn_rate) < 1e-8: 227 | phi = 0 228 | else: 229 | phi = turn_coord_cons(turn_rate, alpha, beta, aircraft.TAS, gamma) 230 | 231 | # Rate of climb constrain 232 | theta = rate_of_climb_cons(gamma, alpha, beta, phi) 233 | 234 | # w = turn_rate * k_h 235 | # k_h = sin(theta) i_b + sin(phi) * cos(theta) j_b + cos(theta) * sin(phi) 236 | # w = p * i_b + q * j_b + r * k_b 237 | p = - turn_rate * sin(theta) 238 | q = turn_rate * sin(phi) * cos(theta) 239 | r = turn_rate * cos(theta) * cos(phi) 240 | 241 | u, v, w = wind2body((aircraft.TAS, 0, 0), alpha=alpha, beta=beta) 242 | 243 | psi = system.full_state.attitude.psi 244 | system.full_state.attitude.update([theta, phi, psi]) 245 | attitude = system.full_state.attitude 246 | 247 | system.full_state.velocity.update([u, v, w], attitude) 248 | system.full_state.angular_vel.update([p, q, r], attitude) 249 | system.full_state.acceleration.update([0, 0, 0], attitude) 250 | system.full_state.angular_accel.update([0, 0, 0], attitude) 251 | 252 | rv = system.steady_state_trim_fun(system.full_state, environment, aircraft, 253 | new_controls) 254 | return rv 255 | --------------------------------------------------------------------------------