├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── atmos ├── __init__.py ├── constants.py ├── data │ └── default_moist_adiabat_data.npz ├── decorators.py ├── equations.py ├── plot.py ├── solve.py ├── tests │ ├── __init__.py │ ├── baseline_images │ │ └── plot_tests │ │ │ ├── linear_skewT.pdf │ │ │ ├── linear_skewT.png │ │ │ ├── linear_skewT_with_barbs.pdf │ │ │ └── linear_skewT_with_barbs.png │ ├── plot_tests.py │ ├── run_tests.py │ └── solve_tests.py └── util.py ├── conda-recipe ├── bld.bat ├── build.sh ├── meta.yaml ├── run_test.bat └── run_test.sh ├── docs └── source │ ├── atmos.rst │ ├── calculate.rst │ ├── conf.py │ ├── index.rst │ ├── intro.rst │ ├── solver.rst │ └── subclassing.rst ├── requirements.txt ├── setup.cfg └── setup.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | */python?.?/* 4 | */site-packages/nose/* 5 | *__init__* 6 | *tests.py -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore cython-generated c source 2 | _*.c 3 | 4 | # Ignore auto-generated cython 5 | _*_float32.pyx 6 | _*_float64.pyx 7 | 8 | # Ignore compiled python 9 | *.pyc 10 | *.pyd 11 | 12 | # Spyder project file 13 | .spyderproject 14 | .spyderworkspace 15 | 16 | # Local install egg 17 | atmos.egg-info 18 | 19 | # build directory 20 | build 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.2" 5 | # - "nightly" 6 | 7 | sudo: true 8 | 9 | addons: 10 | apt: 11 | packages: 12 | - python-scipy 13 | - libudunits2-0 14 | - libudunits2-dev 15 | - udunits-bin 16 | - netcdf4-python 17 | 18 | virtualenv: 19 | system_site_packages: true 20 | 21 | # command to install dependencies 22 | 23 | before_install: 24 | - sudo apt-get -qq update 25 | - sudo apt-get install -y python-scipy, libudunits2-0, libudunits2-dev, udunits-bin, netcdf4-python 26 | 27 | install: 28 | - "pip install ." 29 | - "pip install python-coveralls" 30 | - "pip install coverage" 31 | - "pip install nose" 32 | - "pip install -r requirements.txt" 33 | 34 | # command to run tests 35 | script: 36 | - cd $HOME/build/atmos-python/atmos/atmos/tests 37 | - python run_tests.py 38 | after_success: 39 | - coveralls 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 atmos developers. 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt requirements.txt 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ***** 2 | atmos 3 | ***** 4 | --------------------------------------- 5 | An atmospheric sciences utility library 6 | --------------------------------------- 7 | 8 | **atmos** is a library of Python programming utilities for the atmospheric 9 | sciences. It is in ongoing development. If you have an idea for a feature or 10 | have found a bug, please post it on the `GitHub issue tracker`_. 11 | 12 | Information on how to use the module can be found predominantly by using the 13 | built-in help() function in Python. Many docstrings are automatically 14 | generated by the module and so information may appear to be missing in the 15 | source code. There is also `HTML documentation`_ available. 16 | 17 | This module is currently alpha. The API of components at the base module 18 | level should stay backwards-compatible, but sub-modules are subject to change. 19 | In particular, features in the util module are likely to be changed or removed 20 | entirely. 21 | 22 | .. contents:: 23 | :backlinks: none 24 | :depth: 1 25 | 26 | Features 27 | ======== 28 | 29 | * defined constants used in atmospheric science 30 | 31 | * functions for common atmospheric science equations 32 | 33 | * a simple calculate() interface function for accessing equations 34 | 35 | * no need to remember equation function names or argument order 36 | 37 | * fast calculation of quantities using numexpr 38 | 39 | * skew-T plots integrated into matplotlib 40 | 41 | Dependencies 42 | ============ 43 | 44 | This module is tested to work with Python versions 2.6, 2.7, 3.3, and 3.4 on 45 | Unix. Support is given for all platforms. If there are bugs on your 46 | particular version of Python, please submit it to the `GitHub issue tracker`_. 47 | 48 | Package dependencies: 49 | 50 | * numpy 51 | 52 | * numexpr 53 | 54 | * six 55 | 56 | * nose 57 | 58 | Installation 59 | ============ 60 | 61 | To install this module, download and run the following: 62 | 63 | .. code:: bash 64 | 65 | $ python setup.py install 66 | 67 | If you would like to edit and develop the code, you can instead install in develop mode 68 | 69 | .. code:: bash 70 | 71 | $ python setup.py develop 72 | 73 | If you are running Anaconda, you can install using conda: 74 | 75 | .. code:: bash 76 | 77 | $ conda install -c mcgibbon atmos 78 | 79 | You can also install using pip: 80 | 81 | .. code:: bash 82 | 83 | $ pip install atmos 84 | 85 | Development version 86 | =================== 87 | 88 | The most recent development version can be found in the `GitHub develop 89 | branch`_. 90 | 91 | Examples 92 | ======== 93 | 94 | Calculating pressure from virtual temperature and air density: 95 | 96 | .. code:: python 97 | 98 | >>> import atmos 99 | >>> atmos.calculate('p', Tv=273., rho=1.27) 100 | 99519.638400000011 101 | 102 | 103 | License 104 | ======= 105 | 106 | This module is available under an MIT license. Please see ``LICENSE.txt``. 107 | 108 | .. _`GitHub issue tracker`: https://github.com/mcgibbon/atmos/issues 109 | .. _`GitHub develop branch`: https://github.com/mcgibbon/atmos/tree/develop 110 | .. _`HTML documentation`: https://pythonhosted.org/atmos 111 | -------------------------------------------------------------------------------- /atmos/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | ***** 4 | atmos 5 | ***** 6 | --------------------------------------- 7 | An atmospheric sciences utility library 8 | --------------------------------------- 9 | 10 | **atmos** is a library of Python programming utilities for the atmospheric 11 | sciences. It is in ongoing development. 12 | 13 | Information on how to use the module can be found predominantly by using the 14 | built-in help() function in Python. Many docstrings are automatically 15 | generated by the module and so information may appear to be missing in the 16 | source code. HTML documentation will be available at a later date. 17 | 18 | This module is currently alpha. The API of components at the base module 19 | level should stay backwards-compatible, but sub-modules are subject to change. 20 | In particular, features in the util module are likely to be changed or removed 21 | entirely. 22 | ''' 23 | __all__ = ['constants', 'solve', 'equations', 'util', 'decorators'] 24 | from atmos.solve import calculate, FluidSolver 25 | -------------------------------------------------------------------------------- /atmos/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | constants.py: Scientific constants in SI units. 4 | 5 | Included constants: 6 | 7 | * **g0** : standard acceleration of gravity 8 | * **r_earth** : mean radius of Earth 9 | * **Omega** : angular velocity of Earth 10 | * **Rd** : specific gas constant for dry air 11 | * **Rv** : specific gas constant for water vapor 12 | * **Cpd** : specific heat capacity of dry air at constant pressure at 300K 13 | * **Cl** : specific heat capacity of liquid water 14 | * **Gammad** : dry adiabatic lapse rate 15 | * **Lv0** : latent heat of vaporization for water at 0C 16 | """ 17 | from numpy import pi 18 | 19 | g0 = 9.80665 # standard gravitational acceleration (m/s) 20 | stefan = 5.67e-8 # Stefan-boltzmann constant (W/m^2/K^4) 21 | r_earth = 6370000. # Radius of Earth (m) 22 | Omega = 7.2921159e-5 # Angular velocity of Earth (Rad/s) 23 | Rd = 287.04 # R for dry air (J/kg/K) 24 | Rv = 461.50 # R for water vapor 25 | Cpd = 1005. # Specific heat of dry air at constant pressure (J/kg/K) 26 | Cl = 4186. # Specific heat of liquid water (J/kg/K) 27 | Gammad = g0/Cpd # Dry adabatic lapse rate (K/m) 28 | Lv0 = 2.501e6 # Latent heat of vaporization for water at 0 Celsius (J/kg) 29 | -------------------------------------------------------------------------------- /atmos/data/default_moist_adiabat_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos-python/atmos/97b4f7ed3f3cd345703a5ed76e40418bba55fb97/atmos/data/default_moist_adiabat_data.npz -------------------------------------------------------------------------------- /atmos/decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | decorators.py: Function decorators used by the rest of this module. 4 | """ 5 | from __future__ import division, absolute_import, unicode_literals 6 | import inspect 7 | from atmos.util import quantity_string, assumption_list_string, \ 8 | quantity_spec_string, doc_paragraph 9 | 10 | 11 | # Define some decorators for our equations 12 | def assumes(*args): 13 | '''Stores a function's assumptions as an attribute.''' 14 | args = tuple(args) 15 | 16 | def decorator(func): 17 | func.assumptions = args 18 | return func 19 | return decorator 20 | 21 | 22 | def overridden_by_assumptions(*args): 23 | '''Stores what assumptions a function is overridden by as an attribute.''' 24 | args = tuple(args) 25 | 26 | def decorator(func): 27 | func.overridden_by_assumptions = args 28 | return func 29 | return decorator 30 | 31 | 32 | def equation_docstring(quantity_dict, assumption_dict, 33 | equation=None, references=None, notes=None): 34 | ''' 35 | Creates a decorator that adds a docstring to an equation function. 36 | 37 | Parameters 38 | ---------- 39 | 40 | quantity_dict : dict 41 | A dictionary describing the quantities used in the equations. Its keys 42 | should be abbreviations for the quantities, and its values should be a 43 | dictionary of the form {'name': string, 'units': string}. 44 | assumption_dict : dict 45 | A dictionary describing the assumptions used by the equations. Its keys 46 | should be short forms of the assumptions, and its values should be long 47 | forms of the assumptions, as you would insert into the sentence 48 | 'Calculates (quantity) assuming (assumption 1), (assumption 2), and 49 | (assumption 3).' 50 | equation : string, optional 51 | A string describing the equation the function uses. Should be wrapped 52 | to be no more than 80 characters in length. 53 | references : string, optional 54 | A string providing references for the function. Should be wrapped to be 55 | no more than 80 characters in length. 56 | 57 | Raises 58 | ------ 59 | 60 | ValueError: 61 | If the function name does not follow (varname)_from_(any text here), or 62 | if an argument of the function or the varname (as above) is not present 63 | in quantity_dict, or if an assumption in func.assumptions is not present 64 | in the assumption_dict. 65 | ''' 66 | # Now we have our utility functions, let's define the decorator itself 67 | def decorator(func): 68 | out_name_end_index = func.__name__.find('_from_') 69 | if out_name_end_index == -1: 70 | raise ValueError('equation_docstring decorator must be applied to ' 71 | 'function whose name contains "_from_"') 72 | out_quantity = func.__name__[:out_name_end_index] 73 | in_quantities = inspect.getargspec(func).args 74 | docstring = 'Calculates {0}'.format( 75 | quantity_string(out_quantity, quantity_dict)) 76 | try: 77 | if len(func.assumptions) > 0: 78 | docstring += ' assuming {0}'.format( 79 | assumption_list_string(func.assumptions, assumption_dict)) 80 | except AttributeError: 81 | pass 82 | docstring += '.' 83 | docstring = doc_paragraph(docstring) 84 | docstring += '\n\n' 85 | if equation is not None: 86 | func.equation = equation 87 | docstring += doc_paragraph(':math:`' + equation.strip() + '`') 88 | docstring += '\n\n' 89 | docstring += 'Parameters\n' 90 | docstring += '----------\n\n' 91 | docstring += '\n'.join([quantity_spec_string(q, quantity_dict) 92 | for q in in_quantities]) 93 | docstring += '\n\n' 94 | docstring += 'Returns\n' 95 | docstring += '-------\n\n' 96 | docstring += quantity_spec_string(out_quantity, quantity_dict) 97 | if notes is not None: 98 | docstring += '\n\n' 99 | docstring += 'Notes\n' 100 | docstring += '-----\n\n' 101 | docstring += notes.strip() 102 | if references is not None: 103 | if notes is None: # still need notes header for references 104 | docstring += '\n\n' 105 | docstring += 'Notes\n' 106 | docstring += '-----\n\n' 107 | func.references = references 108 | docstring += '\n\n' 109 | docstring += '**References**\n\n' 110 | docstring += references.strip() 111 | docstring += '\n' 112 | func.__doc__ = docstring 113 | return func 114 | 115 | return decorator 116 | -------------------------------------------------------------------------------- /atmos/equations.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | equations.py: Fluid dynamics equations for atmospheric sciences. 4 | """ 5 | # Note that in this module, if multiple equations are defined that compute 6 | # the same output quantity given the same input quantities, they must take 7 | # their arguments in the same order. This is to simplify overriding default 8 | # equations with optional ones. 9 | 10 | # Design considerations: 11 | # 12 | # Consider e = rho_v*Rv*T for water vapor and water vapor density... 13 | # Isn't water vapor density the same as absolute humidity? Yes it is. 14 | # 15 | # Do we want to use underscores in our variable names, like u_geo instead of 16 | # ugeo, and dp_dx instead of dpdx? 17 | # 18 | # How do we handle latent heat of condensation for water? Constant? Poly fit? 19 | # 20 | # Users may make use of the same computation repeatedly. Should we cache how 21 | # to calculate an output quantity from a set of inputs and methods? 22 | # Precomputing every possible option is probably too memory-intensive because 23 | # of the large number of options, but caching could be viable. 24 | # 25 | # Vertical ordering... What are the consequences, and how to handle it? 26 | # 27 | 28 | # To implement: 29 | # 30 | # Need an equation or two for Td 31 | # 32 | # Need some more "shortcut" equations 33 | # 34 | # Check whether certain inputs are valid (0 < RH < 100, 0 < T, etc.) 35 | # can't use numexpr with unicode_literals because of bugs in older versions 36 | # of numexpr 37 | from __future__ import division, absolute_import 38 | import numexpr as ne 39 | from atmos.constants import g0, Omega, Rd, Rv, Cpd, Lv0, Cl, pi 40 | from atmos.decorators import assumes, overridden_by_assumptions 41 | from atmos.decorators import equation_docstring 42 | 43 | ref = {'AMS Glossary Gammam': ''' 44 | American Meteorological Society Glossary of Meteorology 45 | http://glossary.ametsoc.org/wiki/Saturation-adiabatic_lapse_rate 46 | Retrieved March 25, 2015''', 47 | 'AMS Glossary thetae': ''' 48 | American Meteorological Society Glossary of Meteorology 49 | http://glossary.ametsoc.org/wiki/Equivalent_potential_temperature 50 | Retrieved April 23, 2015''', 51 | 'Petty 2008': ''' 52 | Petty, G.W. 2008: A First Course in Atmospheric Thermodynamics. 1st Ed. 53 | Sundog Publishing.''', 54 | 'Goff-Gratch': ''' 55 | Goff, J. A., and Gratch, S. 1946: Low-pressure properties of water 56 | from -160 to 212 F, in Transactions of the American Society of 57 | Heating and Ventilating Engineers, pp 95-122, presented at the 58 | 52nd annual meeting of the American Society of Heating and 59 | Ventilating Engineers, New York, 1946.''', 60 | 'Wexler 1976': ''' 61 | Wexler, A. (1976): Vapor pressure formulation for water in range 0 to 62 | 100 C. A revision. J. Res. Natl. Bur. Stand. A, 80, 775-785.''', 63 | 'Bolton 1980': ''' 64 | Bolton, D. 1980: The Computation of Equivalent Potential Temperature. 65 | Mon. Wea. Rev., 108, 1046-1053. 66 | doi: http://dx.doi.org/10.1175/1520-0493(1980)108<1046:TCOEPT>2.0.CO;2''', 67 | 'Stull 2011': ''' 68 | Stull, R. 2011: Wet-Bulb Temperature from Relative Humidity and Air 69 | Temperature. J. Appl. Meteor. Climatol., 50, 2267-2269. 70 | doi: http://dx.doi.org/10.1175/JAMC-D-11-0143.1''', 71 | 'Davies-Jones 2009': ''' 72 | Davies-Jones, R. 2009: On Formulas for Equivalent Potential 73 | Temperature. Mon. Wea. Rev., 137, 3137-3148. 74 | doi: http://dx.doi.org/10.1175/2009MWR2774.1''' 75 | } 76 | 77 | # A dictionary describing the quantities used for and computed by the equations 78 | # in this module. This makes it possible to automatically list these in 79 | # documentation. 80 | quantities = { 81 | 'AH': { 82 | 'name': 'absolute humidity', 83 | 'units': 'kg/m^3', 84 | }, 85 | 'DSE': { 86 | 'name': 'dry static energy', 87 | 'units': 'J', 88 | }, 89 | 'e': { 90 | 'name': 'water vapor partial pressure', 91 | 'units': 'Pa', 92 | }, 93 | 'es': { 94 | 'name': 'saturation water vapor partial pressure over water', 95 | 'units': 'Pa', 96 | }, 97 | 'esi': { 98 | 'name': 'saturation water vapor partial pressure over ice', 99 | 'units': 'Pa', 100 | }, 101 | 'f': { 102 | 'name': 'Coriolis parameter', 103 | 'units': 'Hz', 104 | }, 105 | 'Gammam': { 106 | 'name': 'moist adiabatic lapse rate', 107 | 'units': 'K/m', 108 | }, 109 | 'lat': { 110 | 'name': 'latitude', 111 | 'units': 'degrees_north', 112 | }, 113 | 'lon': { 114 | 'name': 'longitude', 115 | 'units': 'degrees_east', 116 | }, 117 | 'MSE': { 118 | 'name': 'moist static energy', 119 | 'units': 'J', 120 | }, 121 | 'N2': { 122 | 'name': 'squared Brunt-Vaisala frequency', 123 | 'units': 'Hz^2', 124 | }, 125 | 'omega': { 126 | 'name': 'vertical velocity expressed as tendency of pressure', 127 | 'units': 'Pa/s', 128 | }, 129 | 'p': { 130 | 'name': 'pressure', 131 | 'units': 'Pa', 132 | }, 133 | 'plcl': { 134 | 'name': 'pressure at lifting condensation level', 135 | 'units': 'Pa', 136 | }, 137 | 'Phi': { 138 | 'name': 'geopotential', 139 | 'units': 'm^2/s^2', 140 | }, 141 | 'qv': { 142 | 'name': 'specific humidity', 143 | 'units': 'kg/kg', 144 | }, 145 | 'qvs': { 146 | 'name': 'saturation specific humidity with respect to liquid water', 147 | 'units': 'kg/kg', 148 | }, 149 | 'qvsi': { 150 | 'name': 'saturation specific humidity with respect to ice', 151 | 'units': 'kg/kg', 152 | }, 153 | 'qi': { 154 | 'name': 'specific humidity with respect to ice', 155 | 'units': 'kg/kg', 156 | }, 157 | 'ql': { 158 | 'name': 'specific humidity with respect to liquid water', 159 | 'units': 'kg/kg', 160 | }, 161 | 'qt': { 162 | 'name': 'specific humidity with respect to total water', 163 | 'units': 'kg/kg', 164 | }, 165 | 'RB': { 166 | 'name': 'bulk Richardson number', 167 | 'units': '1', 168 | }, 169 | 'RH': { 170 | 'name': 'relative humidity with respect to liquid water', 171 | 'units': 'percent', 172 | }, 173 | 'RHi': { 174 | 'name': 'relative humidity with respect to ice', 175 | 'units': 'percent', 176 | }, 177 | 'rho': { 178 | 'name': 'density', 179 | 'units': 'kg/m^3', 180 | }, 181 | 'rv': { 182 | 'name': 'water vapor mixing ratio', 183 | 'units': 'kg/kg', 184 | }, 185 | 'rvs': { 186 | 'name': 'saturation water vapor mixing ratio with respect to liquid ' 187 | 'water', 188 | 'units': 'kg/kg', 189 | }, 190 | 'rvsi': { 191 | 'name': 'saturation water vapor mixing ratio with respect to ice', 192 | 'units': 'kg/kg', 193 | }, 194 | 'ri': { 195 | 'name': 'ice mixing ratio', 196 | 'units': 'kg/kg', 197 | }, 198 | 'rl': { 199 | 'name': 'liquid water mixing ratio', 200 | 'units': 'kg/kg', 201 | }, 202 | 'rt': { 203 | 'name': 'total water mixing ratio', 204 | 'units': 'kg/kg', 205 | }, 206 | 'T': { 207 | 'name': 'temperature', 208 | 'units': 'K', 209 | }, 210 | 'Td': { 211 | 'name': 'dewpoint temperature', 212 | 'units': 'K', 213 | }, 214 | 'Tlcl': { 215 | 'name': 'temperature at lifting condensation level', 216 | 'units': 'K', 217 | }, 218 | 'Tv': { 219 | 'name': 'virtual temperature', 220 | 'units': 'K', 221 | }, 222 | 'Tw': { 223 | 'name': 'wet bulb temperature', 224 | 'units': 'K', 225 | }, 226 | 'theta': { 227 | 'name': 'potential temperature', 228 | 'units': 'K', 229 | }, 230 | 'thetae': { 231 | 'name': 'equivalent potential temperature', 232 | 'units': 'K', 233 | }, 234 | 'thetaes': { 235 | 'name': 'saturation equivalent potential temperature', 236 | 'units': 'K', 237 | }, 238 | 'u': { 239 | 'name': 'eastward zonal wind velocity', 240 | 'units': 'm/s', 241 | }, 242 | 'v': { 243 | 'name': 'northward meridional wind velocity', 244 | 'units': 'm/s', 245 | }, 246 | 'w': { 247 | 'name': 'vertical velocity', 248 | 'units': 'm/s', 249 | }, 250 | 'x': { 251 | 'name': 'x', 252 | 'units': 'm', 253 | }, 254 | 'y': { 255 | 'name': 'y', 256 | 'units': 'm', 257 | }, 258 | 'z': { 259 | 'name': 'height', 260 | 'units': 'm', 261 | }, 262 | 'Z': { 263 | 'name': 'geopotential height', 264 | 'units': 'm', 265 | } 266 | } 267 | 268 | 269 | # A dict of assumptions used by equations in this module. This helps allow 270 | # automatic docstring generation. 271 | assumptions = { 272 | 'hydrostatic': 'hydrostatic balance', 273 | 'constant g': 'g is constant', 274 | 'constant Lv': 'latent heat of vaporization of water is constant', 275 | 'ideal gas': 'the ideal gas law holds', 276 | 'bolton': 'the assumptions in Bolton (1980) hold', 277 | 'goff-gratch': 'the Goff-Gratch equation for es and esi', 278 | 'frozen bulb': 'the bulb is frozen', 279 | 'unfrozen bulb': 'the bulb is not frozen', 280 | 'Tv equals T': 'the virtual temperature correction can be neglected', 281 | 'constant Cp': 'Cp is constant and equal to Cp for dry air at 0C', 282 | 'no liquid water': 'liquid water can be neglected', 283 | 'no ice': 'ice can be neglected', 284 | 'low water vapor': ('terms that are second-order in moisture quantities ' 285 | 'can be neglected (eg. qv == rv)'), 286 | 'cimo': 'the CIMO guide equation for esi', 287 | } 288 | 289 | 290 | # particularize the docstring decorator for this module's quantities and 291 | # assumptions. 292 | def autodoc(**kwargs): 293 | return equation_docstring(quantities, assumptions, **kwargs) 294 | 295 | 296 | # Note that autodoc() must always be placed *above* assumes(), so that it 297 | # has information about the assumptions (each decorator decorates the result 298 | # of what is below it). 299 | @autodoc(equation=r'AH = q_v \rho') 300 | @assumes() 301 | def AH_from_qv_rho(qv, rho): 302 | return ne.evaluate('qv*rho') 303 | 304 | 305 | @autodoc(equation=r'DSE = C_{pd} T + g_0 z') 306 | @assumes('constant g') 307 | def DSE_from_T_z(T, z): 308 | return ne.evaluate('Cpd*T + g0*z') 309 | 310 | 311 | @autodoc(equation=r'DSE = C_{pd} T + \Phi') 312 | @assumes() 313 | def DSE_from_T_Phi(T, Phi): 314 | return ne.evaluate('Cpd*T + Phi') 315 | 316 | 317 | @autodoc(equation=r'e = p \frac{q_v}{0.622+q_v}') 318 | @assumes() 319 | def e_from_p_qv(p, qv): 320 | return ne.evaluate('p*qv/(0.622+qv)') 321 | 322 | 323 | @autodoc(equation=r'e = es(Td)', references=ref['Goff-Gratch']) 324 | @assumes('goff-gratch') 325 | def e_from_Td_Goff_Gratch(Td): 326 | return es_from_T_Goff_Gratch(Td) 327 | 328 | 329 | @autodoc(equation=r'e = es(Td)') 330 | @assumes('bolton') 331 | def e_from_Td_Bolton(Td): 332 | return es_from_T_Bolton(Td) 333 | 334 | 335 | @autodoc(equation=r'e = es(T_w) - (6.60 \times 10^{-4}) ' 336 | '(1 + 0.00115 (T_w-273.15) (T-T_w)) p', 337 | references=ref['Petty 2008']) 338 | @assumes('unfrozen bulb', 'goff-gratch') 339 | def e_from_p_T_Tw_Goff_Gratch(p, T, Tw): 340 | es = es_from_T_Goff_Gratch(Tw) 341 | return ne.evaluate('es-(0.000452679+7.59e-7*Tw)*(T-Tw)*p') 342 | 343 | 344 | @autodoc(equation=r'e = es(T_w) - (5.82 \times 10^{-4}) ' 345 | r'(1 + 0.00115 (T_w-273.15) ' 346 | r' (T-T_w)) p', 347 | references=ref['Petty 2008']) 348 | @assumes('frozen bulb', 'goff-gratch') 349 | def e_from_p_T_Tw_frozen_bulb_Goff_Gratch(p, T, Tw): 350 | es = es_from_T_Goff_Gratch(Tw) 351 | return ne.evaluate('es-(0.000399181+6.693e-7*Tw)*(T-Tw)*p') 352 | 353 | 354 | @autodoc(equation=r'e = es(T_w) - (6.60 \times 10^{-4}) ' 355 | r'(1 + 0.00115 (T_w-273.15) (T-T_w)) p', 356 | references=ref['Petty 2008']) 357 | @assumes('unfrozen bulb', 'bolton') 358 | def e_from_p_T_Tw_Bolton(p, T, Tw): 359 | es = es_from_T_Bolton(Tw) 360 | return ne.evaluate('es-(0.000452679+7.59e-7*Tw)*(T-Tw)*p') 361 | 362 | 363 | @autodoc(equation=r'e = es(T_w) - (5.82 \times 10^{-4}) ' 364 | r'(1 + 0.00115 (T_w-273.15) ' 365 | r' (T-T_w)) p', 366 | references=ref['Petty 2008']) 367 | @assumes('frozen bulb', 'bolton') 368 | def e_from_p_T_Tw_frozen_bulb_Bolton(p, T, Tw): 369 | es = es_from_T_Bolton(Tw) 370 | return ne.evaluate('es-(0.000399181+6.693e-7*Tw)*(T-Tw)*p') 371 | 372 | 373 | @autodoc(references=ref['Goff-Gratch'] + ''' 374 | Goff, J. A. (1957) Saturation pressure of water on the new Kelvin 375 | temperature scale, Transactions of the American Society of Heating and 376 | Ventilating Engineers, pp 347-354, presented at the semi-annual meeting of 377 | the American Society of Heating and Ventilating Engineers, Murray Bay, Que. 378 | Canada. 379 | World Meteorological Organization (1988) General meteorological 380 | standards and recommended practices, Appendix A, WMO Technical Regulations, 381 | WMO-No. 49. 382 | World Meteorological Organization (2000) General meteorological 383 | standards and recommended practices, Appendix A, WMO Technical Regulations, 384 | WMO-No. 49, corrigendum. 385 | Murphy, D. M. and Koop, T. (2005): Review of the vapour pressures of 386 | ice and supercooled water for atmospheric applications, Quarterly Journal 387 | of the Royal Meteorological Society 131(608): 1539-1565. 388 | doi:10.1256/qj.04.94''', 389 | notes=''' 390 | The original Goff-Gratch (1946) equation reads as follows: 391 | 392 | | Log10(es) = -7.90298 (Tst/T-1) 393 | | + 5.02808 Log10(Tst/T) 394 | | - 1.3816*10-7 (10^(11.344 (1-T/Tst)) - 1) 395 | | + 8.1328*10-3 (10^(-3.49149 (Tst/T-1)) - 1) 396 | | + Log10(es_st) 397 | 398 | where: 399 | * Log10 refers to the logarithm in base 10 400 | * es is the saturation water vapor pressure (hPa) 401 | * T is the absolute air temperature in kelvins 402 | * Tst is the steam-point (i.e. boiling point at 1 atm.) temperature (373.16K) 403 | * es_st is es at the steam-point pressure (1 atm = 1013.25 hPa) 404 | 405 | This formula is accurate but computationally intensive. For most purposes, 406 | a more approximate formula is appropriate.''') 407 | @assumes('goff-gratch') 408 | def es_from_T_Goff_Gratch(T): 409 | return ne.evaluate( 410 | '''101325.*10.**(-7.90298*(373.16/T-1.) + 5.02808*log10(373.16/T) 411 | - 1.3816e-7 * (10.**(11.344*(1.-T/373.16))-1.) 412 | + 8.1328e-3*(10.**(-3.49149*(373.16/T-1.))-1.))''') 413 | 414 | 415 | @autodoc(equation=r'es(T) = 611.2 exp(17.67 ' 416 | '\frac{T-273.15}{T-29.65})', 417 | references=ref['Bolton 1980'] + ref['Wexler 1976'], 418 | notes=''' 419 | Fits Wexler's formula to an accuracy of 0.1% for temperatures between 420 | -35C and 35C.''') 421 | @assumes('bolton') 422 | def es_from_T_Bolton(T): 423 | return ne.evaluate('611.2*exp(17.67*(T-273.15)/(T-29.65))') 424 | 425 | 426 | @autodoc(equation=r'T_d = \frac{17.67*273.15 - 29.65 ln(\frac{e}{611.2})}' 427 | r'{17.67-ln(\frac{e}{611.2})}', 428 | references=ref['Bolton 1980'], 429 | notes=''' 430 | Obtained by inverting Bolton's formula, es(Td) = T.''') 431 | @assumes('bolton') 432 | def Td_from_e_Bolton(e): 433 | return ne.evaluate('(17.67*273.15 - 29.65*log(e/611.2))/' 434 | '(17.67-log(e/611.2))') 435 | 436 | 437 | @autodoc( 438 | equation=r'esi(T) = 610.71 * 10^{9.09718 (273.16/T - 1) - 3.56654 ' 439 | r'log_{10}(273.16/T) + 0.876793 (1 - T/273.16)}', 440 | notes=''' 441 | Valid between -100C and 0C.''') 442 | @assumes('goff-gratch') 443 | def esi_from_T_Goff_Gratch(T): 444 | return ne.evaluate( 445 | '''610.71 * 10**(-9.09718*(273.16/T - 1) - 3.56654*log10(273.16/T) 446 | + 0.876793*(1 - T/273.16))''') 447 | 448 | 449 | @autodoc( 450 | equation=r'esi = 6.112*e^{22.46*\frac{T - 273.15}{T - 0.53}}', 451 | notes=''' 452 | Matches Goff-Gratch within 0.2% from -70C to 0C, 2.5% from -100C to -70C.''') 453 | @assumes('cimo') 454 | def esi_from_T_CIMO(T): 455 | return ne.evaluate('611.2*exp(22.46*(T - 273.15)/(T - 0.53))') 456 | 457 | 458 | @autodoc(equation=r'f = 2 \Omega sin(\frac{\pi}{180.} lat)') 459 | @assumes() 460 | def f_from_lat(lat): 461 | return ne.evaluate('2.*Omega*sin(pi/180.*lat)') 462 | 463 | 464 | @autodoc( 465 | equation=r'Gammam = g_0 \frac{1+\frac{L_{v0}*r_{vs}}{R_d T}}{C_{pd}+' 466 | r'\frac{L_{v0}^2*r_{vs}}{R_v T^2}}', 467 | references=ref['AMS Glossary Gammam']) 468 | @assumes('constant g', 'constant Lv') 469 | def Gammam_from_rvs_T(rvs, T): 470 | return ne.evaluate('g0*(1+(Lv0*rvs)/(Rd*T))/(Cpd+(Lv0**2*rvs*0.622)/' 471 | '(Rd*T**2))') 472 | 473 | 474 | @autodoc(equation=r'MSE = DSE + L_{v0} q_v') 475 | @assumes('constant Lv') 476 | def MSE_from_DSE_qv(DSE, qv): 477 | return ne.evaluate('DSE + Lv0*qv') 478 | 479 | 480 | @autodoc(equation=r'\omega = - \rho g_0 w') 481 | @assumes('hydrostatic', 'constant g') 482 | def omega_from_w_rho_hydrostatic(w, rho): 483 | return ne.evaluate('-rho*g0*w') 484 | 485 | 486 | @autodoc(equation=r'p = \rho R_d T_v') 487 | @assumes('ideal gas') 488 | def p_from_rho_Tv_ideal_gas(rho, Tv): 489 | return ne.evaluate('rho*Rd*Tv') 490 | 491 | 492 | @autodoc(equation=r'p_{lcl} = p (\frac{T_{lcl}}{T})^(\frac{C_{pd}}{R_d})') 493 | @assumes('constant Cp') 494 | def plcl_from_p_T_Tlcl(p, T, Tlcl): 495 | return ne.evaluate('p*(Tlcl/T)**(Cpd/Rd)') 496 | 497 | 498 | @autodoc(equation=r'Phi = g_0 z') 499 | @assumes('constant g') 500 | def Phi_from_z(z): 501 | return ne.evaluate('g0*z') 502 | 503 | 504 | @autodoc(equation=r'q_v = \frac{AH}{\rho}') 505 | @assumes() 506 | def qv_from_AH_rho(AH, rho): 507 | return ne.evaluate('AH/rho') 508 | 509 | 510 | @autodoc(equation=r'q_v = \frac{r_v}{1+r_v}') 511 | @assumes() 512 | @overridden_by_assumptions('low water vapor') 513 | def qv_from_rv(rv): 514 | return ne.evaluate('rv/(1.+rv)') 515 | 516 | 517 | @autodoc(equation=r'q_v = r_v') 518 | @assumes('low water vapor') 519 | def qv_from_rv_lwv(rv): 520 | return 1.*rv 521 | 522 | 523 | @autodoc(equation=r'q_v = \frac{R_d}{R_v} \frac{e}{p-(1-\frac{R_d}{R_v}) e}') 524 | @assumes() 525 | @overridden_by_assumptions('low water vapor') 526 | def qv_from_p_e(p, e): 527 | return ne.evaluate('0.622*e/(p-0.378*e)') 528 | 529 | 530 | @autodoc(equation=r'qv = (\frac{R_d}{R_v}) \frac{e}{p}') 531 | @assumes('low water vapor') 532 | def qv_from_p_e_lwv(p, e): 533 | return ne.evaluate('0.622*e/p') 534 | 535 | 536 | @autodoc(equation=r'q_{vs} = \frac{r_{vs}}{1+r_{vs}}') 537 | @assumes() 538 | @overridden_by_assumptions('low water vapor') 539 | def qvs_from_rvs(rvs): 540 | return qv_from_rv(rvs) 541 | 542 | 543 | @autodoc(equation=r'q_v = r_v') 544 | @assumes('low water vapor') 545 | def qvs_from_rvs_lwv(rvs): 546 | return 1.*rvs 547 | 548 | 549 | @autodoc(equation=r'q_{vs} = qv\_from\_p\_e(p, e_s)') 550 | @assumes() 551 | @overridden_by_assumptions('low water vapor') 552 | def qvs_from_p_es(p, es): 553 | return qv_from_p_e(p, es) 554 | 555 | 556 | @autodoc(equation=r'q_{vs} = qv\_from\_p\_e\_lwv(p, e_s)') 557 | @assumes('low water vapor') 558 | def qvs_from_p_es_lwv(p, es): 559 | return qv_from_p_e_lwv(p, es) 560 | 561 | 562 | @autodoc(equation=r'q_{vsi} = qv\_from\_p\_e(p, e_{si})') 563 | @assumes() 564 | @overridden_by_assumptions('low water vapor') 565 | def qvsi_from_p_esi(p, esi): 566 | return qv_from_p_e(p, esi) 567 | 568 | 569 | @autodoc(equation=r'q_{vsi} = qv\_from\_p\_e\_lwv(p, e_{si})') 570 | @assumes('low water vapor') 571 | def qvsi_from_p_esi_lwv(p, esi): 572 | return qv_from_p_e_lwv(p, esi) 573 | 574 | 575 | @autodoc(equation=r'q_t = q_i+q_v+q_l') 576 | @assumes() 577 | @overridden_by_assumptions('no liquid water', 'no ice') 578 | def qt_from_qi_qv_ql(qi, qv, ql): 579 | return ne.evaluate('qi+qv+ql') 580 | 581 | 582 | @autodoc(equation=r'q_t = q_v+q_l') 583 | @assumes('no ice') 584 | @overridden_by_assumptions('no liquid water') 585 | def qt_from_qv_ql(qv, ql): 586 | return ne.evaluate('qv+ql') 587 | 588 | 589 | @autodoc(equation='q_t = q_v') 590 | @assumes('no liquid water', 'no ice') 591 | def qt_from_qv(qv): 592 | return 1.*qv 593 | 594 | 595 | @autodoc(equation='q_t = q_v+q_l') 596 | @assumes('no liquid water') 597 | @overridden_by_assumptions('no ice') 598 | def qt_from_qv_qi(qv, qi): 599 | return ne.evaluate('qv+qi') 600 | 601 | 602 | @autodoc(equation='q_v = q_t') 603 | @assumes('no liquid water', 'no ice') 604 | def qv_from_qt(qt): 605 | return 1.*qt 606 | 607 | 608 | @autodoc(equation='q_v = q_t-q_l-q_i') 609 | @assumes() 610 | @overridden_by_assumptions('no liquid water', 'no ice') 611 | def qv_from_qt_ql_qi(qt, ql, qi): 612 | return ne.evaluate('qt-ql-qi') 613 | 614 | 615 | @autodoc(equation='q_v = q_t-q_l') 616 | @assumes('no ice') 617 | @overridden_by_assumptions('no liquid water') 618 | def qv_from_qt_ql(qt, ql): 619 | return ne.evaluate('qt-ql') 620 | 621 | 622 | @autodoc(equation='q_v = q_t - q_i') 623 | @assumes('no liquid water') 624 | @overridden_by_assumptions('no ice') 625 | def qv_from_qt_qi(qt, qi): 626 | return ne.evaluate('qt-qi') 627 | 628 | 629 | @autodoc(equation='q_i = q_t-q_v-q_l') 630 | @assumes() 631 | @overridden_by_assumptions('no liquid water', 'no ice') 632 | def qi_from_qt_qv_ql(qt, qv, ql): 633 | return ne.evaluate('qt-qv-ql') 634 | 635 | 636 | @autodoc(equation='q_i = q_t-q_v') 637 | @assumes('no liquid water') 638 | @overridden_by_assumptions('no ice') 639 | def qi_from_qt_qv(qt, qv): 640 | return ne.evaluate('qt-qv') 641 | 642 | 643 | @autodoc(equation='q_l = q_t-q_v-q_i') 644 | @assumes() 645 | @overridden_by_assumptions('no liquid water', 'no ice') 646 | def ql_from_qt_qv_qi(qt, qv, qi): 647 | return ne.evaluate('qt-qv-qi') 648 | 649 | 650 | @autodoc(equation='q_l = q_t-q_v') 651 | @assumes('no ice') 652 | @overridden_by_assumptions('no liquid water') 653 | def ql_from_qt_qv(qt, qv): 654 | return ne.evaluate('qt-qv') 655 | 656 | 657 | @autodoc(equation=r'RH = \frac{r_v}{r_{vs}} \times 100') 658 | @assumes() 659 | def RH_from_rv_rvs(rv, rvs): 660 | return ne.evaluate('rv/rvs*100.') 661 | 662 | 663 | @autodoc(equation=r'RH_i = \frac{r_v}{r_{vsi}} \times 100') 664 | @assumes() 665 | def RHi_from_rv_rvsi(rv, rvsi): 666 | return ne.evaluate('rv/rvsi*100.') 667 | 668 | 669 | @autodoc(equation=r'RH = \frac{q_{v}}{q_{vs}} \times 100') 670 | @assumes('low water vapor') 671 | def RH_from_qv_qvs_lwv(qv, qvs): 672 | return ne.evaluate('qv/qvs*100.') 673 | 674 | 675 | @autodoc(equation=r'RH_i = \frac{q_{v}}{q_{vsi}} \times 100') 676 | @assumes('low water vapor') 677 | def RHi_from_qv_qvsi_lwv(qv, qvsi): 678 | return ne.evaluate('qv/qvsi*100.') 679 | 680 | 681 | @autodoc(equation=r'\rho = \frac{AH}{q_v}') 682 | @assumes() 683 | def rho_from_qv_AH(qv, AH): 684 | return ne.evaluate('AH/qv') 685 | 686 | 687 | @autodoc(equation=r'\rho = \frac{p}{R_d T_v}') 688 | @assumes('ideal gas') 689 | def rho_from_p_Tv_ideal_gas(p, Tv): 690 | return ne.evaluate('p/(Rd*Tv)') 691 | 692 | 693 | @autodoc(equation=r'r_v = \frac{q_v}{1-q_v}') 694 | @assumes() 695 | @overridden_by_assumptions('low water vapor') 696 | def rv_from_qv(qv): 697 | return ne.evaluate('qv/(1-qv)') 698 | 699 | 700 | @autodoc(equation='r_v = q_v') 701 | @assumes('low water vapor') 702 | def rv_from_qv_lwv(qv): 703 | return 1.*qv 704 | 705 | 706 | @autodoc(equation='r_v = \frac{-311 (T-T_v)}{500 T - 311 T_v}') 707 | @assumes() 708 | @overridden_by_assumptions('low water vapor') 709 | def rv_from_Tv_T(Tv, T): 710 | return ne.evaluate('-311*(T-Tv)/(500*T-311*Tv)') 711 | 712 | 713 | @autodoc(equation='r_v = (\frac{T_v}{T} - 1)\frac{0.622}{1-0.622}') 714 | @assumes('low water vapor') 715 | def rv_from_Tv_T_lwv(Tv, T): 716 | return ne.evaluate('(Tv/T - 1)*(0.622/(1-0.622))') 717 | 718 | 719 | @autodoc(equation=r'r_v = \frac{RH}{100} r_{vs}') 720 | @assumes() 721 | def rv_from_RH_rvs(RH, rvs): 722 | return ne.evaluate('RH/100.*rvs') 723 | 724 | 725 | @autodoc(equation=r'r_v = \frac{RH_i}{100} r_{vsi}') 726 | @assumes() 727 | def rv_from_RHi_rvsi(RHi, rvsi): 728 | return ne.evaluate('RHi/100.*rvsi') 729 | 730 | 731 | @autodoc(equation=r'rv = (\frac{Rd}{Rv}) \frac{e}{p-e}') 732 | @assumes() 733 | def rv_from_p_e(p, e): 734 | return ne.evaluate('0.622*e/(p-e)') 735 | 736 | 737 | @autodoc(equation=r'r_t = r_i+r_v+r_l') 738 | @assumes() 739 | @overridden_by_assumptions('no liquid water', 'no ice') 740 | def rt_from_ri_rv_rl(ri, rv, rl): 741 | return ne.evaluate('ri+rv+rl') 742 | 743 | 744 | @autodoc(equation=r'r_t = r_v+r_l') 745 | @assumes('no ice') 746 | @overridden_by_assumptions('no liquid water') 747 | def rt_from_rv_rl(rv, rl): 748 | return ne.evaluate('rv+rl') 749 | 750 | 751 | @autodoc(equation=r'r_t = r_v') 752 | @assumes('no liquid water', 'no ice') 753 | def rt_from_rv(rv): 754 | return 1.*rv 755 | 756 | 757 | @autodoc(equation=r'r_t = r_v+r_l') 758 | @assumes('no liquid water') 759 | @overridden_by_assumptions('no ice') 760 | def rt_from_rv_ri(rv, ri): 761 | return ne.evaluate('rv+ri') 762 | 763 | 764 | @autodoc(equation=r'r_v = r_t') 765 | @assumes('no liquid water', 'no ice') 766 | def rv_from_rt(rt): 767 | return 1.*rt 768 | 769 | 770 | @autodoc(equation=r'r_v = r_t-r_l-r_i') 771 | @assumes() 772 | @overridden_by_assumptions('no liquid water', 'no ice') 773 | def rv_from_rt_rl_ri(rt, rl, ri): 774 | return ne.evaluate('rt-rl-ri') 775 | 776 | 777 | @autodoc(equation=r'r_v = r_t-r_l') 778 | @assumes('no ice') 779 | @overridden_by_assumptions('no liquid water') 780 | def rv_from_rt_rl(rt, rl): 781 | return ne.evaluate('rt-rl') 782 | 783 | 784 | @autodoc(equation=r'r_v = r_t - r_i') 785 | @assumes('no liquid water') 786 | @overridden_by_assumptions('no ice') 787 | def rv_from_rt_ri(rt, ri): 788 | return ne.evaluate('rt-ri') 789 | 790 | 791 | @autodoc(equation=r'r_i = r_t-r_v-r_l') 792 | @assumes() 793 | @overridden_by_assumptions('no liquid water', 'no ice') 794 | def ri_from_rt_rv_rl(rt, rv, rl): 795 | return ne.evaluate('rt-rv-rl') 796 | 797 | 798 | @autodoc(equation=r'r_i = r_t-r_v') 799 | @assumes('no liquid water') 800 | @overridden_by_assumptions('no ice') 801 | def ri_from_rt_rv(rt, rv): 802 | return ne.evaluate('rt-rv') 803 | 804 | 805 | @autodoc(equation=r'r_l = r_t-r_v-r_i') 806 | @assumes() 807 | @overridden_by_assumptions('no liquid water', 'no ice') 808 | def rl_from_rt_rv_ri(rt, rv, ri): 809 | return ne.evaluate('rt-rv-ri') 810 | 811 | 812 | @autodoc(equation=r'r_l = r_t-r_v') 813 | @assumes('no ice') 814 | @overridden_by_assumptions('no liquid water') 815 | def rl_from_rt_rv(rt, rv): 816 | return ne.evaluate('rt-rv') 817 | 818 | 819 | @autodoc(equation=r'r_{vs} = rv\_from\_p\_e(p, e_s)') 820 | @assumes() 821 | def rvs_from_p_es(p, es): 822 | return rv_from_p_e(p, es) 823 | 824 | 825 | @autodoc(equation=r'r_{vsi} = rv\_from\_p\_e(p, e_{si})') 826 | @assumes() 827 | def rvsi_from_p_esi(p, esi): 828 | return rv_from_p_e(p, esi) 829 | 830 | 831 | @autodoc(equation=r'r_{vs} = rv\_from\_qv(q_{vs})') 832 | @assumes() 833 | @overridden_by_assumptions('low water vapor') 834 | def rvs_from_qvs(qvs): 835 | return rv_from_qv(qvs) 836 | 837 | 838 | @autodoc(equation=r'r_{vsi} = rv\_from\_qv(q_{vsi})') 839 | @assumes() 840 | @overridden_by_assumptions('low water vapor') 841 | def rvsi_from_qvsi(qvsi): 842 | return rv_from_qv(qvsi) 843 | 844 | 845 | @autodoc(equation=r'r_{vsi} = rv\_from\_qv(q_{vsi})') 846 | @assumes('low water vapor') 847 | def rvsi_from_qvsi_lwv(qvsi): 848 | return rv_from_qv_lwv(qvsi) 849 | 850 | 851 | @autodoc(equation=r'r_v = rv\_from\_qv(q_{vs})') 852 | @assumes('low water vapor') 853 | def rvs_from_qvs_lwv(qvs): 854 | return rv_from_qv_lwv(qvs) 855 | 856 | 857 | @autodoc(equation=r'T = \frac{29.65 log(es)-4880.16}{log(es)-19.48}', 858 | references=ref['Bolton 1980'] + ref['Wexler 1976'], 859 | notes=''' 860 | Fits Wexler's formula to an accuracy of 0.1% for temperatures between 861 | -35C and 35C.''') 862 | @assumes('bolton') 863 | def T_from_es_Bolton(es): 864 | return ne.evaluate('(59300*log(5*es/3056)-9653121)/(2000*log(5*es/3056)-' 865 | '35340)') 866 | 867 | 868 | @autodoc(equation=r'T_{lcl} = ((\frac{1}{T-55}-(\frac{log(\frac{RH}{100})}' 869 | '{2840}))^{-1} + 55', 870 | references=ref['Bolton 1980'], 871 | notes='Uses Bolton (1980) equation 22.') 872 | @assumes('bolton') 873 | def Tlcl_from_T_RH(T, RH): 874 | return ne.evaluate('1./((1./(T-55.))-(log(RH/100.)/2840.)) + 55.') 875 | 876 | 877 | @autodoc(equation=r'T_{lcl} = ((1./(Td-56.))-(log(T/Td)/800.))^{-1} + 56.', 878 | references=ref['Bolton 1980'], 879 | notes='Uses Bolton (1980) equation 15.') 880 | @assumes('bolton') 881 | def Tlcl_from_T_Td(T, Td): 882 | return ne.evaluate('1./((1./(Td-56.))+(log(T/Td)/800.)) + 56.') 883 | 884 | 885 | @autodoc(equation=r'T_{lcl} = \frac{2840}{3.5 log(T) - log(e) - 4.805} + 55', 886 | references=ref['Bolton 1980'], 887 | notes='Uses Bolton(1980) equation 21.') 888 | @assumes('bolton') 889 | def Tlcl_from_T_e(T, e): 890 | return ne.evaluate('2840./(3.5*log(T)-log(e*0.01)-4.805) + 55.') 891 | 892 | 893 | @autodoc(equation=r'T = \theta (\frac{10^5}{p})^{-\frac{R_d}{C_{pd}}}') 894 | @assumes('constant Cp') 895 | def T_from_p_theta(p, theta): 896 | return ne.evaluate('theta*(1e5/p)**(-Rd/Cpd)') 897 | 898 | 899 | @autodoc( 900 | equation=r'T_v = \frac{T}{1-\frac{e}{p}(1-0.622)}', 901 | notes=""" 902 | Neglects density effects of liquid and solid water""") 903 | @assumes() 904 | @overridden_by_assumptions('Tv equals T') 905 | def Tv_from_p_e_T(p, e, T): 906 | return ne.evaluate('T/(1-e/p*(1-0.622))') 907 | 908 | 909 | @autodoc( 910 | equation=r'T = T_v (1-\frac{e}{p}(1-0.622))', 911 | notes=""" 912 | Neglects density effects of liquid and solid water""") 913 | @assumes() 914 | @overridden_by_assumptions('Tv equals T') 915 | def T_from_p_e_Tv(p, e, Tv): 916 | return ne.evaluate('Tv*(1-e/p*(1-0.622))') 917 | 918 | 919 | @autodoc( 920 | equation=r'T_v = T \frac{1 + \frac{r_v}{0.622}}{1+r_v}', 921 | notes=""" 922 | Neglects density effects of liquid and solid water""") 923 | @assumes() 924 | @overridden_by_assumptions('low water vapor', 'Tv equals T') 925 | def Tv_from_T_rv(T, rv): 926 | return ne.evaluate('T*(1+rv/0.622)/(1+rv)') 927 | 928 | 929 | @autodoc( 930 | equation=r'T = T_v \frac{1 + r_v}{1+ \frac{r_v}{0.622}', 931 | notes=""" 932 | Neglects density effects of liquid and solid water""") 933 | @assumes() 934 | @overridden_by_assumptions('low water vapor', 'Tv equals T') 935 | def T_from_Tv_rv(Tv, rv): 936 | return ne.evaluate('Tv/(1+rv/0.622)*(1+rv)') 937 | 938 | 939 | @autodoc( 940 | equation=r'T_v = T (1 + (\frac{1}{0.622} - 1) r_v)', 941 | notes=""" 942 | Neglects density effects of liquid and solid water""") 943 | @assumes('low water vapor') 944 | @overridden_by_assumptions('Tv equals T') 945 | def Tv_from_T_rv_lwv(T, rv): 946 | return ne.evaluate('T*(1+(1/0.622-1)*rv)') 947 | 948 | 949 | @autodoc( 950 | equation=r'T = \frac{T_v}{1 + (\frac{1}{0.622} - 1) r_v}', 951 | notes=""" 952 | Neglects density effects of liquid and solid water""") 953 | @assumes('low water vapor') 954 | @overridden_by_assumptions('Tv equals T') 955 | def T_from_Tv_rv_lwv(Tv, rv): 956 | return ne.evaluate('Tv/(1+(1/0.622-1)*rv)') 957 | 958 | 959 | @autodoc(equation=r'T_v = T', 960 | notes=''' 961 | This function exists to allow using temperature as virtual temperature.''') 962 | @assumes('Tv equals T') 963 | def Tv_from_T_assuming_Tv_equals_T(T): 964 | return 1.*T 965 | 966 | 967 | @autodoc(equation=r'T_v = \frac{p}{\rho R_d}') 968 | @assumes('ideal gas') 969 | def Tv_from_p_rho_ideal_gas(p, rho): 970 | return ne.evaluate('p/(rho*Rd)') 971 | 972 | 973 | @autodoc(references=ref['Stull 2011'], 974 | notes=''' 975 | Uses the empirical inverse solution from Stull (2011). Only valid at 101.3kPa. 976 | ''') 977 | @assumes() 978 | def Tw_from_T_RH_Stull(T, RH): 979 | return ne.evaluate('''((T-273.15)*arctan(0.151977*(RH + 8.313659)**0.5) 980 | + arctan(T-273.15+RH) - arctan(RH - 1.676331) 981 | + 0.00391838*RH**1.5*arctan(0.023101*RH) - 4.686035 + 273.15)''') 982 | 983 | 984 | @autodoc(equation=r'T = T_v', 985 | notes=''' 986 | This function exists to allow using temperature as virtual temperature.''') 987 | @assumes('Tv equals T') 988 | def T_from_Tv_assuming_Tv_equals_T(Tv): 989 | return 1.*Tv 990 | 991 | 992 | @autodoc(equation=r'\theta = T (\frac{10^5}{p})^(\frac{R_d}{C_{pd}})') 993 | @assumes('constant Cp') 994 | def theta_from_p_T(p, T): 995 | return ne.evaluate('T*(1e5/p)**(Rd/Cpd)') 996 | 997 | 998 | @autodoc( 999 | equation=r'\theta_e = T (\frac{10^5}{p})^(\frac{R_d}{C_{pd}})' 1000 | r'(1-0.28 r_v)) exp((\frac{3.376}{T_{lcl}}-0.00254) (r_v \times 10^3) ' 1001 | r'(1+0.81 r_v))', 1002 | references=ref['Bolton 1980'] + ref['Davies-Jones 2009'], 1003 | notes=''' 1004 | This is one of the most accurate ways of computing thetae, with an 1005 | error of less than 0.2K due mainly to assuming Cp does not vary with 1006 | temperature or pressure.''') 1007 | @assumes('bolton', 'constant Cp', 'no liquid water') 1008 | def thetae_from_p_T_Tlcl_rv_Bolton(p, T, Tlcl, rv): 1009 | return ne.evaluate('T*(1e5/p)**((Rd/Cpd)*(1-0.28*rv))*exp((3.376/Tlcl-' 1010 | '0.00254)*rv*1e3*(1+0.81*rv))') 1011 | 1012 | 1013 | @autodoc(equation=r'\theta_e = T (\frac{10^5}{p})^(\frac{R_d}{C_{pd}) + ' 1014 | r'r_t C_l}) RH^{-r_v \frac{R_v}{C_{pd} +' 1015 | r'r_t C_l}} exp(L_v \frac{r_v}{C_{pd}+r_t C_l})', 1016 | references=ref['AMS Glossary thetae']) 1017 | @assumes() 1018 | @overridden_by_assumptions('low water vapor') 1019 | def thetae_from_p_e_T_RH_rv_rt(p, e, T, RH, rv, rt): 1020 | return ne.evaluate('T*(1e5/(p-e))**(Rd/(Cpd + rt*Cl))*RH**(-rv*Rv/(Cpd + ' 1021 | 'rt*Cl))*exp(Lv0*rv/((Cpd+rt*Cl)*T))') 1022 | 1023 | 1024 | @autodoc(equation=r'\theta_e = T*(\frac{10^5}{p})^(\frac{R_d}{C_{pd}}) ' 1025 | r'RH^{-r_v \frac{Rv}{C_{pd}}} exp(L_v \frac{Rv}{C_{pd}})') 1026 | @assumes('low water vapor') 1027 | def thetae_from_T_RH_rv_lwv(T, RH, rv): 1028 | return ne.evaluate('T*(1e5/p)**(Rd/Cpd)*RH**(-rv*Rv/Cpd)*' 1029 | 'exp(Lv0*rv/(Cpd*T)') 1030 | 1031 | 1032 | @autodoc(equation=r'\theta_{es} = thetae\_from\_p\_T\_Tlcl\_rv\_Bolton(p, T, ' 1033 | r'T, r_{vs})', 1034 | references=ref['Bolton 1980'] + ref['Davies-Jones 2009'], 1035 | notes=''' 1036 | See thetae_from_theta_Tlcl_rv_Bolton for more information.''') 1037 | @assumes('bolton', 'constant Cp') 1038 | def thetaes_from_p_T_rvs_Bolton(p, T, rvs): 1039 | return thetae_from_p_T_Tlcl_rv_Bolton(p, T, T, rvs) 1040 | 1041 | 1042 | @autodoc(equation=r'w = - \frac{\omega}{\rho g_0}') 1043 | @assumes('constant g', 'hydrostatic') 1044 | def w_from_omega_rho_hydrostatic(omega, rho): 1045 | return ne.evaluate('-omega/(rho*g0)') 1046 | 1047 | 1048 | @autodoc(equation=r'z = \frac{\Phi}{g_0}') 1049 | @assumes('constant g') 1050 | def z_from_Phi(Phi): 1051 | return ne.evaluate('Phi/g0') 1052 | -------------------------------------------------------------------------------- /atmos/plot.py: -------------------------------------------------------------------------------- 1 | """ 2 | plot.py: Utilities for plotting meteorological data. Importing this package 3 | gives access to the "skewT" projection. 4 | 5 | This file was originally edited from code in MetPy. The MetPy copyright 6 | disclamer is at the bottom of the source code of this file. 7 | """ 8 | 9 | import numpy as np 10 | import matplotlib.transforms as transforms 11 | import matplotlib.axis as maxis 12 | import matplotlib.spines as mspines 13 | from matplotlib.axes import Axes 14 | from matplotlib.collections import LineCollection 15 | from matplotlib.projections import register_projection 16 | from matplotlib.ticker import ScalarFormatter, MultipleLocator 17 | from atmos import calculate 18 | from atmos.constants import g0 19 | from scipy.integrate import odeint 20 | from atmos.util import closest_val 21 | from appdirs import user_cache_dir 22 | import os 23 | 24 | 25 | # The sole purpose of this class is to look at the upper, lower, or total 26 | # interval as appropriate and see what parts of the tick to draw, if any. 27 | class SkewXTick(maxis.XTick): 28 | def draw(self, renderer): 29 | if not self.get_visible(): 30 | return 31 | renderer.open_group(self.__name__) 32 | 33 | lower_interval = self.axes.xaxis.lower_interval 34 | upper_interval = self.axes.xaxis.upper_interval 35 | 36 | if self.gridOn and transforms.interval_contains( 37 | self.axes.xaxis.get_view_interval(), self.get_loc()): 38 | self.gridline.draw(renderer) 39 | 40 | if transforms.interval_contains(lower_interval, self.get_loc()): 41 | if self.tick1On: 42 | self.tick1line.draw(renderer) 43 | if self.label1On: 44 | self.label1.draw(renderer) 45 | 46 | if transforms.interval_contains(upper_interval, self.get_loc()): 47 | if self.tick2On: 48 | self.tick2line.draw(renderer) 49 | if self.label2On: 50 | self.label2.draw(renderer) 51 | 52 | renderer.close_group(self.__name__) 53 | 54 | 55 | # This class exists to provide two separate sets of intervals to the tick, 56 | # as well as create instances of the custom tick 57 | class SkewXAxis(maxis.XAxis): 58 | def __init__(self, *args, **kwargs): 59 | maxis.XAxis.__init__(self, *args, **kwargs) 60 | self.upper_interval = 0.0, 1.0 61 | 62 | def _get_tick(self, major): 63 | return SkewXTick(self.axes, 0, '', major=major) 64 | 65 | @property 66 | def lower_interval(self): 67 | return self.axes.viewLim.intervalx 68 | 69 | def get_view_interval(self): 70 | return self.upper_interval[0], self.axes.viewLim.intervalx[1] 71 | 72 | 73 | class SkewYAxis(maxis.YAxis): 74 | pass 75 | 76 | 77 | # This class exists to calculate the separate data range of the 78 | # upper X-axis and draw the spine there. It also provides this range 79 | # to the X-axis artist for ticking and gridlines 80 | class SkewSpine(mspines.Spine): 81 | def _adjust_location(self): 82 | trans = self.axes.transDataToAxes.inverted() 83 | if self.spine_type == 'top': 84 | yloc = 1.0 85 | else: 86 | yloc = 0.0 87 | left = trans.transform_point((0.0, yloc))[0] 88 | right = trans.transform_point((1.0, yloc))[0] 89 | 90 | pts = self._path.vertices 91 | pts[0, 0] = left 92 | pts[1, 0] = right 93 | self.axis.upper_interval = (left, right) 94 | 95 | 96 | # This class handles registration of the skew-xaxes as a projection as well 97 | # as setting up the appropriate transformations. It also overrides standard 98 | # spines and axes instances as appropriate. 99 | class SkewTAxes(Axes): 100 | # The projection must specify a name. This will be used be the 101 | # user to select the projection, i.e. ``subplot(111, 102 | # projection='skewx')``. 103 | name = 'skewT' 104 | default_xlim = (-40, 50) 105 | default_ylim = (1050, 100) 106 | 107 | def __init__(self, *args, **kwargs): 108 | # This needs to be popped and set before moving on 109 | self.rot = kwargs.pop('rotation', 45) 110 | # set booleans to keep track of extra axes that are plotted 111 | self._mixing_lines = [] 112 | self._dry_adiabats = [] 113 | self._moist_adiabats = [] 114 | Axes.__init__(self, *args, **kwargs) 115 | 116 | def _init_axis(self): 117 | # Taken from Axes and modified to use our modified X-axis 118 | self.xaxis = SkewXAxis(self) 119 | self.spines['top'].register_axis(self.xaxis) 120 | self.spines['bottom'].register_axis(self.xaxis) 121 | self.yaxis = maxis.YAxis(self) 122 | self.yaxis.set_major_formatter(ScalarFormatter()) 123 | self.yaxis.set_major_locator(MultipleLocator(100)) 124 | self.spines['left'].register_axis(self.yaxis) 125 | self.spines['right'].register_axis(self.yaxis) 126 | 127 | def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'): 128 | # pylint: disable=unused-argument 129 | spines = {'top': SkewSpine.linear_spine(self, 'top'), 130 | 'bottom': mspines.Spine.linear_spine(self, 'bottom'), 131 | 'left': mspines.Spine.linear_spine(self, 'left'), 132 | 'right': mspines.Spine.linear_spine(self, 'right')} 133 | return spines 134 | 135 | def _set_lim_and_transforms(self): 136 | """ 137 | This is called once when the plot is created to set up all the 138 | transforms for the data, text and grids. 139 | """ 140 | # Get the standard transform setup from the Axes base class 141 | Axes._set_lim_and_transforms(self) 142 | 143 | # Need to put the skew in the middle, after the scale and limits, 144 | # but before the transAxes. This way, the skew is done in Axes 145 | # coordinates thus performing the transform around the proper origin 146 | # We keep the pre-transAxes transform around for other users, like the 147 | # spines for finding bounds 148 | self.transDataToAxes = (self.transScale + 149 | (self.transLimits + 150 | transforms.Affine2D().skew_deg(self.rot, 0))) 151 | 152 | # Create the full transform from Data to Pixels 153 | self.transData = self.transDataToAxes + self.transAxes 154 | 155 | # Blended transforms like this need to have the skewing applied using 156 | # both axes, in axes coords like before. 157 | self._xaxis_transform = (transforms.blended_transform_factory( 158 | self.transScale + self.transLimits, 159 | transforms.IdentityTransform()) + 160 | transforms.Affine2D().skew_deg(self.rot, 0)) + self.transAxes 161 | 162 | def cla(self): 163 | Axes.cla(self) 164 | # Disables the log-formatting that comes with semilogy 165 | self.yaxis.set_major_formatter(ScalarFormatter()) 166 | self.yaxis.set_major_locator(MultipleLocator(100)) 167 | if not self.yaxis_inverted(): 168 | self.invert_yaxis() 169 | 170 | # Try to make sane default temperature plotting 171 | self.xaxis.set_major_locator(MultipleLocator(5)) 172 | self.xaxis.set_major_formatter(ScalarFormatter()) 173 | self.set_xlim(*self.default_xlim) 174 | self.set_ylim(*self.default_ylim) 175 | 176 | def semilogy(self, p, T, *args, **kwargs): 177 | r'''Plot data. 178 | 179 | Simple wrapper around plot so that pressure is the first (independent) 180 | input. This is essentially a wrapper around `semilogy`. 181 | 182 | Parameters 183 | ---------- 184 | p : array_like 185 | pressure values 186 | T : array_like 187 | temperature values, can also be used for things like dew point 188 | args 189 | Other positional arguments to pass to `semilogy` 190 | kwargs 191 | Other keyword arguments to pass to `semilogy` 192 | 193 | See Also 194 | -------- 195 | `matplotlib.Axes.semilogy` 196 | ''' 197 | # We need to replace the overridden plot with the original Axis plot 198 | # since it is called within Axes.semilogy 199 | no_plot = SkewTAxes.plot 200 | SkewTAxes.plot = Axes.plot 201 | Axes.semilogy(self, T, p, *args, **kwargs) 202 | # Be sure to put back the overridden plot method 203 | SkewTAxes.plot = no_plot 204 | self.yaxis.set_major_formatter(ScalarFormatter()) 205 | self.yaxis.set_major_locator(MultipleLocator(100)) 206 | labels = self.xaxis.get_ticklabels() 207 | for label in labels: 208 | label.set_rotation(45) 209 | label.set_horizontalalignment('right') 210 | label.set_fontsize(8) 211 | label.set_color('#B31515') 212 | self.grid(True) 213 | self.grid(axis='top', color='#B31515', linestyle='-', linewidth=1, 214 | alpha=0.5, zorder=1.1) 215 | self.grid(axis='x', color='#B31515', linestyle='-', linewidth=1, 216 | alpha=0.5, zorder=1.1) 217 | self.grid(axis='y', color='k', linestyle='-', linewidth=0.5, alpha=0.5, 218 | zorder=1.1) 219 | self.set_xlabel(r'Temperature ($^{\circ} C$)', color='#B31515') 220 | self.set_ylabel('Pressure ($hPa$)') 221 | if len(self._mixing_lines) == 0: 222 | self.plot_mixing_lines() 223 | if len(self._dry_adiabats) == 0: 224 | self.plot_dry_adiabats() 225 | if len(self._moist_adiabats) == 0: 226 | self.plot_moist_adiabats() 227 | 228 | def plot(self, *args, **kwargs): 229 | r'''Plot data. 230 | 231 | Simple wrapper around plot so that pressure is the first (independent) 232 | input. This is essentially a wrapper around `semilogy`. 233 | 234 | Parameters 235 | ---------- 236 | p : array_like 237 | pressure values 238 | T : array_like 239 | temperature values, can also be used for things like dew point 240 | args 241 | Other positional arguments to pass to `semilogy` 242 | kwargs 243 | Other keyword arguments to pass to `semilogy` 244 | 245 | See Also 246 | -------- 247 | `matplotlib.Axes.semilogy` 248 | ''' 249 | self.semilogy(*args, **kwargs) 250 | 251 | def semilogx(self, *args, **kwargs): 252 | r'''Plot data. 253 | 254 | Simple wrapper around plot so that pressure is the first (independent) 255 | input. This is essentially a wrapper around `semilogy`. 256 | 257 | Parameters 258 | ---------- 259 | p : array_like 260 | pressure values 261 | T : array_like 262 | temperature values, can also be used for things like dew point 263 | args 264 | Other positional arguments to pass to `semilogy` 265 | kwargs 266 | Other keyword arguments to pass to `semilogy` 267 | 268 | See Also 269 | -------- 270 | `matplotlib.Axes.semilogy` 271 | ''' 272 | self.semilogy(*args, **kwargs) 273 | 274 | def plot_barbs(self, p, u, v, xloc=1.0, x_clip_radius=0.08, 275 | y_clip_radius=0.08, **kwargs): 276 | r'''Plot wind barbs. 277 | 278 | Adds wind barbs to the skew-T plot. This is a wrapper around the 279 | `barbs` command that adds to appropriate transform to place the 280 | barbs in a vertical line, located as a function of pressure. 281 | 282 | Parameters 283 | ---------- 284 | p : array_like 285 | pressure values 286 | u : array_like 287 | U (East-West) component of wind 288 | v : array_like 289 | V (North-South) component of wind 290 | xloc : float, optional 291 | Position for the barbs, in normalized axes coordinates, where 0.0 292 | denotes far left and 1.0 denotes far right. Defaults to far right. 293 | x_clip_radius : float, optional 294 | Space, in normalized axes coordinates, to leave before clipping 295 | wind barbs in the x-direction. Defaults to 0.08. 296 | y_clip_radius : float, optional 297 | Space, in normalized axes coordinates, to leave above/below plot 298 | before clipping wind barbs in the y-direction. Defaults to 0.08. 299 | kwargs 300 | Other keyword arguments to pass to `barbs` 301 | 302 | See Also 303 | -------- 304 | `matplotlib.Axes.barbs` 305 | ''' 306 | #kwargs.setdefault('length', 7) 307 | 308 | # Assemble array of x-locations in axes space 309 | x = np.empty_like(p) 310 | x.fill(xloc) 311 | 312 | # Do barbs plot at this location 313 | b = self.barbs(x, p, u, v, 314 | transform=self.get_yaxis_transform(which='tick2'), 315 | clip_on=True, **kwargs) 316 | 317 | # Override the default clip box, which is the axes rectangle, so we can 318 | # have barbs that extend outside. 319 | ax_bbox = transforms.Bbox([[xloc-x_clip_radius, -y_clip_radius], 320 | [xloc+x_clip_radius, 1.0 + y_clip_radius]]) 321 | b.set_clip_box(transforms.TransformedBbox(ax_bbox, self.transAxes)) 322 | 323 | def plot_dry_adiabats(self, p=None, theta=None, **kwargs): 324 | r'''Plot dry adiabats. 325 | 326 | Adds dry adiabats (lines of constant potential temperature) to the 327 | plot. The default style of these lines is dashed red lines with an 328 | alpha value of 0.5. These can be overridden using keyword arguments. 329 | 330 | Parameters 331 | ---------- 332 | p : array_like, optional 333 | 1-dimensional array of pressure values to be included in the dry 334 | adiabats. If not specified, they will be linearly distributed 335 | across the current plotted pressure range. 336 | theta : array_like, optional 337 | 1-dimensional array of potential temperature values for dry 338 | adiabats. By default these will be generated based on the current 339 | temperature limits. 340 | kwargs 341 | Other keyword arguments to pass to 342 | `matplotlib.collections.LineCollection` 343 | 344 | See Also#B85C00 345 | -------- 346 | plot_moist_adiabats 347 | `matplotlib.collections.LineCollection` 348 | `metpy.calc.dry_lapse` 349 | ''' 350 | for artist in self._dry_adiabats: 351 | artist.remove() 352 | self._dry_adiabats = [] 353 | 354 | # Determine set of starting temps if necessary 355 | if theta is None: 356 | xmin, xmax = self.get_xlim() 357 | theta = np.arange(xmin, xmax + 201, 10) 358 | 359 | # Get pressure levels based on ylims if necessary 360 | if p is None: 361 | p = np.linspace(*self.get_ylim()) 362 | 363 | # Assemble into data for plotting 364 | t = calculate('T', theta=theta[:, None], p=p, p_units='hPa', 365 | T_units='degC', theta_units='degC') 366 | linedata = [np.vstack((ti, p)).T for ti in t] 367 | 368 | # Add to plot 369 | kwargs.setdefault('clip_on', True) 370 | kwargs.setdefault('colors', '#A65300') 371 | kwargs.setdefault('linestyles', '-') 372 | kwargs.setdefault('alpha', 1) 373 | kwargs.setdefault('linewidth', 0.5) 374 | kwargs.setdefault('zorder', 1.1) 375 | collection = LineCollection(linedata, **kwargs) 376 | self._dry_adiabats.append(collection) 377 | self.add_collection(collection) 378 | theta = theta.flatten() 379 | T_label = calculate('T', p=140, p_units='hPa', theta=theta, 380 | T_units='degC', theta_units='degC') 381 | for i in range(len(theta)): 382 | text = self.text( 383 | T_label[i], 140, '{:.0f}'.format(theta[i]), 384 | fontsize=8, ha='left', va='center', rotation=-60, 385 | color='#A65300', bbox={ 386 | 'facecolor': 'w', 'edgecolor': 'w', 'alpha': 0, 387 | }, zorder=1.2) 388 | text.set_clip_on(True) 389 | self._dry_adiabats.append(text) 390 | 391 | def plot_moist_adiabats(self, p=None, thetaes=None, **kwargs): 392 | r'''Plot moist adiabats. 393 | 394 | Adds saturated pseudo-adiabats (lines of constant equivalent potential 395 | temperature) to the plot. The default style of these lines is dashed 396 | blue lines with an alpha value of 0.5. These can be overridden using 397 | keyword arguments. 398 | 399 | Parameters 400 | ---------- 401 | p : array_like, optional 402 | 1-dimensional array of pressure values to be included in the moist 403 | adiabats. If not specified, they will be linearly distributed 404 | across the current plotted pressure range. 405 | thetaes : array_like, optional 406 | 1-dimensional array of saturation equivalent potential temperature 407 | values for moist adiabats. By default these will be generated based 408 | on the current temperature limits. 409 | kwargs 410 | Other keyword arguments to pass to 411 | `matplotlib.collections.LineCollection` 412 | 413 | See Also 414 | -------- 415 | plot_dry_adiabats 416 | `matplotlib.collections.LineCollection` 417 | `metpy.calc.moist_lapse` 418 | ''' 419 | for artist in self._moist_adiabats: 420 | artist.remove() 421 | self._moist_adiabats = [] 422 | 423 | def dT_dp(y, p0): 424 | return calculate('Gammam', T=y, p=p0, RH=100., p_units='hPa', 425 | T_units='degC')/( 426 | g0*calculate('rho', T=y, p=p0, p_units='hPa', T_units='degC', 427 | RH=100.))*100. 428 | 429 | if thetaes is None: 430 | xmin, xmax = self.get_xlim() 431 | thetaes = np.concatenate((np.arange(xmin, 0, 5), 432 | np.arange(0, xmax + 51, 5))) 433 | # Get pressure levels based on ylims if necessary 434 | if p is None: 435 | p = np.linspace(self.get_ylim()[0], self.get_ylim()[1]) 436 | 437 | cache_dir = user_cache_dir('atmos') 438 | if not os.path.isdir(cache_dir): 439 | os.mkdir(cache_dir) 440 | cache_filename = os.path.join(cache_dir, 'moist_adiabat_data.npz') 441 | request_str = 'p:{},\nthetaes:{}'.format( 442 | np.array_str(p), np.array_str(thetaes)) 443 | t = None 444 | cached_data = None 445 | if os.path.isfile(cache_filename): 446 | cached_data = np.load(cache_filename) 447 | if request_str in cached_data.keys(): 448 | t = cached_data[request_str] 449 | if t is None: 450 | # did not find cached data 451 | # Assemble into data for plotting 452 | thetaes_base = odeint( 453 | dT_dp, thetaes, np.array([1e3, p[0]], dtype=np.float64))[-1, :] 454 | result = odeint(dT_dp, thetaes_base, p) 455 | t = result.T 456 | data_to_cache = {} 457 | if cached_data is not None: 458 | data_to_cache.update(cached_data) 459 | data_to_cache[request_str] = t 460 | np.savez(cache_filename, **data_to_cache) 461 | linedata = [np.vstack((ti, p)).T for ti in t] 462 | 463 | # Add to plot 464 | kwargs.setdefault('clip_on', True) 465 | kwargs.setdefault('colors', '#166916') 466 | kwargs.setdefault('linestyles', '-') 467 | kwargs.setdefault('alpha', 1) 468 | kwargs.setdefault('linewidth', 0.5) 469 | kwargs.setdefault('zorder', 1.1) 470 | collection = LineCollection(linedata, **kwargs) 471 | self._moist_adiabats.append(collection) 472 | self.add_collection(collection) 473 | label_index = closest_val(240., p) 474 | T_label = t[:, label_index].flatten() 475 | for i in range(len(thetaes)): 476 | text = self.text( 477 | T_label[i], p[label_index], 478 | '{:.0f}'.format(thetaes[i]), 479 | fontsize=8, ha='left', va='center', rotation=-65, 480 | color='#166916', bbox={ 481 | 'facecolor': 'w', 'edgecolor': 'w', 'alpha': 0, 482 | }, zorder=1.2) 483 | text.set_clip_on(True) 484 | self._moist_adiabats.append(text) 485 | 486 | def plot_mixing_lines(self, p=None, rv=None, **kwargs): 487 | r'''Plot lines of constant mixing ratio. 488 | 489 | Adds lines of constant mixing ratio (isohumes) to the 490 | plot. The default style of these lines is dashed green lines with an 491 | alpha value of 0.8. These can be overridden using keyword arguments. 492 | 493 | Parameters 494 | ---------- 495 | rv : array_like, optional 496 | 1-dimensional array of unitless mixing ratio values to plot. If 497 | none are given, default values are used. 498 | p : array_like, optional 499 | 1-dimensional array of pressure values to be included in the 500 | isohumes. If not specified, they will be linearly distributed 501 | across the current plotted pressure range. 502 | kwargs 503 | Other keyword arguments to pass to 504 | `matplotlib.collections.LineCollection` 505 | 506 | See Also 507 | -------- 508 | `matplotlib.collections.LineCollection` 509 | ''' 510 | for artist in self._mixing_lines: 511 | artist.remove() 512 | self._mixing_lines = [] 513 | 514 | # Default mixing level values if necessary 515 | if rv is None: 516 | rv = np.array([ 517 | 0.1e-3, 0.2e-3, 0.5e-3, 1e-3, 1.5e-3, 2e-3, 3e-3, 4e-3, 6e-3, 518 | 8e-3, 10e-3, 12e-3, 15e-3, 20e-3, 30e-3, 40e-3, 519 | 50e-3]).reshape(-1, 1) 520 | else: 521 | rv = np.asarray(rv).reshape(-1, 1) 522 | 523 | # Set pressure range if necessary 524 | if p is None: 525 | p = np.linspace(min(self.get_ylim()), max(self.get_ylim())) 526 | else: 527 | p = np.asarray(p) 528 | 529 | # Assemble data for plotting 530 | Td = calculate( 531 | 'Td', p=p, rv=rv, p_units='hPa', rv_units='kg/kg', 532 | Td_units='degC') 533 | Td_label = calculate('Td', p=550, p_units='hPa', rv=rv, 534 | Td_units='degC') 535 | linedata = [np.vstack((t, p)).T for t in Td] 536 | 537 | # Add to plot 538 | kwargs.setdefault('clip_on', True) 539 | kwargs.setdefault('colors', '#166916') 540 | kwargs.setdefault('linestyles', '--') 541 | kwargs.setdefault('alpha', 1) 542 | kwargs.setdefault('linewidth', 0.5) 543 | kwargs.setdefault('zorder', 1.1) 544 | collection = LineCollection(linedata, **kwargs) 545 | self._mixing_lines.append(collection) 546 | self.add_collection(collection) 547 | rv = rv.flatten() * 1000 548 | for i in range(len(rv)): 549 | if rv[i] < 1: 550 | format_string = '{:.1f}' 551 | else: 552 | format_string = '{:.0f}' 553 | t = self.text(Td_label[i], 550, format_string.format(rv[i]), 554 | fontsize=8, ha='right', va='center', rotation=60, 555 | color='#166916', bbox={ 556 | 'facecolor': 'w', 'edgecolor': 'w', 'alpha': 0, 557 | }, zorder=1.2) 558 | t.set_clip_on(True) 559 | self._mixing_lines.append(t) 560 | 561 | # Now register the projection with matplotlib so the user can select 562 | # it. 563 | register_projection(SkewTAxes) 564 | 565 | 566 | if __name__ == '__main__': 567 | import matplotlib.pyplot as plt 568 | # fig = plt.figure(figsize=(6, 6)) 569 | # ax = fig.add_subplot(1, 1, 1, projection='skewT') 570 | fig, ax = plt.subplots(1, 1, figsize=(6, 6), 571 | subplot_kw={'projection': 'skewT'}) 572 | # ax = plt.subplot(projection='skewT') 573 | ax.plot(np.linspace(1e3, 100, 100), np.linspace(0, -50, 100)) 574 | ax.plot_barbs(np.linspace(1e3, 100, 10), np.linspace(50, -50, 10), 575 | np.linspace(-50, 50, 10), xloc=0.95) 576 | plt.tight_layout() 577 | plt.show() 578 | 579 | # Copyright (c) 2008-2014, MetPy Developers. 580 | # All rights reserved. 581 | # 582 | # Redistribution and use in source and binary forms, with or without 583 | # modification, are permitted provided that the following conditions are 584 | # met: 585 | # 586 | # * Redistributions of source code must retain the above copyright 587 | # notice, this list of conditions and the following disclaimer. 588 | # 589 | # * Redistributions in binary form must reproduce the above 590 | # copyright notice, this list of conditions and the following 591 | # disclaimer in the documentation and/or other materials provided 592 | # with the distribution. 593 | # 594 | # * Neither the name of the MetPy Developers nor the names of any 595 | # contributors may be used to endorse or promote products derived 596 | # from this software without specific prior written permission. 597 | # 598 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 599 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 600 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 601 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 602 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 603 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 604 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 605 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 606 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 607 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 608 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 609 | -------------------------------------------------------------------------------- /atmos/solve.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | solve.py: Utilities that use equations to solve for quantities, given other 4 | quantities and a set of assumptions. 5 | """ 6 | from __future__ import division, absolute_import, unicode_literals 7 | import inspect 8 | from atmos import equations 9 | import numpy as np 10 | from six import add_metaclass, string_types 11 | from textwrap import wrap 12 | import re 13 | import cfunits 14 | try: 15 | # Python >= 2.7 16 | from inspect import getfullargspec 17 | except ImportError: 18 | # Python == 2.6 19 | from inspect import getargspec as getfullargspec 20 | 21 | _unit_kwarg_prog = re.compile(r'^(.+)_unit$|^(.+)_units$') 22 | 23 | 24 | class ExcludeError(Exception): 25 | """ 26 | Used in calculating shortest solutions to indicate solutions where all 27 | remaining variables to calculated are excluded. 28 | """ 29 | pass 30 | 31 | 32 | def get_calculatable_quantities(inputs, methods): 33 | ''' 34 | Given an interable of input quantity names and a methods dictionary, 35 | returns a list of output quantities that can be calculated. 36 | ''' 37 | output_quantities = [] 38 | updated = True 39 | while updated: 40 | updated = False 41 | for output in methods.keys(): 42 | if output in output_quantities or output in inputs: 43 | # we already know we can calculate this 44 | continue 45 | for args, func in methods[output].items(): 46 | if all([arg in inputs or arg in output_quantities 47 | for arg in args]): 48 | output_quantities.append(output) 49 | updated = True 50 | break 51 | return tuple(output_quantities) + tuple(inputs) 52 | 53 | 54 | def _get_methods_that_calculate_outputs(inputs, outputs, methods): 55 | ''' 56 | Given iterables of input variable names, output variable names, 57 | and a methods dictionary, returns the subset of the methods dictionary 58 | that can be calculated, doesn't calculate something we already have, 59 | and only contains equations that might help calculate the outputs from 60 | the inputs. 61 | ''' 62 | # Get a list of everything that we can possibly calculate 63 | # This is useful in figuring out whether we can calculate arguments 64 | intermediates = get_calculatable_quantities(inputs, methods) 65 | # Initialize our return dictionary 66 | return_methods = {} 67 | # list so that we can append arguments that need to be output for 68 | # some of the paths 69 | outputs = list(outputs) 70 | # keep track of when to exit the while loop 71 | keep_going = True 72 | while keep_going: 73 | # If there are no updates in a pass, the loop will exit 74 | keep_going = False 75 | for output in outputs: 76 | try: 77 | output_dict = return_methods[output] 78 | except: 79 | output_dict = {} 80 | for args, func in methods[output].items(): 81 | # only check the method if we're not already returning it 82 | if args not in output_dict.keys(): 83 | # Initialize a list of intermediates needed to use 84 | # this method, to add to outputs if we find we can 85 | # use it. 86 | needed = [] 87 | for arg in args: 88 | if arg in inputs: 89 | # we have this argument 90 | pass 91 | elif arg in outputs: 92 | # we may need to calculate one output using 93 | # another output 94 | pass 95 | elif arg in intermediates: 96 | if arg not in outputs: 97 | # don't need to add to needed if it's already 98 | # been put in outputs 99 | needed.append(arg) 100 | else: 101 | # Can't do this func 102 | break 103 | else: # did not break, can calculate this 104 | output_dict[args] = func 105 | if len(needed) > 0: 106 | # We added an output, so need another loop 107 | outputs.extend(needed) 108 | keep_going = True 109 | if len(output_dict) > 0: 110 | return_methods[output] = output_dict 111 | return return_methods 112 | 113 | 114 | def _get_calculatable_methods_dict(inputs, methods): 115 | ''' 116 | Given an iterable of input variable names and a methods dictionary, 117 | returns the subset of that methods dictionary that can be calculated and 118 | which doesn't calculate something we already have. Additionally it may 119 | only contain one method for any given output variable, which is the one 120 | with the fewest possible arguments. 121 | ''' 122 | # Initialize a return dictionary 123 | calculatable_methods = {} 124 | # Iterate through each potential method output 125 | for var in methods.keys(): 126 | # See if we already have this output 127 | if var in inputs: 128 | continue # if we have it, we don't need to calculate it! 129 | else: 130 | # Initialize a dict for this output variable 131 | var_dict = {} 132 | for args, func in methods[var].items(): 133 | # See if we have what we need to solve this equation 134 | if all([arg in inputs for arg in args]): 135 | # If we do, add it to the var_dict 136 | var_dict[args] = func 137 | if len(var_dict) == 0: 138 | # No methods for this variable, keep going 139 | continue 140 | elif len(var_dict) == 1: 141 | # Exactly one method, perfect. 142 | calculatable_methods[var] = var_dict 143 | else: 144 | # More than one method, find the one with the least arguments 145 | min_args = min(var_dict.keys(), key=lambda x: len(x)) 146 | calculatable_methods[var] = {min_args: var_dict[min_args]} 147 | return calculatable_methods 148 | 149 | 150 | def _get_shortest_solution(outputs, inputs, exclude, methods): 151 | ''' 152 | Parameters 153 | ---------- 154 | outputs: tuple 155 | Strings corresponding to the final variables we want output 156 | inputs: tuple 157 | Strings corresponding to the variables we have so far 158 | exclude: tuple 159 | Strings corresponding to variables that won't help calculate the 160 | outputs. 161 | methods: dict 162 | A methods dictionary 163 | 164 | Returns (funcs, func_args, extra_values). 165 | ''' 166 | methods = _get_methods_that_calculate_outputs(inputs, outputs, 167 | methods) 168 | calculatable_methods = _get_calculatable_methods_dict(inputs, methods) 169 | # Check if we can already directly compute the outputs 170 | if all([(o in calculatable_methods.keys()) or (o in inputs) 171 | for o in outputs]): 172 | funcs = [] 173 | args = [] 174 | extra_values = [] 175 | for o in outputs: 176 | if o not in inputs: 177 | o_args, o_func = list(calculatable_methods[o].items())[0] 178 | funcs.append(o_func) 179 | args.append(o_args) 180 | extra_values.append(o) 181 | return tuple(funcs), tuple(args), tuple(extra_values) 182 | # Check if there's nothing left to try to calculate 183 | if len(calculatable_methods) == 0: 184 | raise ValueError('cannot calculate outputs from inputs') 185 | next_variables = [key for key in calculatable_methods.keys() 186 | if key not in exclude] 187 | if len(next_variables) == 0: 188 | raise ExcludeError 189 | # We're not in a base case, so recurse this function 190 | results = [] 191 | intermediates = [] 192 | for i in range(len(next_variables)): 193 | try: 194 | results.append(_get_shortest_solution( 195 | outputs, inputs + (next_variables[i],), exclude, methods)) 196 | except ExcludeError: 197 | continue 198 | intermediates.append(next_variables[i]) 199 | exclude = exclude + (next_variables[i],) 200 | if len(results) == 0: 201 | # all subresults raised ExcludeError 202 | raise ExcludeError 203 | 204 | # sort based on shortest solution, with tiebreaker based on total args 205 | # passed 206 | def option_key(a): 207 | return len(a[0]) + 0.001*sum(len(b) for b in a[1]) 208 | best_result = min(results, key=option_key) 209 | best_index = results.index(best_result) 210 | best_intermediate = intermediates[best_index] 211 | args, func = list(calculatable_methods[best_intermediate].items())[0] 212 | best_option = ((func,) + best_result[0], (args,) + best_result[1], 213 | (best_intermediate,) + best_result[2]) 214 | return best_option 215 | 216 | 217 | def _get_module_methods(module): 218 | ''' 219 | Returns a methods list corresponding to the equations in the given 220 | module. Each entry is a dictionary with keys 'output', 'args', and 221 | 'func' corresponding to the output, arguments, and function of the 222 | method. The entries may optionally include 'assumptions' and 223 | 'overridden_by_assumptions' as keys, stating which assumptions are 224 | required to use the method, and which assumptions mean the method 225 | should not be used because it is overridden. 226 | ''' 227 | # Set up the methods dict we will eventually return 228 | methods = [] 229 | funcs = [] 230 | for item in inspect.getmembers(equations): 231 | if (item[0][0] != '_' and '_from_' in item[0]): 232 | func = item[1] 233 | output = item[0][:item[0].find('_from_')] 234 | # avoid returning duplicates 235 | if func in funcs: 236 | continue 237 | else: 238 | funcs.append(func) 239 | args = tuple(getfullargspec(func).args) 240 | try: 241 | assumptions = tuple(func.assumptions) 242 | except AttributeError: 243 | raise NotImplementedError('function {0} in equations module has no' 244 | ' assumption ' 245 | 'definition'.format(func.__name__)) 246 | try: 247 | overridden_by_assumptions = func.overridden_by_assumptions 248 | except AttributeError: 249 | overridden_by_assumptions = () 250 | methods.append({ 251 | 'func': func, 252 | 'args': args, 253 | 'output': output, 254 | 'assumptions': assumptions, 255 | 'overridden_by_assumptions': overridden_by_assumptions, 256 | }) 257 | return methods 258 | 259 | 260 | def _fill_doc(s, module, default_assumptions): 261 | assumptions = module.assumptions 262 | s = s.replace( 263 | '', 264 | '\n'.join(sorted( 265 | ["* **{0}** -- {1}".format(a, desc) for a, desc in 266 | assumptions.items()], 267 | key=lambda x: x.lower()))) 268 | s = s.replace( 269 | '', 270 | '\n'.join( 271 | wrap('Default assumptions are ' + 272 | ', '.join(["'{0}'".format(a) for a in 273 | default_assumptions]) + '.', width=80))) 274 | s = s.replace( 275 | '', 276 | '\n'.join(sorted([ 277 | '* **{0}** -- {1} ({2})'.format( 278 | q, info['name'], info['units']) 279 | for q, info in 280 | module.quantities.items() 281 | ], key=lambda x: x.lower()))) 282 | return s 283 | 284 | 285 | def _check_scalar(value): 286 | '''If value is a 0-dimensional array, returns the contents of value. 287 | Otherwise, returns value. 288 | ''' 289 | if isinstance(value, np.ndarray): 290 | if value.ndim == 0: 291 | # We have a 0-dimensional array 292 | return value[None][0] 293 | return value 294 | 295 | 296 | # We need to define a MetaClass in order to have dynamic docstrings for our 297 | # Solver objects, generated based on the equations module 298 | class SolverMeta(type): 299 | ''' 300 | Metaclass for BaseSolver to automatically generate docstrings and assumption 301 | lists for subclasses of BaseSolver. 302 | ''' 303 | 304 | def __new__(cls, name, parents, dct): 305 | if dct['_equation_module'] is not None: 306 | # Update the class docstring 307 | if '__doc__' in dct.keys(): 308 | dct['__doc__'] = _fill_doc( 309 | dct['__doc__'], dct['_equation_module'], 310 | dct['default_assumptions']) 311 | dct['_ref_units'] = {} 312 | for quantity in dct['_equation_module'].quantities.keys(): 313 | dct['_ref_units'][quantity] = \ 314 | cfunits.Units(dct['_equation_module'].quantities[ 315 | quantity]['units']) 316 | assumptions = set([]) 317 | for f in inspect.getmembers(equations): 318 | try: 319 | assumptions.update(f[1].assumptions) 320 | except AttributeError: 321 | pass 322 | dct['all_assumptions'] = tuple(assumptions) 323 | 324 | # we need to call type.__new__ to complete the initialization 325 | instance = super(SolverMeta, cls).__new__(cls, name, parents, dct) 326 | return instance 327 | 328 | 329 | @add_metaclass(SolverMeta) 330 | class BaseSolver(object): 331 | ''' 332 | Base class for solving systems of equations. Should not be instantiated, 333 | as it is not associated with any equations. 334 | 335 | Initializes with the given assumptions enabled, and variables passed as 336 | keyword arguments stored. 337 | 338 | Parameters 339 | ---------- 340 | 341 | assumptions : tuple, optional 342 | Strings specifying which assumptions to enable. Overrides the default 343 | assumptions. See below for a list of default assumptions. 344 | add_assumptions : tuple, optional 345 | Strings specifying assumptions to use in addition to the default 346 | assumptions. May not be given in combination with the assumptions kwarg. 347 | remove_assumptions : tuple, optional 348 | Strings specifying assumptions not to use from the default assumptions. 349 | May not be given in combination with the assumptions kwarg. May not 350 | contain strings that are contained in add_assumptions, if given. 351 | **kwargs : ndarray, optional 352 | Keyword arguments used to pass in arrays of data that correspond to 353 | quantities used for calculations, or unit specifications for quantities. 354 | For a complete list of kwargs that may be used, see the Quantity Parameters 355 | section below. 356 | 357 | Returns 358 | ------- 359 | out : BaseSolver 360 | A BaseSolver object with the specified assumptions and variables. 361 | 362 | Notes 363 | ----- 364 | 365 | **Quantity kwargs** 366 | 367 | 368 | 369 | In addition to the quantities above, kwargs of the form _unit or 370 | _units can be used with a string specifying a unit for the quantity. 371 | This will cause input data for that quantity to be assumed to be in that 372 | unit, and output data for that quantity to be given in that unit. Note this 373 | must be specified separately for *each* quantity. Acceptable units are the 374 | units available in the Pint package, with the exception that RH can be in 375 | units of "fraction" or "percent". 376 | 377 | **Assumptions** 378 | 379 | 380 | 381 | **Assumption descriptions** 382 | 383 | 384 | ''' 385 | 386 | _equation_module = None 387 | _solutions = None 388 | 389 | def __init__(self, **kwargs): 390 | if self._equation_module is None: 391 | raise NotImplementedError('Class needs _equation_module ' 392 | 'defined') 393 | if 'debug' in kwargs.keys(): 394 | self._debug = kwargs.pop('debug') 395 | else: 396 | self._debug = False 397 | # make sure add and remove assumptions are tuples, not strings 398 | if ('add_assumptions' in kwargs.keys() and 399 | isinstance(kwargs['add_assumptions'], string_types)): 400 | kwargs['add_assumptions'] = (kwargs['add_assumptions'],) 401 | if ('remove_assumptions' in kwargs.keys() and 402 | isinstance(kwargs['remove_assumptions'], string_types)): 403 | kwargs['remove_assumptions'] = (kwargs['remove_assumptions'],) 404 | # See if an assumption set was given 405 | if 'assumptions' in kwargs.keys(): 406 | # If it was, make sure it wasn't given with other ways of 407 | # setting assumptions (by modifying the default assumptions) 408 | if ('add_assumptions' in kwargs.keys() or 409 | 'remove_assumptions' in kwargs.keys()): 410 | raise ValueError('cannot give kwarg assumptions with ' 411 | 'add_assumptions or remove_assumptions') 412 | assumptions = kwargs.pop('assumptions') 413 | else: 414 | # if it wasn't, modify the default assumptions 415 | assumptions = self.default_assumptions 416 | if 'add_assumptions' in kwargs.keys(): 417 | if 'remove_assumptions' in kwargs.keys(): 418 | # make sure there is no overlap 419 | if any([a in kwargs['remove_assumptions'] 420 | for a in kwargs['add_assumptions']]): 421 | raise ValueError('assumption may not be present in ' 422 | 'both add_assumptions and ' 423 | 'remove_assumptions') 424 | # add assumptions, avoiding duplicates 425 | assumptions = assumptions + tuple( 426 | [a for a in kwargs.pop('add_assumptions') if a not in 427 | assumptions]) 428 | if 'remove_assumptions' in kwargs.keys(): 429 | # remove assumptions if present 430 | remove_assumptions = kwargs.pop('remove_assumptions') 431 | self._ensure_assumptions(*assumptions) 432 | assumptions = tuple([a for a in assumptions if a not in 433 | remove_assumptions]) 434 | # Make sure all set assumptions are valid (not misspelt, for instance) 435 | self._ensure_assumptions(*assumptions) 436 | # now that we have our assumptions, use them to set the methods 437 | self.methods = self._get_methods(assumptions) 438 | self.assumptions = assumptions 439 | # take out any unit designations 440 | self.units = {} 441 | remove_kwargs = [] 442 | for kwarg in kwargs: 443 | m = _unit_kwarg_prog.match(kwarg) 444 | if m is not None: 445 | # select whichever group is not None 446 | var = m.group(1) or m.group(2) 447 | self._ensure_quantities(var) 448 | if var in self.units: 449 | raise ValueError( 450 | 'units for {} specified multiple times'.format(var)) 451 | unit_str = kwargs[kwarg] 452 | remove_kwargs.append(kwarg) 453 | if not isinstance(unit_str, string_types): 454 | raise TypeError('units must be strings') 455 | self.units[var] = cfunits.Units(unit_str) 456 | for kwarg in remove_kwargs: 457 | kwargs.pop(kwarg) 458 | # make sure the remaining variables are quantities 459 | self._ensure_quantities(*kwargs.keys()) 460 | # convert quantities to reference units 461 | for kwarg in kwargs: 462 | if (kwarg in self.units and 463 | self.units[kwarg] != self._ref_units[kwarg]): 464 | # special unit defined 465 | # convert to reference unit for calculations 466 | kwargs[kwarg] = cfunits.Units.conform( 467 | kwargs[kwarg], self.units[kwarg], self._ref_units[kwarg]) 468 | # also store the quantities 469 | self.vars = kwargs 470 | 471 | def _ensure_assumptions(self, *args): 472 | '''Raises ValueError if any of the args are not strings corresponding 473 | to short forms of assumptions for this Solver. 474 | ''' 475 | for arg in args: 476 | if arg not in self.all_assumptions: 477 | raise ValueError('{0} does not correspond to a valid ' 478 | 'assumption.'.format(arg)) 479 | 480 | def _ensure_quantities(self, *args): 481 | '''Raises ValueError if any of the args are not strings corresponding 482 | to quantity abbreviations for this Solver. 483 | ''' 484 | for arg in args: 485 | if arg not in self._equation_module.quantities.keys(): 486 | raise ValueError('{0} does not correspond to a valid ' 487 | 'quantity.'.format(arg)) 488 | 489 | def calculate(self, *args): 490 | ''' 491 | Calculates and returns a requested quantity from quantities stored in this 492 | object at initialization. 493 | 494 | Parameters 495 | ---------- 496 | *args : string 497 | Name of quantity to be calculated. 498 | 499 | Returns 500 | ------- 501 | quantity : ndarray 502 | Calculated quantity, in units listed under quantity parameters. 503 | 504 | Notes 505 | ----- 506 | See the documentation for this object for a complete list of quantities 507 | that may be calculated, in the "Quantity Parameters" section. 508 | 509 | Raises 510 | ------ 511 | ValueError: 512 | If the output quantity cannot be determined from the input 513 | quantities. 514 | 515 | Examples 516 | -------- 517 | 518 | Calculating pressure from virtual temperature and density: 519 | 520 | >>> solver = FluidSolver(Tv=273., rho=1.27) 521 | >>> solver.calculate('p') 522 | 99519.638400000011 523 | 524 | Same calculation, but also returning a list of functions used: 525 | 526 | >>> solver = FluidSolver(Tv=273., rho=1.27, debug=True) 527 | >>> p, funcs = solver.calculate('p') 528 | >>> funcs 529 | (,) 530 | 531 | Same calculation with temperature instead, ignoring virtual temperature 532 | correction: 533 | 534 | >>> solver = FluidSolver(T=273., rho=1.27, add_assumptions=('Tv equals T',)) 535 | >>> solver.calculate('p',) 536 | 99519.638400000011 537 | ''' 538 | self._ensure_quantities(*args) 539 | possible_quantities = get_calculatable_quantities(self.vars.keys(), 540 | self.methods) 541 | for arg in args: 542 | if arg not in possible_quantities: 543 | raise ValueError('cannot calculate {0} from inputs'.format( 544 | arg)) 545 | # prepare a signature for this solution request 546 | sig = (args, tuple(self.vars.keys()), tuple(self.assumptions)) 547 | # check if we've already tried to solve this 548 | if self.__class__._solutions is not None: 549 | if sig in self.__class__._solutions: 550 | # if we have, use the cached solution 551 | funcs, func_args, extra_values = self.__class__._solutions[sig] 552 | else: 553 | # solve and cache the solution 554 | funcs, func_args, extra_values = \ 555 | _get_shortest_solution(args, tuple(self.vars.keys()), (), 556 | self.methods) 557 | self.__class__._solutions[sig] = (funcs, func_args, 558 | extra_values) 559 | else: # no solution caching for this class 560 | funcs, func_args, extra_values = \ 561 | _get_shortest_solution(args, tuple(self.vars.keys()), (), 562 | self.methods) 563 | # Above method completed successfully if no ValueError has been raised 564 | # Calculate each quantity we need to calculate in order 565 | for i, func in enumerate(funcs): 566 | # Compute this quantity 567 | value = func(*[self.vars[varname] for varname in func_args[i]]) 568 | # Add it to our dictionary of quantities for successive functions 569 | self.vars[extra_values[i]] = value 570 | return_list = [] 571 | for arg in args: 572 | # do corrections for non-standard units 573 | if arg in self.units and self.units[arg] != self._ref_units[arg]: 574 | self.vars[arg] = cfunits.Units.conform( 575 | self.vars[arg], self._ref_units[arg], self.units[arg]) 576 | return_list.append(self.vars[arg]) 577 | if self._debug: 578 | # We should return a list of funcs as the last item returned 579 | if len(return_list) == 1: 580 | return _check_scalar(return_list[0]), funcs 581 | else: 582 | return ([_check_scalar(val) for val in return_list] + 583 | [funcs, ]) 584 | else: 585 | # no function debugging, just return the quantities 586 | if len(args) == 1: 587 | return _check_scalar(return_list[0]) 588 | else: 589 | return [_check_scalar(val) for val in return_list] 590 | 591 | def _get_methods(self, assumptions): 592 | ''' 593 | Returns a dictionary of methods including the default methods of the 594 | class and specified optional methods. Will override a default method 595 | if an optional method is given that takes the same inputs and produces 596 | the same output. 597 | 598 | Parameters 599 | ---------- 600 | methods: iterable 601 | Strings referring to optional methods in self.optional_methods. 602 | 603 | Returns 604 | ------- 605 | methods : dict 606 | A dictionary whose keys are strings indicating output variables, 607 | and values are dictionaries indicating equations for that output. The 608 | equation dictionary's keys are strings indicating variables to use 609 | as function arguments, and its values are the functions themselves. 610 | 611 | Raises 612 | ------ 613 | ValueError 614 | If a method given is not present in self.optional_methods. 615 | If multiple optional methods are selected which calculate the same 616 | output quantity from the same input quantities. 617 | ''' 618 | # make sure all assumptions actually apply to equations 619 | # this will warn the user of typos 620 | for a in assumptions: 621 | if a not in self.all_assumptions: 622 | raise ValueError('assumption {0} matches no ' 623 | 'equations'.format(a)) 624 | # create a dictionary to which we will add methods 625 | methods = {} 626 | # get a set of all the methods in the module 627 | module_methods = _get_module_methods(self._equation_module) 628 | # Go through each output variable 629 | for dct in module_methods: 630 | # Make sure this method is not overridden 631 | if any(item in assumptions for item in 632 | dct['overridden_by_assumptions']): 633 | continue 634 | # Make sure all assumptions of the method are satisfied 635 | elif all(item in assumptions for item in dct['assumptions']): 636 | # Make sure we have a dict entry for this output quantity 637 | if dct['output'] not in methods.keys(): 638 | methods[dct['output']] = {} 639 | # Make sure we aren't defining methods with same signature 640 | if dct['args'] in methods[dct['output']].keys(): 641 | raise ValueError( 642 | 'assumptions given define duplicate ' 643 | 'equations {0} and {1}'.format( 644 | str(dct['func']), 645 | str(methods[dct['output']][dct['args']]))) 646 | # Add the method to the methods dict 647 | methods[dct['output']][dct['args']] = dct['func'] 648 | return methods 649 | 650 | 651 | class FluidSolver(BaseSolver): 652 | ''' 653 | Initializes with the given assumptions enabled, and variables passed as 654 | keyword arguments stored. 655 | 656 | Parameters 657 | ---------- 658 | 659 | assumptions : tuple, optional 660 | Strings specifying which assumptions to enable. Overrides the default 661 | assumptions. See below for a list of default assumptions. 662 | add_assumptions : tuple, optional 663 | Strings specifying assumptions to use in addition to the default 664 | assumptions. May not be given in combination with the assumptions kwarg. 665 | remove_assumptions : tuple, optional 666 | Strings specifying assumptions not to use from the default assumptions. 667 | May not be given in combination with the assumptions kwarg. May not 668 | contain strings that are contained in add_assumptions, if given. 669 | **kwargs : ndarray, optional 670 | Keyword arguments used to pass in arrays of data that correspond to 671 | quantities used for calculations, or unit specifications for quantities. 672 | For a complete list of kwargs that may be used, see the Quantity Parameters 673 | section below. 674 | 675 | Returns 676 | ------- 677 | out : FluidSolver 678 | A FluidSolver object with the specified assumptions and variables. 679 | 680 | Notes 681 | ----- 682 | 683 | **Quantity kwargs** 684 | 685 | 686 | 687 | In addition to the quantities above, kwargs of the form _unit or 688 | _units can be used with a string specifying a unit for the quantity. 689 | This will cause input data for that quantity to be assumed to be in that 690 | unit, and output data for that quantity to be given in that unit. Note this 691 | must be specified separately for *each* quantity. Acceptable units are the 692 | units available in the Pint package, with the exception that RH can be in 693 | units of "fraction" or "percent". 694 | 695 | **Assumptions** 696 | 697 | 698 | 699 | **Assumption descriptions** 700 | 701 | 702 | 703 | Examples 704 | -------- 705 | 706 | Calculating pressure from virtual temperature and density: 707 | 708 | >>> solver = FluidSolver(Tv=273., rho=1.27) 709 | >>> solver.calculate('p') 710 | 99519.638400000011 711 | 712 | Same calculation, but also returning a list of functions used: 713 | 714 | >>> solver = FluidSolver(Tv=273., rho=1.27, debug=True) 715 | >>> p, funcs = solver.calculate('p') 716 | >>> funcs 717 | (,) 718 | 719 | Same calculation with temperature instead, ignoring virtual temperature 720 | correction: 721 | 722 | >>> solver = FluidSolver(T=273., rho=1.27, add_assumptions=('Tv equals T',)) 723 | >>> solver.calculate('p',) 724 | 99519.638400000011 725 | ''' 726 | 727 | # module containing fluid dynamics equations 728 | _equation_module = equations 729 | # which assumptions to use by default 730 | default_assumptions = ( 731 | 'ideal gas', 'hydrostatic', 'constant g', 'constant Lv', 'constant Cp', 732 | 'no liquid water', 'no ice', 'bolton', 'cimo') 733 | _solutions = {} 734 | 735 | 736 | def calculate(*args, **kwargs): 737 | ''' 738 | Calculates and returns a requested quantity from quantities passed in as 739 | keyword arguments. 740 | 741 | Parameters 742 | ---------- 743 | 744 | \*args : string 745 | Names of quantities to be calculated. 746 | assumptions : tuple, optional 747 | Strings specifying which assumptions to enable. Overrides the default 748 | assumptions. See below for a list of default assumptions. 749 | add_assumptions : tuple, optional 750 | Strings specifying assumptions to use in addition to the default 751 | assumptions. May not be given in combination with the assumptions kwarg. 752 | remove_assumptions : tuple, optional 753 | Strings specifying assumptions not to use from the default assumptions. 754 | May not be given in combination with the assumptions kwarg. May not 755 | contain strings that are contained in add_assumptions, if given. 756 | \*\*kwargs : ndarray, optional 757 | Keyword arguments used to pass in arrays of data that correspond to 758 | quantities used for calculations, or unit specifications for quantities. 759 | For a complete list of kwargs that may be used, see the Quantity Parameters 760 | section below. 761 | 762 | Returns 763 | ------- 764 | 765 | quantity : ndarray 766 | Calculated quantity. 767 | Return type is the same as quantity parameter types. 768 | If multiple quantities are requested, returns a tuple containing the 769 | quantities. 770 | 771 | Notes 772 | ----- 773 | 774 | Calculating multiple quantities at once can avoid re-computing intermediate 775 | quantities, but requires more memory. 776 | 777 | **Quantity kwargs** 778 | 779 | 780 | 781 | In addition to the quantities above, kwargs of the form _unit or 782 | _units can be used with a string specifying a unit for the quantity. 783 | This will cause input data for that quantity to be assumed to be in that 784 | unit, and output data for that quantity to be given in that unit. Note this 785 | must be specified separately for *each* quantity. Acceptable units are the 786 | units available in the Pint package, with the exception that RH can be in 787 | units of "fraction" or "percent". 788 | 789 | **Assumptions** 790 | 791 | 792 | 793 | **Assumption descriptions** 794 | 795 | 796 | 797 | Examples 798 | -------- 799 | 800 | Calculating pressure from virtual temperature and density: 801 | 802 | >>> calculate('p', Tv=273., rho=1.27) 803 | 99519.638400000011 804 | 805 | Same calculation, but also returning a list of functions used: 806 | 807 | >>> p, funcs = calculate('p', Tv=273., rho=1.27, debug=True) 808 | >>> funcs 809 | (,) 810 | 811 | Same calculation with temperature instead, ignoring virtual temperature 812 | correction: 813 | 814 | >>> calculate('p', T=273., rho=1.27, add_assumptions=('Tv equals T',)) 815 | 99519.638400000011 816 | ''' 817 | if len(args) == 0: 818 | raise ValueError('must specify quantities to calculate') 819 | # initialize a solver to do the work 820 | solver = FluidSolver(**kwargs) 821 | # get the output 822 | return solver.calculate(*args) 823 | 824 | # autocomplete some sections of the docstring for calculate 825 | calculate.__doc__ = _fill_doc(calculate.__doc__, equations, 826 | FluidSolver.default_assumptions) 827 | -------------------------------------------------------------------------------- /atmos/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Thu Sep 17 11:10:40 2015 4 | 5 | @author: mcgibbon 6 | """ 7 | 8 | -------------------------------------------------------------------------------- /atmos/tests/baseline_images/plot_tests/linear_skewT.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos-python/atmos/97b4f7ed3f3cd345703a5ed76e40418bba55fb97/atmos/tests/baseline_images/plot_tests/linear_skewT.pdf -------------------------------------------------------------------------------- /atmos/tests/baseline_images/plot_tests/linear_skewT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos-python/atmos/97b4f7ed3f3cd345703a5ed76e40418bba55fb97/atmos/tests/baseline_images/plot_tests/linear_skewT.png -------------------------------------------------------------------------------- /atmos/tests/baseline_images/plot_tests/linear_skewT_with_barbs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos-python/atmos/97b4f7ed3f3cd345703a5ed76e40418bba55fb97/atmos/tests/baseline_images/plot_tests/linear_skewT_with_barbs.pdf -------------------------------------------------------------------------------- /atmos/tests/baseline_images/plot_tests/linear_skewT_with_barbs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos-python/atmos/97b4f7ed3f3cd345703a5ed76e40418bba55fb97/atmos/tests/baseline_images/plot_tests/linear_skewT_with_barbs.png -------------------------------------------------------------------------------- /atmos/tests/plot_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Thu Sep 17 11:11:20 2015 4 | 5 | @author: mcgibbon 6 | """ 7 | from __future__ import division, unicode_literals 8 | import nose 9 | import numpy as np 10 | from atmos import plot 11 | import matplotlib.pyplot as plt 12 | from matplotlib.testing.decorators import image_comparison 13 | 14 | 15 | @image_comparison(baseline_images=['linear_skewT'], 16 | extensions=['png', 'pdf']) 17 | def test_linear_skewT(): 18 | plt.style.use('bmh') 19 | fig, ax = plt.subplots(1, 1, figsize=(6, 6), 20 | subplot_kw={'projection': 'skewT'}) 21 | ax.plot(np.linspace(1e3, 100, 100), np.linspace(0, -50, 100)) 22 | fig.tight_layout() 23 | 24 | 25 | @image_comparison(baseline_images=['linear_skewT_with_barbs'], 26 | extensions=['png', 'pdf']) 27 | def test_linear_skewT_with_barbs(): 28 | plt.style.use('bmh') 29 | fig, ax = plt.subplots(1, 1, figsize=(6, 6), 30 | subplot_kw={'projection': 'skewT'}) 31 | ax.plot(np.linspace(1e3, 100, 100), np.linspace(0, -50, 100)) 32 | ax.plot_barbs(np.linspace(1e3, 100, 10), np.linspace(50, -50, 10), 33 | np.linspace(-50, 50, 10), xloc=0.95) 34 | fig.tight_layout() 35 | 36 | 37 | if __name__ == '__main__': 38 | nose.run() 39 | -------------------------------------------------------------------------------- /atmos/tests/run_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Thu Sep 17 11:57:13 2015 4 | 5 | @author: mcgibbon 6 | """ 7 | import matplotlib 8 | import nose 9 | matplotlib.use('agg') 10 | 11 | if __name__ == '__main__': 12 | nose.main(argv=['--with-coverage']) 13 | -------------------------------------------------------------------------------- /atmos/tests/solve_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | test.py: Testing suite for other modules. 4 | """ 5 | from __future__ import division, unicode_literals 6 | import unittest 7 | import nose 8 | import numpy as np 9 | from atmos import equations 10 | from atmos import util 11 | from atmos import decorators 12 | from nose.tools import raises 13 | from atmos.constants import Rd 14 | from atmos.solve import BaseSolver, FluidSolver, calculate, \ 15 | _get_module_methods, _get_calculatable_methods_dict,\ 16 | _get_shortest_solution 17 | from atmos.util import quantity_string, assumption_list_string, \ 18 | quantity_spec_string, doc_paragraph, \ 19 | strings_to_list_string 20 | try: 21 | from inspect import getfullargspec 22 | except ImportError: 23 | from inspect import getargspec as getfullargspec 24 | 25 | 26 | def test_quantities_dict_complete(): 27 | names = [item['output'] for item in _get_module_methods(equations)] 28 | for name in names: 29 | if name not in equations.quantities.keys(): 30 | try: 31 | util.parse_derivative_string(name) 32 | except ValueError: 33 | raise AssertionError('{0} not in quantities dict'.format(name)) 34 | 35 | 36 | def test_get_module_methods_nonempty(): 37 | result = _get_module_methods(equations) 38 | assert len(result) > 0 39 | 40 | 41 | def test_default_assumptions_exist(): 42 | for m in FluidSolver.default_assumptions: 43 | if m not in FluidSolver.all_assumptions: 44 | raise AssertionError('{0} not a valid assumption'.format(m)) 45 | 46 | 47 | class ddxTests(unittest.TestCase): 48 | 49 | def setUp(self): 50 | self.data = np.zeros((2, 3)) 51 | self.data[:] = np.array([0., 5., 10.])[None, :] 52 | self.axis1 = np.array([5., 6., 7.]) 53 | self.axis2 = np.array([[5., 6., 7.], [8., 10., 12.]]) 54 | self.deriv1 = 5.*np.ones((2, 3)) 55 | self.deriv2 = 5.*np.ones((2, 3)) 56 | self.deriv2[1, :] *= 0.5 57 | 58 | @raises(ValueError) 59 | def test_invalid_data_axis(self): 60 | util.ddx(self.data, axis=2) 61 | 62 | @raises(ValueError) 63 | def test_invalid_axis_x(self): 64 | util.ddx(self.data, axis=2, x=self.axis1, axis_x=1) 65 | 66 | 67 | class ClosestValTests(unittest.TestCase): 68 | 69 | def setUp(self): 70 | self.list = [1., 5., 10.] 71 | self.array = np.array([1., 5., 10.]) 72 | 73 | def tearDown(self): 74 | self.list = None 75 | self.array = None 76 | 77 | @raises(ValueError) 78 | def testValueErrorOnEmptyList(self): 79 | util.closest_val(1., []) 80 | 81 | @raises(ValueError) 82 | def testValueErrorOnEmptyArray(self): 83 | util.closest_val(1., np.array([])) 84 | 85 | def testClosestValSingle(self): 86 | val = util.closest_val(1., np.array([50.])) 87 | assert val == 0 88 | 89 | def testClosestValInternal(self): 90 | val = util.closest_val(3.5, self.array) 91 | assert val == 1 92 | 93 | def testClosestValBelow(self): 94 | val = util.closest_val(-5., self.array) 95 | assert val == 0 96 | 97 | def testClosestValAbove(self): 98 | val = util.closest_val(20., self.array) 99 | assert val == 2 100 | 101 | def testClosestValInternalNegative(self): 102 | val = util.closest_val(-3.5, -1*self.array) 103 | assert val == 1 104 | 105 | def testClosestValInternalList(self): 106 | val = util.closest_val(3.5, self.list) 107 | assert val == 1 108 | 109 | def testClosestValBelowList(self): 110 | val = util.closest_val(-5., self.list) 111 | assert val == 0 112 | 113 | def testClosestValAboveList(self): 114 | val = util.closest_val(20., self.list) 115 | assert val == 2 116 | 117 | 118 | class AreaPolySphereTests(unittest.TestCase): 119 | 120 | def setUp(self): 121 | self.lat1 = np.array([0.0, 90.0, 0.0]) 122 | self.lon1 = np.array([0.0, 0.0, 90.0]) 123 | self.area1 = 1.5708 124 | self.tol1 = 0.0001 125 | 126 | @raises(ValueError) 127 | def test_area_poly_sphere_insufficient_vertices(self): 128 | util.area_poly_sphere(self.lat1[:2], self.lon1[:2], 1.) 129 | 130 | def test_area_poly_sphere(self): 131 | area_calc = util.area_poly_sphere(self.lat1, self.lon1, 1.) 132 | assert abs(area_calc - self.area1) < self.tol1 133 | 134 | def test_area_poly_sphere_different_radius(self): 135 | area_calc = util.area_poly_sphere(self.lat1, self.lon1, 2.) 136 | assert abs(area_calc - 4*self.area1) < 4*self.tol1 137 | 138 | 139 | class DecoratorTests(unittest.TestCase): 140 | 141 | def setUp(self): 142 | def dummyfunction(x): 143 | '''Dummy docstring''' 144 | return x 145 | self.func = dummyfunction 146 | self.func_name = dummyfunction.__name__ 147 | self.func_argspec = getfullargspec(dummyfunction) 148 | self.quantity_dict = { 149 | 'T': {'name': 'air temperature', 'units': 'K'}, 150 | 'qv': {'name': 'specific humidity', 'units': 'kg/kg'}, 151 | 'p': {'name': 'air pressure', 'units': 'Pa'}, 152 | } 153 | self.assumption_dict = { 154 | 'a1': 'a1_long', 155 | 'a2': 'a2_long', 156 | 'a3': 'a3_long', 157 | } 158 | self.func_argspec = getfullargspec(self.func) 159 | 160 | def tearDown(self): 161 | self.func = None 162 | self.quantity_dict = None 163 | self.assumption_dict = None 164 | 165 | def test_assumes_empty(self, **kwargs): 166 | func = decorators.assumes()(self.func) 167 | assert func.assumptions == () 168 | assert func.__name__ == self.func_name 169 | assert getfullargspec(func) == self.func_argspec 170 | 171 | def test_assumes_single(self, **kwargs): 172 | func = decorators.assumes('a1')(self.func) 173 | assert func.assumptions == ('a1',) 174 | assert func.__name__ == self.func_name 175 | assert getfullargspec(func) == self.func_argspec 176 | 177 | def test_assumes_triple(self, **kwargs): 178 | func = decorators.assumes('a1', 'a2', 'a3')(self.func) 179 | assert func.assumptions == ('a1', 'a2', 'a3',) 180 | assert func.__name__ == self.func_name 181 | assert getfullargspec(func) == self.func_argspec 182 | 183 | def test_overridden_by_assumptions_empty(self, **kwargs): 184 | func = decorators.overridden_by_assumptions()(self.func) 185 | assert func.overridden_by_assumptions == () 186 | assert func.__name__ == self.func_name 187 | assert getfullargspec(func) == self.func_argspec 188 | 189 | def test_overridden_by_assumptions_single(self, **kwargs): 190 | func = decorators.overridden_by_assumptions('a1')(self.func) 191 | assert func.overridden_by_assumptions == ('a1',) 192 | assert func.__name__ == self.func_name 193 | assert getfullargspec(func) == self.func_argspec 194 | 195 | def test_overridden_by_assumptions_triple(self, **kwargs): 196 | func = decorators.overridden_by_assumptions( 197 | 'a1', 'a2', 'a3')(self.func) 198 | assert func.overridden_by_assumptions == ('a1', 'a2', 'a3',) 199 | assert func.__name__ == self.func_name 200 | assert getfullargspec(func) == self.func_argspec 201 | 202 | @raises(ValueError) 203 | def test_autodoc_invalid_no_extra_args(self): 204 | def invalid_function(T, p): 205 | return T 206 | decorators.equation_docstring( 207 | self.quantity_dict, self.assumption_dict)(invalid_function) 208 | 209 | @raises(ValueError) 210 | def test_autodoc_invalid_args_function(self): 211 | def T_from_x_y(x, y): 212 | return x 213 | decorators.equation_docstring( 214 | self.quantity_dict, self.assumption_dict)(T_from_x_y) 215 | 216 | @raises(ValueError) 217 | def test_autodoc_invalid_extra_args(self): 218 | def invalid_function(T, p): 219 | return T 220 | decorators.equation_docstring( 221 | self.quantity_dict, self.assumption_dict, equation='x=y', 222 | references='reference here', notes='c sharp')(invalid_function) 223 | 224 | 225 | class StringUtilityTests(unittest.TestCase): 226 | 227 | def setUp(self): 228 | self.quantity_dict = { 229 | 'T': {'name': 'air temperature', 'units': 'K'}, 230 | 'qv': {'name': 'specific humidity', 'units': 'kg/kg'}, 231 | 'p': {'name': 'air pressure', 'units': 'Pa'}, 232 | } 233 | self.assumption_dict = { 234 | 'a1': 'a1_long', 235 | 'a2': 'a2_long', 236 | 'a3': 'a3_long', 237 | } 238 | 239 | def tearDown(self): 240 | self.quantity_dict = None 241 | self.assumption_dict = None 242 | 243 | def test_quantity_string(self): 244 | string = quantity_string('T', self.quantity_dict) 245 | assert string == 'air temperature (K)' 246 | 247 | @raises(ValueError) 248 | def test_quantity_string_invalid_quantity(self): 249 | quantity_string('rhombus', self.quantity_dict) 250 | 251 | @raises(ValueError) 252 | def test_strings_to_list_string_empty(self): 253 | string = strings_to_list_string(()) 254 | assert string == '' 255 | 256 | @raises(TypeError) 257 | def test_strings_to_list_string_input_string(self): 258 | strings_to_list_string('hello') 259 | 260 | def test_strings_to_list_string_single(self): 261 | string = strings_to_list_string(('string1',)) 262 | assert string == 'string1' 263 | 264 | def test_strings_to_list_string_double(self): 265 | string = strings_to_list_string(('string1', 'string2')) 266 | assert string == 'string1 and string2' 267 | 268 | def test_strings_to_list_string_triple(self): 269 | string = strings_to_list_string(('string1', 'string2', 'string3')) 270 | assert string == 'string1, string2, and string3' 271 | 272 | @raises(ValueError) 273 | def test_assumption_list_string_empty(self): 274 | assumption_list_string((), self.assumption_dict) 275 | 276 | @raises(TypeError) 277 | def test_assumption_list_string_input_string(self): 278 | assumption_list_string('hello', self.assumption_dict) 279 | 280 | @raises(ValueError) 281 | def test_assumption_list_string_invalid(self): 282 | assumption_list_string(('ooglymoogly',), self.assumption_dict) 283 | 284 | def test_assumption_list_string_single(self): 285 | string = assumption_list_string(('a1',), self.assumption_dict) 286 | assert string == 'a1_long' 287 | 288 | def test_assumption_list_string_double(self): 289 | string = assumption_list_string(('a1', 'a2'), self.assumption_dict) 290 | assert string == 'a1_long and a2_long' 291 | 292 | def test_assumption_list_string_triple(self): 293 | string = assumption_list_string(('a1', 'a2', 'a3'), 294 | self.assumption_dict) 295 | assert string == 'a1_long, a2_long, and a3_long' 296 | 297 | @raises(ValueError) 298 | def test_quantity_spec_string_invalid(self): 299 | quantity_spec_string('rv', self.quantity_dict) 300 | 301 | def test_quantity_spec_string_valid(self): 302 | string = quantity_spec_string('T', self.quantity_dict) 303 | assert string == ('T : float or ndarray\n' 304 | ' Data for air temperature (K).') 305 | 306 | def test_doc_paragraph_no_wrap_for_short_string(self): 307 | string = doc_paragraph('The quick brown fox jumped over the yellow ' 308 | 'doge. The quick brown fox jumped over') 309 | assert '\n' not in string 310 | 311 | def test_doc_paragraph_wraps_on_word(self): 312 | string = doc_paragraph('The quick brown fox jumped over the yellow ' 313 | 'doge. The quick brown fox jumped over.') 314 | if (string != 'The quick brown fox jumped over the yellow doge. ' 315 | 'The quick brown fox jumped\nover.'): 316 | raise AssertionError('incorrect string "{0}"'.format(string)) 317 | 318 | def test_doc_paragraph_indent_wrap(self): 319 | string = doc_paragraph('The quick brown fox jumped over the yellow ' 320 | 'doge. The quick brown fox jumped over', 321 | indent=1) 322 | if (string != ' The quick brown fox jumped over the yellow doge. ' 323 | 'The quick brown fox jumped\n over'): 324 | raise AssertionError('incorrect string "{0}"'.format(string)) 325 | 326 | def test_doc_paragraph_zero_indent(self): 327 | string = doc_paragraph('The quick brown fox jumped over the yellow ' 328 | 'doge. The quick brown fox jumped over.', 329 | indent=0) 330 | if (string != 'The quick brown fox jumped over the yellow doge. ' 331 | 'The quick brown fox jumped\nover.'): 332 | raise AssertionError('incorrect string "{0}"'.format(string)) 333 | 334 | 335 | class ParseDerivativeStringTests(unittest.TestCase): 336 | 337 | def setUp(self): 338 | self.quantity_dict = { 339 | 'T': {'name': 'air temperature', 'units': 'K'}, 340 | 'qv': {'name': 'specific humidity', 'units': 'kg/kg'}, 341 | 'p': {'name': 'air pressure', 'units': 'Pa'}, 342 | } 343 | 344 | def tearDown(self): 345 | self.quantity_dict = None 346 | 347 | @raises(ValueError) 348 | def test_invalid_format(self): 349 | util.parse_derivative_string('ooglymoogly', self.quantity_dict) 350 | 351 | @raises(ValueError) 352 | def test_invalid_variable(self): 353 | util.parse_derivative_string('dpdz', self.quantity_dict) 354 | 355 | def test_dTdp(self): 356 | var1, var2 = util.parse_derivative_string('dTdp', self.quantity_dict) 357 | assert var1 == 'T' 358 | assert var2 == 'p' 359 | 360 | @raises(ValueError) 361 | def test_dpdT(self): 362 | util.parse_derivative_string('dpdT', self.quantity_dict) 363 | 364 | 365 | class OverriddenByAssumptionsTests(unittest.TestCase): 366 | def test_overridden_by_assumptions_empty(self): 367 | @decorators.overridden_by_assumptions() 368 | def foo(): 369 | return None 370 | assert foo.overridden_by_assumptions == () 371 | 372 | def test_overridden_by_assumptions_single(self): 373 | @decorators.overridden_by_assumptions('test assumption') 374 | def foo(): 375 | return None 376 | assert foo.overridden_by_assumptions == ('test assumption',) 377 | 378 | def test_overridden_by_assumptions_multiple(self): 379 | @decorators.overridden_by_assumptions('test assumption', 'a2') 380 | def foo(): 381 | return None 382 | assert foo.overridden_by_assumptions == ('test assumption', 'a2') 383 | 384 | 385 | class GetCalculatableMethodsDictTests(unittest.TestCase): 386 | 387 | def test_get_calculatable_methods_dict_empty(self): 388 | methods = {} 389 | out_methods = _get_calculatable_methods_dict((), methods) 390 | assert isinstance(out_methods, dict) 391 | assert len(out_methods) == 0 392 | 393 | def test_get_calculatable_methods_dict_returns_correct_type(self): 394 | methods = {'a': {('b',): lambda x: x}} 395 | out_methods = _get_calculatable_methods_dict(('b',), methods) 396 | assert isinstance(out_methods, dict) 397 | 398 | def test_get_calculatable_methods_dict_gets_single_method(self): 399 | methods = {'a': {('b',): lambda x: x}} 400 | out_methods = _get_calculatable_methods_dict(('b',), methods) 401 | assert 'a' in out_methods.keys() 402 | 403 | def test_get_calculatable_methods_dict_removes_correct_second_method( 404 | self): 405 | methods = {'a': {('b',): lambda x: x, 406 | ('c', 'b'): lambda x: x}} 407 | out_methods = _get_calculatable_methods_dict(('b', 'c'), methods) 408 | assert 'a' in out_methods.keys() 409 | assert ('b',) in out_methods['a'].keys() 410 | assert len(out_methods['a']) == 1 411 | 412 | def test_get_calculatable_methods_dict_removes_irrelevant_second_method( 413 | self): 414 | methods = {'a': {('b', 'd'): lambda x, y: x, 415 | ('c',): lambda x: x}} 416 | out_methods = _get_calculatable_methods_dict(('b', 'd'), methods) 417 | assert 'a' in out_methods.keys() 418 | assert ('b', 'd') in out_methods['a'].keys() 419 | assert len(out_methods['a']) == 1 420 | 421 | def test_get_calculatable_methods_dict_gets_no_methods(self): 422 | methods = {'a': {('b',): lambda x: x}, 423 | 'x': {('y', 'z'): lambda y, z: y*z} 424 | } 425 | out_methods = _get_calculatable_methods_dict(('q',), methods) 426 | assert isinstance(out_methods, dict) 427 | assert len(out_methods) == 0 428 | 429 | def test_get_calculatable_methods_dict_doesnt_calculate_input(self): 430 | methods = {'a': {('b',): lambda x: x}, 431 | 'x': {('y', 'z'): lambda y, z: y*z} 432 | } 433 | out_methods = _get_calculatable_methods_dict(('b', 'a'), methods) 434 | assert isinstance(out_methods, dict) 435 | assert len(out_methods) == 0 436 | 437 | 438 | class BaseSolverTests(unittest.TestCase): 439 | 440 | @raises(NotImplementedError) 441 | def test_cannot_instantiate(self): 442 | BaseSolver() 443 | 444 | 445 | class FluidSolverTests(unittest.TestCase): 446 | 447 | def setUp(self): 448 | shape = (3, 4, 2, 2) 449 | self.vars1 = {'Tv': np.ones(shape), 450 | 'p': np.ones(shape)} 451 | self.vars2 = {'T': np.ones(shape), 452 | 'p': np.ones(shape)} 453 | 454 | def tearDown(self): 455 | self.vars1 = None 456 | self.vars2 = None 457 | 458 | def test_creation_no_arguments(self): 459 | FluidSolver() 460 | 461 | def test_is_instance_of_BaseSolver(self): 462 | deriv = FluidSolver() 463 | assert isinstance(deriv, BaseSolver) 464 | 465 | def test_creation_one_method(self): 466 | FluidSolver(assumptions=('hydrostatic',)) 467 | 468 | def test_creation_compatible_methods(self): 469 | FluidSolver(assumptions=('hydrostatic', 'Tv equals T',)) 470 | 471 | @raises(ValueError) 472 | def test_creation_incompatible_methods(self): 473 | FluidSolver(assumptions=('Goff-Gratch', 'Wexler',)) 474 | 475 | @raises(ValueError) 476 | def test_creation_undefined_method(self): 477 | FluidSolver(assumptions=('moocow',)) 478 | 479 | @raises(ValueError) 480 | def test_creation_undefined_method_with_defined_method(self): 481 | FluidSolver(assumptions=('hydrostatic', 'moocow',)) 482 | 483 | def test_creation_with_vars(self): 484 | FluidSolver(**self.vars1) 485 | 486 | def test_creation_with_vars_and_method(self): 487 | FluidSolver(assumptions=('Tv equals T',), **self.vars1) 488 | 489 | def test_simple_calculation(self): 490 | deriver = FluidSolver(**self.vars1) 491 | rho = deriver.calculate('rho') 492 | assert (rho == 1/Rd).all() 493 | assert isinstance(rho, np.ndarray) 494 | 495 | def test_depth_2_calculation(self): 496 | deriver = FluidSolver(assumptions=FluidSolver.default_assumptions + 497 | ('Tv equals T',), **self.vars2) 498 | rho = deriver.calculate('rho') 499 | assert (rho == 1/Rd).all() 500 | assert isinstance(rho, np.ndarray) 501 | 502 | def test_double_calculation(self): 503 | deriver = FluidSolver(add_assumptions=('Tv equals T',), **self.vars2) 504 | Tv = deriver.calculate('Tv') 505 | rho = deriver.calculate('rho') 506 | assert (rho == 1/Rd).all() 507 | assert isinstance(rho, np.ndarray) 508 | assert isinstance(Tv, np.ndarray) 509 | 510 | def test_double_reverse_calculation(self): 511 | deriver = FluidSolver(add_assumptions=('Tv equals T',), **self.vars2) 512 | rho = deriver.calculate('rho') 513 | print('now Tv') 514 | Tv = deriver.calculate('Tv') 515 | assert (rho == 1/Rd).all() 516 | assert isinstance(rho, np.ndarray) 517 | assert isinstance(Tv, np.ndarray) 518 | 519 | 520 | class calculateTests(unittest.TestCase): 521 | 522 | def setUp(self): 523 | self.shape = (3, 4, 2, 2) 524 | self.vars1 = {'Tv': np.ones(self.shape), 525 | 'p': np.ones(self.shape)} 526 | self.vars2 = {'T': np.ones(self.shape), 527 | 'p': np.ones(self.shape)} 528 | 529 | def tearDown(self): 530 | self.vars1 = None 531 | self.vars2 = None 532 | 533 | def test_simple_calculation(self): 534 | rho = calculate('rho', **self.vars1) 535 | assert (rho.shape == self.shape) 536 | assert (rho == 1/Rd).all() 537 | self.assertTrue(isinstance(rho, np.ndarray), 538 | 'returned rho should be ndarray') 539 | 540 | def test_returns_float(self): 541 | rho = calculate('rho', Tv=1., p=1.) 542 | self.assertTrue(isinstance(rho, float), 543 | 'returned rho should be float') 544 | 545 | def test_depth_2_calculation(self): 546 | rho = calculate('rho', add_assumptions=('Tv equals T',), **self.vars2) 547 | assert rho.shape == self.shape 548 | assert (rho == 1/Rd).all() 549 | self.assertTrue(isinstance(rho, np.ndarray), 550 | 'returned rho should be ndarray') 551 | 552 | def test_double_calculation(self): 553 | Tv, rho = calculate('Tv', 'rho', add_assumptions=('Tv equals T',), 554 | **self.vars2) 555 | assert Tv.shape == self.shape 556 | assert rho.shape == self.shape 557 | assert (rho == 1/Rd).all() 558 | self.assertTrue(isinstance(rho, np.ndarray), 559 | 'returned rho should be ndarray') 560 | self.assertTrue(isinstance(Tv, np.ndarray), 561 | 'returned Tv should be ndarray') 562 | 563 | def test_double_reverse_calculation(self): 564 | rho, Tv = calculate('rho', 'Tv', add_assumptions=('Tv equals T',), 565 | **self.vars2) 566 | assert (rho == 1/Rd).all() 567 | self.assertTrue(isinstance(rho, np.ndarray), 568 | 'returned rho should be ndarray') 569 | self.assertTrue(isinstance(Tv, np.ndarray), 570 | 'returned Tv should be ndarray') 571 | 572 | def test_T_from_Tv(self): 573 | assert calculate('T', Tv=1., add_assumptions=('Tv equals T',)) == 1. 574 | assert calculate('T', Tv=5., add_assumptions=('Tv equals T',)) == 5. 575 | 576 | def test_rv_from_qv(self): 577 | self.assertAlmostEqual(calculate('rv', qv=0.005), 0.005025125628140704) 578 | 579 | def test_qv_from_rv(self): 580 | self.assertAlmostEqual(calculate('qv', rv=0.005), 0.004975124378109453) 581 | 582 | 583 | class CalculateWithUnitsTests(unittest.TestCase): 584 | 585 | def setUp(self): 586 | self.units_dict = { 587 | 'T': ('K', 'degC', 'degF'), 588 | 'p': ('hPa', 'Pa'), 589 | 'Tv': ('K', 'degC', 'degF'), 590 | 'rv': ('g/kg', 'kg/kg'), 591 | 'qv': ('g/kg', 'kg/kg'), 592 | 'rvs': ('g/kg', 'kg/kg'), 593 | 'RH': ('percent',), 594 | } 595 | 596 | def tearDown(self): 597 | self.units_dict = None 598 | 599 | def test_returns_same_value(self): 600 | for quantity in self.units_dict.keys(): 601 | for unit in self.units_dict[quantity]: 602 | kwargs = {} 603 | kwargs[quantity + '_unit'] = unit 604 | kwargs[quantity] = 1.5 605 | result = calculate(quantity, **kwargs) 606 | print(result, quantity, unit) 607 | self.assertAlmostEqual( 608 | result, 1.5, msg='calculate should return the same value ' 609 | 'when given a value as input') 610 | 611 | def test_input_unit(self): 612 | rho = calculate('rho', Tv=1., p=0.01, p_unit='hPa') 613 | self.assertEqual(rho, 1./Rd) 614 | 615 | def test_output_unit(self): 616 | p = calculate('p', Tv=1., rho=1./Rd, p_unit='millibar') 617 | self.assertEqual(p, 0.01) 618 | 619 | 620 | class TestSolveValuesNearSkewT(unittest.TestCase): 621 | 622 | def setUp(self): 623 | self.quantities = {'p': 9e4, 'theta': 14.+273.15, 624 | 'rv': 1e-3, 'Tlcl': -22.+273.15, 625 | 'thetae': 17.+273.15, 'Tw': -4.+273.15, 626 | 'Td': -18.+273.15, 'plcl': 65000., 627 | } 628 | self.quantities['T'] = calculate('T', **self.quantities) 629 | self.quantities['Tv'] = calculate('Tv', **self.quantities) 630 | self.quantities['rho'] = calculate('rho', **self.quantities) 631 | self.add_assumptions = ('bolton', 'unfrozen bulb') 632 | 633 | def _generator(self, quantity, tolerance): 634 | skew_T_value = self.quantities.pop(quantity) 635 | calculated_value, funcs = calculate( 636 | quantity, add_assumptions=self.add_assumptions, 637 | debug=True, **self.quantities) 638 | diff = abs(skew_T_value - calculated_value) 639 | if diff > tolerance: 640 | err_msg = ('Value {0:.4f} is too far away from ' 641 | '{1:.4f} for {2}.'.format( 642 | calculated_value, skew_T_value, quantity)) 643 | err_msg += '\nfunctions used:\n' 644 | err_msg += '\n'.join([f.__name__ for f in funcs]) 645 | raise AssertionError(err_msg) 646 | 647 | def tearDown(self): 648 | self.quantities = None 649 | 650 | def test_calculate_precursors(self): 651 | pass 652 | 653 | def test_calculate_p(self): 654 | self._generator('p', 10000.) 655 | 656 | def test_calculate_Tv(self): 657 | self._generator('Tv', 1.) 658 | 659 | def test_calculate_Td(self): 660 | self._generator('Td', 1.) 661 | 662 | def test_calculate_theta(self): 663 | self._generator('theta', 1.) 664 | 665 | def test_calculate_rv(self): 666 | self._generator('rv', 1e-3) 667 | 668 | def test_calculate_Tlcl(self): 669 | self._generator('Tlcl', 1.) 670 | 671 | def test_calculate_thetae(self): 672 | quantity = 'thetae' 673 | skew_T_value = self.quantities.pop(quantity) 674 | self.quantities.pop('Tlcl') # let us calculate this ourselves 675 | calculated_value, funcs = calculate( 676 | quantity, add_assumptions=('bolton', 'unfrozen bulb'), 677 | debug=True, 678 | **self.quantities) 679 | diff = abs(skew_T_value - calculated_value) 680 | if diff > 2.: 681 | err_msg = ('Value {:.2f} is too far away from ' 682 | '{:.2f} for {}.'.format( 683 | calculated_value, skew_T_value, quantity)) 684 | err_msg += '\nfunctions used:\n' 685 | err_msg += '\n'.join([f.__name__ for f in funcs]) 686 | raise AssertionError(err_msg) 687 | 688 | def test_calculate_plcl(self): 689 | self._generator('plcl', 10000.) 690 | 691 | 692 | class TestSolveValuesNearSkewTAlternateUnits(unittest.TestCase): 693 | 694 | def setUp(self): 695 | self.quantities = {'p': 8.9e2, 'theta': 14.+273.15, 696 | 'rv': 1., 'Tlcl': -22.5+273.15, 697 | 'thetae': 17.+273.15, 'Tw': -2.5, 698 | 'Td': -18.5+273.15, 'plcl': 62500., 699 | } 700 | self.units = {'p_unit': 'hPa', 'Tv_units': 'degC', 'Tw_unit': 'degC', 701 | 'rv_unit': 'g/kg'} 702 | kwargs = {} 703 | kwargs.update(self.quantities) 704 | kwargs.update(self.units) 705 | self.quantities['T'] = calculate('T', **kwargs) 706 | self.quantities['Tv'] = calculate('Tv', **kwargs) 707 | self.quantities['rho'] = calculate('rho', **kwargs) 708 | self.add_assumptions = ('bolton', 'unfrozen bulb') 709 | 710 | def _generator(self, quantity, tolerance): 711 | skew_T_value = self.quantities.pop(quantity) 712 | kwargs = {} 713 | kwargs.update(self.quantities) 714 | kwargs.update(self.units) 715 | calculated_value, funcs = calculate( 716 | quantity, add_assumptions=self.add_assumptions, 717 | debug=True, **kwargs) 718 | diff = abs(skew_T_value - calculated_value) 719 | if diff > tolerance: 720 | err_msg = ('Value {:.2f} is too far away from ' 721 | '{:.2f} for {}.'.format( 722 | calculated_value, skew_T_value, quantity)) 723 | err_msg += '\nfunctions used:\n' 724 | err_msg += '\n'.join([f.__name__ for f in funcs]) 725 | raise AssertionError(err_msg) 726 | 727 | def tearDown(self): 728 | self.quantities = None 729 | 730 | def test_calculate_precursors(self): 731 | pass 732 | 733 | def test_calculate_p(self): 734 | self._generator('p', 100.) 735 | 736 | def test_calculate_Tv(self): 737 | self._generator('Tv', 1.) 738 | 739 | def test_calculate_theta(self): 740 | self._generator('theta', 1.) 741 | 742 | def test_calculate_rv(self): 743 | self._generator('rv', 1e-3) 744 | 745 | def test_calculate_Tlcl(self): 746 | self._generator('Tlcl', 1.) 747 | 748 | def test_calculate_thetae(self): 749 | quantity = 'thetae' 750 | skew_T_value = self.quantities.pop(quantity) 751 | self.quantities.pop('Tlcl') # let us calculate this ourselves 752 | kwargs = {} 753 | kwargs.update(self.quantities) 754 | kwargs.update(self.units) 755 | calculated_value, funcs = calculate( 756 | quantity, add_assumptions=('bolton', 'unfrozen bulb'), 757 | debug=True, 758 | **kwargs) 759 | diff = abs(skew_T_value - calculated_value) 760 | if diff > 1.: 761 | err_msg = ('Value {0:.2f} is too far away from ' 762 | '{1:.2f} for {2}.'.format( 763 | calculated_value, skew_T_value, quantity)) 764 | err_msg += '\nfunctions used:\n' 765 | err_msg += '\n'.join([f.__name__ for f in funcs]) 766 | raise AssertionError(err_msg) 767 | 768 | def test_calculate_Tw(self): 769 | quantity = 'Tw' 770 | skew_T_value = self.quantities.pop(quantity) 771 | kwargs = {} 772 | kwargs.update(self.quantities) 773 | kwargs.update(self.units) 774 | calculated_value, funcs = calculate( 775 | quantity, add_assumptions=('bolton', 'unfrozen bulb'), 776 | debug=True, 777 | **kwargs) 778 | diff = abs(skew_T_value - calculated_value) 779 | if diff > 1.: 780 | err_msg = ('Value {:.2f} is too far away from ' 781 | '{:.2f} for {}.'.format( 782 | calculated_value, skew_T_value, quantity)) 783 | err_msg += '\nfunctions used:\n' 784 | err_msg += '\n'.join([f.__name__ for f in funcs]) 785 | raise AssertionError(err_msg) 786 | 787 | # def test_calculate_Td(self): 788 | # self._generator('Td', 1.) 789 | 790 | def test_calculate_plcl(self): 791 | self._generator('plcl', 10000.) 792 | 793 | 794 | class TestSolveValuesNearSkewTAssumingLowMoisture(TestSolveValuesNearSkewT): 795 | 796 | def setUp(self): 797 | super(TestSolveValuesNearSkewTAssumingLowMoisture, self).setUp() 798 | self.add_assumptions = ('bolton', 'unfrozen bulb', 'low water vapor') 799 | 800 | 801 | class TestSolveValuesNearSkewTVeryMoist(TestSolveValuesNearSkewT): 802 | 803 | def setUp(self): 804 | self.quantities = {'p': 8.9e4, 'theta': 18.4+273.15, 805 | 'rv': 6e-3, 'Tlcl': 3.8+273.15, 806 | 'thetae': 36.5+273.15, 807 | 'Tw': 6.5+273.15, 'Td': 4.8+273.15, 'plcl': 83500., 808 | } 809 | self.quantities['T'] = calculate('T', **self.quantities) 810 | self.quantities['Tv'] = calculate('Tv', **self.quantities) 811 | self.quantities['rho'] = calculate('rho', **self.quantities) 812 | self.add_assumptions = ('bolton', 'unfrozen bulb') 813 | 814 | 815 | class GetShortestSolutionTests(unittest.TestCase): 816 | 817 | def setUp(self): 818 | self.methods = { 819 | 'x': {('a', 'b'): lambda a, b: a}, 820 | 'y': {('x',): lambda x: x}, 821 | 'z': {('x', 'y'): lambda x, y: x*y}, 822 | 'w': {('z', 'y'): lambda x, y: x*y}, 823 | } 824 | 825 | def tearDown(self): 826 | self.methods = None 827 | 828 | def test_no_calculation(self): 829 | sol = _get_shortest_solution(('x',), ('x',), (), self.methods) 830 | assert isinstance(sol, tuple) 831 | assert len(sol) == 3 832 | assert len(sol[0]) == 0 833 | assert len(sol[1]) == 0 834 | assert len(sol[2]) == 0 835 | 836 | def test_simplest_calculation(self): 837 | sol = _get_shortest_solution(('y',), ('x',), (), self.methods) 838 | assert len(sol[0]) == 1 839 | 840 | def test_depth_2_calculation(self): 841 | _get_shortest_solution(('z',), ('a', 'b'), (), self.methods) 842 | 843 | def test_depth_3_calculation(self): 844 | _get_shortest_solution(('w',), ('a', 'b'), (), self.methods) 845 | 846 | 847 | class EquationTests(unittest.TestCase): 848 | 849 | def _assert_accurate_values(self, func, in_values, out_values, tols): 850 | for i, args in enumerate(in_values): 851 | out_calc = func(*args) 852 | if abs(out_calc - out_values[i]) > tols[i]: 853 | raise AssertionError('Calculated value ' 854 | '{0} from inputs {1} is more than {2} ' 855 | 'away from {3}'.format(out_calc, args, tols[i], out_values[i])) 856 | 857 | def test_e_from_Td_Bolton(self): 858 | func = equations.e_from_Td_Bolton 859 | in_values = [(273.15,), (273.15+20,), (273.15+40,), (273.15+50,)] 860 | out_values = [603, 2310, 7297, 12210] 861 | tols = [603*0.02, 2310*0.02, 7297*0.02, 12210*0.02] 862 | self._assert_accurate_values(func, in_values, out_values, tols) 863 | 864 | def test_e_from_Td_Goff_Gratch(self): 865 | func = equations.e_from_Td_Goff_Gratch 866 | in_values = [(273.15,), (273.15+20,), (273.15+40,), (273.15+50,)] 867 | out_values = [603, 2310, 7297, 12210] 868 | tols = [603*0.02, 2310*0.02, 7297*0.02, 12210*0.02] 869 | self._assert_accurate_values(func, in_values, out_values, tols) 870 | 871 | def test_e_from_p_T_Tw_Bolton(self): 872 | func = equations.e_from_p_T_Tw_Bolton 873 | in_values = [(1e5, 273.15+10, 273.15+5), (8e4, 273.15+15, 273.15+5.4)] 874 | out_values = [549.7, 382.8] 875 | tols = [549.7*0.1/3.45, 0.05*382.8] 876 | self._assert_accurate_values(func, in_values, out_values, tols) 877 | 878 | def test_e_from_p_T_Tw_Goff_Gratch(self): 879 | func = equations.e_from_p_T_Tw_Goff_Gratch 880 | in_values = [(1e5, 273.15+10, 273.15+5), (8e4, 273.15+15, 273.15+5.4)] 881 | out_values = [549.7, 382.8] 882 | tols = [549.7*0.1/3.45, 0.05*382.8] 883 | self._assert_accurate_values(func, in_values, out_values, tols) 884 | 885 | def test_es_from_T_Bolton(self): 886 | func = equations.es_from_T_Bolton 887 | in_values = [(273.15,), (273.15+20,), (273.15+40,), (273.15+50,)] 888 | out_values = [603, 2310, 7297, 12210] 889 | tols = [603*0.02, 2310*0.02, 7297*0.02, 12210*0.02] 890 | self._assert_accurate_values(func, in_values, out_values, tols) 891 | 892 | def test_es_from_T_Goff_Gratch(self): 893 | func = equations.es_from_T_Goff_Gratch 894 | in_values = [(273.15,), (273.15+20,), (273.15+40,), (273.15+50,)] 895 | out_values = [603, 2310, 7297, 12210] 896 | tols = [603*0.02, 2310*0.02, 7297*0.02, 12210*0.02] 897 | self._assert_accurate_values(func, in_values, out_values, tols) 898 | 899 | def test_f_from_lat(self): 900 | func = equations.f_from_lat 901 | in_values = [(0.,), (45.,), (90.,)] 902 | out_values = [0., 1.031e-4, 1.458e-4] 903 | tols = [0.001e-4, 0.001e-4, 0.001e-4] 904 | self._assert_accurate_values(func, in_values, out_values, tols) 905 | 906 | def test_Gammam_from_rvs_T(self): 907 | func = equations.Gammam_from_rvs_T 908 | in_values = [] 909 | out_values = [] 910 | tols = [] 911 | self._assert_accurate_values(func, in_values, out_values, tols) 912 | 913 | def test_MSE_from_DSE_qv(self): 914 | func = equations.MSE_from_DSE_qv 915 | in_values = [] 916 | out_values = [] 917 | tols = [] 918 | self._assert_accurate_values(func, in_values, out_values, tols) 919 | 920 | def test_omega_from_w_rho_hydrostatic(self): 921 | func = equations.omega_from_w_rho_hydrostatic 922 | in_values = [] 923 | out_values = [] 924 | tols = [] 925 | self._assert_accurate_values(func, in_values, out_values, tols) 926 | 927 | def test_p_from_rho_Tv_ideal_gas(self): 928 | func = equations.p_from_rho_Tv_ideal_gas 929 | in_values = [] 930 | out_values = [] 931 | tols = [] 932 | self._assert_accurate_values(func, in_values, out_values, tols) 933 | 934 | def test_plcl_from_p_T_Tlcl(self): 935 | func = equations.plcl_from_p_T_Tlcl 936 | in_values = [] 937 | out_values = [] 938 | tols = [] 939 | self._assert_accurate_values(func, in_values, out_values, tols) 940 | 941 | def test_Phi_from_z(self): 942 | func = equations.Phi_from_z 943 | in_values = [(0.,), (30.,), (100.,)] 944 | out_values = [0., 294.3, 981.] 945 | tols = [0.01, 0.2, 1.] 946 | self._assert_accurate_values(func, in_values, out_values, tols) 947 | 948 | def test_qv_from_AH_rho(self): 949 | func = equations.qv_from_AH_rho 950 | in_values = [] 951 | out_values = [] 952 | tols = [] 953 | self._assert_accurate_values(func, in_values, out_values, tols) 954 | 955 | def test_qv_from_rv(self): 956 | func = equations.qv_from_rv 957 | in_values = [(0.,), (0.005,), (0.1,)] 958 | out_values = [0., 0.00498, 0.091] 959 | tols = [0.01, 0.00001, 0.001] 960 | self._assert_accurate_values(func, in_values, out_values, tols) 961 | 962 | def test_qv_from_rv_lwv(self): 963 | func = equations.qv_from_rv_lwv 964 | in_values = [(0.,), (0.005,), (0.1,)] 965 | out_values = [0., 0.005, 0.1] 966 | tols = [1e-8, 1e-8, 1e-8] 967 | self._assert_accurate_values(func, in_values, out_values, tols) 968 | 969 | def test_qv_from_p_e(self): 970 | func = equations.qv_from_p_e 971 | in_values = [] 972 | out_values = [] 973 | tols = [] 974 | self._assert_accurate_values(func, in_values, out_values, tols) 975 | 976 | def test_qv_from_p_e_lwv(self): 977 | func = equations.qv_from_p_e_lwv 978 | in_values = [] 979 | out_values = [] 980 | tols = [] 981 | self._assert_accurate_values(func, in_values, out_values, tols) 982 | 983 | def test_qvs_from_rvs(self): 984 | func = equations.qvs_from_rvs 985 | in_values = [(0.,), (0.005,), (0.1,)] 986 | out_values = [0., 0.00498, 0.091] 987 | tols = [0.01, 0.00001, 0.001] 988 | self._assert_accurate_values(func, in_values, out_values, tols) 989 | 990 | def test_qvs_from_rvs_lwv(self): 991 | func = equations.qvs_from_rvs_lwv 992 | in_values = [(0.,), (0.005,), (0.1,)] 993 | out_values = [0., 0.005, 0.1] 994 | tols = [1e-8, 1e-8, 1e-8] 995 | self._assert_accurate_values(func, in_values, out_values, tols) 996 | 997 | def test_qvs_from_p_es(self): 998 | func = equations.qvs_from_p_es 999 | in_values = [] 1000 | out_values = [] 1001 | tols = [] 1002 | self._assert_accurate_values(func, in_values, out_values, tols) 1003 | 1004 | def test_qvs_from_p_es_lwv(self): 1005 | func = equations.qvs_from_p_es_lwv 1006 | in_values = [] 1007 | out_values = [] 1008 | tols = [] 1009 | self._assert_accurate_values(func, in_values, out_values, tols) 1010 | 1011 | def test_qt_from_qi_qv_ql(self): 1012 | func = equations.qt_from_qi_qv_ql 1013 | in_values = [(0., 0., 0.), (0.005, 0.001, 0.004)] 1014 | out_values = [0., 0.01] 1015 | tols = [1e-8, 1e-8] 1016 | self._assert_accurate_values(func, in_values, out_values, tols) 1017 | 1018 | def test_qt_from_qv_ql(self): 1019 | func = equations.qt_from_qv_ql 1020 | in_values = [(0., 0.), (0.003, 0.002)] 1021 | out_values = [0., 0.005] 1022 | tols = [1e-8, 1e-8] 1023 | self._assert_accurate_values(func, in_values, out_values, tols) 1024 | 1025 | def test_qt_from_qv(self): 1026 | func = equations.qt_from_qv 1027 | in_values = [(0.,), (0.005,), (0.1,)] 1028 | out_values = [0., 0.005, 0.1] 1029 | tols = [1e-8, 1e-8, 1e-8] 1030 | self._assert_accurate_values(func, in_values, out_values, tols) 1031 | 1032 | def test_qt_from_qv_qi(self): 1033 | func = equations.qt_from_qv_qi 1034 | in_values = [(0., 0.), (0.003, 0.002)] 1035 | out_values = [0., 0.005] 1036 | tols = [1e-8, 1e-8] 1037 | self._assert_accurate_values(func, in_values, out_values, tols) 1038 | 1039 | def test_qv_from_qt(self): 1040 | func = equations.qv_from_qt 1041 | in_values = [(0.,), (0.005,), (0.1,)] 1042 | out_values = [0., 0.005, 0.1] 1043 | tols = [1e-8, 1e-8, 1e-8] 1044 | self._assert_accurate_values(func, in_values, out_values, tols) 1045 | 1046 | def test_qv_from_qt_ql_qi(self): 1047 | func = equations.qv_from_qt_ql_qi 1048 | in_values = [(0., 0., 0.), (0.01, 0.003, 0.002)] 1049 | out_values = [0., 0.005] 1050 | tols = [1e-8, 1e-8] 1051 | self._assert_accurate_values(func, in_values, out_values, tols) 1052 | 1053 | def test_qv_from_qt_ql(self): 1054 | func = equations.qv_from_qt_ql 1055 | in_values = [(0., 0.), (0.01, 0.005)] 1056 | out_values = [0., 0.005] 1057 | tols = [1e-8, 1e-8] 1058 | self._assert_accurate_values(func, in_values, out_values, tols) 1059 | 1060 | def test_qv_from_qt_qi(self): 1061 | func = equations.qv_from_qt_qi 1062 | in_values = [(0., 0.), (0.01, 0.005)] 1063 | out_values = [0., 0.005] 1064 | tols = [1e-8, 1e-8] 1065 | self._assert_accurate_values(func, in_values, out_values, tols) 1066 | 1067 | def test_qi_from_qt_qv_ql(self): 1068 | func = equations.qv_from_qt_ql_qi 1069 | in_values = [(0., 0., 0.), (0.01, 0.003, 0.002)] 1070 | out_values = [0., 0.005] 1071 | tols = [1e-8, 1e-8] 1072 | self._assert_accurate_values(func, in_values, out_values, tols) 1073 | 1074 | def test_qi_from_qt_qv(self): 1075 | func = equations.qi_from_qt_qv 1076 | in_values = [(0., 0.), (0.01, 0.005)] 1077 | out_values = [0., 0.005] 1078 | tols = [1e-8, 1e-8] 1079 | self._assert_accurate_values(func, in_values, out_values, tols) 1080 | 1081 | def test_ql_from_qt_qv_qi(self): 1082 | func = equations.qv_from_qt_ql_qi 1083 | in_values = [(0., 0., 0.), (0.01, 0.003, 0.002)] 1084 | out_values = [0., 0.005] 1085 | tols = [1e-8, 1e-8] 1086 | self._assert_accurate_values(func, in_values, out_values, tols) 1087 | 1088 | def test_ql_from_qt_qv(self): 1089 | func = equations.ql_from_qt_qv 1090 | in_values = [(0., 0.), (0.01, 0.005)] 1091 | out_values = [0., 0.005] 1092 | tols = [1e-8, 1e-8] 1093 | self._assert_accurate_values(func, in_values, out_values, tols) 1094 | 1095 | def test_RH_from_rv_rvs(self): 1096 | func = equations.RH_from_rv_rvs 1097 | in_values = [(5., 100.), (1e-3, 2e-3)] 1098 | out_values = [5., 50.] 1099 | tols = [0.01, 0.01] 1100 | self._assert_accurate_values(func, in_values, out_values, tols) 1101 | 1102 | def test_RH_from_qv_qvs_lwv(self): 1103 | func = equations.RH_from_qv_qvs_lwv 1104 | in_values = [(5., 100.), (1e-3, 2e-3)] 1105 | out_values = [5., 50.] 1106 | tols = [0.01, 0.01] 1107 | self._assert_accurate_values(func, in_values, out_values, tols) 1108 | 1109 | def test_rho_from_qv_AH(self): 1110 | func = equations.rho_from_qv_AH 1111 | in_values = [] 1112 | out_values = [] 1113 | tols = [] 1114 | self._assert_accurate_values(func, in_values, out_values, tols) 1115 | 1116 | def test_rho_from_p_Tv_ideal_gas(self): 1117 | func = equations.rho_from_p_Tv_ideal_gas 1118 | in_values = [] 1119 | out_values = [] 1120 | tols = [] 1121 | self._assert_accurate_values(func, in_values, out_values, tols) 1122 | 1123 | def test_rv_from_qv(self): 1124 | func = equations.rv_from_qv 1125 | in_values = [(0.,), (0.05,), (0.1,)] 1126 | out_values = [0., 0.05263, 0.11111] 1127 | tols = [1e-8, 0.0001, 0.0001] 1128 | self._assert_accurate_values(func, in_values, out_values, tols) 1129 | 1130 | def test_rv_from_qv_lwv(self): 1131 | func = equations.rv_from_qv_lwv 1132 | in_values = [(0.,), (0.005,), (0.1,)] 1133 | out_values = [0., 0.005, 0.1] 1134 | tols = [1e-8, 1e-8, 1e-8] 1135 | self._assert_accurate_values(func, in_values, out_values, tols) 1136 | 1137 | def test_rv_from_p_e(self): 1138 | func = equations.rv_from_p_e 1139 | in_values = [(1e5, 500.), (9e4, 1000.), (8e4, 0.)] 1140 | out_values = [0.003125, 0.006988, 0.] 1141 | tols = [1e-6, 1e-6, 1e-8] 1142 | self._assert_accurate_values(func, in_values, out_values, tols) 1143 | 1144 | def test_rt_from_ri_rv_rl(self): 1145 | func = equations.rt_from_ri_rv_rl 1146 | in_values = [(0., 0., 0.), (0.005, 0.001, 0.004)] 1147 | out_values = [0., 0.01] 1148 | tols = [1e-8, 1e-8] 1149 | self._assert_accurate_values(func, in_values, out_values, tols) 1150 | 1151 | def test_rt_from_rv_rl(self): 1152 | func = equations.rt_from_rv_rl 1153 | in_values = [(0., 0.), (0.003, 0.002)] 1154 | out_values = [0., 0.005] 1155 | tols = [1e-8, 1e-8] 1156 | self._assert_accurate_values(func, in_values, out_values, tols) 1157 | 1158 | def test_rt_from_rv(self): 1159 | func = equations.rt_from_rv 1160 | in_values = [(0.,), (0.005,), (0.1,)] 1161 | out_values = [0., 0.005, 0.1] 1162 | tols = [1e-8, 1e-8, 1e-8] 1163 | self._assert_accurate_values(func, in_values, out_values, tols) 1164 | 1165 | def test_rt_from_rv_ri(self): 1166 | func = equations.rt_from_rv_ri 1167 | in_values = [(0., 0.), (0.003, 0.002)] 1168 | out_values = [0., 0.005] 1169 | tols = [1e-8, 1e-8] 1170 | self._assert_accurate_values(func, in_values, out_values, tols) 1171 | 1172 | def test_rv_from_rt(self): 1173 | func = equations.rv_from_rt 1174 | in_values = [(0.,), (0.005,), (0.1,)] 1175 | out_values = [0., 0.005, 0.1] 1176 | tols = [1e-8, 1e-8, 1e-8] 1177 | self._assert_accurate_values(func, in_values, out_values, tols) 1178 | 1179 | def test_rv_from_rt_rl_ri(self): 1180 | func = equations.rv_from_rt_rl_ri 1181 | in_values = [(0., 0., 0.), (0.01, 0.003, 0.002)] 1182 | out_values = [0., 0.005] 1183 | tols = [1e-8, 1e-8] 1184 | self._assert_accurate_values(func, in_values, out_values, tols) 1185 | 1186 | def test_rv_from_rt_rl(self): 1187 | func = equations.rv_from_rt_rl 1188 | in_values = [(0., 0.), (0.01, 0.005)] 1189 | out_values = [0., 0.005] 1190 | tols = [1e-8, 1e-8] 1191 | self._assert_accurate_values(func, in_values, out_values, tols) 1192 | 1193 | def test_rv_from_rt_ri(self): 1194 | func = equations.rv_from_rt_ri 1195 | in_values = [(0., 0.), (0.01, 0.005)] 1196 | out_values = [0., 0.005] 1197 | tols = [1e-8, 1e-8] 1198 | self._assert_accurate_values(func, in_values, out_values, tols) 1199 | 1200 | def test_ri_from_rt_rv_rl(self): 1201 | func = equations.rv_from_rt_rl_ri 1202 | in_values = [(0., 0., 0.), (0.01, 0.003, 0.002)] 1203 | out_values = [0., 0.005] 1204 | tols = [1e-8, 1e-8] 1205 | self._assert_accurate_values(func, in_values, out_values, tols) 1206 | 1207 | def test_ri_from_rt_rv(self): 1208 | func = equations.ri_from_rt_rv 1209 | in_values = [(0., 0.), (0.01, 0.005)] 1210 | out_values = [0., 0.005] 1211 | tols = [1e-8, 1e-8] 1212 | self._assert_accurate_values(func, in_values, out_values, tols) 1213 | 1214 | def test_rl_from_rt_rv_ri(self): 1215 | func = equations.rv_from_rt_rl_ri 1216 | in_values = [(0., 0., 0.), (0.01, 0.003, 0.002)] 1217 | out_values = [0., 0.005] 1218 | tols = [1e-8, 1e-8] 1219 | self._assert_accurate_values(func, in_values, out_values, tols) 1220 | 1221 | def test_rl_from_rt_rv(self): 1222 | func = equations.rl_from_rt_rv 1223 | in_values = [(0., 0.), (0.01, 0.005)] 1224 | out_values = [0., 0.005] 1225 | tols = [1e-8, 1e-8] 1226 | self._assert_accurate_values(func, in_values, out_values, tols) 1227 | 1228 | def test_rvs_from_p_es(self): 1229 | func = equations.rvs_from_p_es 1230 | in_values = [(1e5, 500.), (9e4, 1000.), (8e4, 0.)] 1231 | out_values = [0.003125, 0.006988, 0.] 1232 | tols = [1e-6, 1e-6, 1e-8] 1233 | self._assert_accurate_values(func, in_values, out_values, tols) 1234 | 1235 | def test_rvs_from_qvs(self): 1236 | func = equations.rvs_from_qvs 1237 | in_values = [(0.,), (0.05,), (0.1,)] 1238 | out_values = [0., 0.05263, 0.11111] 1239 | tols = [1e-8, 0.0001, 0.0001] 1240 | self._assert_accurate_values(func, in_values, out_values, tols) 1241 | 1242 | def test_rvs_from_qvs_lwv(self): 1243 | func = equations.rvs_from_qvs_lwv 1244 | in_values = [(0.,), (0.005,), (0.1,)] 1245 | out_values = [0., 0.005, 0.1] 1246 | tols = [1e-8, 1e-8, 1e-8] 1247 | self._assert_accurate_values(func, in_values, out_values, tols) 1248 | 1249 | def test_T_from_es_Bolton(self): 1250 | func = equations.T_from_es_Bolton 1251 | in_values = [(603,), (2310,), (7297,), (12210,)] 1252 | out_values = [273.15, 273.15+20, 273.15+40, 273.15+50] 1253 | tols = [1, 1, 1, 1] 1254 | self._assert_accurate_values(func, in_values, out_values, tols) 1255 | 1256 | def test_Tlcl_from_T_RH(self): 1257 | func = equations.Tlcl_from_T_RH 1258 | in_values = [] 1259 | out_values = [] 1260 | tols = [] 1261 | self._assert_accurate_values(func, in_values, out_values, tols) 1262 | 1263 | def test_Tlcl_from_T_Td(self): 1264 | func = equations.Tlcl_from_T_Td 1265 | in_values = [] 1266 | out_values = [] 1267 | tols = [] 1268 | self._assert_accurate_values(func, in_values, out_values, tols) 1269 | 1270 | def test_Tlcl_from_T_e(self): 1271 | func = equations.Tlcl_from_T_e 1272 | in_values = [] 1273 | out_values = [] 1274 | tols = [] 1275 | self._assert_accurate_values(func, in_values, out_values, tols) 1276 | 1277 | def test_Tv_from_T_assuming_Tv_equals_T(self): 1278 | func = equations.Tv_from_T_assuming_Tv_equals_T 1279 | in_values = [(273.15,), (100.,), (300.,)] 1280 | out_values = [273.15, 100., 300.] 1281 | tols = [0.001, 0.001, 0.001] 1282 | self._assert_accurate_values(func, in_values, out_values, tols) 1283 | 1284 | def test_Tv_from_p_rho_ideal_gas(self): 1285 | func = equations.Tv_from_p_rho_ideal_gas 1286 | in_values = [] 1287 | out_values = [] 1288 | tols = [] 1289 | self._assert_accurate_values(func, in_values, out_values, tols) 1290 | 1291 | def test_Tw_from_T_RH_Stull(self): 1292 | func = equations.Tw_from_T_RH_Stull 1293 | in_values = [(20+273.15, 50)] 1294 | out_values = [13.7+273.15, ] 1295 | tols = [0.1] 1296 | self._assert_accurate_values(func, in_values, out_values, tols) 1297 | 1298 | def test_T_from_Tv_assuming_Tv_equals_T(self): 1299 | func = equations.T_from_Tv_assuming_Tv_equals_T 1300 | in_values = [(273.15,), (100.,), (300.,)] 1301 | out_values = [273.15, 100., 300.] 1302 | tols = [0.001, 0.001, 0.001] 1303 | self._assert_accurate_values(func, in_values, out_values, tols) 1304 | 1305 | def test_theta_from_p_T(self): 1306 | func = equations.theta_from_p_T 1307 | in_values = [(75000., 273.15), (1e5, 253.15), (10000., 253.15)] 1308 | out_values = [296.57, 253.15, 489.07] 1309 | tols = [0.1, 0.01, 0.5] 1310 | self._assert_accurate_values(func, in_values, out_values, tols) 1311 | 1312 | def test_thetae_from_p_T_Tlcl_rv_Bolton(self): 1313 | func = equations.thetae_from_p_T_Tlcl_rv_Bolton 1314 | in_values = [] 1315 | out_values = [] 1316 | tols = [] 1317 | self._assert_accurate_values(func, in_values, out_values, tols) 1318 | 1319 | def test_thetae_from_p_e_T_RH_rv_rt(self): 1320 | func = equations.thetae_from_p_e_T_RH_rv_rt 1321 | in_values = [] 1322 | out_values = [] 1323 | tols = [] 1324 | self._assert_accurate_values(func, in_values, out_values, tols) 1325 | 1326 | def test_thetae_from_T_RH_rv_lwv(self): 1327 | func = equations.thetae_from_T_RH_rv_lwv 1328 | in_values = [] 1329 | out_values = [] 1330 | tols = [] 1331 | self._assert_accurate_values(func, in_values, out_values, tols) 1332 | 1333 | def test_thetaes_from_p_T_rvs_Bolton(self): 1334 | func = equations.thetaes_from_p_T_rvs_Bolton 1335 | in_values = [] 1336 | out_values = [] 1337 | tols = [] 1338 | self._assert_accurate_values(func, in_values, out_values, tols) 1339 | 1340 | def test_w_from_omega_rho_hydrostatic(self): 1341 | func = equations.w_from_omega_rho_hydrostatic 1342 | in_values = [] 1343 | out_values = [] 1344 | tols = [] 1345 | self._assert_accurate_values(func, in_values, out_values, tols) 1346 | 1347 | def test_z_from_Phi(self): 1348 | func = equations.z_from_Phi 1349 | in_values = [(0.,), (294.3,), (981.,)] 1350 | out_values = [0., 30., 100.] 1351 | tols = [1e-8, 0.1, 0.1] 1352 | self._assert_accurate_values(func, in_values, out_values, tols) 1353 | 1354 | 1355 | if __name__ == '__main__': 1356 | nose.run() 1357 | -------------------------------------------------------------------------------- /atmos/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Fri Mar 27 13:11:26 2015 4 | 5 | @author: mcgibbon 6 | """ 7 | from __future__ import division, absolute_import, unicode_literals 8 | import numpy as np 9 | import re 10 | import six 11 | from scipy.ndimage.filters import convolve1d 12 | 13 | derivative_prog = re.compile(r'd(.+)d(p|x|y|theta|z|sigma|t|lat|lon)') 14 | from textwrap import wrap 15 | 16 | 17 | def sma(array, window_size, axis=-1, mode='reflect', **kwargs): 18 | """ 19 | Computes a 1D simple moving average along the given axis. 20 | 21 | Parameters 22 | ---------- 23 | array : ndarray 24 | Array on which to perform the convolution. 25 | window_size: int 26 | Width of the simple moving average window in indices. 27 | axis : int, optional 28 | Axis along which to perform the moving average 29 | mode : {‘reflect’, ‘constant’, ‘nearest’, ‘mirror’, ‘wrap’}, optional 30 | The mode parameter determines how the array borders are handled, where 31 | cval is the value when mode is equal to ‘constant’. Default is ‘reflect’. 32 | kwargs : optional 33 | Other arguments to pass to `scipy.ndimage.filters.convolve1d` 34 | 35 | Returns 36 | ------- 37 | sma : ndarray 38 | Simple moving average of the given array with the specified window size 39 | along the requested axis. 40 | 41 | Raises 42 | ------ 43 | TypeError: 44 | If window_size or axis are not integers. 45 | """ 46 | kwargs['axis'] = axis 47 | kwargs['mode'] = mode 48 | if not isinstance(window_size, int): 49 | raise TypeError('window_size must be an integer') 50 | if not isinstance(kwargs['axis'], int): 51 | raise TypeError('axis must be an integer') 52 | return convolve1d(array, np.repeat(1.0, window_size)/window_size, **kwargs) 53 | 54 | 55 | def quantity_string(name, quantity_dict): 56 | '''Takes in an abbreviation for a quantity and a quantity dictionary, 57 | and returns a more descriptive string of the quantity as "name (units)." 58 | Raises ValueError if the name is not in quantity_dict 59 | ''' 60 | if name not in quantity_dict.keys(): 61 | raise ValueError('{0} is not present in quantity_dict'.format(name)) 62 | return '{0} ({1})'.format(quantity_dict[name]['name'], 63 | quantity_dict[name]['units']) 64 | 65 | 66 | def strings_to_list_string(strings): 67 | '''Takes a list of strings presumably containing words and phrases, 68 | and returns a "list" form of those strings, like: 69 | 70 | >>> strings_to_list_string(('cats', 'dogs')) 71 | >>> 'cats and dogs' 72 | 73 | or 74 | 75 | >>> strings_to_list_string(('pizza', 'pop', 'chips')) 76 | >>> 'pizza, pop, and chips' 77 | 78 | Raises ValueError if strings is empty. 79 | ''' 80 | if isinstance(strings, six.string_types): 81 | raise TypeError('strings must be an iterable of strings, not a string ' 82 | 'itself') 83 | if len(strings) == 0: 84 | raise ValueError('strings may not be empty') 85 | elif len(strings) == 1: 86 | return strings[0] 87 | elif len(strings) == 2: 88 | return ' and '.join(strings) 89 | else: 90 | return '{0}, and {1}'.format(', '.join(strings[:-1]), 91 | strings[-1]) 92 | 93 | 94 | def assumption_list_string(assumptions, assumption_dict): 95 | ''' 96 | Takes in a list of short forms of assumptions and an assumption 97 | dictionary, and returns a "list" form of the long form of the 98 | assumptions. 99 | 100 | Raises 101 | ------ 102 | ValueError 103 | if one of the assumptions is not in assumption_dict. 104 | ''' 105 | if isinstance(assumptions, six.string_types): 106 | raise TypeError('assumptions must be an iterable of strings, not a ' 107 | 'string itself') 108 | for a in assumptions: 109 | if a not in assumption_dict.keys(): 110 | raise ValueError('{} not present in assumption_dict'.format(a)) 111 | assumption_strings = [assumption_dict[a] for a in assumptions] 112 | return strings_to_list_string(assumption_strings) 113 | 114 | 115 | def quantity_spec_string(name, quantity_dict): 116 | ''' 117 | Returns a quantity specification for docstrings. 118 | 119 | Example 120 | ------- 121 | 122 | >>> quantity_spec_string('Tv') 123 | >>> 'Tv : float or ndarray\n Data for virtual temperature.' 124 | ''' 125 | if name not in quantity_dict.keys(): 126 | raise ValueError('{0} not present in quantity_dict'.format(name)) 127 | s = '{0} : float or ndarray\n'.format(name) 128 | s += doc_paragraph('Data for {0}.'.format( 129 | quantity_string(name, quantity_dict)), indent=4) 130 | return s 131 | 132 | 133 | def doc_paragraph(s, indent=0): 134 | '''Takes in a string without wrapping corresponding to a paragraph, 135 | and returns a version of that string wrapped to be at most 80 136 | characters in length on each line. 137 | If indent is given, ensures each line is indented to that number 138 | of spaces. 139 | ''' 140 | return '\n'.join([' '*indent + l for l in wrap(s, width=80-indent)]) 141 | 142 | 143 | def parse_derivative_string(string, quantity_dict): 144 | ''' 145 | Assuming the string is of the form d(var1)d(var2), returns var1, var2. 146 | Raises ValueError if the string is not of this form, or if the vars 147 | are not keys in the quantity_dict, or if var2 is not a coordinate-like 148 | variable. 149 | ''' 150 | match = derivative_prog.match(string) 151 | if match is None: 152 | raise ValueError('string is not in the form of a derivative') 153 | varname = match.group(1) 154 | coordname = match.group(2) 155 | if (varname not in quantity_dict.keys() or 156 | coordname not in quantity_dict.keys()): 157 | raise ValueError('variable in string not a valid quantity') 158 | return varname, coordname 159 | 160 | 161 | def closest_val(x, L): 162 | ''' 163 | Finds the index value in an iterable closest to a desired value. 164 | 165 | Parameters 166 | ---------- 167 | x : object 168 | The desired value. 169 | L : iterable 170 | The iterable in which to search for the desired value. 171 | 172 | Returns 173 | ------- 174 | index : int 175 | The index of the closest value to x in L. 176 | 177 | Notes 178 | ----- 179 | Assumes x and the entries of L are of comparable types. 180 | 181 | Raises 182 | ------ 183 | ValueError: 184 | if L is empty 185 | ''' 186 | # Make sure the iterable is nonempty 187 | if len(L) == 0: 188 | raise ValueError('L must not be empty') 189 | if isinstance(L, np.ndarray): 190 | # use faster numpy methods if using a numpy array 191 | return (np.abs(L-x)).argmin() 192 | # for a general iterable (like a list) we need general Python 193 | # start by assuming the first item is closest 194 | min_index = 0 195 | min_diff = abs(L[0] - x) 196 | i = 1 197 | while i < len(L): 198 | # check if each other item is closer than our current closest 199 | diff = abs(L[i] - x) 200 | if diff < min_diff: 201 | # if it is, set it as the new closest 202 | min_index = i 203 | min_diff = diff 204 | i += 1 205 | return min_index 206 | 207 | 208 | def area_poly_sphere(lat, lon, r_sphere): 209 | ''' 210 | Calculates the area enclosed by an arbitrary polygon on the sphere. 211 | 212 | Parameters 213 | ---------- 214 | 215 | lat : iterable 216 | The latitudes, in degrees, of the vertex locations of the polygon, in 217 | clockwise order. 218 | lon : iterable 219 | The longitudes, in degrees, of the vertex locations of the polygon, in 220 | clockwise order. 221 | 222 | Returns 223 | ------- 224 | 225 | area : float 226 | The desired spherical area in the same units as r_sphere. 227 | 228 | Notes 229 | ----- 230 | 231 | This function assumes the vertices form a valid polygon (edges do not 232 | intersect each other). 233 | 234 | **References** 235 | 236 | Computing the Area of a Spherical Polygon of Arbitrary Shape 237 | Bevis and Cambareri (1987) 238 | Mathematical Geology, vol.19, Issue 4, pp 335-346 239 | ''' 240 | dtr = np.pi/180. 241 | 242 | def _tranlon(plat, plon, qlat, qlon): 243 | t = np.sin((qlon-plon)*dtr)*np.cos(qlat*dtr) 244 | b = (np.sin(qlat*dtr)*np.cos(plat*dtr) - 245 | np.cos(qlat*dtr)*np.sin(plat*dtr)*np.cos((qlon-plon)*dtr)) 246 | return np.arctan2(t, b) 247 | 248 | if len(lat) < 3: 249 | raise ValueError('lat must have at least 3 vertices') 250 | if len(lat) != len(lon): 251 | raise ValueError('lat and lon must have the same length') 252 | total = 0. 253 | for i in range(-1, len(lat)-1): 254 | fang = _tranlon(lat[i], lon[i], lat[i+1], lon[i+1]) 255 | bang = _tranlon(lat[i], lon[i], lat[i-1], lon[i-1]) 256 | fvb = bang - fang 257 | if fvb < 0: 258 | fvb += 2.*np.pi 259 | total += fvb 260 | return (total - np.pi*float(len(lat)-2))*r_sphere**2 261 | 262 | 263 | def ddx(data, axis=0, dx=None, x=None, axis_x=0, boundary='forward-backward'): 264 | ''' 265 | Calculates a second-order centered finite difference derivative of data 266 | along the specified axis. 267 | 268 | Parameters 269 | ---------- 270 | 271 | data : ndarray 272 | Data on which we are taking a derivative. 273 | axis : int 274 | Index of the data array on which to take the derivative. 275 | dx : float, optional 276 | Constant grid spacing value. Will assume constant grid spacing if 277 | given. May not be used with argument x. Default value is 1 unless 278 | x is given. 279 | x : ndarray, optional 280 | Values of the axis along which we are taking a derivative to allow 281 | variable grid spacing. May not be given with argument dx. 282 | axis_x : int, optional 283 | Index of the x array on which to take the derivative. Does nothing if 284 | x is not given as an argument. 285 | boundary : string, optional 286 | Boundary condition. If 'periodic', assume periodic boundary condition 287 | for centered difference. If 'forward-backward', take first-order 288 | forward or backward derivatives at boundary. 289 | 290 | Returns 291 | ------- 292 | 293 | derivative : ndarray 294 | Derivative of the data along the specified axis. 295 | 296 | Raises 297 | ------ 298 | 299 | ValueError: 300 | If an invalid boundary condition choice is given, if both dx and x are 301 | specified, if axis is out of the valid range for the shape of the data, 302 | or if x is specified and axis_x is out of the valid range for the shape 303 | of x. 304 | ''' 305 | if abs(axis) >= len(data.shape): 306 | raise ValueError('axis is out of bounds for the shape of data') 307 | if x is not None and abs(axis_x) > len(x.shape): 308 | raise ValueError('axis_x is out of bounds for the shape of x') 309 | if dx is not None and x is not None: 310 | raise ValueError('may not give both x and dx as keyword arguments') 311 | if boundary == 'periodic': 312 | deriv = (np.roll(data, -1, axis) - np.roll(data, 1, axis) 313 | )/(np.roll(x, -1, axis_x) - np.roll(x, 1, axis_x)) 314 | elif boundary == 'forward-backward': 315 | # We will take forward-backward differencing at edges 316 | # need some fancy indexing to handle arbitrary derivative axis 317 | # Initialize our index lists 318 | front = [slice(s) for s in data.shape] 319 | back = [slice(s) for s in data.shape] 320 | target = [slice(s) for s in data.shape] 321 | # Set our index values for the derivative axis 322 | # front is the +1 index for derivative 323 | front[axis] = np.array([1, -1]) 324 | # back is the -1 index for derivative 325 | back[axis] = np.array([0, -2]) 326 | # target is the position where the derivative is being calculated 327 | target[axis] = np.array([0, -1]) 328 | if dx is not None: # grid spacing is constant 329 | deriv = (np.roll(data, -1, axis) - np.roll(data, 1, axis))/(2.*dx) 330 | deriv[target] = (data[front]-data[back])/dx 331 | else: # grid spacing is arbitrary 332 | # Need to calculate differences for our grid positions, too! 333 | # first take care of the centered differences 334 | dx = (np.roll(x, -1, axis_x) - np.roll(x, 1, axis_x)) 335 | # now handle those annoying edge points, like with the data above 336 | front_x = [slice(s) for s in x.shape] 337 | back_x = [slice(s) for s in x.shape] 338 | target_x = [slice(s) for s in x.shape] 339 | front_x[axis_x] = np.array([1, -1]) 340 | back_x[axis_x] = np.array([0, -2]) 341 | target_x[axis] = np.array([0, -1]) 342 | dx[target_x] = (x[front_x] - x[back_x]) 343 | # Here dx spans two grid indices, no need for *2 344 | deriv = (np.roll(data, -1, axis) - np.roll(data, 1, axis))/dx 345 | deriv[target] = (data[front] - data[back])/dx 346 | else: # invalid boundary condition was given 347 | raise ValueError('Invalid option {} for boundary ' 348 | 'condition.'.format(boundary)) 349 | return deriv 350 | 351 | 352 | def d_x(data, axis, boundary='forward-backward'): 353 | ''' 354 | Calculates a second-order centered finite difference of data along the 355 | specified axis. 356 | 357 | Parameters 358 | ---------- 359 | 360 | data : ndarray 361 | Data on which we are taking a derivative. 362 | axis : int 363 | Index of the data array on which to take the difference. 364 | boundary : string, optional 365 | Boundary condition. If 'periodic', assume periodic boundary condition 366 | for centered difference. If 'forward-backward', take first-order 367 | forward or backward derivatives at boundary. 368 | 369 | Returns 370 | ------- 371 | 372 | derivative : ndarray 373 | Derivative of the data along the specified axis. 374 | 375 | Raises 376 | ------ 377 | 378 | ValueError: 379 | If an invalid boundary condition choice is given, if both dx and x are 380 | specified, if axis is out of the valid range for the shape of the data, 381 | or if x is specified and axis_x is out of the valid range for the shape 382 | of x. 383 | ''' 384 | if abs(axis) > len(data.shape): 385 | raise ValueError('axis is out of bounds for the shape of data') 386 | if boundary == 'periodic': 387 | diff = np.roll(data, -1, axis) - np.roll(data, 1, axis) 388 | elif boundary == 'forward-backward': 389 | # We will take forward-backward differencing at edges 390 | # need some fancy indexing to handle arbitrary derivative axis 391 | # Initialize our index lists 392 | front = [slice(s) for s in data.shape] 393 | back = [slice(s) for s in data.shape] 394 | target = [slice(s) for s in data.shape] 395 | # Set our index values for the derivative axis 396 | # front is the +1 index for derivative 397 | front[axis] = np.array([1, -1]) 398 | # back is the -1 index for derivative 399 | back[axis] = np.array([0, -2]) 400 | # target is the position where the derivative is being calculated 401 | target[axis] = np.array([0, -1]) 402 | diff = (np.roll(data, -1, axis) - np.roll(data, 1, axis))/(2.) 403 | diff[target] = (data[front]-data[back]) 404 | else: # invalid boundary condition was given 405 | raise ValueError('Invalid option {} for boundary ' 406 | 'condition.'.format(boundary)) 407 | return diff 408 | -------------------------------------------------------------------------------- /conda-recipe/bld.bat: -------------------------------------------------------------------------------- 1 | xcopy /e "%RECIPE_DIR%\.." "%SRC_DIR%" 2 | "%PYTHON%" setup.py install 3 | if errorlevel 1 exit 1 -------------------------------------------------------------------------------- /conda-recipe/build.sh: -------------------------------------------------------------------------------- 1 | cp -r $RECIPE_DIR/.. $SRC_DIR 2 | $PYTHON setup.py install -------------------------------------------------------------------------------- /conda-recipe/meta.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: atmos 3 | version: "0.2.4" # version of package. Should use the PEP-386 verlib 4 | # conventions. Note that YAML will interpret 5 | # versions like 1.0 as floats, meaning that 1.0 will 6 | # be the same as 1. To avoid this, always put the 7 | # version in quotes, so that it will be interpreted 8 | # as a string. 9 | 10 | # The version cannot contain a dash '-' character. 11 | 12 | # the build and runtime requirements. Dependencies of these requirements 13 | # are included automatically. 14 | requirements: 15 | # Packages required to build the package. python and numpy must be 16 | # listed explicitly if they are required. 17 | build: 18 | - python 19 | - six >=1.9.0 20 | - numexpr >=2.0.1 21 | - numpy >=1.6 22 | - nose >=1.3 23 | - setuptools >=15.0 24 | - cfunits >=1.0 25 | - scipy >=0.16.0 26 | # Packages required to run the package. These are the dependencies that 27 | # will be installed automatically whenever the package is installed. 28 | # Package names should be any valid conda spec (see "Specifying versions 29 | # in requirements" below). 30 | run: 31 | - python 32 | - six >=1.9.0 33 | - numexpr >=2.3.0 34 | - numpy >=1.6 35 | - nose >=1.3 36 | - setuptools >=15.0 37 | - pint >=0.6 38 | - cfunits >=1.0 39 | - scipy >=0.16.0 40 | 41 | test: 42 | # in addition to the run-time requirements, you can specify requirements 43 | # needed during testing. The run time requirements specified above are 44 | # included automatically. 45 | #requires: 46 | # - nose >=1.3 47 | # Python imports we want to make sure work, which we expect the package 48 | # to install 49 | imports: 50 | - atmos 51 | # The script run_test.sh (or .bat/.py/.pl) will be run automatically 52 | # if it is part of the recipe. 53 | # (.py/.pl scripts are only valid as part of Python/Perl packages, respectively) 54 | 55 | about: 56 | home: https://github.com/mcgibbon/atmos.git 57 | license: MIT 58 | summary: an atmospheric sciences utility library 59 | -------------------------------------------------------------------------------- /conda-recipe/run_test.bat: -------------------------------------------------------------------------------- 1 | nosetests atmos 2 | -------------------------------------------------------------------------------- /conda-recipe/run_test.sh: -------------------------------------------------------------------------------- 1 | nosetests atmos 2 | -------------------------------------------------------------------------------- /docs/source/atmos.rst: -------------------------------------------------------------------------------- 1 | atmos package reference 2 | ======================= 3 | 4 | Module contents 5 | --------------- 6 | 7 | .. automodule:: atmos 8 | :members: 9 | :inherited-members: 10 | :undoc-members: 11 | :show-inheritance: 12 | 13 | Submodules 14 | ---------- 15 | 16 | atmos.constants module 17 | ---------------------- 18 | 19 | .. automodule:: atmos.constants 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | 24 | atmos.decorators module 25 | ----------------------- 26 | 27 | .. automodule:: atmos.decorators 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | 32 | atmos.equations module 33 | ---------------------- 34 | 35 | .. automodule:: atmos.equations 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | 40 | atmos.solve module 41 | ------------------ 42 | 43 | .. automodule:: atmos.solve 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | 48 | atmos.util module 49 | ----------------- 50 | 51 | .. automodule:: atmos.util 52 | :members: 53 | :undoc-members: 54 | :show-inheritance: 55 | 56 | .. role:: raw-latex(raw) 57 | :format: latex html 58 | -------------------------------------------------------------------------------- /docs/source/calculate.rst: -------------------------------------------------------------------------------- 1 | Using calculate() 2 | ================= 3 | 4 | What does calculate do? 5 | ----------------------- 6 | 7 | :func:`atmos.calculate` takes input variables (like 8 | pressure, virtual temperature, water vapor mixing ratio, etc.) and information 9 | about what assumptions you're willing to make (hydrostatic? low water vapor? 10 | ignore virtual temperature correction? use an empirical formula for 11 | equivalent potential temperature?), and from that calculates any desired 12 | output variables that you request and can be calculated. 13 | 14 | This function is essentially a wrapper for :class:`atmos.FluidSolver`, so 15 | much or all of its functionality will be the same, and the documentation for 16 | the two is very similar. 17 | 18 | What can it calculate? 19 | ---------------------- 20 | 21 | Anything that can be calculated by equations in :mod:`atmos.equations`. 22 | If you find that calculate() can't do a calculation you might expect it 23 | to, check the equations it has available and make sure you're using the right 24 | variables, or enabling the right assumptions. A common problem is using *T* 25 | instead of *Tv* and expecting the ideal gas law to work. 26 | 27 | A simple example 28 | ---------------- 29 | 30 | By default, a certain set of assumptions are used, such as that we are 31 | considering an ideal gas, and so can use ideal gas law. This allows us to do 32 | simple calculations that use the default assumptions. For example, to 33 | calculate pressure from virtual temperature and density:: 34 | 35 | >>> import atmos 36 | >>> atmos.calculate('p', Tv=273., rho=1.27) 37 | 99519.638400000011 38 | 39 | Or to calculate relative humidity from water vapor mixing ratio and 40 | saturation water vapor mixing ratio (which needs no assumptions):: 41 | 42 | >>> import atmos 43 | >>> atmos.calculate('RH', rv=0.001, rvs=0.002) 44 | 50.0 45 | 46 | For a full list of default assumptions, see :func:`atmos.calculate`. 47 | 48 | Specifying Units 49 | ---------------- 50 | 51 | By default, SI units are assumed. These can be overridden with keyword 52 | arguments of the form {quantity name}_unit or {quantity name}_units. 53 | Specifying units makes it so that both inputs and outputs of the quantity 54 | will be in the specified units. 55 | 56 | To get pressure in hPa:: 57 | 58 | >>> import atmos 59 | >>> atmos.calculate('p', p_units='hPa', Tv=273., rho=1.27) 60 | 995.19638400000008 61 | 62 | To specify mixing ratio in g/kg:: 63 | 64 | >>> import atmos 65 | >>> atmos.calculate('RH', rv=1, rvs=0.002, rv_unit='g/kg') 66 | 50.0 67 | 68 | Note that either "_unit" or "_units" can be used, and that units must be 69 | specified for each quantity independently. 70 | 71 | Unit handling is performed by the cfunits_ package, and so any units available 72 | in that package (notably any units recognized by UDUNITS_) should be recognized. 73 | 74 | Viewing equation functions used 75 | ------------------------------- 76 | 77 | Calculating pressure from virtual temperature and density, also returning a 78 | list of functions used:: 79 | 80 | >>> import atmos 81 | >>> p, funcs = atmos.calculate('p', Tv=273., rho=1.27, debug=True) 82 | >>> funcs 83 | (,) 84 | 85 | Adding and removing assumptions 86 | ------------------------------- 87 | 88 | If you want to use assumptions that are not enabled by default (such as 89 | ignoring the virtual temperature correction), you can use the add_assumptions 90 | keyword argument, which takes a tuple of strings specifying assumptions. 91 | The exact string to enter for each assumption is detailed in 92 | :func:`atmos.calculate`. For example, to calculate T instead of Tv, neglecting 93 | the virtual temperature correction:: 94 | 95 | >>> import atmos 96 | >>> atmos.calculate('p', T=273., rho=1.27, add_assumptions=('Tv equals T',)) 97 | 99519.638400000011 98 | 99 | Overriding assumptions 100 | ---------------------- 101 | 102 | If you want to ignore the default assumptions entirely, you could specify 103 | your own assumptions:: 104 | 105 | >>> import atmos 106 | >>> assumptions = ('ideal gas', 'bolton') 107 | >>> atmos.calculate('p', Tv=273., rho=1.27, assumptions=assumptions) 108 | 99519.638400000011 109 | 110 | Specifying quantities with a dictionary 111 | --------------------------------------- 112 | 113 | If you are repeatedly calculating different quantities, you may want to use 114 | a dictionary to more easily pass in quantities as keyword arguments. Adding 115 | \*\* to the beginning of a dictionary variable as an argument passes in 116 | each of the (key, value) pairs in that dictionary as a separate keyword 117 | argument. For example:: 118 | 119 | >>> import atmos 120 | >>> data = {'Tv': 273., 'rho': 1.27} 121 | >>> data['p'] = atmos.calculate('p', **data) 122 | >>> data['p'] 123 | 99519.638400000011 124 | 125 | Function reference 126 | ------------------ 127 | 128 | .. autofunction:: atmos.calculate 129 | 130 | .. _cfunits: https://pypi.python.org/pypi/cfunits/1.0.1 131 | .. _UDUNITS: http://www.unidata.ucar.edu/software/udunits/ 132 | 133 | 134 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # atmos documentation build configuration file, created by 4 | # sphinx-quickstart on Tue May 26 14:24:20 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinxcontrib.napoleon', 34 | 'sphinx.ext.mathjax', 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # The suffix of source filenames. 41 | source_suffix = '.rst' 42 | 43 | # The encoding of source files. 44 | #source_encoding = 'utf-8-sig' 45 | 46 | # The master toctree document. 47 | master_doc = 'index' 48 | 49 | # General information about the project. 50 | project = u'atmos' 51 | copyright = u'2015, Jeremy McGibbon' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = '0.2.2' 59 | # The full version, including alpha/beta/rc tags. 60 | release = '0.2.2' 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | #language = None 65 | 66 | # There are two options for replacing |today|: either, you set today to some 67 | # non-false value, then it is used: 68 | #today = '' 69 | # Else, today_fmt is used as the format for a strftime call. 70 | #today_fmt = '%B %d, %Y' 71 | 72 | # List of patterns, relative to source directory, that match files and 73 | # directories to ignore when looking for source files. 74 | exclude_patterns = ['_build'] 75 | 76 | # The reST default role (used for this markup: `text`) to use for all 77 | # documents. 78 | #default_role = None 79 | 80 | # If true, '()' will be appended to :func: etc. cross-reference text. 81 | #add_function_parentheses = True 82 | 83 | # If true, the current module name will be prepended to all description 84 | # unit titles (such as .. function::). 85 | #add_module_names = True 86 | 87 | # If true, sectionauthor and moduleauthor directives will be shown in the 88 | # output. They are ignored by default. 89 | #show_authors = False 90 | 91 | # The name of the Pygments (syntax highlighting) style to use. 92 | pygments_style = 'sphinx' 93 | 94 | # A list of ignored prefixes for module index sorting. 95 | #modindex_common_prefix = [] 96 | 97 | # If true, keep warnings as "system message" paragraphs in the built documents. 98 | #keep_warnings = False 99 | 100 | 101 | # -- Options for HTML output ---------------------------------------------- 102 | 103 | # The theme to use for HTML and HTML Help pages. See the documentation for 104 | # a list of builtin themes. 105 | html_theme = 'default' 106 | 107 | # Theme options are theme-specific and customize the look and feel of a theme 108 | # further. For a list of options available for each theme, see the 109 | # documentation. 110 | #html_theme_options = {} 111 | 112 | # Add any paths that contain custom themes here, relative to this directory. 113 | #html_theme_path = [] 114 | 115 | # The name for this set of Sphinx documents. If None, it defaults to 116 | # " v documentation". 117 | #html_title = None 118 | 119 | # A shorter title for the navigation bar. Default is the same as html_title. 120 | #html_short_title = None 121 | 122 | # The name of an image file (relative to this directory) to place at the top 123 | # of the sidebar. 124 | #html_logo = None 125 | 126 | # The name of an image file (within the static path) to use as favicon of the 127 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 128 | # pixels large. 129 | #html_favicon = None 130 | 131 | # Add any paths that contain custom static files (such as style sheets) here, 132 | # relative to this directory. They are copied after the builtin static files, 133 | # so a file named "default.css" will overwrite the builtin "default.css". 134 | html_static_path = ['_static'] 135 | 136 | # Add any extra paths that contain custom files (such as robots.txt or 137 | # .htaccess) here, relative to this directory. These files are copied 138 | # directly to the root of the documentation. 139 | #html_extra_path = [] 140 | 141 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 142 | # using the given strftime format. 143 | #html_last_updated_fmt = '%b %d, %Y' 144 | 145 | # If true, SmartyPants will be used to convert quotes and dashes to 146 | # typographically correct entities. 147 | #html_use_smartypants = True 148 | 149 | # Custom sidebar templates, maps document names to template names. 150 | #html_sidebars = {} 151 | 152 | # Additional templates that should be rendered to pages, maps page names to 153 | # template names. 154 | #html_additional_pages = {} 155 | 156 | # If false, no module index is generated. 157 | #html_domain_indices = True 158 | 159 | # If false, no index is generated. 160 | #html_use_index = True 161 | 162 | # If true, the index is split into individual pages for each letter. 163 | #html_split_index = False 164 | 165 | # If true, links to the reST sources are added to the pages. 166 | #html_show_sourcelink = True 167 | 168 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 169 | #html_show_sphinx = True 170 | 171 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 172 | #html_show_copyright = True 173 | 174 | # If true, an OpenSearch description file will be output, and all pages will 175 | # contain a tag referring to it. The value of this option must be the 176 | # base URL from which the finished HTML is served. 177 | #html_use_opensearch = '' 178 | 179 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 180 | #html_file_suffix = None 181 | 182 | # Output file base name for HTML help builder. 183 | htmlhelp_basename = 'atmosdoc' 184 | 185 | 186 | # -- Options for LaTeX output --------------------------------------------- 187 | 188 | latex_elements = { 189 | # The paper size ('letterpaper' or 'a4paper'). 190 | #'papersize': 'letterpaper', 191 | 192 | # The font size ('10pt', '11pt' or '12pt'). 193 | #'pointsize': '10pt', 194 | 195 | # Additional stuff for the LaTeX preamble. 196 | #'preamble': '', 197 | } 198 | 199 | # Grouping the document tree into LaTeX files. List of tuples 200 | # (source start file, target name, title, 201 | # author, documentclass [howto, manual, or own class]). 202 | latex_documents = [ 203 | ('index', 'atmos.tex', u'atmos Documentation', 204 | u'Jeremy McGibbon', 'manual'), 205 | ] 206 | 207 | # The name of an image file (relative to this directory) to place at the top of 208 | # the title page. 209 | #latex_logo = None 210 | 211 | # For "manual" documents, if this is true, then toplevel headings are parts, 212 | # not chapters. 213 | #latex_use_parts = False 214 | 215 | # If true, show page references after internal links. 216 | #latex_show_pagerefs = False 217 | 218 | # If true, show URL addresses after external links. 219 | #latex_show_urls = False 220 | 221 | # Documents to append as an appendix to all manuals. 222 | #latex_appendices = [] 223 | 224 | # If false, no module index is generated. 225 | #latex_domain_indices = True 226 | 227 | 228 | # -- Options for manual page output --------------------------------------- 229 | 230 | # One entry per manual page. List of tuples 231 | # (source start file, name, description, authors, manual section). 232 | man_pages = [ 233 | ('index', 'atmos', u'atmos Documentation', 234 | [u'Jeremy McGibbon'], 1) 235 | ] 236 | 237 | # If true, show URL addresses after external links. 238 | #man_show_urls = False 239 | 240 | 241 | # -- Options for Texinfo output ------------------------------------------- 242 | 243 | # Grouping the document tree into Texinfo files. List of tuples 244 | # (source start file, target name, title, author, 245 | # dir menu entry, description, category) 246 | texinfo_documents = [ 247 | ('index', 'atmos', u'atmos Documentation', 248 | u'Jeremy McGibbon', 'atmos', 'One line description of project.', 249 | 'Miscellaneous'), 250 | ] 251 | 252 | # Documents to append as an appendix to all manuals. 253 | #texinfo_appendices = [] 254 | 255 | # If false, no module index is generated. 256 | #texinfo_domain_indices = True 257 | 258 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 259 | #texinfo_show_urls = 'footnote' 260 | 261 | # If true, do not generate a @detailmenu in the "Top" node's menu. 262 | #texinfo_no_detailmenu = False 263 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. atmos documentation master file, created by 2 | sphinx-quickstart on Tue May 26 14:24:20 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to atmos's documentation! 7 | ================================= 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | intro 15 | calculate 16 | solver 17 | subclassing 18 | atmos 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | 28 | -------------------------------------------------------------------------------- /docs/source/intro.rst: -------------------------------------------------------------------------------- 1 | Introduction to atmos 2 | ===================== 3 | 4 | What this package does 5 | ---------------------- 6 | 7 | **atmos** is meant to be a library of utility code for use in atmospheric 8 | sciences. Its main functionality is currently to take input variables (like 9 | pressure, virtual temperature, water vapor mixing ratio, etc.) and information 10 | about what assumptions you're willing to make (hydrostatic? low water vapor? 11 | ignore virtual temperature correction? use an empirical formula for 12 | equivalent potential temperature?), and from that calculate any desired 13 | output variables that you request and can be calculated. 14 | 15 | Variable names 16 | -------------- 17 | 18 | To make coding simpler by avoiding long names for quantities, a set of fairly 19 | reasonable short-forms for different quantities are used by this package. 20 | For example, air density is represented by "rho", and air temperature by "T". 21 | For a complete list of quantities and their abbreviations, see the 22 | documentation for :func:`atmos.calculate` or :class:`atmos.FluidSolver`. 23 | 24 | Units 25 | ----- 26 | 27 | By default, all quantities are input and output in SI units. Notably, pressure 28 | quantities are input and output in Pascals, and temperature quantities are in 29 | degrees Kelvin. A full list of units for different variables is available 30 | in the documentation for :func:`atmos.calculate` or 31 | :class:`atmos.FluidSolver`. These units can be overridden with keyword 32 | arguments of the form {quantity name}_unit or {quantity name}_units. 33 | Specifying units makes it so that both inputs and outputs of the quantity 34 | will be in the specified units. 35 | 36 | To get pressure in hPa:: 37 | 38 | >>> import atmos 39 | >>> atmos.calculate('p', p_units='hPa', Tv=273., rho=1.27) 40 | 99519.638400000011 41 | 42 | To specify mixing ratio in g/kg:: 43 | 44 | >>> import atmos 45 | >>> atmos.calculate('RH', rv=1, rvs=0.002, rv_unit='g/kg') 46 | 50.0 47 | 48 | Note that either "_unit" or "_units" can be used, and that units must be 49 | specified for each quantity independently. 50 | 51 | Unit handling is performed by the cfunits_ package, and so any units available 52 | in that package (notably any units recognized by UDUNITS_) should be recognized. 53 | 54 | Assumptions 55 | ----------- 56 | 57 | By default, a set of (what are hopefully) fairly reasonable assumptions are 58 | used by :class:`atmos.FluidSolver` and :func:`atmos.calculate`. These can be 59 | added to or removed from 60 | by tuples of string options supplied as keyword arguments *add_assumptions* 61 | and *remove_assumptions*, respectively, or completely overridden by supplying 62 | a tuple for the keyword argument *assumptions*. For information on what 63 | default assumptions are used and all assumptions available, see the 64 | documentation for :func:`atmos.calculate` or :class:`atmos.FluidSolver`. 65 | 66 | Requests and Feedback 67 | --------------------- 68 | 69 | This module is in ongoing development, and feedback is appreciated. In 70 | particular, if there is functionality you would like to see or equations 71 | that should be added (or corrected), please e-mail mcgibbon (at) uw {dot} edu. 72 | 73 | .. _cfunits: https://pypi.python.org/pypi/cfunits/1.0.1 74 | .. _UDUNITS: http://www.unidata.ucar.edu/software/udunits/ 75 | 76 | -------------------------------------------------------------------------------- /docs/source/solver.rst: -------------------------------------------------------------------------------- 1 | Using FluidSolver 2 | ================= 3 | 4 | What does FluidSolver do? 5 | ------------------------- 6 | 7 | :class:`atmos.FluidSolver` takes input variables (like 8 | pressure, virtual temperature, water vapor mixing ratio, etc.) and information 9 | about what assumptions you're willing to make (hydrostatic? low water vapor? 10 | ignore virtual temperature correction? use an empirical formula for 11 | equivalent potential temperature?), and from that calculates any desired 12 | output variables that you request and can be calculated. 13 | 14 | The main benefit of using :class:`atmos.FluidSolver` instead of 15 | :func:`atmos.calculate` is that the FluidSolver object has memory. It can keep 16 | track of what assumptions you enabled, as well as what quantities you've given 17 | it and it has calculated. 18 | 19 | What can it calculate? 20 | ---------------------- 21 | 22 | Anything that can be calculated by equations in :mod:`atmos.equations`. 23 | If you find that the FluidSolver can't do a calculation you might expect it 24 | to, check the equations it has available and make sure you're using the right 25 | variables, or enabling the right assumptions. A common problem is using *T* 26 | instead of *Tv* and expecting the ideal gas law to work. 27 | 28 | A simple example 29 | ---------------- 30 | 31 | By default, a certain set of assumptions are used, such as that we are 32 | considering an ideal gas, and so can use ideal gas law. This allows us to do 33 | simple calculations that use the default assumptions. For example, to 34 | calculate pressure from virtual temperature and density:: 35 | 36 | >>> import atmos 37 | >>> solver = atmos.FluidSolver(Tv=273., rho=1.27) 38 | >>> solver.calculate('p') 39 | 99519.638400000011 40 | 41 | Or to calculate relative humidity from water vapor mixing ratio and 42 | saturation water vapor mixing ratio (which needs no assumptions):: 43 | 44 | >>> import atmos 45 | >>> solver = atmos.FluidSolver(rv=0.001, rvs=0.002) 46 | >>> solver.calculate('RH') 47 | 50.0 48 | 49 | For a full list of default assumptions, see :class:`atmos.FluidSolver`. 50 | 51 | 52 | Specifying Units 53 | ---------------- 54 | 55 | By default, SI units are assumed. These can be overridden with keyword 56 | arguments of the form {quantity name}_unit or {quantity name}_units. 57 | Specifying units makes it so that both inputs and outputs of the quantity 58 | will be in the specified units. 59 | 60 | To get pressure in hPa:: 61 | 62 | >>> import atmos 63 | >>> solver = atmos.FluidSolver(Tv=273., rho=1.27, p_units='hPa') 64 | >>> atmos.calculate('p') 65 | 995.19638400000008 66 | 67 | To specify mixing ratio in g/kg:: 68 | 69 | >>> import atmos 70 | >>> solver = atmos.FluidSolver(rv=1, rvs=0.002, rv_unit='g/kg') 71 | >>> atmos.calculate('RH') 72 | 50.0 73 | 74 | Note that either "_unit" or "_units" can be used, and that units must be 75 | specified for each quantity independently. 76 | 77 | Unit handling is performed by the cfunits_ package, and so any units available 78 | in that package (notably any units recognized by UDUNITS_) should be recognized. 79 | 80 | Viewing equation functions used 81 | ------------------------------- 82 | 83 | Calculating pressure from virtual temperature and density, also returning a 84 | list of functions used:: 85 | 86 | >>> import atmos 87 | >>> solver = atmos.FluidSolver(Tv=273., rho=1.27, debug=True) 88 | >>> p, funcs = solver.calculate('p') 89 | >>> funcs 90 | (,) 91 | 92 | Adding and removing assumptions 93 | ------------------------------- 94 | 95 | If you want to use assumptions that are not enabled by default (such as 96 | ignoring the virtual temperature correction), you can use the add_assumptions 97 | keyword argument, which takes a tuple of strings specifying assumptions. 98 | The exact string to enter for each assumption is detailed in 99 | :class:`atmos.FluidSolver`. For example, to calculate T instead of Tv, 100 | neglecting the virtual temperature correction:: 101 | 102 | >>> import atmos 103 | >>> solver = atmos.FluidSolver(T=273., rho=1.27, add_assumptions=('Tv equals T',)) 104 | >>> solver.calculate('p') 105 | 99519.638400000011 106 | 107 | Overriding assumptions 108 | ---------------------- 109 | 110 | If you want to ignore the default assumptions entirely, you could specify 111 | your own assumptions:: 112 | 113 | >>> import atmos 114 | >>> solver = atmos.FluidSolver(Tv=273., rho=1.27, assumptions=('ideal gas', 'bolton')) 115 | >>> solver.calculate('p') 116 | 99519.638400000011 117 | 118 | Class reference 119 | --------------- 120 | 121 | .. autoclass:: atmos.FluidSolver 122 | 123 | .. _cfunits: https://pypi.python.org/pypi/cfunits/1.0.1 124 | .. _UDUNITS: http://www.unidata.ucar.edu/software/udunits/ 125 | 126 | -------------------------------------------------------------------------------- /docs/source/subclassing.rst: -------------------------------------------------------------------------------- 1 | Subclassing BaseSolver and FluidSolver 2 | ====================================== 3 | 4 | Subclassing FluidSolver 5 | ----------------------- 6 | 7 | You may want to subclass FluidSolver to have a class with the same equations 8 | as FluidSolver, but a different set of default assumptions. The default 9 | assumptions are a tuple *default_assumptions* that may be overridden:: 10 | 11 | class DryFluidSolver(FluidSolver): 12 | default_assumptions = ('no liquid water', 'no ice', 'low water vapor', 13 | 'ideal gas', 'bolton', 'constant g', 14 | 'constant Lv', 'hydrostatic', 'constant Cp') 15 | # also set a new solution dictionary so it will not be inherited 16 | # from FluidSolver 17 | _solutions = {} 18 | 19 | 20 | The new class can then be instantiated with, for instance:: 21 | 22 | >>> solver = DryFluidSolver(p=1e5, T=300.) 23 | 24 | Subclassing BaseSolver 25 | ---------------------- 26 | 27 | :mod:`atmos.equations` has a particular set of equations relevant to 28 | atmospheric science. It may be that you want to use a different set of 29 | equations, whether because you want more equations than :class:`FluidSolver` 30 | has available or because you want to use a completely different set of 31 | equations, say, for oceanic science. In order to do this, you can write 32 | a new module in the same style as :mod:`atmos.equations`, and create a 33 | Solver that uses your equation module. For details on writing an equations 34 | module, see the next section. 35 | 36 | Assuming your equations module is called **myequations**, this is how you 37 | would create a solver for it:: 38 | 39 | import myequations 40 | 41 | class MySolver(BaseSolver): 42 | # This should be set to your equation module 43 | _equation_module = myequations 44 | # This contains a list of assumptions from your module that you want 45 | # to enable by default 46 | default_assumptions = ('constant density', 'constant g') 47 | # a solution dictionary is necessary for caching solutions between 48 | # calls to calculate() 49 | _solutions = {} 50 | 51 | Writing an Equation Module 52 | -------------------------- 53 | 54 | In order for BaseSolver to propertly interpret your equation module, it needs 55 | to follow a certain format specification, which are described below. 56 | 57 | quantities dictionary 58 | +++++++++++++++++++++ 59 | 60 | There must be a variable *quantities* defined whose keys are the 61 | abbreviations of quantities you will use (such as 'rho', 'T', 'x', etc.). 62 | The values of the dictionary should be dictionaries with the keys 'name' and 63 | 'units', whose values are a descriptive name of the quantity (such as 'air 64 | density', 'air temperature', or 'zonal position'), and an udunits-compatible 65 | specification of the units of the quantity (such as 'kg/m^3', 'K', or 'm'). 66 | 67 | Some key requirements to note: 68 | 69 | * Any quantity output by an equation or taken as input for an equation in 70 | your module must be present in the quantities dictionary 71 | * Your equations must always take in quantities with the units specified in 72 | this dictionary, and return quantities with the units specified in this 73 | dictionary. 74 | 75 | Example:: 76 | 77 | quantities = { 78 | 'AH': { 79 | 'name': 'absolute humidity', 80 | 'units': 'kg/m^3', 81 | }, 82 | 'DSE': { 83 | 'name': 'dry static energy', 84 | 'units': 'J', 85 | }, 86 | 'e': { 87 | 'name': 'water vapor partial pressure', 88 | 'units': 'Pa', 89 | } 90 | } 91 | 92 | assumptions dictionary 93 | ++++++++++++++++++++++ 94 | 95 | There must be a variable *assumptions* defined whose keys are abbreviations 96 | for assumptions you will use (such as 'constant g', 'hydrostatic', etc.), 97 | and values are longer (but still short) descriptions for those assumptions 98 | (such as 'g is constant and equal to 9.81', 'the hydrostatic approximation 99 | is valid', etc.). The values are only used when generating documentation for 100 | your equations (and when other users are looking at your code to figure out 101 | what your assumptions mean). 102 | 103 | Example:: 104 | 105 | assumptions = { 106 | 'hydrostatic': 'hydrostatic balance', 107 | 'constant g': 'g is constant', 108 | 'constant Lv': 'latent heat of vaporization of water is constant', 109 | 110 | automatic documentation 111 | +++++++++++++++++++++++ 112 | 113 | Docstrings can be automatically generated for your equation functions using a 114 | decorator generated by :func:`atmos.decorators.equation_docstring`. The 115 | easiest way to use this decorator is to define the following function in 116 | your equation module:: 117 | 118 | def autodoc(**kwargs): 119 | return equation_docstring(quantities, assumptions, **kwargs) 120 | 121 | You do not *need* to use this generator (or have any documentation) for your 122 | equations, but since automatically generating docstrings ensures they are 123 | always kept up to date when you update your functions, it is recommended. 124 | 125 | writing equation functions 126 | ++++++++++++++++++++++++++ 127 | 128 | Any function in your module whose name follows the convention 129 | "{quantity}_from_*" is considered to be an equation function by BaseSolver. 130 | Here is an example equation function (assuming **autodoc** has been defined as 131 | above):: 132 | 133 | @autodoc(equation=r'qv = \frac{(\frac{Tv}{T} - 1)}{0.608}') 134 | @assumes('no liquid water', 'no ice') 135 | @overridden_by_assumptions('Tv equals T') 136 | def qv_from_Tv_T(Tv, T): 137 | return (Tv/T - 1.)/0.608 138 | 139 | At the least, an equation function must: 140 | 141 | * have a name conforming to "{quantity}_from_*" with a quantity that is present 142 | in your quantity dictionary 143 | * be decorated by :func:`atmos.decorators.assumes` 144 | * take in only variables that are abbreviations present in your quantity 145 | dictionary 146 | * return only one variable, which is the result of the equation 147 | 148 | If you choose to decorate with **autodoc**, you must have the decorator above 149 | any other equation decorators so that it can use the metadata they attach to 150 | the equation function. **autodoc** takes in an optional keyword argument 151 | *equation* which is a latex-formatted string of the equation used. 152 | 153 | The decorator :func:`atmos.decorators.assumes` takes in 154 | assumption abbreviation strings which correspond to assumptions made by the 155 | equation described in the function. This equation will only 156 | be enabled if all of its assumptions are enabled. 157 | 158 | The decorator :func:`atmos.decorators.overridden_by_assumptions` is 159 | optional, and if present takes in assumption abbreviation strings which 160 | correspond to assumptions that override the equation described in the function. 161 | If any of these assumptions are enabled, this equation will be disabled. 162 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six>=1.9.0 2 | numexpr>=2.3.0 3 | numpy>=1.6 4 | nose>=1.3 5 | setuptools>=15.0 6 | cfunits>=1.0 7 | scipy>=0.9.0 8 | matplotlib>=1.0 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | 4 | [nosetests] 5 | verbosity=1 6 | detailed-errors=1 7 | debug=nose.loader 8 | 9 | [build_sphinx] 10 | source-dir = docs/source 11 | build-dir = docs/build 12 | all_files = 1 13 | 14 | [upload_sphinx] 15 | upload-dir = docs/build/html 16 | 17 | [upload_docs] 18 | upload-dir = docs/build/html 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | try: # for pip >= 10 3 | from pip._internal.req import parse_requirements 4 | except ImportError: # for pip <= 9.0.3 5 | from pip.req import parse_requirements 6 | 7 | # parse_requirements() returns generator of pip.req.InstallRequirement objects 8 | install_reqs = parse_requirements('requirements.txt', session=False) 9 | 10 | # reqs is a list of requirement 11 | # e.g. ['django==1.5.1', 'mezzanine==1.4.6'] 12 | # Newer pip versions use the ParsedRequirement class which has the `requirement` attribute instead of `req` 13 | try: 14 | reqs = [str(ir.req) for ir in install_reqs] 15 | except AttributeError: 16 | reqs = [str(ir.requirement) for ir in install_reqs] 17 | 18 | setup( 19 | name='atmos', 20 | packages=['atmos'], 21 | version='0.2.5-develop', 22 | description='Atmospheric sciences utility library', 23 | author='Jeremy McGibbon', 24 | author_email='mcgibbon@uw.edu', 25 | install_requires=reqs, 26 | url='https://github.com/mcgibbon/atmos', 27 | keywords=['atmos', 'atmospheric', 'equations', 'geoscience', 'science'], 28 | classifiers=[], 29 | license='MIT', 30 | ) 31 | --------------------------------------------------------------------------------