├── aeropy ├── __init__.py └── atmosphere │ ├── __init__.py │ └── isa.py ├── README.md ├── setup.py ├── .gitignore ├── LICENSE └── test ├── performance.py └── test_isa.py /aeropy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aeropy/atmosphere/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | aeropy 2 | ====== 3 | 4 | Python tools for Aeronautical calculations. 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup(name="aeropy", version="0.1.0-dev", 4 | packages=["aeropy", 5 | "aeropy.atmosphere"]) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /test/performance.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import imp 4 | from functools import wraps 5 | from timeit import repeat 6 | 7 | size = [1, 10, 100, 1000, 10000, 100000] 8 | ignore_folders = ['.git', 'test'] 9 | test_folder = 'test' 10 | 11 | 12 | def path_to_project(function): 13 | @wraps(function) 14 | def wrapper(*args, **kwargs): 15 | 16 | current = os.getcwd() 17 | filename = os.path.basename(__file__) 18 | test_path = os.path.abspath(__file__)[: -(1 + len(filename))] 19 | 20 | os.chdir(test_path) 21 | os.chdir('..') 22 | value = function(*args, **kwargs) 23 | os.chdir(current) 24 | 25 | return value 26 | return wrapper 27 | 28 | 29 | @path_to_project 30 | def get_folders(): 31 | folders = [] 32 | for folder in os.listdir(): 33 | if(os.path.isdir(folder) and (folder not in ignore_folders)): 34 | folders.append(folder) 35 | return folders 36 | 37 | 38 | @path_to_project 39 | def performance(name, size, loops=100): 40 | 41 | libpath = os.path.join(os.getcwd(), name) 42 | sys.path.append(libpath) 43 | 44 | try: 45 | atm 46 | except NameError: 47 | import isa 48 | imp.reload(isa) 49 | 50 | times = [] 51 | for element in size: 52 | element = int(element) 53 | if element == 1: 54 | time = repeat('atm(0.)', setup='from isa import atm', 55 | number=loops, repeat=3) 56 | elif element > 1: 57 | time = repeat('atm(h)', 58 | setup='from isa import atm\n' 59 | 'from numpy import linspace\n' 60 | 'h = linspace(0., 11000., {})' 61 | .format(element), 62 | number=loops, repeat=3) 63 | time = 1e3 * min(time) / loops 64 | times.append(time) 65 | 66 | sys.path.remove(libpath) 67 | 68 | return times 69 | 70 | 71 | if __name__ == '__main__': 72 | 73 | test = dict() 74 | for name in get_folders(): 75 | print('{} running...'.format(name)) 76 | test[name] = performance(name, size) 77 | 78 | line = '{:>7}'.format('size') 79 | name = None 80 | for name in test.keys(): 81 | line += ' {:>15}'.format(name) 82 | if name is not None: 83 | print(line) 84 | for i in range(0, len(test[name])): 85 | line = '{:>7}'.format(size[i]) 86 | for name in test.keys(): 87 | line += ' {:12.3f} ms'.format(test[name][i]) 88 | print(line) 89 | else: 90 | print('No files found.') 91 | -------------------------------------------------------------------------------- /aeropy/atmosphere/isa.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Sat Dec 13 21:25:16 2014 4 | 5 | @author: alex.saez.12@gmail.com 6 | """ 7 | 8 | import numpy as np 9 | import warnings 10 | 11 | 12 | R_a = 287.05287 # J/(Kg·K) 13 | g = 9.80665 # m/s^2 14 | 15 | # Used for adimensionalization 16 | T_0 = 288.15 17 | P_0 = 101325 18 | rho_0 = P_0 / (R_a * T_0) 19 | 20 | 21 | def layer(h0, T0, P0, alpha): 22 | """ 23 | Layer constructor 24 | 25 | Parameters 26 | ---------- 27 | h0 : float 28 | Initial height of the layer [m]. 29 | T0 : float 30 | Initial temperature of the layer [K]. 31 | P0 : float 32 | Initial pressure of the layer [Pa]. 33 | alpha : float 34 | Temperature gradient of the layer [K/m]. 35 | 36 | Returns 37 | ------- 38 | out : func 39 | function that accepts height as input 40 | and returns T, P, rho. 41 | 42 | Notes 43 | ----- 44 | This could be implemented as a class with methods 45 | __init__ and __call__ 46 | 47 | """ 48 | 49 | rho0 = P0 / (R_a * T0) 50 | 51 | if alpha == 0: 52 | 53 | def constant_temp_layer(h): 54 | """ 55 | Layer with constant temperature 56 | """ 57 | 58 | T = T0 * np.ones_like(h) 59 | h = h - h0 60 | 61 | constant = - g / (R_a * T0) 62 | exp = np.exp(constant * h) 63 | 64 | P = P0 * exp 65 | rho = rho0 * exp 66 | 67 | return T, P, rho 68 | 69 | return constant_temp_layer 70 | 71 | else: 72 | 73 | def linear_temp_layer(h): 74 | """ 75 | Layer with lineal temperature variation 76 | """ 77 | 78 | h = h - h0 79 | 80 | constant_1 = alpha / T0 81 | constant_2 = -g / (R_a * alpha) 82 | 83 | base = (1 + constant_1 * h) 84 | 85 | T = T0 * base 86 | P = P0 * base ** constant_2 87 | rho = rho0 * base ** (constant_2 - 1) 88 | 89 | return T, P, rho 90 | 91 | return linear_temp_layer 92 | 93 | 94 | def atm(h, deltaT=0.0, adim=False): 95 | """ 96 | Standard atmosphere temperature, pressure and density values. 97 | 98 | Parameters 99 | ---------- 100 | h : float, ndarray 101 | height or heights for variables calculation. 102 | deltaT : float, optional 103 | Set a temperature offset. Not verified results!! 104 | adim : bool, optional 105 | If True results returned in adimensional form theta, sigma, delta. 106 | 107 | Returns 108 | ------- 109 | T : float, ndarray 110 | Temperature [K] for each height in h. 111 | P : float, ndarray 112 | Pressure [Pa] for each height in h. 113 | rho : float, ndarray 114 | Density [kg/m^3] for each height in h. 115 | 116 | """ 117 | 118 | if hasattr(h, '__iter__'): 119 | 120 | h = np.asarray(h) 121 | 122 | if any(h) < 0 or any(h) > 32000: 123 | warnings.warn("Altitude value outside range", RuntimeWarning) 124 | 125 | else: 126 | 127 | if h > 32000 or h < 0: 128 | warnings.warn("Altitude value outside range", RuntimeWarning) 129 | 130 | # First layer: Troposphere 131 | # 0 m < h <= 11000 m 132 | h0 = 0 133 | T0 = T_0 + deltaT # Base temperature [K] 134 | P0 = P_0 # Base pressure [Pa] 135 | alpha0 = -6.5e-3 # T gradient [K/m] 136 | 137 | troposphere = layer(h0, T0, P0, alpha0) 138 | 139 | # Second layer: Tropopause 140 | # 11000 m < h <= 20000 m 141 | h11 = 11000 142 | T11, P11, _ = troposphere(h11) 143 | alpha11 = 0 # T gradient [K/m] 144 | 145 | tropopause = layer(h11, T11, P11, alpha11) 146 | 147 | # Third layer: Stratosphere 148 | # 20000 m < h <= 32000 149 | h20 = 20000 150 | T20, P20, _ = tropopause(h20) 151 | alpha20 = 1e-3 # T gradient [K/m] 152 | 153 | stratosphere = layer(h20, T20, P20, alpha20) 154 | 155 | condlist = [h < 0, # Out of range 156 | h <= 11000, # First layer: Troposphere 157 | h <= 20000, # Second layer: Tropopause 158 | h <= 32000, # Third layer: Stratosphere 159 | h > 32000] # Out of range 160 | 161 | choicelist = [np.nan, 162 | troposphere(h), 163 | tropopause(h), 164 | stratosphere(h), 165 | np.nan] 166 | 167 | T, P, rho = np.select(condlist, choicelist) 168 | 169 | if adim: 170 | 171 | T /= T_0 172 | P /= P_0 173 | rho /= rho_0 174 | 175 | return T, P, rho 176 | -------------------------------------------------------------------------------- /test/test_isa.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """Tests of the ISA functions. 3 | 4 | All numerical results are validated against the `COESA`_ standard. 5 | 6 | .. _`COESA`: http://hdl.handle.net/2060/19770009539 7 | 8 | Based on scikit-aero (c) 2012 scikit-aero authors. 9 | 10 | """ 11 | import numpy as np 12 | from numpy.testing import (assert_equal, assert_almost_equal, 13 | assert_array_equal, assert_array_almost_equal) 14 | 15 | import pytest 16 | 17 | from aeropy.atmosphere.isa import atm 18 | 19 | 20 | def test_sea_level(): 21 | h = 0.0 # m 22 | expected_T = 288.15 # K 23 | expected_p = 101325.0 # Pa 24 | expected_rho = 1.2250 # kg / m3 25 | 26 | T, p, rho = atm(h) 27 | 28 | # Reads: "Assert if T equals expected_T" 29 | assert_equal(T, expected_T) 30 | assert_equal(p, expected_p) 31 | assert_almost_equal(rho, expected_rho, decimal=4) 32 | 33 | 34 | def test_scalar_input_returns_scalar_output(): 35 | h = 0.0 # m 36 | 37 | T, p, rho = atm(h) 38 | 39 | # Reads: "Assert if T is a float" 40 | assert isinstance(T, float) 41 | assert isinstance(p, float) 42 | assert isinstance(rho, float) 43 | 44 | 45 | def test_array_input_returns_array_output(): 46 | num = 5 47 | h = np.zeros(5) # m 48 | 49 | T, p, rho = atm(h) 50 | 51 | # Reads: "Assert if the length of T equals num" 52 | # Notice that T has to be a sequence in the first place or len(T) 53 | # will raise TypeError 54 | assert_equal(len(T), num) 55 | assert_equal(len(p), num) 56 | assert_equal(len(rho), num) 57 | 58 | 59 | def test_emits_warning_for_altitude_outside_range(recwarn): 60 | h = -1.0 # m 61 | 62 | atm(h) 63 | warning = recwarn.pop(RuntimeWarning) 64 | 65 | assert issubclass(warning.category, RuntimeWarning) 66 | 67 | 68 | def test_values_outside_range_are_nan(): 69 | h = np.array([-1.0, 0.0]) # m 70 | 71 | T, p, rho = atm(h) 72 | 73 | assert_equal(T[0], np.nan) 74 | assert_equal(p[0], np.nan) 75 | assert_equal(rho[0], np.nan) 76 | 77 | 78 | def test_results_under_11km(): 79 | h = np.array([0.0, 80 | 50.0, 81 | 550.0, 82 | 6500.0, 83 | 10000.0, 84 | 11000.0 85 | ]) # m 86 | expected_T = np.array([288.150, 87 | 287.825, 88 | 284.575, 89 | 245.900, 90 | 223.150, 91 | 216.650 92 | ]) # K 93 | expected_p = np.array([101325.0, 94 | 100720.0, 95 | 94890.0, 96 | 44034.0, 97 | 26436.0, 98 | 22632.0 99 | ]) # Pa 100 | expected_rho = np.array([1.2250, 101 | 1.2191, 102 | 1.1616, 103 | 0.62384, 104 | 0.41271, 105 | 0.36392 106 | ]) # kg / m3 107 | 108 | T, p, rho = atm(h) 109 | 110 | assert_array_almost_equal(T, expected_T, decimal=3) 111 | assert_array_almost_equal(p, expected_p, decimal=-1) 112 | assert_array_almost_equal(rho, expected_rho, decimal=4) 113 | 114 | 115 | def test_results_under_20km(): 116 | h = np.array([12000, 117 | 14200, 118 | 17500, 119 | 20000 120 | ]) # m 121 | expected_T = np.array([216.650, 122 | 216.650, 123 | 216.650, 124 | 216.650, 125 | ]) # K 126 | expected_p = np.array([19330.0, 127 | 13663.0, 128 | 8120.5, 129 | 5474.8 130 | ]) # Pa 131 | expected_rho = np.array([0.31083, 132 | 0.21971, 133 | 0.13058, 134 | 0.088035 135 | ]) # kg / m3 136 | 137 | T, p, rho = atm(h) 138 | 139 | assert_array_almost_equal(T, expected_T, decimal=3) 140 | assert_array_almost_equal(p, expected_p, decimal=0) 141 | assert_array_almost_equal(rho, expected_rho, decimal=5) 142 | 143 | 144 | def test_results_under_32km(): 145 | h = np.array([22100, 146 | 24000, 147 | 28800, 148 | 32000 149 | ]) # m 150 | expected_T = np.array([218.750, 151 | 220.650, 152 | 225.450, 153 | 228.650 154 | ]) # K 155 | expected_p = np.array([3937.7, 156 | 2930.4, 157 | 1404.8, 158 | 868.01 159 | ]) # Pa 160 | expected_rho = np.array([0.062711, 161 | 0.046267, 162 | 0.021708, 163 | 0.013225 164 | ]) # kg / m3 165 | 166 | T, p, rho = atm(h) 167 | 168 | assert_array_almost_equal(T, expected_T, decimal=3) 169 | assert_array_almost_equal(p, expected_p, decimal=1) 170 | assert_array_almost_equal(rho, expected_rho, decimal=5) 171 | --------------------------------------------------------------------------------