diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index aea49d47d98..0020b09e24a 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -40,3 +40,11 @@ jobs: REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | doc8 docs | reviewdog -efm='%f:%l: %m' -name=doc8 -reporter=github-check -filter-mode=nofilter + + black: + name: Black + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: psf/black@stable diff --git a/.gitignore b/.gitignore index 0780fd24694..a65523c60dc 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ docs/tutorials docs/api/generated examples/scripts test_output/ + +.venv/ diff --git a/ci/gen_versions_json.py b/ci/gen_versions_json.py index fe1eb27d7d6..c826df54731 100755 --- a/ci/gen_versions_json.py +++ b/ci/gen_versions_json.py @@ -6,6 +6,6 @@ import glob -with open('versions.json', 'wt') as version_file: - version_strings = ','.join('"{}"'.format(d) for d in sorted(glob.glob('v*.[0-9]*'))) - version_file.write('{"versions":["latest","dev",' + version_strings + ']}\n') +with open("versions.json", "wt") as version_file: + version_strings = ",".join('"{}"'.format(d) for d in sorted(glob.glob("v*.[0-9]*"))) + version_file.write('{"versions":["latest","dev",' + version_strings + "]}\n") diff --git a/conftest.py b/conftest.py index 42d2d9e386f..92714c724f1 100644 --- a/conftest.py +++ b/conftest.py @@ -18,25 +18,27 @@ import metpy.calc # Need to disable fallback before importing pint -os.environ['PINT_ARRAY_PROTOCOL_FALLBACK'] = '0' +os.environ["PINT_ARRAY_PROTOCOL_FALLBACK"] = "0" import pint # noqa: I100, E402 def pytest_report_header(config, startdir): """Add dependency information to pytest output.""" - return (f'Dep Versions: Matplotlib {matplotlib.__version__}, ' - f'NumPy {numpy.__version__}, SciPy {scipy.__version__}, ' - f'Xarray {xarray.__version__}, Pint {pint.__version__}, ' - f'Pandas {pandas.__version__}, Traitlets {traitlets.__version__}, ' - f'Pooch {pooch.version.full_version}') + return ( + f"Dep Versions: Matplotlib {matplotlib.__version__}, " + f"NumPy {numpy.__version__}, SciPy {scipy.__version__}, " + f"Xarray {xarray.__version__}, Pint {pint.__version__}, " + f"Pandas {pandas.__version__}, Traitlets {traitlets.__version__}, " + f"Pooch {pooch.version.full_version}" + ) @pytest.fixture(autouse=True) def doctest_available_modules(doctest_namespace): """Make modules available automatically to doctests.""" - doctest_namespace['metpy'] = metpy - doctest_namespace['metpy.calc'] = metpy.calc - doctest_namespace['plt'] = matplotlib.pyplot + doctest_namespace["metpy"] = metpy + doctest_namespace["metpy.calc"] = metpy.calc + doctest_namespace["plt"] = matplotlib.pyplot @pytest.fixture() @@ -46,7 +48,7 @@ def ccrs(): Any testing function/fixture that needs access to ``cartopy.crs`` can simply add this to their parameter list. """ - return pytest.importorskip('cartopy.crs') + return pytest.importorskip("cartopy.crs") @pytest.fixture @@ -56,91 +58,85 @@ def cfeature(): Any testing function/fixture that needs access to ``cartopy.feature`` can simply add this to their parameter list. """ - return pytest.importorskip('cartopy.feature') + return pytest.importorskip("cartopy.feature") @pytest.fixture() def test_da_lonlat(): """Return a DataArray with a lon/lat grid and no time coordinate for use in tests.""" - pytest.importorskip('cartopy') + pytest.importorskip("cartopy") data = numpy.linspace(300, 250, 3 * 4 * 4).reshape((3, 4, 4)) ds = xarray.Dataset( - {'temperature': (['isobaric', 'lat', 'lon'], data)}, + {"temperature": (["isobaric", "lat", "lon"], data)}, coords={ - 'isobaric': xarray.DataArray( - numpy.array([850., 700., 500.]), - name='isobaric', - dims=['isobaric'], - attrs={'units': 'hPa'} + "isobaric": xarray.DataArray( + numpy.array([850.0, 700.0, 500.0]), + name="isobaric", + dims=["isobaric"], + attrs={"units": "hPa"}, ), - 'lat': xarray.DataArray( + "lat": xarray.DataArray( numpy.linspace(30, 40, 4), - name='lat', - dims=['lat'], - attrs={'units': 'degrees_north'} + name="lat", + dims=["lat"], + attrs={"units": "degrees_north"}, ), - 'lon': xarray.DataArray( + "lon": xarray.DataArray( numpy.linspace(260, 270, 4), - name='lon', - dims=['lon'], - attrs={'units': 'degrees_east'} - ) - } + name="lon", + dims=["lon"], + attrs={"units": "degrees_east"}, + ), + }, ) - ds['temperature'].attrs['units'] = 'kelvin' + ds["temperature"].attrs["units"] = "kelvin" - return ds.metpy.parse_cf('temperature') + return ds.metpy.parse_cf("temperature") @pytest.fixture() def test_da_xy(): """Return a DataArray with a x/y grid and a time coordinate for use in tests.""" - pytest.importorskip('cartopy') + pytest.importorskip("cartopy") data = numpy.linspace(300, 250, 3 * 3 * 4 * 4).reshape((3, 3, 4, 4)) ds = xarray.Dataset( - {'temperature': (['time', 'isobaric', 'y', 'x'], data), - 'lambert_conformal': ([], '')}, + {"temperature": (["time", "isobaric", "y", "x"], data), "lambert_conformal": ([], "")}, coords={ - 'time': xarray.DataArray( - numpy.array([numpy.datetime64('2018-07-01T00:00'), - numpy.datetime64('2018-07-01T06:00'), - numpy.datetime64('2018-07-01T12:00')]), - name='time', - dims=['time'] + "time": xarray.DataArray( + numpy.array( + [ + numpy.datetime64("2018-07-01T00:00"), + numpy.datetime64("2018-07-01T06:00"), + numpy.datetime64("2018-07-01T12:00"), + ] + ), + name="time", + dims=["time"], ), - 'isobaric': xarray.DataArray( - numpy.array([850., 700., 500.]), - name='isobaric', - dims=['isobaric'], - attrs={'units': 'hPa'} + "isobaric": xarray.DataArray( + numpy.array([850.0, 700.0, 500.0]), + name="isobaric", + dims=["isobaric"], + attrs={"units": "hPa"}, ), - 'y': xarray.DataArray( - numpy.linspace(-1000, 500, 4), - name='y', - dims=['y'], - attrs={'units': 'km'} + "y": xarray.DataArray( + numpy.linspace(-1000, 500, 4), name="y", dims=["y"], attrs={"units": "km"} ), - 'x': xarray.DataArray( - numpy.linspace(0, 1500, 4), - name='x', - dims=['x'], - attrs={'units': 'km'} - ) - } + "x": xarray.DataArray( + numpy.linspace(0, 1500, 4), name="x", dims=["x"], attrs={"units": "km"} + ), + }, ) - ds['temperature'].attrs = { - 'units': 'kelvin', - 'grid_mapping': 'lambert_conformal' - } - ds['lambert_conformal'].attrs = { - 'grid_mapping_name': 'lambert_conformal_conic', - 'standard_parallel': 50.0, - 'longitude_of_central_meridian': -107.0, - 'latitude_of_projection_origin': 50.0, - 'earth_shape': 'spherical', - 'earth_radius': 6367470.21484375 + ds["temperature"].attrs = {"units": "kelvin", "grid_mapping": "lambert_conformal"} + ds["lambert_conformal"].attrs = { + "grid_mapping_name": "lambert_conformal_conic", + "standard_parallel": 50.0, + "longitude_of_central_meridian": -107.0, + "latitude_of_projection_origin": 50.0, + "earth_shape": "spherical", + "earth_radius": 6367470.21484375, } - return ds.metpy.parse_cf('temperature') + return ds.metpy.parse_cf("temperature") diff --git a/docs/conf.py b/docs/conf.py index c74b5798bdb..b46397c0891 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,45 +15,44 @@ import metpy - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('.')) -sys.path.insert(0, os.path.abspath(os.path.join('..', '..'))) +sys.path.insert(0, os.path.abspath(".")) +sys.path.insert(0, os.path.abspath(os.path.join("..", ".."))) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '2.1' +needs_sphinx = "2.1" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.coverage', - 'sphinx.ext.intersphinx', - 'sphinx.ext.mathjax', - 'sphinx.ext.napoleon', - 'sphinx.ext.viewcode', - 'sphinx_gallery.gen_gallery', - 'matplotlib.sphinxext.plot_directive', - 'myst_parser' + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.coverage", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "sphinx_gallery.gen_gallery", + "matplotlib.sphinxext.plot_directive", + "myst_parser", ] sphinx_gallery_conf = { - 'doc_module': ('metpy',), - 'reference_url': { - 'metpy': None, + "doc_module": ("metpy",), + "reference_url": { + "metpy": None, }, - 'examples_dirs': [os.path.join('..', 'examples'), os.path.join('..', 'tutorials')], - 'gallery_dirs': ['examples', 'tutorials'], - 'filename_pattern': r'\.py', - 'backreferences_dir': os.path.join('api', 'generated'), - 'default_thumb_file': os.path.join('_static', 'metpy_150x150_white_bg.png'), - 'abort_on_example_error': True + "examples_dirs": [os.path.join("..", "examples"), os.path.join("..", "tutorials")], + "gallery_dirs": ["examples", "tutorials"], + "filename_pattern": r"\.py", + "backreferences_dir": os.path.join("api", "generated"), + "default_thumb_file": os.path.join("_static", "metpy_150x150_white_bg.png"), + "abort_on_example_error": True, } # Turn off code and image links for embedded mpl plots @@ -62,25 +61,25 @@ # Set up mapping for other projects' docs intersphinx_mapping = { - # 'pint': ('http://pint.readthedocs.io/en/stable/', None), - 'matplotlib': ('https://matplotlib.org/', None), - 'python': ('https://docs.python.org/3/', None), - 'numpy': ('https://numpy.org/doc/stable/', None), - 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), - 'xarray': ('https://xarray.pydata.org/en/stable/', None) - } + # 'pint': ('http://pint.readthedocs.io/en/stable/', None), + "matplotlib": ("https://matplotlib.org/", None), + "python": ("https://docs.python.org/3/", None), + "numpy": ("https://numpy.org/doc/stable/", None), + "scipy": ("https://docs.scipy.org/doc/scipy/reference/", None), + "xarray": ("https://xarray.pydata.org/en/stable/", None), +} # Tweak how docs are formatted napoleon_use_rtype = False # Control main class documentation -autoclass_content = 'both' +autoclass_content = "both" # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = ['.rst', '.md'] +source_suffix = [".rst", ".md"] # Controlling automatically generating summary tables in the docs autosummary_generate = True @@ -90,14 +89,16 @@ # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'MetPy' +project = "MetPy" # noinspection PyShadowingBuiltins -copyright = ('2008-2020, MetPy Developers. ' - 'Development supported by National Science Foundation grants ' - 'AGS-1344155, OAC-1740315, and AGS-1901712.') +copyright = ( + "2008-2020, MetPy Developers. " + "Development supported by National Science Foundation grants " + "AGS-1344155, OAC-1740315, and AGS-1901712." +) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -105,16 +106,18 @@ # # The short X.Y version. verinfo = metpy.__version__ -full_version = verinfo.split('+')[0] -version = full_version.rsplit('.', 1)[0] +full_version = verinfo.split("+")[0] +version = full_version.rsplit(".", 1)[0] # The full version, including alpha/beta/rc tags. release = verinfo -rst_prolog = ''' +rst_prolog = """ .. |cite_version| replace:: {0} .. |cite_year| replace:: {1:%Y} .. |access_date| replace:: {1:%d %B %Y} -'''.format(full_version, datetime.utcnow()) +""".format( + full_version, datetime.utcnow() +) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -124,7 +127,7 @@ # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. -today_fmt = '%B %d, %Y' +today_fmt = "%B %d, %Y" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -132,7 +135,7 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -default_role = 'autolink' +default_role = "autolink" # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True @@ -146,7 +149,7 @@ # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -162,7 +165,8 @@ # html_theme = 'default' try: import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' + + html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] except ImportError: pass @@ -170,20 +174,21 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -html_theme_options = {'canonical_url': 'https://unidata.github.io/MetPy/latest/'} -if 'sphinx_rtd_theme' in vars() and sphinx_rtd_theme.__version__ == '0.2.5b1.post1': - html_theme_options['versions'] = {'latest': '../latest', 'dev': '../dev'} +html_theme_options = {"canonical_url": "https://unidata.github.io/MetPy/latest/"} +if "sphinx_rtd_theme" in vars() and sphinx_rtd_theme.__version__ == "0.2.5b1.post1": + html_theme_options["versions"] = {"latest": "../latest", "dev": "../dev"} # Extra variables that will be available to the templates. Used to create the # links to the Github repository sources and issues html_context = { - 'doc_path': 'docs', - 'galleries': sphinx_gallery_conf['gallery_dirs'], - 'gallery_dir': dict(zip(sphinx_gallery_conf['gallery_dirs'], - sphinx_gallery_conf['examples_dirs'])), - 'api_dir': 'api/generated', - 'github_repo': 'Unidata/MetPy', - 'github_version': 'master', # Make changes to the master branch + "doc_path": "docs", + "galleries": sphinx_gallery_conf["gallery_dirs"], + "gallery_dir": dict( + zip(sphinx_gallery_conf["gallery_dirs"], sphinx_gallery_conf["examples_dirs"]) + ), + "api_dir": "api/generated", + "github_repo": "Unidata/MetPy", + "github_version": "master", # Make changes to the master branch } # Add any paths that contain custom themes here, relative to this directory. @@ -191,27 +196,27 @@ # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -html_title = ' '.join((project, version)) +html_title = " ".join((project, version)) # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = os.path.join('_static', 'metpy_150x150.png') +html_logo = os.path.join("_static", "metpy_150x150.png") # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -html_favicon = os.path.join('_static', 'metpy_32x32.ico') +html_favicon = os.path.join("_static", "metpy_32x32.ico") # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] -html_css_files = ['theme_override.css'] -html_js_files = ['pop_ver.js'] +html_css_files = ["theme_override.css"] +html_js_files = ["pop_ver.js"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied @@ -220,7 +225,7 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -html_last_updated_fmt = '%b %d, %Y at %H:%M:%S' +html_last_updated_fmt = "%b %d, %Y at %H:%M:%S" # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. @@ -260,7 +265,7 @@ # html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'MetPydoc' +htmlhelp_basename = "MetPydoc" # -- Options for LaTeX output --------------------------------------------- @@ -268,10 +273,8 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # 'preamble': '', } @@ -280,8 +283,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'MetPy.tex', 'MetPy Documentation', - 'MetPy Developers', 'manual'), + ("index", "MetPy.tex", "MetPy Documentation", "MetPy Developers", "manual"), ] # The name of an image file (relative to this directory) to place at the top of @@ -309,10 +311,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'metpy', 'MetPy Documentation', - ['MetPy Developers'], 1) -] +man_pages = [("index", "metpy", "MetPy Documentation", ["MetPy Developers"], 1)] # If true, show URL addresses after external links. # man_show_urls = False @@ -323,9 +322,15 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'MetPy', 'MetPy Documentation', - 'MetPy Developers', 'MetPy', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "MetPy", + "MetPy Documentation", + "MetPy Developers", + "MetPy", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. @@ -340,10 +345,12 @@ # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False -linkcheck_ignore = [r'https://www\.youtube\.com/watch\?v=[\d\w\-_]+', - r'https://codecov.io/github/Unidata/MetPy', - r'https://youtu\.be/[\d\w\-_]+', +linkcheck_ignore = [ + r"https://www\.youtube\.com/watch\?v=[\d\w\-_]+", + r"https://codecov.io/github/Unidata/MetPy", + r"https://youtu\.be/[\d\w\-_]+", # AMS DOIs should be stable, but resolved link consistently 403's with linkcheck - r'https://doi.org/10.1175/.*'] + r"https://doi.org/10.1175/.*", +] -linkcheck_request_headers = {'https://www.ncdc.noaa.gov/': {'Accept-Encoding': 'deflate'}} +linkcheck_request_headers = {"https://www.ncdc.noaa.gov/": {"Accept-Encoding": "deflate"}} diff --git a/docs/override_check.py b/docs/override_check.py index 1b8fa125a87..c97e516a4a6 100644 --- a/docs/override_check.py +++ b/docs/override_check.py @@ -9,15 +9,14 @@ import os import sys - -modules_to_skip = ['metpy.xarray'] +modules_to_skip = ["metpy.xarray"] failed = False -for full_path in glob.glob('_templates/overrides/metpy.*.rst'): +for full_path in glob.glob("_templates/overrides/metpy.*.rst"): filename = os.path.basename(full_path) - module = filename.split('.rst')[0] + module = filename.split(".rst")[0] if module in modules_to_skip: continue @@ -36,11 +35,16 @@ if missing_functions: failed = True - print('ERROR - The following functions are missing from the override file ' + - filename + ': ' + ', '.join(missing_functions), file=sys.stderr) + print( + "ERROR - The following functions are missing from the override file " + + filename + + ": " + + ", ".join(missing_functions), + file=sys.stderr, + ) # Report status if failed: sys.exit(1) else: - print('Override check successful.') + print("Override check successful.") diff --git a/examples/Advanced_Sounding.py b/examples/Advanced_Sounding.py index e6774224486..e5141af0464 100644 --- a/examples/Advanced_Sounding.py +++ b/examples/Advanced_Sounding.py @@ -18,31 +18,36 @@ import metpy.calc as mpcalc from metpy.cbook import get_test_data -from metpy.plots import add_metpy_logo, SkewT +from metpy.plots import SkewT, add_metpy_logo from metpy.units import units ########################################### # Upper air data can be obtained using the siphon package, but for this example we will use # some of MetPy's sample data. -col_names = ['pressure', 'height', 'temperature', 'dewpoint', 'direction', 'speed'] +col_names = ["pressure", "height", "temperature", "dewpoint", "direction", "speed"] -df = pd.read_fwf(get_test_data('may4_sounding.txt', as_file_obj=False), - skiprows=5, usecols=[0, 1, 2, 3, 6, 7], names=col_names) +df = pd.read_fwf( + get_test_data("may4_sounding.txt", as_file_obj=False), + skiprows=5, + usecols=[0, 1, 2, 3, 6, 7], + names=col_names, +) # Drop any rows with all NaN values for T, Td, winds -df = df.dropna(subset=('temperature', 'dewpoint', 'direction', 'speed'), how='all' - ).reset_index(drop=True) +df = df.dropna( + subset=("temperature", "dewpoint", "direction", "speed"), how="all" +).reset_index(drop=True) ########################################### # We will pull the data out of the example dataset into individual variables and # assign units. -p = df['pressure'].values * units.hPa -T = df['temperature'].values * units.degC -Td = df['dewpoint'].values * units.degC -wind_speed = df['speed'].values * units.knots -wind_dir = df['direction'].values * units.degrees +p = df["pressure"].values * units.hPa +T = df["temperature"].values * units.degC +Td = df["dewpoint"].values * units.degC +wind_speed = df["speed"].values * units.knots +wind_dir = df["direction"].values * units.degrees u, v = mpcalc.wind_components(wind_speed, wind_dir) ########################################### @@ -54,8 +59,8 @@ # Plot the data using normal plotting functions, in this case using # log scaling in Y, as dictated by the typical meteorological plot. -skew.plot(p, T, 'r') -skew.plot(p, Td, 'g') +skew.plot(p, T, "r") +skew.plot(p, Td, "g") skew.plot_barbs(p, u, v) skew.ax.set_ylim(1000, 100) skew.ax.set_xlim(-40, 60) @@ -66,11 +71,11 @@ # i.e. start from low value, 250 mb, to a high value, 1000 mb, the `-1` index # should be selected. lcl_pressure, lcl_temperature = mpcalc.lcl(p[0], T[0], Td[0]) -skew.plot(lcl_pressure, lcl_temperature, 'ko', markerfacecolor='black') +skew.plot(lcl_pressure, lcl_temperature, "ko", markerfacecolor="black") # Calculate full parcel profile and add to plot as black line -prof = mpcalc.parcel_profile(p, T[0], Td[0]).to('degC') -skew.plot(p, prof, 'k', linewidth=2) +prof = mpcalc.parcel_profile(p, T[0], Td[0]).to("degC") +skew.plot(p, prof, "k", linewidth=2) # Shade areas of CAPE and CIN skew.shade_cin(p, T, prof, Td) @@ -78,7 +83,7 @@ # An example of a slanted line at constant T -- in this case the 0 # isotherm -skew.ax.axvline(0, color='c', linestyle='--', linewidth=2) +skew.ax.axvline(0, color="c", linestyle="--", linewidth=2) # Add the relevant special lines skew.plot_dry_adiabats() diff --git a/examples/Four_Panel_Map.py b/examples/Four_Panel_Map.py index 4bb7e1fbd4f..db7ec233d3d 100644 --- a/examples/Four_Panel_Map.py +++ b/examples/Four_Panel_Map.py @@ -33,8 +33,8 @@ # Function used to create the map subplots def plot_background(ax): - ax.set_extent([235., 290., 20., 55.]) - ax.add_feature(cfeature.COASTLINE.with_scale('50m'), linewidth=0.5) + ax.set_extent([235.0, 290.0, 20.0, 55.0]) + ax.add_feature(cfeature.COASTLINE.with_scale("50m"), linewidth=0.5) ax.add_feature(cfeature.STATES, linewidth=0.5) ax.add_feature(cfeature.BORDERS, linewidth=0.5) return ax @@ -43,84 +43,96 @@ def plot_background(ax): ########################################### # Open the example netCDF data -ds = xr.open_dataset(get_test_data('gfs_output.nc', False)) +ds = xr.open_dataset(get_test_data("gfs_output.nc", False)) print(ds) ########################################### # Combine 1D latitude and longitudes into a 2D grid of locations -lon_2d, lat_2d = np.meshgrid(ds['lon'], ds['lat']) +lon_2d, lat_2d = np.meshgrid(ds["lon"], ds["lat"]) ########################################### # Pull out the data -vort_500 = ds['vort_500'][0] -surface_temp = ds['temp'][0] -precip_water = ds['precip_water'][0] -winds_300 = ds['winds_300'][0] +vort_500 = ds["vort_500"][0] +surface_temp = ds["temp"][0] +precip_water = ds["precip_water"][0] +winds_300 = ds["winds_300"][0] ########################################### # Do unit conversions to what we wish to plot vort_500 = vort_500 * 1e5 -surface_temp = surface_temp.metpy.convert_units('degF') -precip_water = precip_water.metpy.convert_units('inches') -winds_300 = winds_300.metpy.convert_units('knots') +surface_temp = surface_temp.metpy.convert_units("degF") +precip_water = precip_water.metpy.convert_units("inches") +winds_300 = winds_300.metpy.convert_units("knots") ########################################### # Smooth the height data -heights_300 = ndimage.gaussian_filter(ds['heights_300'][0], sigma=1.5, order=0) -heights_500 = ndimage.gaussian_filter(ds['heights_500'][0], sigma=1.5, order=0) +heights_300 = ndimage.gaussian_filter(ds["heights_300"][0], sigma=1.5, order=0) +heights_500 = ndimage.gaussian_filter(ds["heights_500"][0], sigma=1.5, order=0) ########################################### # Create the figure and plot background on different axes -fig, axarr = plt.subplots(nrows=2, ncols=2, figsize=(20, 13), constrained_layout=True, - subplot_kw={'projection': crs}) -add_metpy_logo(fig, 140, 120, size='large') +fig, axarr = plt.subplots( + nrows=2, ncols=2, figsize=(20, 13), constrained_layout=True, subplot_kw={"projection": crs} +) +add_metpy_logo(fig, 140, 120, size="large") axlist = axarr.flatten() for ax in axlist: plot_background(ax) # Upper left plot - 300-hPa winds and geopotential heights -cf1 = axlist[0].contourf(lon_2d, lat_2d, winds_300, cmap='cool', transform=ccrs.PlateCarree()) -c1 = axlist[0].contour(lon_2d, lat_2d, heights_300, colors='black', linewidths=2, - transform=ccrs.PlateCarree()) -axlist[0].clabel(c1, fontsize=10, inline=1, inline_spacing=1, fmt='%i', rightside_up=True) -axlist[0].set_title('300-hPa Wind Speeds and Heights', fontsize=16) -cb1 = fig.colorbar(cf1, ax=axlist[0], orientation='horizontal', shrink=0.74, pad=0) -cb1.set_label('knots', size='x-large') +cf1 = axlist[0].contourf(lon_2d, lat_2d, winds_300, cmap="cool", transform=ccrs.PlateCarree()) +c1 = axlist[0].contour( + lon_2d, lat_2d, heights_300, colors="black", linewidths=2, transform=ccrs.PlateCarree() +) +axlist[0].clabel(c1, fontsize=10, inline=1, inline_spacing=1, fmt="%i", rightside_up=True) +axlist[0].set_title("300-hPa Wind Speeds and Heights", fontsize=16) +cb1 = fig.colorbar(cf1, ax=axlist[0], orientation="horizontal", shrink=0.74, pad=0) +cb1.set_label("knots", size="x-large") # Upper right plot - 500mb absolute vorticity and geopotential heights -cf2 = axlist[1].contourf(lon_2d, lat_2d, vort_500, cmap='BrBG', transform=ccrs.PlateCarree(), - zorder=0, norm=plt.Normalize(-32, 32)) -c2 = axlist[1].contour(lon_2d, lat_2d, heights_500, colors='k', linewidths=2, - transform=ccrs.PlateCarree()) -axlist[1].clabel(c2, fontsize=10, inline=1, inline_spacing=1, fmt='%i', rightside_up=True) -axlist[1].set_title('500-hPa Absolute Vorticity and Heights', fontsize=16) -cb2 = fig.colorbar(cf2, ax=axlist[1], orientation='horizontal', shrink=0.74, pad=0) -cb2.set_label(r'$10^{-5}$ s$^{-1}$', size='x-large') +cf2 = axlist[1].contourf( + lon_2d, + lat_2d, + vort_500, + cmap="BrBG", + transform=ccrs.PlateCarree(), + zorder=0, + norm=plt.Normalize(-32, 32), +) +c2 = axlist[1].contour( + lon_2d, lat_2d, heights_500, colors="k", linewidths=2, transform=ccrs.PlateCarree() +) +axlist[1].clabel(c2, fontsize=10, inline=1, inline_spacing=1, fmt="%i", rightside_up=True) +axlist[1].set_title("500-hPa Absolute Vorticity and Heights", fontsize=16) +cb2 = fig.colorbar(cf2, ax=axlist[1], orientation="horizontal", shrink=0.74, pad=0) +cb2.set_label(r"$10^{-5}$ s$^{-1}$", size="x-large") # Lower left plot - surface temperatures -cf3 = axlist[2].contourf(lon_2d, lat_2d, surface_temp, cmap='YlOrRd', - transform=ccrs.PlateCarree(), zorder=0) -axlist[2].set_title('Surface Temperatures', fontsize=16) -cb3 = fig.colorbar(cf3, ax=axlist[2], orientation='horizontal', shrink=0.74, pad=0) -cb3.set_label('\N{DEGREE FAHRENHEIT}', size='x-large') +cf3 = axlist[2].contourf( + lon_2d, lat_2d, surface_temp, cmap="YlOrRd", transform=ccrs.PlateCarree(), zorder=0 +) +axlist[2].set_title("Surface Temperatures", fontsize=16) +cb3 = fig.colorbar(cf3, ax=axlist[2], orientation="horizontal", shrink=0.74, pad=0) +cb3.set_label("\N{DEGREE FAHRENHEIT}", size="x-large") # Lower right plot - precipitable water entire atmosphere -cf4 = axlist[3].contourf(lon_2d, lat_2d, precip_water, cmap='Greens', - transform=ccrs.PlateCarree(), zorder=0) -axlist[3].set_title('Precipitable Water', fontsize=16) -cb4 = fig.colorbar(cf4, ax=axlist[3], orientation='horizontal', shrink=0.74, pad=0) -cb4.set_label('in.', size='x-large') +cf4 = axlist[3].contourf( + lon_2d, lat_2d, precip_water, cmap="Greens", transform=ccrs.PlateCarree(), zorder=0 +) +axlist[3].set_title("Precipitable Water", fontsize=16) +cb4 = fig.colorbar(cf4, ax=axlist[3], orientation="horizontal", shrink=0.74, pad=0) +cb4.set_label("in.", size="x-large") # Set height padding for plots -fig.set_constrained_layout_pads(w_pad=0., h_pad=0.1, hspace=0., wspace=0.) +fig.set_constrained_layout_pads(w_pad=0.0, h_pad=0.1, hspace=0.0, wspace=0.0) # Set figure title -fig.suptitle(ds['time'][0].dt.strftime('%d %B %Y %H:%MZ').values, fontsize=24) +fig.suptitle(ds["time"][0].dt.strftime("%d %B %Y %H:%MZ").values, fontsize=24) # Display the plot plt.show() diff --git a/examples/XArray_Projections.py b/examples/XArray_Projections.py index db75c632817..66822a7adc8 100644 --- a/examples/XArray_Projections.py +++ b/examples/XArray_Projections.py @@ -15,20 +15,24 @@ # Any import of metpy will activate the accessors from metpy.cbook import get_test_data -ds = xr.open_dataset(get_test_data('narr_example.nc', as_file_obj=False)) -data_var = ds.metpy.parse_cf('Temperature') +ds = xr.open_dataset(get_test_data("narr_example.nc", as_file_obj=False)) +data_var = ds.metpy.parse_cf("Temperature") x = data_var.x y = data_var.y -im_data = data_var.isel(time=0).sel(isobaric=1000.) +im_data = data_var.isel(time=0).sel(isobaric=1000.0) fig = plt.figure(figsize=(14, 14)) ax = fig.add_subplot(1, 1, 1, projection=data_var.metpy.cartopy_crs) -ax.imshow(im_data, extent=(x.min(), x.max(), y.min(), y.max()), - cmap='RdBu', origin='lower' if y[0] < y[-1] else 'upper') -ax.coastlines(color='tab:green', resolution='10m') -ax.add_feature(cfeature.LAKES.with_scale('10m'), facecolor='none', edgecolor='tab:blue') -ax.add_feature(cfeature.RIVERS.with_scale('10m'), edgecolor='tab:blue') +ax.imshow( + im_data, + extent=(x.min(), x.max(), y.min(), y.max()), + cmap="RdBu", + origin="lower" if y[0] < y[-1] else "upper", +) +ax.coastlines(color="tab:green", resolution="10m") +ax.add_feature(cfeature.LAKES.with_scale("10m"), facecolor="none", edgecolor="tab:blue") +ax.add_feature(cfeature.RIVERS.with_scale("10m"), edgecolor="tab:blue") plt.show() diff --git a/examples/calculations/Angle_to_Direction.py b/examples/calculations/Angle_to_Direction.py index e1068713978..87f7148a913 100644 --- a/examples/calculations/Angle_to_Direction.py +++ b/examples/calculations/Angle_to_Direction.py @@ -15,7 +15,7 @@ ########################################### # Create a test value of an angle -angle_deg = 70 * units('degree') +angle_deg = 70 * units("degree") print(angle_deg) ########################################### diff --git a/examples/calculations/Dewpoint_and_Mixing_Ratio.py b/examples/calculations/Dewpoint_and_Mixing_Ratio.py index b30e474e000..b2f0dcaf67e 100644 --- a/examples/calculations/Dewpoint_and_Mixing_Ratio.py +++ b/examples/calculations/Dewpoint_and_Mixing_Ratio.py @@ -17,13 +17,13 @@ ########################################### # Create a test value of mixing ratio in grams per kilogram -mixing = 10 * units('g/kg') +mixing = 10 * units("g/kg") print(mixing) ########################################### # Now throw that value with units into the function to calculate # the corresponding vapor pressure, given a surface pressure of 1000 mb -e = mpcalc.vapor_pressure(1000. * units.mbar, mixing) +e = mpcalc.vapor_pressure(1000.0 * units.mbar, mixing) print(e) ########################################### @@ -37,14 +37,14 @@ ########################################### # Which can of course be converted to Fahrenheit -print(td.to('degF')) +print(td.to("degF")) ########################################### # Now do the same thing for 850 mb, approximately the pressure of Denver -e = mpcalc.vapor_pressure(850. * units.mbar, mixing) +e = mpcalc.vapor_pressure(850.0 * units.mbar, mixing) print(e.to(units.mbar)) ########################################### # And print the corresponding dewpoint td = mpcalc.dewpoint(e) -print(td, td.to('degF')) +print(td, td.to("degF")) diff --git a/examples/calculations/Gradient.py b/examples/calculations/Gradient.py index 482104d56a4..b6890d8eec8 100644 --- a/examples/calculations/Gradient.py +++ b/examples/calculations/Gradient.py @@ -17,6 +17,7 @@ from metpy.units import units ########################################### +# fmt: off # Create some test data to use for our example data = np.array([[23, 24, 23], [25, 26, 25], @@ -33,12 +34,13 @@ [2, 2, 2], [3, 3, 3], [4, 4, 4]]) * units.kilometer +# fmt: on ########################################### # Calculate the gradient using the coordinates of the data grad = mpcalc.gradient(data, coordinates=(y, x)) -print('Gradient in y direction: ', grad[0]) -print('Gradient in x direction: ', grad[1]) +print("Gradient in y direction: ", grad[0]) +print("Gradient in x direction: ", grad[1]) ########################################### # It's also possible that we do not have the position of data points, but know @@ -47,18 +49,13 @@ x_delta = 2 * units.km y_delta = 1 * units.km grad = mpcalc.gradient(data, deltas=(y_delta, x_delta)) -print('Gradient in y direction: ', grad[0]) -print('Gradient in x direction: ', grad[1]) +print("Gradient in y direction: ", grad[0]) +print("Gradient in x direction: ", grad[1]) ########################################### # Finally, the deltas can be arrays for unevenly spaced data. -x_deltas = np.array([[2, 3], - [1, 3], - [2, 3], - [1, 2]]) * units.kilometer -y_deltas = np.array([[2, 3, 1], - [1, 3, 2], - [2, 3, 1]]) * units.kilometer +x_deltas = np.array([[2, 3], [1, 3], [2, 3], [1, 2]]) * units.kilometer +y_deltas = np.array([[2, 3, 1], [1, 3, 2], [2, 3, 1]]) * units.kilometer grad = mpcalc.gradient(data, deltas=(y_deltas, x_deltas)) -print('Gradient in y direction: ', grad[0]) -print('Gradient in x direction: ', grad[1]) +print("Gradient in y direction: ", grad[0]) +print("Gradient in x direction: ", grad[1]) diff --git a/examples/calculations/Parse_Angles.py b/examples/calculations/Parse_Angles.py index a58376ca1dd..158ed79a028 100644 --- a/examples/calculations/Parse_Angles.py +++ b/examples/calculations/Parse_Angles.py @@ -15,7 +15,7 @@ ########################################### # Create a test value of a directional text -dir_str = 'SOUTH SOUTH EAST' +dir_str = "SOUTH SOUTH EAST" print(dir_str) ########################################### @@ -27,6 +27,6 @@ ########################################### # The function can also handle arrays of string # in many different abbrieviations and capitalizations -dir_str_list = ['ne', 'NE', 'NORTHEAST', 'NORTH_EAST', 'NORTH east'] +dir_str_list = ["ne", "NE", "NORTHEAST", "NORTH_EAST", "NORTH east"] angle_deg_list = mpcalc.parse_angle(dir_str_list) print(angle_deg_list) diff --git a/examples/calculations/Smoothing.py b/examples/calculations/Smoothing.py index 2afc2e25535..2b27bc56608 100644 --- a/examples/calculations/Smoothing.py +++ b/examples/calculations/Smoothing.py @@ -28,48 +28,48 @@ raw_data = np.random.random((size, size)) * 0.3 + distance / distance.max() * 0.7 fig, ax = plt.subplots(1, 1, figsize=(4, 4)) -ax.set_title('Raw Data') +ax.set_title("Raw Data") ax.imshow(raw_data, vmin=0, vmax=1) -ax.axis('off') +ax.axis("off") plt.show() ########################################### # Now, create a grid showing different smoothing options fig, ax = plt.subplots(3, 3, figsize=(12, 12)) for i, j in product(range(3), range(3)): - ax[i, j].axis('off') + ax[i, j].axis("off") # Gaussian Smoother ax[0, 0].imshow(mpcalc.smooth_gaussian(raw_data, 3), vmin=0, vmax=1) -ax[0, 0].set_title('Gaussian - Low Degree') +ax[0, 0].set_title("Gaussian - Low Degree") ax[0, 1].imshow(mpcalc.smooth_gaussian(raw_data, 8), vmin=0, vmax=1) -ax[0, 1].set_title('Gaussian - High Degree') +ax[0, 1].set_title("Gaussian - High Degree") # Rectangular Smoother ax[0, 2].imshow(mpcalc.smooth_rectangular(raw_data, (3, 7), 2), vmin=0, vmax=1) -ax[0, 2].set_title('Rectangular - 3x7 Window\n2 Passes') +ax[0, 2].set_title("Rectangular - 3x7 Window\n2 Passes") # 5-point smoother ax[1, 0].imshow(mpcalc.smooth_n_point(raw_data, 5, 1), vmin=0, vmax=1) -ax[1, 0].set_title('5-Point - 1 Pass') +ax[1, 0].set_title("5-Point - 1 Pass") ax[1, 1].imshow(mpcalc.smooth_n_point(raw_data, 5, 4), vmin=0, vmax=1) -ax[1, 1].set_title('5-Point - 4 Passes') +ax[1, 1].set_title("5-Point - 4 Passes") # Circular Smoother ax[1, 2].imshow(mpcalc.smooth_circular(raw_data, 2, 2), vmin=0, vmax=1) -ax[1, 2].set_title('Circular - Radius 2\n2 Passes') +ax[1, 2].set_title("Circular - Radius 2\n2 Passes") # 9-point smoother ax[2, 0].imshow(mpcalc.smooth_n_point(raw_data, 9, 1), vmin=0, vmax=1) -ax[2, 0].set_title('9-Point - 1 Pass') +ax[2, 0].set_title("9-Point - 1 Pass") ax[2, 1].imshow(mpcalc.smooth_n_point(raw_data, 9, 4), vmin=0, vmax=1) -ax[2, 1].set_title('9-Point - 4 Passes') +ax[2, 1].set_title("9-Point - 4 Passes") # Arbitrary Window Smoother ax[2, 2].imshow(mpcalc.smooth_window(raw_data, np.diag(np.ones(5)), 2), vmin=0, vmax=1) -ax[2, 2].set_title('Custom Window (Diagonal) \n2 Passes') +ax[2, 2].set_title("Custom Window (Diagonal) \n2 Passes") plt.show() diff --git a/examples/cross_section.py b/examples/cross_section.py index 10a4dce8747..f895772fc98 100644 --- a/examples/cross_section.py +++ b/examples/cross_section.py @@ -30,7 +30,7 @@ # We use MetPy's CF parsing to get the data ready for use, and squeeze down the size-one time # dimension. -data = xr.open_dataset(get_test_data('narr_example.nc', False)) +data = xr.open_dataset(get_test_data("narr_example.nc", False)) data = data.metpy.parse_cf().squeeze() print(data) @@ -43,27 +43,23 @@ ############################## # Get the cross section, and convert lat/lon to supplementary coordinates: -cross = cross_section(data, start, end).set_coords(('lat', 'lon')) +cross = cross_section(data, start, end).set_coords(("lat", "lon")) print(cross) ############################## # For this example, we will be plotting potential temperature, relative humidity, and # tangential/normal winds. And so, we need to calculate those, and add them to the dataset: -cross['Potential_temperature'] = mpcalc.potential_temperature( - cross['isobaric'], - cross['Temperature'] +cross["Potential_temperature"] = mpcalc.potential_temperature( + cross["isobaric"], cross["Temperature"] ) -cross['Relative_humidity'] = mpcalc.relative_humidity_from_specific_humidity( - cross['isobaric'], - cross['Temperature'], - cross['Specific_humidity'] +cross["Relative_humidity"] = mpcalc.relative_humidity_from_specific_humidity( + cross["isobaric"], cross["Temperature"], cross["Specific_humidity"] ) -cross['u_wind'] = cross['u_wind'].metpy.convert_units('knots') -cross['v_wind'] = cross['v_wind'].metpy.convert_units('knots') -cross['t_wind'], cross['n_wind'] = mpcalc.cross_section_components( - cross['u_wind'], - cross['v_wind'] +cross["u_wind"] = cross["u_wind"].metpy.convert_units("knots") +cross["v_wind"] = cross["v_wind"].metpy.convert_units("knots") +cross["t_wind"], cross["n_wind"] = mpcalc.cross_section_components( + cross["u_wind"], cross["v_wind"] ) print(cross) @@ -72,62 +68,94 @@ # Now, we can make the plot. # Define the figure object and primary axes -fig = plt.figure(1, figsize=(16., 9.)) +fig = plt.figure(1, figsize=(16.0, 9.0)) ax = plt.axes() # Plot RH using contourf -rh_contour = ax.contourf(cross['lon'], cross['isobaric'], cross['Relative_humidity'], - levels=np.arange(0, 1.05, .05), cmap='YlGnBu') +rh_contour = ax.contourf( + cross["lon"], + cross["isobaric"], + cross["Relative_humidity"], + levels=np.arange(0, 1.05, 0.05), + cmap="YlGnBu", +) rh_colorbar = fig.colorbar(rh_contour) # Plot potential temperature using contour, with some custom labeling -theta_contour = ax.contour(cross['lon'], cross['isobaric'], cross['Potential_temperature'], - levels=np.arange(250, 450, 5), colors='k', linewidths=2) -theta_contour.clabel(theta_contour.levels[1::2], fontsize=8, colors='k', inline=1, - inline_spacing=8, fmt='%i', rightside_up=True, use_clabeltext=True) +theta_contour = ax.contour( + cross["lon"], + cross["isobaric"], + cross["Potential_temperature"], + levels=np.arange(250, 450, 5), + colors="k", + linewidths=2, +) +theta_contour.clabel( + theta_contour.levels[1::2], + fontsize=8, + colors="k", + inline=1, + inline_spacing=8, + fmt="%i", + rightside_up=True, + use_clabeltext=True, +) # Plot winds using the axes interface directly, with some custom indexing to make the barbs # less crowded wind_slc_vert = list(range(0, 19, 2)) + list(range(19, 29)) wind_slc_horz = slice(5, 100, 5) -ax.barbs(cross['lon'][wind_slc_horz], cross['isobaric'][wind_slc_vert], - cross['t_wind'][wind_slc_vert, wind_slc_horz], - cross['n_wind'][wind_slc_vert, wind_slc_horz], color='k') +ax.barbs( + cross["lon"][wind_slc_horz], + cross["isobaric"][wind_slc_vert], + cross["t_wind"][wind_slc_vert, wind_slc_horz], + cross["n_wind"][wind_slc_vert, wind_slc_horz], + color="k", +) # Adjust the y-axis to be logarithmic -ax.set_yscale('symlog') +ax.set_yscale("symlog") ax.set_yticklabels(np.arange(1000, 50, -100)) -ax.set_ylim(cross['isobaric'].max(), cross['isobaric'].min()) +ax.set_ylim(cross["isobaric"].max(), cross["isobaric"].min()) ax.set_yticks(np.arange(1000, 50, -100)) # Define the CRS and inset axes -data_crs = data['Geopotential_height'].metpy.cartopy_crs +data_crs = data["Geopotential_height"].metpy.cartopy_crs ax_inset = fig.add_axes([0.125, 0.665, 0.25, 0.25], projection=data_crs) # Plot geopotential height at 500 hPa using xarray's contour wrapper -ax_inset.contour(data['x'], data['y'], data['Geopotential_height'].sel(isobaric=500.), - levels=np.arange(5100, 6000, 60), cmap='inferno') +ax_inset.contour( + data["x"], + data["y"], + data["Geopotential_height"].sel(isobaric=500.0), + levels=np.arange(5100, 6000, 60), + cmap="inferno", +) # Plot the path of the cross section -endpoints = data_crs.transform_points(ccrs.Geodetic(), - *np.vstack([start, end]).transpose()[::-1]) -ax_inset.scatter(endpoints[:, 0], endpoints[:, 1], c='k', zorder=2) -ax_inset.plot(cross['x'], cross['y'], c='k', zorder=2) +endpoints = data_crs.transform_points( + ccrs.Geodetic(), *np.vstack([start, end]).transpose()[::-1] +) +ax_inset.scatter(endpoints[:, 0], endpoints[:, 1], c="k", zorder=2) +ax_inset.plot(cross["x"], cross["y"], c="k", zorder=2) # Add geographic features ax_inset.coastlines() -ax_inset.add_feature(cfeature.STATES.with_scale('50m'), edgecolor='k', alpha=0.2, zorder=0) +ax_inset.add_feature(cfeature.STATES.with_scale("50m"), edgecolor="k", alpha=0.2, zorder=0) # Set the titles and axes labels -ax_inset.set_title('') -ax.set_title('NARR Cross-Section \u2013 {} to {} \u2013 Valid: {}\n' - 'Potential Temperature (K), Tangential/Normal Winds (knots), ' - 'Relative Humidity (dimensionless)\n' - 'Inset: Cross-Section Path and 500 hPa Geopotential Height'.format( - start, end, cross['time'].dt.strftime('%Y-%m-%d %H:%MZ').item())) -ax.set_ylabel('Pressure (hPa)') -ax.set_xlabel('Longitude (degrees east)') -rh_colorbar.set_label('Relative Humidity (dimensionless)') +ax_inset.set_title("") +ax.set_title( + "NARR Cross-Section \u2013 {} to {} \u2013 Valid: {}\n" + "Potential Temperature (K), Tangential/Normal Winds (knots), " + "Relative Humidity (dimensionless)\n" + "Inset: Cross-Section Path and 500 hPa Geopotential Height".format( + start, end, cross["time"].dt.strftime("%Y-%m-%d %H:%MZ").item() + ) +) +ax.set_ylabel("Pressure (hPa)") +ax.set_xlabel("Longitude (degrees east)") +rh_colorbar.set_label("Relative Humidity (dimensionless)") plt.show() diff --git a/examples/formats/GINI_Water_Vapor.py b/examples/formats/GINI_Water_Vapor.py index 2088918af7b..44dc1f80928 100644 --- a/examples/formats/GINI_Water_Vapor.py +++ b/examples/formats/GINI_Water_Vapor.py @@ -19,7 +19,7 @@ ########################################### # Open the GINI file from the test data -f = GiniFile(get_test_data('WEST-CONUS_4km_WV_20151208_2200.gini')) +f = GiniFile(get_test_data("WEST-CONUS_4km_WV_20151208_2200.gini")) print(f) ########################################### @@ -28,9 +28,9 @@ # handle parsing some netCDF Climate and Forecasting (CF) metadata to simplify working with # projections. ds = xr.open_dataset(f) -x = ds.variables['x'][:] -y = ds.variables['y'][:] -dat = ds.metpy.parse_cf('WV') +x = ds.variables["x"][:] +y = ds.variables["y"][:] +dat = ds.metpy.parse_cf("WV") ########################################### # Plot the image. We use MetPy's xarray/cartopy integration to automatically handle parsing @@ -38,11 +38,16 @@ fig = plt.figure(figsize=(10, 12)) add_metpy_logo(fig, 125, 145) ax = fig.add_subplot(1, 1, 1, projection=dat.metpy.cartopy_crs) -wv_norm, wv_cmap = colortables.get_with_range('WVCIMSS', 100, 260) -wv_cmap.set_under('k') -im = ax.imshow(dat[:], cmap=wv_cmap, norm=wv_norm, - extent=(x.min(), x.max(), y.min(), y.max()), origin='upper') -ax.add_feature(cfeature.COASTLINE.with_scale('50m')) +wv_norm, wv_cmap = colortables.get_with_range("WVCIMSS", 100, 260) +wv_cmap.set_under("k") +im = ax.imshow( + dat[:], + cmap=wv_cmap, + norm=wv_norm, + extent=(x.min(), x.max(), y.min(), y.max()), + origin="upper", +) +ax.add_feature(cfeature.COASTLINE.with_scale("50m")) add_timestamp(ax, f.prod_desc.datetime, y=0.02, high_contrast=True) plt.show() diff --git a/examples/formats/NEXRAD_Level_2_File.py b/examples/formats/NEXRAD_Level_2_File.py index 78056172f06..d8fafdf7a41 100644 --- a/examples/formats/NEXRAD_Level_2_File.py +++ b/examples/formats/NEXRAD_Level_2_File.py @@ -17,7 +17,7 @@ ########################################### # Open the file -name = get_test_data('KTLX20130520_201643_V06.gz', as_file_obj=False) +name = get_test_data("KTLX20130520_201643_V06.gz", as_file_obj=False) f = Level2File(name) print(f.sweeps[0][0]) @@ -32,17 +32,17 @@ # 5th item is a dict mapping a var name (byte string) to a tuple # of (header, data array) -ref_hdr = f.sweeps[sweep][0][4][b'REF'][0] +ref_hdr = f.sweeps[sweep][0][4][b"REF"][0] ref_range = np.arange(ref_hdr.num_gates) * ref_hdr.gate_width + ref_hdr.first_gate -ref = np.array([ray[4][b'REF'][1] for ray in f.sweeps[sweep]]) +ref = np.array([ray[4][b"REF"][1] for ray in f.sweeps[sweep]]) -rho_hdr = f.sweeps[sweep][0][4][b'RHO'][0] +rho_hdr = f.sweeps[sweep][0][4][b"RHO"][0] rho_range = (np.arange(rho_hdr.num_gates + 1) - 0.5) * rho_hdr.gate_width + rho_hdr.first_gate -rho = np.array([ray[4][b'RHO'][1] for ray in f.sweeps[sweep]]) +rho = np.array([ray[4][b"RHO"][1] for ray in f.sweeps[sweep]]) ########################################### fig, axes = plt.subplots(1, 2, figsize=(15, 8)) -add_metpy_logo(fig, 190, 85, size='large') +add_metpy_logo(fig, 190, 85, size="large") for var_data, var_range, ax in zip((ref, rho), (ref_range, rho_range), axes): # Turn into an array, then mask data = np.ma.array(var_data) @@ -53,8 +53,8 @@ ylocs = var_range * np.cos(np.deg2rad(az[:, np.newaxis])) # Plot the data - ax.pcolormesh(xlocs, ylocs, data, cmap='viridis') - ax.set_aspect('equal', 'datalim') + ax.pcolormesh(xlocs, ylocs, data, cmap="viridis") + ax.set_aspect("equal", "datalim") ax.set_xlim(-40, 20) ax.set_ylim(-30, 30) add_timestamp(ax, f.dt, y=0.02, high_contrast=True) diff --git a/examples/formats/NEXRAD_Level_3_File.py b/examples/formats/NEXRAD_Level_3_File.py index 1f23c5a84f3..ae67431142e 100644 --- a/examples/formats/NEXRAD_Level_3_File.py +++ b/examples/formats/NEXRAD_Level_3_File.py @@ -16,22 +16,21 @@ ########################################### fig, axes = plt.subplots(1, 2, figsize=(15, 8)) -add_metpy_logo(fig, 190, 85, size='large') -ctables = (('NWSStormClearReflectivity', -20, 0.5), # dBZ - ('NWS8bitVel', -100, 1.0)) # m/s -for v, ctable, ax in zip(('N0Q', 'N0U'), ctables, axes): +add_metpy_logo(fig, 190, 85, size="large") +ctables = (("NWSStormClearReflectivity", -20, 0.5), ("NWS8bitVel", -100, 1.0)) # dBZ # m/s +for v, ctable, ax in zip(("N0Q", "N0U"), ctables, axes): # Open the file - name = get_test_data(f'nids/KOUN_SDUS54_{v}TLX_201305202016', as_file_obj=False) + name = get_test_data(f"nids/KOUN_SDUS54_{v}TLX_201305202016", as_file_obj=False) f = Level3File(name) # Pull the data out of the file object datadict = f.sym_block[0][0] # Turn into an array using the scale specified by the file - data = f.map_data(datadict['data']) + data = f.map_data(datadict["data"]) # Grab azimuths and calculate a range based on number of gates - az = np.array(datadict['start_az'] + [datadict['end_az'][-1]]) + az = np.array(datadict["start_az"] + [datadict["end_az"][-1]]) rng = np.linspace(0, f.max_range, data.shape[-1] + 1) # Convert az,range to x,y @@ -41,9 +40,9 @@ # Plot the data norm, cmap = colortables.get_with_steps(*ctable) ax.pcolormesh(xlocs, ylocs, data, norm=norm, cmap=cmap) - ax.set_aspect('equal', 'datalim') + ax.set_aspect("equal", "datalim") ax.set_xlim(-40, 20) ax.set_ylim(-30, 30) - add_timestamp(ax, f.metadata['prod_time'], y=0.02, high_contrast=True) + add_timestamp(ax, f.metadata["prod_time"], y=0.02, high_contrast=True) plt.show() diff --git a/examples/gridding/Find_Natural_Neighbors_Verification.py b/examples/gridding/Find_Natural_Neighbors_Verification.py index 6fadfdfa200..740431d43e2 100644 --- a/examples/gridding/Find_Natural_Neighbors_Verification.py +++ b/examples/gridding/Find_Natural_Neighbors_Verification.py @@ -31,8 +31,8 @@ test_points = np.array([[2, 2], [5, 10], [12, 13.4], [12, 8], [20, 20]]) for i, (x, y) in enumerate(test_points): - ax.plot(x, y, 'k.', markersize=6) - ax.annotate('test ' + str(i), xy=(x, y)) + ax.plot(x, y, "k.", markersize=6) + ax.annotate("test " + str(i), xy=(x, y)) ########################################### # Since finding natural neighbors already calculates circumcenters, return @@ -58,11 +58,16 @@ # Using circumcenters and calculated circumradii, plot the circumcircles for idx, cc in enumerate(circumcenters): - ax.plot(cc[0], cc[1], 'k.', markersize=5) - circ = plt.Circle(cc, circumcircle_radius(*tri.points[tri.simplices[idx]]), - edgecolor='k', facecolor='none', transform=fig.axes[0].transData) + ax.plot(cc[0], cc[1], "k.", markersize=5) + circ = plt.Circle( + cc, + circumcircle_radius(*tri.points[tri.simplices[idx]]), + edgecolor="k", + facecolor="none", + transform=fig.axes[0].transData, + ) ax.add_artist(circ) -ax.set_aspect('equal', 'datalim') +ax.set_aspect("equal", "datalim") plt.show() diff --git a/examples/gridding/Inverse_Distance_Verification.py b/examples/gridding/Inverse_Distance_Verification.py index 6819cd973c0..ad87e6763f9 100644 --- a/examples/gridding/Inverse_Distance_Verification.py +++ b/examples/gridding/Inverse_Distance_Verification.py @@ -90,21 +90,21 @@ def draw_circle(ax, x, y, r, m, label): # Plot all of the affiliated information and interpolation values. fig, ax = plt.subplots(1, 1, figsize=(15, 10)) for i, zval in enumerate(zp): - ax.plot(pts[i, 0], pts[i, 1], '.') - ax.annotate(str(zval) + ' F', xy=(pts[i, 0] + 2, pts[i, 1])) + ax.plot(pts[i, 0], pts[i, 1], ".") + ax.annotate(str(zval) + " F", xy=(pts[i, 0] + 2, pts[i, 1])) -ax.plot(sim_gridx, sim_gridy, '+', markersize=10) +ax.plot(sim_gridx, sim_gridy, "+", markersize=10) -ax.plot(x1, y1, 'ko', fillstyle='none', markersize=10, label='grid 0 matches') -ax.plot(x2, y2, 'ks', fillstyle='none', markersize=10, label='grid 1 matches') +ax.plot(x1, y1, "ko", fillstyle="none", markersize=10, label="grid 0 matches") +ax.plot(x2, y2, "ks", fillstyle="none", markersize=10, label="grid 1 matches") -draw_circle(ax, sim_gridx[0], sim_gridy[0], m='k-', r=radius, label='grid 0 radius') -draw_circle(ax, sim_gridx[1], sim_gridy[1], m='b-', r=radius, label='grid 1 radius') +draw_circle(ax, sim_gridx[0], sim_gridy[0], m="k-", r=radius, label="grid 0 radius") +draw_circle(ax, sim_gridx[1], sim_gridy[1], m="b-", r=radius, label="grid 1 radius") -ax.annotate(f'grid 0: cressman {cress_val:.3f}', xy=(sim_gridx[0] + 2, sim_gridy[0])) -ax.annotate(f'grid 1: barnes {barnes_val:.3f}', xy=(sim_gridx[1] + 2, sim_gridy[1])) +ax.annotate(f"grid 0: cressman {cress_val:.3f}", xy=(sim_gridx[0] + 2, sim_gridy[0])) +ax.annotate(f"grid 1: barnes {barnes_val:.3f}", xy=(sim_gridx[1] + 2, sim_gridy[1])) -ax.set_aspect('equal', 'datalim') +ax.set_aspect("equal", "datalim") ax.legend() ########################################### @@ -114,25 +114,25 @@ def draw_circle(ax, x, y, r, m, label): # Plot the grid point, observations within radius of the grid point, their locations, and # their distances from the grid point. fig, ax = plt.subplots(1, 1, figsize=(15, 10)) -ax.annotate(f'grid 0: ({sim_gridx[0]}, {sim_gridy[0]})', xy=(sim_gridx[0] + 2, sim_gridy[0])) -ax.plot(sim_gridx[0], sim_gridy[0], '+', markersize=10) +ax.annotate(f"grid 0: ({sim_gridx[0]}, {sim_gridy[0]})", xy=(sim_gridx[0] + 2, sim_gridy[0])) +ax.plot(sim_gridx[0], sim_gridy[0], "+", markersize=10) mx, my = obs_tree.data[indices[0]].T mz = zp[indices[0]] for x, y, z in zip(mx, my, mz): - d = np.sqrt((sim_gridx[0] - x)**2 + (y - sim_gridy[0])**2) - ax.plot([sim_gridx[0], x], [sim_gridy[0], y], '--') + d = np.sqrt((sim_gridx[0] - x) ** 2 + (y - sim_gridy[0]) ** 2) + ax.plot([sim_gridx[0], x], [sim_gridy[0], y], "--") xave = np.mean([sim_gridx[0], x]) yave = np.mean([sim_gridy[0], y]) - ax.annotate(f'distance: {d}', xy=(xave, yave)) - ax.annotate(f'({x}, {y}) : {z} F', xy=(x, y)) + ax.annotate(f"distance: {d}", xy=(xave, yave)) + ax.annotate(f"({x}, {y}) : {z} F", xy=(x, y)) ax.set_xlim(0, 80) ax.set_ylim(0, 80) -ax.set_aspect('equal', 'datalim') +ax.set_aspect("equal", "datalim") ########################################### # Step through the cressman calculations. @@ -146,43 +146,43 @@ def draw_circle(ax, x, y, r, m, label): val = cressman_point(cress_dist, cress_obs, radius) -print('Manual cressman value for grid 1:\t', np.sum(value)) -print('Metpy cressman value for grid 1:\t', val) +print("Manual cressman value for grid 1:\t", np.sum(value)) +print("Metpy cressman value for grid 1:\t", val) ########################################### # Now repeat for grid 1, except use barnes interpolation. fig, ax = plt.subplots(1, 1, figsize=(15, 10)) -ax.annotate(f'grid 1: ({sim_gridx[1]}, {sim_gridy[1]})', xy=(sim_gridx[1] + 2, sim_gridy[1])) -ax.plot(sim_gridx[1], sim_gridy[1], '+', markersize=10) +ax.annotate(f"grid 1: ({sim_gridx[1]}, {sim_gridy[1]})", xy=(sim_gridx[1] + 2, sim_gridy[1])) +ax.plot(sim_gridx[1], sim_gridy[1], "+", markersize=10) mx, my = obs_tree.data[indices[1]].T mz = zp[indices[1]] for x, y, z in zip(mx, my, mz): - d = np.sqrt((sim_gridx[1] - x)**2 + (y - sim_gridy[1])**2) - ax.plot([sim_gridx[1], x], [sim_gridy[1], y], '--') + d = np.sqrt((sim_gridx[1] - x) ** 2 + (y - sim_gridy[1]) ** 2) + ax.plot([sim_gridx[1], x], [sim_gridy[1], y], "--") xave = np.mean([sim_gridx[1], x]) yave = np.mean([sim_gridy[1], y]) - ax.annotate(f'distance: {d}', xy=(xave, yave)) - ax.annotate(f'({x}, {y}) : {z} F', xy=(x, y)) + ax.annotate(f"distance: {d}", xy=(xave, yave)) + ax.annotate(f"({x}, {y}) : {z} F", xy=(x, y)) ax.set_xlim(40, 80) ax.set_ylim(40, 100) -ax.set_aspect('equal', 'datalim') +ax.set_aspect("equal", "datalim") ########################################### # Step through barnes calculations. dists = np.array([9.21954445729, 22.4722050542, 27.892651362, 38.8329756779]) values = np.array([2.809, 6.241, 4.489, 2.704]) -weights = np.exp(-dists**2 / kappa) +weights = np.exp(-(dists ** 2) / kappa) total_weights = np.sum(weights) value = np.sum(values * (weights / total_weights)) -print('Manual barnes value:\t', value) -print('Metpy barnes value:\t', barnes_point(barnes_dist, barnes_obs, kappa)) +print("Manual barnes value:\t", value) +print("Metpy barnes value:\t", barnes_point(barnes_dist, barnes_obs, kappa)) plt.show() diff --git a/examples/gridding/Natural_Neighbor_Verification.py b/examples/gridding/Natural_Neighbor_Verification.py index 5bbb6b1e9a4..88cddb0e9e3 100644 --- a/examples/gridding/Natural_Neighbor_Verification.py +++ b/examples/gridding/Natural_Neighbor_Verification.py @@ -51,7 +51,7 @@ # 8. Repeat steps 4 through 7 for each grid cell. import matplotlib.pyplot as plt import numpy as np -from scipy.spatial import ConvexHull, Delaunay, delaunay_plot_2d, Voronoi, voronoi_plot_2d +from scipy.spatial import ConvexHull, Delaunay, Voronoi, delaunay_plot_2d, voronoi_plot_2d from scipy.spatial.distance import euclidean from metpy.interpolate import geometry @@ -80,25 +80,28 @@ delaunay_plot_2d(tri, ax=ax) for i, zval in enumerate(zp): - ax.annotate(f'{zval} F', xy=(pts[i, 0] + 2, pts[i, 1])) + ax.annotate(f"{zval} F", xy=(pts[i, 0] + 2, pts[i, 1])) -sim_gridx = [30., 60.] -sim_gridy = [30., 60.] +sim_gridx = [30.0, 60.0] +sim_gridy = [30.0, 60.0] -ax.plot(sim_gridx, sim_gridy, '+', markersize=10) -ax.set_aspect('equal', 'datalim') -ax.set_title('Triangulation of observations and test grid cell ' - 'natural neighbor interpolation values') +ax.plot(sim_gridx, sim_gridy, "+", markersize=10) +ax.set_aspect("equal", "datalim") +ax.set_title( + "Triangulation of observations and test grid cell " "natural neighbor interpolation values" +) members, circumcenters = geometry.find_natural_neighbors(tri, list(zip(sim_gridx, sim_gridy))) -val = natural_neighbor_point(xp, yp, zp, (sim_gridx[0], sim_gridy[0]), tri, members[0], - circumcenters) -ax.annotate(f'grid 0: {val:.3f}', xy=(sim_gridx[0] + 2, sim_gridy[0])) +val = natural_neighbor_point( + xp, yp, zp, (sim_gridx[0], sim_gridy[0]), tri, members[0], circumcenters +) +ax.annotate(f"grid 0: {val:.3f}", xy=(sim_gridx[0] + 2, sim_gridy[0])) -val = natural_neighbor_point(xp, yp, zp, (sim_gridx[1], sim_gridy[1]), tri, members[1], - circumcenters) -ax.annotate(f'grid 1: {val:.3f}', xy=(sim_gridx[1] + 2, sim_gridy[1])) +val = natural_neighbor_point( + xp, yp, zp, (sim_gridx[1], sim_gridy[1]), tri, members[1], circumcenters +) +ax.annotate(f"grid 1: {val:.3f}", xy=(sim_gridx[1] + 2, sim_gridy[1])) ########################################### @@ -115,24 +118,24 @@ def draw_circle(ax, x, y, r, m, label): fig, ax = plt.subplots(1, 1, figsize=(15, 10)) ax.ishold = lambda: True # Work-around for Matplotlib 3.0.0 incompatibility delaunay_plot_2d(tri, ax=ax) -ax.plot(sim_gridx, sim_gridy, 'ks', markersize=10) +ax.plot(sim_gridx, sim_gridy, "ks", markersize=10) for i, (x_t, y_t) in enumerate(circumcenters): r = geometry.circumcircle_radius(*tri.points[tri.simplices[i]]) if i in members[1] and i in members[0]: - draw_circle(ax, x_t, y_t, r, 'm-', str(i) + ': grid 1 & 2') + draw_circle(ax, x_t, y_t, r, "m-", str(i) + ": grid 1 & 2") ax.annotate(str(i), xy=(x_t, y_t), fontsize=15) elif i in members[0]: - draw_circle(ax, x_t, y_t, r, 'r-', str(i) + ': grid 0') + draw_circle(ax, x_t, y_t, r, "r-", str(i) + ": grid 0") ax.annotate(str(i), xy=(x_t, y_t), fontsize=15) elif i in members[1]: - draw_circle(ax, x_t, y_t, r, 'b-', str(i) + ': grid 1') + draw_circle(ax, x_t, y_t, r, "b-", str(i) + ": grid 1") ax.annotate(str(i), xy=(x_t, y_t), fontsize=15) else: - draw_circle(ax, x_t, y_t, r, 'k:', str(i) + ': no match') + draw_circle(ax, x_t, y_t, r, "k:", str(i) + ": no match") ax.annotate(str(i), xy=(x_t, y_t), fontsize=9) -ax.set_aspect('equal', 'datalim') +ax.set_aspect("equal", "datalim") ax.legend() ########################################### @@ -141,9 +144,11 @@ def draw_circle(ax, x, y, r, m, label): x_t, y_t = circumcenters[8] r = geometry.circumcircle_radius(*tri.points[tri.simplices[8]]) -print('Distance between grid0 and Triangle 8 circumcenter:', - euclidean([x_t, y_t], [sim_gridx[0], sim_gridy[0]])) -print('Triangle 8 circumradius:', r) +print( + "Distance between grid0 and Triangle 8 circumcenter:", + euclidean([x_t, y_t], [sim_gridx[0], sim_gridy[0]]), +) +print("Triangle 8 circumradius:", r) ########################################### # Lets do a manual check of the above interpolation value for grid 0 (southernmost grid) @@ -151,8 +156,8 @@ def draw_circle(ax, x, y, r, m, label): cc = np.array(circumcenters) r = np.array([geometry.circumcircle_radius(*tri.points[tri.simplices[m]]) for m in members[0]]) -print('circumcenters:\n', cc) -print('radii\n', r) +print("circumcenters:\n", cc) +print("radii\n", r) ########################################### # Draw the natural neighbor triangles and their circumcenters. Also plot a `Voronoi diagram @@ -173,36 +178,42 @@ def draw_circle(ax, x, y, r, m, label): y_0 = yp[nn_ind] for x, y, z in zip(x_0, y_0, z_0): - ax.annotate(f'{x}, {y}: {z:.3f} F', xy=(x, y)) - -ax.plot(sim_gridx[0], sim_gridy[0], 'k+', markersize=10) -ax.annotate(f'{sim_gridx[0]}, {sim_gridy[0]}', xy=(sim_gridx[0] + 2, sim_gridy[0])) -ax.plot(cc[:, 0], cc[:, 1], 'ks', markersize=15, fillstyle='none', - label='natural neighbor\ncircumcenters') + ax.annotate(f"{x}, {y}: {z:.3f} F", xy=(x, y)) + +ax.plot(sim_gridx[0], sim_gridy[0], "k+", markersize=10) +ax.annotate(f"{sim_gridx[0]}, {sim_gridy[0]}", xy=(sim_gridx[0] + 2, sim_gridy[0])) +ax.plot( + cc[:, 0], + cc[:, 1], + "ks", + markersize=15, + fillstyle="none", + label="natural neighbor\ncircumcenters", +) for center in cc: - ax.annotate(f'{center[0]:.3f}, {center[1]:.3f}', xy=(center[0] + 1, center[1] + 1)) + ax.annotate(f"{center[0]:.3f}, {center[1]:.3f}", xy=(center[0] + 1, center[1] + 1)) tris = tri.points[tri.simplices[members[0]]] for triangle in tris: x = [triangle[0, 0], triangle[1, 0], triangle[2, 0], triangle[0, 0]] y = [triangle[0, 1], triangle[1, 1], triangle[2, 1], triangle[0, 1]] - ax.plot(x, y, ':', linewidth=2) + ax.plot(x, y, ":", linewidth=2) ax.legend() -ax.set_aspect('equal', 'datalim') +ax.set_aspect("equal", "datalim") def draw_polygon_with_info(ax, polygon, off_x=0, off_y=0): """Draw one of the natural neighbor polygons with some information.""" pts = np.array(polygon)[ConvexHull(polygon).vertices] for i, pt in enumerate(pts): - ax.plot([pt[0], pts[(i + 1) % len(pts)][0]], - [pt[1], pts[(i + 1) % len(pts)][1]], 'k-') + ax.plot([pt[0], pts[(i + 1) % len(pts)][0]], [pt[1], pts[(i + 1) % len(pts)][1]], "k-") avex, avey = np.mean(pts, axis=0) - ax.annotate(f'area: {geometry.area(pts):.3f}', xy=(avex + off_x, avey + off_y), - fontsize=12) + ax.annotate( + f"area: {geometry.area(pts):.3f}", xy=(avex + off_x, avey + off_y), fontsize=12 + ) cc1 = geometry.circumcenter((53, 66), (15, 60), (30, 30)) @@ -242,8 +253,9 @@ def draw_polygon_with_info(ax, polygon, off_x=0, off_y=0): ########################################### # The sum of this array is the interpolation value! interpolation_value = np.sum(contributions) -function_output = natural_neighbor_point(xp, yp, zp, (sim_gridx[0], sim_gridy[0]), tri, - members[0], circumcenters) +function_output = natural_neighbor_point( + xp, yp, zp, (sim_gridx[0], sim_gridy[0]), tri, members[0], circumcenters +) print(interpolation_value, function_output) diff --git a/examples/gridding/Point_Interpolation.py b/examples/gridding/Point_Interpolation.py index 86336945765..532d137462c 100644 --- a/examples/gridding/Point_Interpolation.py +++ b/examples/gridding/Point_Interpolation.py @@ -15,8 +15,11 @@ import numpy as np from metpy.cbook import get_test_data -from metpy.interpolate import (interpolate_to_grid, remove_nan_observations, - remove_repeat_coordinates) +from metpy.interpolate import ( + interpolate_to_grid, + remove_nan_observations, + remove_repeat_coordinates, +) from metpy.plots import add_metpy_logo @@ -24,34 +27,54 @@ def basic_map(proj, title): """Make our basic default map for plotting""" fig = plt.figure(figsize=(15, 10)) - add_metpy_logo(fig, 0, 80, size='large') + add_metpy_logo(fig, 0, 80, size="large") view = fig.add_axes([0, 0, 1, 1], projection=proj) view.set_title(title) view.set_extent([-120, -70, 20, 50]) - view.add_feature(cfeature.STATES.with_scale('50m')) + view.add_feature(cfeature.STATES.with_scale("50m")) view.add_feature(cfeature.OCEAN) view.add_feature(cfeature.COASTLINE) - view.add_feature(cfeature.BORDERS, linestyle=':') + view.add_feature(cfeature.BORDERS, linestyle=":") return fig, view def station_test_data(variable_names, proj_from=None, proj_to=None): - with get_test_data('station_data.txt') as f: - all_data = np.loadtxt(f, skiprows=1, delimiter=',', - usecols=(1, 2, 3, 4, 5, 6, 7, 17, 18, 19), - dtype=np.dtype([('stid', '3S'), ('lat', 'f'), ('lon', 'f'), - ('slp', 'f'), ('air_temperature', 'f'), - ('cloud_fraction', 'f'), ('dewpoint', 'f'), - ('weather', '16S'), - ('wind_dir', 'f'), ('wind_speed', 'f')])) - - all_stids = [s.decode('ascii') for s in all_data['stid']] - - data = np.concatenate([all_data[all_stids.index(site)].reshape(1, ) for site in all_stids]) + with get_test_data("station_data.txt") as f: + all_data = np.loadtxt( + f, + skiprows=1, + delimiter=",", + usecols=(1, 2, 3, 4, 5, 6, 7, 17, 18, 19), + dtype=np.dtype( + [ + ("stid", "3S"), + ("lat", "f"), + ("lon", "f"), + ("slp", "f"), + ("air_temperature", "f"), + ("cloud_fraction", "f"), + ("dewpoint", "f"), + ("weather", "16S"), + ("wind_dir", "f"), + ("wind_speed", "f"), + ] + ), + ) + + all_stids = [s.decode("ascii") for s in all_data["stid"]] + + data = np.concatenate( + [ + all_data[all_stids.index(site)].reshape( + 1, + ) + for site in all_stids + ] + ) value = data[variable_names] - lon = data['lon'] - lat = data['lat'] + lon = data["lon"] + lat = data["lat"] if proj_from is not None and proj_to is not None: @@ -72,10 +95,10 @@ def station_test_data(variable_names, proj_from=None, proj_to=None): to_proj = ccrs.AlbersEqualArea(central_longitude=-97.0000, central_latitude=38.0000) levels = list(range(-20, 20, 1)) -cmap = plt.get_cmap('magma') +cmap = plt.get_cmap("magma") norm = BoundaryNorm(levels, ncolors=cmap.N, clip=True) -x, y, temp = station_test_data('air_temperature', from_proj, to_proj) +x, y, temp = station_test_data("air_temperature", from_proj, to_proj) x, y, temp = remove_nan_observations(x, y, temp) x, y, temp = remove_repeat_coordinates(x, y, temp) @@ -83,21 +106,21 @@ def station_test_data(variable_names, proj_from=None, proj_to=None): ########################################### # Scipy.interpolate linear # ------------------------ -gx, gy, img = interpolate_to_grid(x, y, temp, interp_type='linear', hres=75000) +gx, gy, img = interpolate_to_grid(x, y, temp, interp_type="linear", hres=75000) img = np.ma.masked_where(np.isnan(img), img) -fig, view = basic_map(to_proj, 'Linear') +fig, view = basic_map(to_proj, "Linear") mmb = view.pcolormesh(gx, gy, img, cmap=cmap, norm=norm) -fig.colorbar(mmb, shrink=.4, pad=0, boundaries=levels) +fig.colorbar(mmb, shrink=0.4, pad=0, boundaries=levels) ########################################### # Natural neighbor interpolation (MetPy implementation) # ----------------------------------------------------- # `Reference `_ -gx, gy, img = interpolate_to_grid(x, y, temp, interp_type='natural_neighbor', hres=75000) +gx, gy, img = interpolate_to_grid(x, y, temp, interp_type="natural_neighbor", hres=75000) img = np.ma.masked_where(np.isnan(img), img) -fig, view = basic_map(to_proj, 'Natural Neighbor') +fig, view = basic_map(to_proj, "Natural Neighbor") mmb = view.pcolormesh(gx, gy, img, cmap=cmap, norm=norm) -fig.colorbar(mmb, shrink=.4, pad=0, boundaries=levels) +fig.colorbar(mmb, shrink=0.4, pad=0, boundaries=levels) ########################################### # Cressman interpolation @@ -107,12 +130,13 @@ def station_test_data(variable_names, proj_from=None, proj_to=None): # grid resolution = 25 km # # min_neighbors = 1 -gx, gy, img = interpolate_to_grid(x, y, temp, interp_type='cressman', minimum_neighbors=1, - hres=75000, search_radius=100000) +gx, gy, img = interpolate_to_grid( + x, y, temp, interp_type="cressman", minimum_neighbors=1, hres=75000, search_radius=100000 +) img = np.ma.masked_where(np.isnan(img), img) -fig, view = basic_map(to_proj, 'Cressman') +fig, view = basic_map(to_proj, "Cressman") mmb = view.pcolormesh(gx, gy, img, cmap=cmap, norm=norm) -fig.colorbar(mmb, shrink=.4, pad=0, boundaries=levels) +fig.colorbar(mmb, shrink=0.4, pad=0, boundaries=levels) ########################################### # Barnes Interpolation @@ -120,22 +144,24 @@ def station_test_data(variable_names, proj_from=None, proj_to=None): # search_radius = 100km # # min_neighbors = 3 -gx, gy, img1 = interpolate_to_grid(x, y, temp, interp_type='barnes', hres=75000, - search_radius=100000) +gx, gy, img1 = interpolate_to_grid( + x, y, temp, interp_type="barnes", hres=75000, search_radius=100000 +) img1 = np.ma.masked_where(np.isnan(img1), img1) -fig, view = basic_map(to_proj, 'Barnes') +fig, view = basic_map(to_proj, "Barnes") mmb = view.pcolormesh(gx, gy, img1, cmap=cmap, norm=norm) -fig.colorbar(mmb, shrink=.4, pad=0, boundaries=levels) +fig.colorbar(mmb, shrink=0.4, pad=0, boundaries=levels) ########################################### # Radial basis function interpolation # ------------------------------------ # linear -gx, gy, img = interpolate_to_grid(x, y, temp, interp_type='rbf', hres=75000, rbf_func='linear', - rbf_smooth=0) +gx, gy, img = interpolate_to_grid( + x, y, temp, interp_type="rbf", hres=75000, rbf_func="linear", rbf_smooth=0 +) img = np.ma.masked_where(np.isnan(img), img) -fig, view = basic_map(to_proj, 'Radial Basis Function') +fig, view = basic_map(to_proj, "Radial Basis Function") mmb = view.pcolormesh(gx, gy, img, cmap=cmap, norm=norm) -fig.colorbar(mmb, shrink=.4, pad=0, boundaries=levels) +fig.colorbar(mmb, shrink=0.4, pad=0, boundaries=levels) plt.show() diff --git a/examples/gridding/Wind_SLP_Interpolation.py b/examples/gridding/Wind_SLP_Interpolation.py index 8970e8c7356..a3d1b8dbf9a 100644 --- a/examples/gridding/Wind_SLP_Interpolation.py +++ b/examples/gridding/Wind_SLP_Interpolation.py @@ -21,36 +21,45 @@ from metpy.plots import add_metpy_logo from metpy.units import units -to_proj = ccrs.AlbersEqualArea(central_longitude=-97., central_latitude=38.) +to_proj = ccrs.AlbersEqualArea(central_longitude=-97.0, central_latitude=38.0) ########################################### # Read in data -with get_test_data('station_data.txt') as f: - data = pd.read_csv(f, header=0, usecols=(2, 3, 4, 5, 18, 19), - names=['latitude', 'longitude', 'slp', 'temperature', 'wind_dir', - 'wind_speed'], - na_values=-99999) +with get_test_data("station_data.txt") as f: + data = pd.read_csv( + f, + header=0, + usecols=(2, 3, 4, 5, 18, 19), + names=["latitude", "longitude", "slp", "temperature", "wind_dir", "wind_speed"], + na_values=-99999, + ) ########################################### # Project the lon/lat locations to our final projection -lon = data['longitude'].values -lat = data['latitude'].values +lon = data["longitude"].values +lat = data["latitude"].values xp, yp, _ = to_proj.transform_points(ccrs.Geodetic(), lon, lat).T ########################################### # Remove all missing data from pressure -x_masked, y_masked, pres = remove_nan_observations(xp, yp, data['slp'].values) +x_masked, y_masked, pres = remove_nan_observations(xp, yp, data["slp"].values) ########################################### # Interpolate pressure using Cressman interpolation -slpgridx, slpgridy, slp = interpolate_to_grid(x_masked, y_masked, pres, interp_type='cressman', - minimum_neighbors=1, search_radius=400000, - hres=100000) +slpgridx, slpgridy, slp = interpolate_to_grid( + x_masked, + y_masked, + pres, + interp_type="cressman", + minimum_neighbors=1, + search_radius=400000, + hres=100000, +) ########################################## # Get wind information and mask where either speed or direction is unavailable -wind_speed = (data['wind_speed'].values * units('m/s')).to('knots') -wind_dir = data['wind_dir'].values * units.degree +wind_speed = (data["wind_speed"].values * units("m/s")).to("knots") +wind_dir = data["wind_dir"].values * units.degree good_indices = np.where((~np.isnan(wind_dir)) & (~np.isnan(wind_speed))) @@ -65,45 +74,53 @@ # Both will have the same underlying grid so throw away grid returned from v interpolation. u, v = wind_components(wind_speed, wind_dir) -windgridx, windgridy, uwind = interpolate_to_grid(x_masked, y_masked, np.array(u), - interp_type='cressman', search_radius=400000, - hres=100000) +windgridx, windgridy, uwind = interpolate_to_grid( + x_masked, y_masked, np.array(u), interp_type="cressman", search_radius=400000, hres=100000 +) -_, _, vwind = interpolate_to_grid(x_masked, y_masked, np.array(v), interp_type='cressman', - search_radius=400000, hres=100000) +_, _, vwind = interpolate_to_grid( + x_masked, y_masked, np.array(v), interp_type="cressman", search_radius=400000, hres=100000 +) ########################################### # Get temperature information -x_masked, y_masked, t = remove_nan_observations(xp, yp, data['temperature'].values) -tempx, tempy, temp = interpolate_to_grid(x_masked, y_masked, t, interp_type='cressman', - minimum_neighbors=3, search_radius=400000, hres=35000) +x_masked, y_masked, t = remove_nan_observations(xp, yp, data["temperature"].values) +tempx, tempy, temp = interpolate_to_grid( + x_masked, + y_masked, + t, + interp_type="cressman", + minimum_neighbors=3, + search_radius=400000, + hres=35000, +) temp = np.ma.masked_where(np.isnan(temp), temp) ########################################### # Set up the map and plot the interpolated grids appropriately. levels = list(range(-20, 20, 1)) -cmap = plt.get_cmap('viridis') +cmap = plt.get_cmap("viridis") norm = BoundaryNorm(levels, ncolors=cmap.N, clip=True) fig = plt.figure(figsize=(20, 10)) -add_metpy_logo(fig, 360, 120, size='large') +add_metpy_logo(fig, 360, 120, size="large") view = fig.add_subplot(1, 1, 1, projection=to_proj) view.set_extent([-120, -70, 20, 50]) -view.add_feature(cfeature.STATES.with_scale('50m')) +view.add_feature(cfeature.STATES.with_scale("50m")) view.add_feature(cfeature.OCEAN) -view.add_feature(cfeature.COASTLINE.with_scale('50m')) -view.add_feature(cfeature.BORDERS, linestyle=':') +view.add_feature(cfeature.COASTLINE.with_scale("50m")) +view.add_feature(cfeature.BORDERS, linestyle=":") -cs = view.contour(slpgridx, slpgridy, slp, colors='k', levels=list(range(990, 1034, 4))) -view.clabel(cs, inline=1, fontsize=12, fmt='%i') +cs = view.contour(slpgridx, slpgridy, slp, colors="k", levels=list(range(990, 1034, 4))) +view.clabel(cs, inline=1, fontsize=12, fmt="%i") mmb = view.pcolormesh(tempx, tempy, temp, cmap=cmap, norm=norm) -fig.colorbar(mmb, shrink=.4, pad=0.02, boundaries=levels) +fig.colorbar(mmb, shrink=0.4, pad=0.02, boundaries=levels) -view.barbs(windgridx, windgridy, uwind, vwind, alpha=.4, length=5) +view.barbs(windgridx, windgridy, uwind, vwind, alpha=0.4, length=5) -view.set_title('Surface Temperature (shaded), SLP, and Wind.') +view.set_title("Surface Temperature (shaded), SLP, and Wind.") plt.show() diff --git a/examples/isentropic_example.py b/examples/isentropic_example.py index fee7532ac66..e91f940ce96 100644 --- a/examples/isentropic_example.py +++ b/examples/isentropic_example.py @@ -30,7 +30,7 @@ # for 18 UTC 04 April 1987 from the National Centers for Environmental Information will be # used. -data = xr.open_dataset(get_test_data('narr_example.nc', False)) +data = xr.open_dataset(get_test_data("narr_example.nc", False)) ########################## print(list(data.variables)) @@ -39,13 +39,13 @@ # We will reduce the dimensionality of the data as it is pulled in to remove an empty time # dimension, as well as add longitude and latitude as coordinates (instead of data variables). -data = data.squeeze().set_coords(['lon', 'lat']) +data = data.squeeze().set_coords(["lon", "lat"]) ############################# # To properly interpolate to isentropic coordinates, the function must know the desired output # isentropic levels. An array with these levels will be created below. -isentlevs = [296.] * units.kelvin +isentlevs = [296.0] * units.kelvin #################################### # **Conversion to Isentropic Coordinates** @@ -59,11 +59,11 @@ isent_data = mpcalc.isentropic_interpolation_as_dataset( isentlevs, - data['Temperature'], - data['u_wind'], - data['v_wind'], - data['Specific_humidity'], - data['Geopotential_height'] + data["Temperature"], + data["u_wind"], + data["v_wind"], + data["Specific_humidity"], + data["Geopotential_height"], ) ##################################### @@ -75,8 +75,8 @@ # Note that the units on our wind variables are not ideal for plotting. Instead, let us # convert them to more appropriate values. -isent_data['u_wind'] = isent_data['u_wind'].metpy.convert_units('kt') -isent_data['v_wind'] = isent_data['v_wind'].metpy.convert_units('kt') +isent_data["u_wind"] = isent_data["u_wind"].metpy.convert_units("kt") +isent_data["v_wind"] = isent_data["v_wind"].metpy.convert_units("kt") ################################# # **Converting to Relative Humidity** @@ -84,57 +84,83 @@ # The NARR only gives specific humidity on isobaric vertical levels, so relative humidity will # have to be calculated after the interpolation to isentropic space. -isent_data['Relative_humidity'] = mpcalc.relative_humidity_from_specific_humidity( - isent_data['pressure'], - isent_data['temperature'], - isent_data['Specific_humidity'] -).metpy.convert_units('percent') +isent_data["Relative_humidity"] = mpcalc.relative_humidity_from_specific_humidity( + isent_data["pressure"], isent_data["temperature"], isent_data["Specific_humidity"] +).metpy.convert_units("percent") ####################################### # **Plotting the Isentropic Analysis** # Set up our projection and coordinates crs = ccrs.LambertConformal(central_longitude=-100.0, central_latitude=45.0) -lon = isent_data['pressure'].metpy.longitude -lat = isent_data['pressure'].metpy.latitude +lon = isent_data["pressure"].metpy.longitude +lat = isent_data["pressure"].metpy.latitude # Coordinates to limit map area -bounds = [(-122., -75., 25., 50.)] +bounds = [(-122.0, -75.0, 25.0, 50.0)] # Choose a level to plot, in this case 296 K (our sole level in this example) level = 0 -fig = plt.figure(figsize=(17., 12.)) -add_metpy_logo(fig, 120, 245, size='large') +fig = plt.figure(figsize=(17.0, 12.0)) +add_metpy_logo(fig, 120, 245, size="large") ax = fig.add_subplot(1, 1, 1, projection=crs) ax.set_extent(*bounds, crs=ccrs.PlateCarree()) -ax.add_feature(cfeature.COASTLINE.with_scale('50m'), linewidth=0.75) +ax.add_feature(cfeature.COASTLINE.with_scale("50m"), linewidth=0.75) ax.add_feature(cfeature.STATES, linewidth=0.5) # Plot the surface clevisent = np.arange(0, 1000, 25) -cs = ax.contour(lon, lat, isent_data['pressure'].isel(isentropic_level=level), - clevisent, colors='k', linewidths=1.0, linestyles='solid', - transform=ccrs.PlateCarree()) -cs.clabel(fontsize=10, inline=1, inline_spacing=7, fmt='%i', rightside_up=True, - use_clabeltext=True) +cs = ax.contour( + lon, + lat, + isent_data["pressure"].isel(isentropic_level=level), + clevisent, + colors="k", + linewidths=1.0, + linestyles="solid", + transform=ccrs.PlateCarree(), +) +cs.clabel( + fontsize=10, inline=1, inline_spacing=7, fmt="%i", rightside_up=True, use_clabeltext=True +) # Plot RH -cf = ax.contourf(lon, lat, isent_data['Relative_humidity'].isel(isentropic_level=level), - range(10, 106, 5), cmap=plt.cm.gist_earth_r, transform=ccrs.PlateCarree()) -cb = fig.colorbar(cf, orientation='horizontal', aspect=65, shrink=0.5, pad=0.05, - extendrect='True') -cb.set_label('Relative Humidity', size='x-large') +cf = ax.contourf( + lon, + lat, + isent_data["Relative_humidity"].isel(isentropic_level=level), + range(10, 106, 5), + cmap=plt.cm.gist_earth_r, + transform=ccrs.PlateCarree(), +) +cb = fig.colorbar( + cf, orientation="horizontal", aspect=65, shrink=0.5, pad=0.05, extendrect="True" +) +cb.set_label("Relative Humidity", size="x-large") # Plot wind barbs -ax.barbs(lon.values, lat.values, isent_data['u_wind'].isel(isentropic_level=level).values, - isent_data['v_wind'].isel(isentropic_level=level).values, length=6, - regrid_shape=20, transform=ccrs.PlateCarree()) +ax.barbs( + lon.values, + lat.values, + isent_data["u_wind"].isel(isentropic_level=level).values, + isent_data["v_wind"].isel(isentropic_level=level).values, + length=6, + regrid_shape=20, + transform=ccrs.PlateCarree(), +) # Make some titles -ax.set_title(f'{isentlevs[level]:~.0f} Isentropic Pressure (hPa), Wind (kt), ' - 'Relative Humidity (percent)', loc='left') -add_timestamp(ax, isent_data['time'].values.astype('datetime64[ms]').astype('O'), - y=0.02, high_contrast=True) +ax.set_title( + f"{isentlevs[level]:~.0f} Isentropic Pressure (hPa), Wind (kt), " + "Relative Humidity (percent)", + loc="left", +) +add_timestamp( + ax, + isent_data["time"].values.astype("datetime64[ms]").astype("O"), + y=0.02, + high_contrast=True, +) fig.tight_layout() ###################################### @@ -146,45 +172,77 @@ # Calculate Montgomery Streamfunction and scale by 10^-2 for plotting -msf = mpcalc.montgomery_streamfunction( - isent_data['Geopotential_height'], - isent_data['temperature'] -).values / 100. +msf = ( + mpcalc.montgomery_streamfunction( + isent_data["Geopotential_height"], isent_data["temperature"] + ).values + / 100.0 +) # Choose a level to plot, in this case 296 K level = 0 -fig = plt.figure(figsize=(17., 12.)) -add_metpy_logo(fig, 120, 250, size='large') +fig = plt.figure(figsize=(17.0, 12.0)) +add_metpy_logo(fig, 120, 250, size="large") ax = plt.subplot(111, projection=crs) ax.set_extent(*bounds, crs=ccrs.PlateCarree()) -ax.add_feature(cfeature.COASTLINE.with_scale('50m'), linewidth=0.75) -ax.add_feature(cfeature.STATES.with_scale('50m'), linewidth=0.5) +ax.add_feature(cfeature.COASTLINE.with_scale("50m"), linewidth=0.75) +ax.add_feature(cfeature.STATES.with_scale("50m"), linewidth=0.5) # Plot the surface clevmsf = np.arange(0, 4000, 5) -cs = ax.contour(lon, lat, msf[level, :, :], clevmsf, - colors='k', linewidths=1.0, linestyles='solid', transform=ccrs.PlateCarree()) -cs.clabel(fontsize=10, inline=1, inline_spacing=7, fmt='%i', rightside_up=True, - use_clabeltext=True) +cs = ax.contour( + lon, + lat, + msf[level, :, :], + clevmsf, + colors="k", + linewidths=1.0, + linestyles="solid", + transform=ccrs.PlateCarree(), +) +cs.clabel( + fontsize=10, inline=1, inline_spacing=7, fmt="%i", rightside_up=True, use_clabeltext=True +) # Plot RH -cf = ax.contourf(lon, lat, isent_data['Relative_humidity'].isel(isentropic_level=level), - range(10, 106, 5), cmap=plt.cm.gist_earth_r, transform=ccrs.PlateCarree()) -cb = fig.colorbar(cf, orientation='horizontal', aspect=65, shrink=0.5, pad=0.05, - extendrect='True') -cb.set_label('Relative Humidity', size='x-large') +cf = ax.contourf( + lon, + lat, + isent_data["Relative_humidity"].isel(isentropic_level=level), + range(10, 106, 5), + cmap=plt.cm.gist_earth_r, + transform=ccrs.PlateCarree(), +) +cb = fig.colorbar( + cf, orientation="horizontal", aspect=65, shrink=0.5, pad=0.05, extendrect="True" +) +cb.set_label("Relative Humidity", size="x-large") # Plot wind barbs -ax.barbs(lon.values, lat.values, isent_data['u_wind'].isel(isentropic_level=level).values, - isent_data['v_wind'].isel(isentropic_level=level).values, length=6, - regrid_shape=20, transform=ccrs.PlateCarree()) +ax.barbs( + lon.values, + lat.values, + isent_data["u_wind"].isel(isentropic_level=level).values, + isent_data["v_wind"].isel(isentropic_level=level).values, + length=6, + regrid_shape=20, + transform=ccrs.PlateCarree(), +) # Make some titles -ax.set_title(f'{isentlevs[level]:~.0f} Montgomery Streamfunction ' - r'($10^{-2} m^2 s^{-2}$), Wind (kt), Relative Humidity (percent)', loc='left') -add_timestamp(ax, isent_data['time'].values.astype('datetime64[ms]').astype('O'), - y=0.02, pretext='Valid: ', high_contrast=True) +ax.set_title( + f"{isentlevs[level]:~.0f} Montgomery Streamfunction " + r"($10^{-2} m^2 s^{-2}$), Wind (kt), Relative Humidity (percent)", + loc="left", +) +add_timestamp( + ax, + isent_data["time"].values.astype("datetime64[ms]").astype("O"), + y=0.02, + pretext="Valid: ", + high_contrast=True, +) fig.tight_layout() plt.show() diff --git a/examples/meteogram_metpy.py b/examples/meteogram_metpy.py index b830442b145..9abec4ee25b 100644 --- a/examples/meteogram_metpy.py +++ b/examples/meteogram_metpy.py @@ -26,12 +26,12 @@ def calc_mslp(t, p, h): # Make meteogram plot class Meteogram: - """ Plot a time series of meteorological data from a particular station as a + """Plot a time series of meteorological data from a particular station as a meteogram with standard variables to visualize, including thermodynamic, kinematic, and pressure. The functions below control the plotting of each variable. TO DO: Make the subplot creation dynamic so the number of rows is not - static as it is currently. """ + static as it is currently.""" def __init__(self, fig, dates, probeid, time=None, axis=0): """ @@ -50,8 +50,8 @@ def __init__(self, fig, dates, probeid, time=None, axis=0): self.end = dates[-1] self.axis_num = 0 self.dates = mpl.dates.date2num(dates) - self.time = time.strftime('%Y-%m-%d %H:%M UTC') - self.title = f'Latest Ob Time: {self.time}\nProbe ID: {probeid}' + self.time = time.strftime("%Y-%m-%d %H:%M UTC") + self.title = f"Latest Ob Time: {self.time}\nProbe ID: {probeid}" def plot_winds(self, ws, wd, wsmax, plot_range=None): """ @@ -64,27 +64,34 @@ def plot_winds(self, ws, wd, wsmax, plot_range=None): """ # PLOT WIND SPEED AND WIND DIRECTION self.ax1 = fig.add_subplot(4, 1, 1) - ln1 = self.ax1.plot(self.dates, ws, label='Wind Speed') + ln1 = self.ax1.plot(self.dates, ws, label="Wind Speed") self.ax1.fill_between(self.dates, ws, 0) self.ax1.set_xlim(self.start, self.end) if not plot_range: plot_range = [0, 20, 1] - self.ax1.set_ylabel('Wind Speed (knots)', multialignment='center') + self.ax1.set_ylabel("Wind Speed (knots)", multialignment="center") self.ax1.set_ylim(plot_range[0], plot_range[1], plot_range[2]) - self.ax1.grid(b=True, which='major', axis='y', color='k', linestyle='--', - linewidth=0.5) - ln2 = self.ax1.plot(self.dates, wsmax, '.r', label='3-sec Wind Speed Max') + self.ax1.grid( + b=True, which="major", axis="y", color="k", linestyle="--", linewidth=0.5 + ) + ln2 = self.ax1.plot(self.dates, wsmax, ".r", label="3-sec Wind Speed Max") ax7 = self.ax1.twinx() - ln3 = ax7.plot(self.dates, wd, '.k', linewidth=0.5, label='Wind Direction') - ax7.set_ylabel('Wind\nDirection\n(degrees)', multialignment='center') + ln3 = ax7.plot(self.dates, wd, ".k", linewidth=0.5, label="Wind Direction") + ax7.set_ylabel("Wind\nDirection\n(degrees)", multialignment="center") ax7.set_ylim(0, 360) - ax7.set_yticks(np.arange(45, 405, 90), ['NE', 'SE', 'SW', 'NW']) + ax7.set_yticks(np.arange(45, 405, 90), ["NE", "SE", "SW", "NW"]) lines = ln1 + ln2 + ln3 labs = [line.get_label() for line in lines] - ax7.xaxis.set_major_formatter(mpl.dates.DateFormatter('%d/%H UTC')) - ax7.legend(lines, labs, loc='upper center', - bbox_to_anchor=(0.5, 1.2), ncol=3, prop={'size': 12}) + ax7.xaxis.set_major_formatter(mpl.dates.DateFormatter("%d/%H UTC")) + ax7.legend( + lines, + labs, + loc="upper center", + bbox_to_anchor=(0.5, 1.2), + ncol=3, + prop={"size": 12}, + ) def plot_thermo(self, t, td, plot_range=None): """ @@ -98,25 +105,32 @@ def plot_thermo(self, t, td, plot_range=None): if not plot_range: plot_range = [10, 90, 2] self.ax2 = fig.add_subplot(4, 1, 2, sharex=self.ax1) - ln4 = self.ax2.plot(self.dates, t, 'r-', label='Temperature') - self.ax2.fill_between(self.dates, t, td, color='r') + ln4 = self.ax2.plot(self.dates, t, "r-", label="Temperature") + self.ax2.fill_between(self.dates, t, td, color="r") - self.ax2.set_ylabel('Temperature\n(F)', multialignment='center') - self.ax2.grid(b=True, which='major', axis='y', color='k', linestyle='--', - linewidth=0.5) + self.ax2.set_ylabel("Temperature\n(F)", multialignment="center") + self.ax2.grid( + b=True, which="major", axis="y", color="k", linestyle="--", linewidth=0.5 + ) self.ax2.set_ylim(plot_range[0], plot_range[1], plot_range[2]) - ln5 = self.ax2.plot(self.dates, td, 'g-', label='Dewpoint') - self.ax2.fill_between(self.dates, td, self.ax2.get_ylim()[0], color='g') + ln5 = self.ax2.plot(self.dates, td, "g-", label="Dewpoint") + self.ax2.fill_between(self.dates, td, self.ax2.get_ylim()[0], color="g") ax_twin = self.ax2.twinx() ax_twin.set_ylim(plot_range[0], plot_range[1], plot_range[2]) lines = ln4 + ln5 labs = [line.get_label() for line in lines] - ax_twin.xaxis.set_major_formatter(mpl.dates.DateFormatter('%d/%H UTC')) + ax_twin.xaxis.set_major_formatter(mpl.dates.DateFormatter("%d/%H UTC")) - self.ax2.legend(lines, labs, loc='upper center', - bbox_to_anchor=(0.5, 1.2), ncol=2, prop={'size': 12}) + self.ax2.legend( + lines, + labs, + loc="upper center", + bbox_to_anchor=(0.5, 1.2), + ncol=2, + prop={"size": 12}, + ) def plot_rh(self, rh, plot_range=None): """ @@ -129,15 +143,16 @@ def plot_rh(self, rh, plot_range=None): if not plot_range: plot_range = [0, 100, 4] self.ax3 = fig.add_subplot(4, 1, 3, sharex=self.ax1) - self.ax3.plot(self.dates, rh, 'g-', label='Relative Humidity') - self.ax3.legend(loc='upper center', bbox_to_anchor=(0.5, 1.22), prop={'size': 12}) - self.ax3.grid(b=True, which='major', axis='y', color='k', linestyle='--', - linewidth=0.5) + self.ax3.plot(self.dates, rh, "g-", label="Relative Humidity") + self.ax3.legend(loc="upper center", bbox_to_anchor=(0.5, 1.22), prop={"size": 12}) + self.ax3.grid( + b=True, which="major", axis="y", color="k", linestyle="--", linewidth=0.5 + ) self.ax3.set_ylim(plot_range[0], plot_range[1], plot_range[2]) - self.ax3.fill_between(self.dates, rh, self.ax3.get_ylim()[0], color='g') - self.ax3.set_ylabel('Relative Humidity\n(%)', multialignment='center') - self.ax3.xaxis.set_major_formatter(mpl.dates.DateFormatter('%d/%H UTC')) + self.ax3.fill_between(self.dates, rh, self.ax3.get_ylim()[0], color="g") + self.ax3.set_ylabel("Relative Humidity\n(%)", multialignment="center") + self.ax3.xaxis.set_major_formatter(mpl.dates.DateFormatter("%d/%H UTC")) axtwin = self.ax3.twinx() axtwin.set_ylim(plot_range[0], plot_range[1], plot_range[2]) @@ -152,18 +167,19 @@ def plot_pressure(self, p, plot_range=None): if not plot_range: plot_range = [970, 1030, 2] self.ax4 = fig.add_subplot(4, 1, 4, sharex=self.ax1) - self.ax4.plot(self.dates, p, 'm', label='Mean Sea Level Pressure') - self.ax4.set_ylabel('Mean Sea\nLevel Pressure\n(mb)', multialignment='center') + self.ax4.plot(self.dates, p, "m", label="Mean Sea Level Pressure") + self.ax4.set_ylabel("Mean Sea\nLevel Pressure\n(mb)", multialignment="center") self.ax4.set_ylim(plot_range[0], plot_range[1], plot_range[2]) axtwin = self.ax4.twinx() axtwin.set_ylim(plot_range[0], plot_range[1], plot_range[2]) - axtwin.fill_between(self.dates, p, axtwin.get_ylim()[0], color='m') - axtwin.xaxis.set_major_formatter(mpl.dates.DateFormatter('%d/%H UTC')) + axtwin.fill_between(self.dates, p, axtwin.get_ylim()[0], color="m") + axtwin.xaxis.set_major_formatter(mpl.dates.DateFormatter("%d/%H UTC")) - self.ax4.legend(loc='upper center', bbox_to_anchor=(0.5, 1.2), prop={'size': 12}) - self.ax4.grid(b=True, which='major', axis='y', color='k', linestyle='--', - linewidth=0.5) + self.ax4.legend(loc="upper center", bbox_to_anchor=(0.5, 1.2), prop={"size": 12}) + self.ax4.grid( + b=True, which="major", axis="y", color="k", linestyle="--", linewidth=0.5 + ) # OTHER OPTIONAL AXES TO PLOT # plot_irradiance # plot_precipitation @@ -174,45 +190,54 @@ def plot_pressure(self, p, plot_range=None): starttime = endtime - dt.timedelta(hours=24) # Height of the station to calculate MSLP -hgt_example = 292. +hgt_example = 292.0 # Parse dates from .csv file, knowing their format as a string and convert to datetime def parse_date(date): - return dt.datetime.strptime(date.decode('ascii'), '%Y-%m-%d %H:%M:%S') + return dt.datetime.strptime(date.decode("ascii"), "%Y-%m-%d %H:%M:%S") -testdata = np.genfromtxt(get_test_data('timeseries.csv', False), names=True, dtype=None, - usecols=list(range(1, 8)), - converters={'DATE': parse_date}, delimiter=',') +testdata = np.genfromtxt( + get_test_data("timeseries.csv", False), + names=True, + dtype=None, + usecols=list(range(1, 8)), + converters={"DATE": parse_date}, + delimiter=",", +) # Temporary variables for ease -temp = testdata['T'] -pres = testdata['P'] -rh = testdata['RH'] -ws = testdata['WS'] -wsmax = testdata['WSMAX'] -wd = testdata['WD'] -date = testdata['DATE'] +temp = testdata["T"] +pres = testdata["P"] +rh = testdata["RH"] +ws = testdata["WS"] +wsmax = testdata["WSMAX"] +wd = testdata["WD"] +date = testdata["DATE"] # ID For Plotting on Meteogram -probe_id = '0102A' - -data = {'wind_speed': (np.array(ws) * units('m/s')).to(units('knots')), - 'wind_speed_max': (np.array(wsmax) * units('m/s')).to(units('knots')), - 'wind_direction': np.array(wd) * units('degrees'), - 'dewpoint': dewpoint_from_relative_humidity((np.array(temp) * units.degC).to(units.K), - np.array(rh) / 100.).to(units('degF')), - 'air_temperature': (np.array(temp) * units('degC')).to(units('degF')), - 'mean_slp': calc_mslp(np.array(temp), np.array(pres), hgt_example) * units('hPa'), - 'relative_humidity': np.array(rh), 'times': np.array(date)} +probe_id = "0102A" + +data = { + "wind_speed": (np.array(ws) * units("m/s")).to(units("knots")), + "wind_speed_max": (np.array(wsmax) * units("m/s")).to(units("knots")), + "wind_direction": np.array(wd) * units("degrees"), + "dewpoint": dewpoint_from_relative_humidity( + (np.array(temp) * units.degC).to(units.K), np.array(rh) / 100.0 + ).to(units("degF")), + "air_temperature": (np.array(temp) * units("degC")).to(units("degF")), + "mean_slp": calc_mslp(np.array(temp), np.array(pres), hgt_example) * units("hPa"), + "relative_humidity": np.array(rh), + "times": np.array(date), +} fig = plt.figure(figsize=(20, 16)) add_metpy_logo(fig, 250, 180) -meteogram = Meteogram(fig, data['times'], probe_id) -meteogram.plot_winds(data['wind_speed'], data['wind_direction'], data['wind_speed_max']) -meteogram.plot_thermo(data['air_temperature'], data['dewpoint']) -meteogram.plot_rh(data['relative_humidity']) -meteogram.plot_pressure(data['mean_slp']) +meteogram = Meteogram(fig, data["times"], probe_id) +meteogram.plot_winds(data["wind_speed"], data["wind_direction"], data["wind_speed_max"]) +meteogram.plot_thermo(data["air_temperature"], data["dewpoint"]) +meteogram.plot_rh(data["relative_humidity"]) +meteogram.plot_pressure(data["mean_slp"]) fig.subplots_adjust(hspace=0.5) plt.show() diff --git a/examples/plots/Combined_plotting.py b/examples/plots/Combined_plotting.py index 25bf220c06f..940bb042976 100644 --- a/examples/plots/Combined_plotting.py +++ b/examples/plots/Combined_plotting.py @@ -17,31 +17,31 @@ from metpy.units import units # Use sample NARR data for plotting -narr = xr.open_dataset(get_test_data('narr_example.nc', as_file_obj=False)) +narr = xr.open_dataset(get_test_data("narr_example.nc", as_file_obj=False)) ########################### # Create a contour plot of temperature contour = ContourPlot() contour.data = narr -contour.field = 'Temperature' +contour.field = "Temperature" contour.level = 850 * units.hPa -contour.linecolor = 'red' +contour.linecolor = "red" contour.contours = 15 ########################### # Create an image plot of Geopotential height img = ImagePlot() img.data = narr -img.field = 'Geopotential_height' +img.field = "Geopotential_height" img.level = 850 * units.hPa ########################### # Plot the data on a map panel = MapPanel() -panel.area = 'us' -panel.layers = ['coastline', 'borders', 'states', 'rivers', 'ocean', 'land'] -panel.title = 'NARR Example' +panel.area = "us" +panel.layers = ["coastline", "borders", "states", "rivers", "ocean", "land"] +panel.title = "NARR Example" panel.plots = [contour, img] pc = PanelContainer() diff --git a/examples/plots/Hodograph_Inset.py b/examples/plots/Hodograph_Inset.py index 76d4d26a388..0d778585a9d 100644 --- a/examples/plots/Hodograph_Inset.py +++ b/examples/plots/Hodograph_Inset.py @@ -14,32 +14,37 @@ import metpy.calc as mpcalc from metpy.cbook import get_test_data -from metpy.plots import add_metpy_logo, Hodograph, SkewT +from metpy.plots import Hodograph, SkewT, add_metpy_logo from metpy.units import units ########################################### # Upper air data can be obtained using the siphon package, but for this example we will use # some of MetPy's sample data. -col_names = ['pressure', 'height', 'temperature', 'dewpoint', 'direction', 'speed'] +col_names = ["pressure", "height", "temperature", "dewpoint", "direction", "speed"] -df = pd.read_fwf(get_test_data('may4_sounding.txt', as_file_obj=False), - skiprows=5, usecols=[0, 1, 2, 3, 6, 7], names=col_names) +df = pd.read_fwf( + get_test_data("may4_sounding.txt", as_file_obj=False), + skiprows=5, + usecols=[0, 1, 2, 3, 6, 7], + names=col_names, +) # Drop any rows with all NaN values for T, Td, winds -df = df.dropna(subset=('temperature', 'dewpoint', 'direction', 'speed' - ), how='all').reset_index(drop=True) +df = df.dropna( + subset=("temperature", "dewpoint", "direction", "speed"), how="all" +).reset_index(drop=True) ########################################### # We will pull the data out of the example dataset into individual variables and # assign units. -hght = df['height'].values * units.hPa -p = df['pressure'].values * units.hPa -T = df['temperature'].values * units.degC -Td = df['dewpoint'].values * units.degC -wind_speed = df['speed'].values * units.knots -wind_dir = df['direction'].values * units.degrees +hght = df["height"].values * units.hPa +p = df["pressure"].values * units.hPa +T = df["temperature"].values * units.degC +Td = df["dewpoint"].values * units.degC +wind_speed = df["speed"].values * units.knots +wind_dir = df["direction"].values * units.degrees u, v = mpcalc.wind_components(wind_speed, wind_dir) ########################################### @@ -53,8 +58,8 @@ # Plot the data using normal plotting functions, in this case using # log scaling in Y, as dictated by the typical meteorological plot -skew.plot(p, T, 'r') -skew.plot(p, Td, 'g') +skew.plot(p, T, "r") +skew.plot(p, Td, "g") skew.plot_barbs(p, u, v) skew.ax.set_ylim(1000, 100) @@ -67,8 +72,8 @@ skew.ax.set_xlim(-50, 60) # Create a hodograph -ax_hod = inset_axes(skew.ax, '40%', '40%', loc=1) -h = Hodograph(ax_hod, component_range=80.) +ax_hod = inset_axes(skew.ax, "40%", "40%", loc=1) +h = Hodograph(ax_hod, component_range=80.0) h.add_grid(increment=20) h.plot_colormapped(u, v, hght) diff --git a/examples/plots/Mesonet_Stationplot.py b/examples/plots/Mesonet_Stationplot.py index d505eb05dc1..9725a71f3a4 100644 --- a/examples/plots/Mesonet_Stationplot.py +++ b/examples/plots/Mesonet_Stationplot.py @@ -17,7 +17,7 @@ import metpy.calc as mpcalc from metpy.cbook import get_test_data -from metpy.plots import add_metpy_logo, StationPlot +from metpy.plots import StationPlot, add_metpy_logo from metpy.units import units ########################################### @@ -30,10 +30,10 @@ # Current observations can be downloaded here: # https://www.mesonet.org/index.php/weather/category/past_data_files -data = pd.read_csv(get_test_data('mesonet_sample.txt'), na_values=' ') +data = pd.read_csv(get_test_data("mesonet_sample.txt"), na_values=" ") # Drop stations with missing values of data we want -data = data.dropna(how='any', subset=['PRES', 'TAIR', 'TDEW', 'WDIR', 'WSPD']) +data = data.dropna(how="any", subset=["PRES", "TAIR", "TDEW", "WDIR", "WSPD"]) ########################################### # The mesonet has so many stations that it would clutter the plot if we used them all. @@ -41,7 +41,7 @@ # Reduce the density of observations so the plot is readable proj = ccrs.LambertConformal(central_longitude=-98) -point_locs = proj.transform_points(ccrs.PlateCarree(), data['LON'].values, data['LAT'].values) +point_locs = proj.transform_points(ccrs.PlateCarree(), data["LON"].values, data["LAT"].values) data = data[mpcalc.reduce_point_density(point_locs, 50 * units.km)] ########################################### @@ -52,18 +52,18 @@ # - Get wind components from speed and direction # Read in the data and assign units as defined by the Mesonet -temperature = data['TAIR'].values * units.degF -dewpoint = data['TDEW'].values * units.degF -pressure = data['PRES'].values * units.hPa -wind_speed = data['WSPD'].values * units.mph -wind_direction = data['WDIR'] -latitude = data['LAT'] -longitude = data['LON'] -station_id = data['STID'] +temperature = data["TAIR"].values * units.degF +dewpoint = data["TDEW"].values * units.degF +pressure = data["PRES"].values * units.hPa +wind_speed = data["WSPD"].values * units.mph +wind_direction = data["WDIR"] +latitude = data["LAT"] +longitude = data["LON"] +station_id = data["STID"] # Take cardinal direction and convert to degrees, then convert to components wind_direction = mpcalc.parse_angle(list(wind_direction)) -u, v = mpcalc.wind_components(wind_speed.to('knots'), wind_direction) +u, v = mpcalc.wind_components(wind_speed.to("knots"), wind_direction) ########################################### # Create the figure @@ -71,28 +71,34 @@ # Create the figure and an axes set to the projection. fig = plt.figure(figsize=(20, 8)) -add_metpy_logo(fig, 70, 30, size='large') +add_metpy_logo(fig, 70, 30, size="large") ax = fig.add_subplot(1, 1, 1, projection=proj) # Add some various map elements to the plot to make it recognizable. ax.add_feature(cfeature.LAND) -ax.add_feature(cfeature.STATES.with_scale('50m')) +ax.add_feature(cfeature.STATES.with_scale("50m")) # Set plot bounds ax.set_extent((-104, -93, 33.4, 37.2)) -stationplot = StationPlot(ax, longitude.values, latitude.values, clip_on=True, - transform=ccrs.PlateCarree(), fontsize=12) +stationplot = StationPlot( + ax, + longitude.values, + latitude.values, + clip_on=True, + transform=ccrs.PlateCarree(), + fontsize=12, +) # Plot the temperature and dew point to the upper and lower left, respectively, of # the center point. Each one uses a different color. -stationplot.plot_parameter('NW', temperature, color='red') -stationplot.plot_parameter('SW', dewpoint, color='darkgreen') +stationplot.plot_parameter("NW", temperature, color="red") +stationplot.plot_parameter("SW", dewpoint, color="darkgreen") # A more complex example uses a custom formatter to control how the sea-level pressure # values are plotted. This uses the standard trailing 3-digits of the pressure value # in tenths of millibars. -stationplot.plot_parameter('NE', pressure.m, formatter=lambda v: format(10 * v, '.0f')[-3:]) +stationplot.plot_parameter("NE", pressure.m, formatter=lambda v: format(10 * v, ".0f")[-3:]) # Add wind barbs stationplot.plot_barb(u, v) @@ -102,6 +108,6 @@ stationplot.plot_text((2, -1), station_id) # Add title and display figure -plt.title('Oklahoma Mesonet Observations', fontsize=16, loc='left') -plt.title('Time: 2100 UTC 09 September 2019', fontsize=16, loc='right') +plt.title("Oklahoma Mesonet Observations", fontsize=16, loc="left") +plt.title("Time: 2100 UTC 09 September 2019", fontsize=16, loc="right") plt.show() diff --git a/examples/plots/Simple_Sounding.py b/examples/plots/Simple_Sounding.py index 05a80776b27..4445234e13c 100644 --- a/examples/plots/Simple_Sounding.py +++ b/examples/plots/Simple_Sounding.py @@ -14,38 +14,42 @@ import metpy.calc as mpcalc from metpy.cbook import get_test_data -from metpy.plots import add_metpy_logo, SkewT +from metpy.plots import SkewT, add_metpy_logo from metpy.units import units - ########################################### # Change default to be better for skew-T -plt.rcParams['figure.figsize'] = (9, 9) +plt.rcParams["figure.figsize"] = (9, 9) ########################################### # Upper air data can be obtained using the siphon package, but for this example we will use # some of MetPy's sample data. -col_names = ['pressure', 'height', 'temperature', 'dewpoint', 'direction', 'speed'] +col_names = ["pressure", "height", "temperature", "dewpoint", "direction", "speed"] -df = pd.read_fwf(get_test_data('jan20_sounding.txt', as_file_obj=False), - skiprows=5, usecols=[0, 1, 2, 3, 6, 7], names=col_names) +df = pd.read_fwf( + get_test_data("jan20_sounding.txt", as_file_obj=False), + skiprows=5, + usecols=[0, 1, 2, 3, 6, 7], + names=col_names, +) # Drop any rows with all NaN values for T, Td, winds -df = df.dropna(subset=('temperature', 'dewpoint', 'direction', 'speed' - ), how='all').reset_index(drop=True) +df = df.dropna( + subset=("temperature", "dewpoint", "direction", "speed"), how="all" +).reset_index(drop=True) ########################################### # We will pull the data out of the example dataset into individual variables and # assign units. -p = df['pressure'].values * units.hPa -T = df['temperature'].values * units.degC -Td = df['dewpoint'].values * units.degC -wind_speed = df['speed'].values * units.knots -wind_dir = df['direction'].values * units.degrees +p = df["pressure"].values * units.hPa +T = df["temperature"].values * units.degC +Td = df["dewpoint"].values * units.degC +wind_speed = df["speed"].values * units.knots +wind_dir = df["direction"].values * units.degrees u, v = mpcalc.wind_components(wind_speed, wind_dir) ########################################### @@ -54,8 +58,8 @@ # Plot the data using normal plotting functions, in this case using # log scaling in Y, as dictated by the typical meteorological plot -skew.plot(p, T, 'r') -skew.plot(p, Td, 'g') +skew.plot(p, T, "r") +skew.plot(p, Td, "g") skew.plot_barbs(p, u, v) # Add the relevant special lines @@ -75,11 +79,11 @@ # Plot the data using normal plotting functions, in this case using # log scaling in Y, as dictated by the typical meteorological plot -skew.plot(p, T, 'r') -skew.plot(p, Td, 'g') +skew.plot(p, T, "r") +skew.plot(p, Td, "g") # Set spacing interval--Every 50 mb from 1000 to 100 mb -my_interval = np.arange(100, 1000, 50) * units('mbar') +my_interval = np.arange(100, 1000, 50) * units("mbar") # Get indexes of values closest to defined interval ix = mpcalc.resample_nn_1d(p, my_interval) diff --git a/examples/plots/Simplified_Image_Plot.py b/examples/plots/Simplified_Image_Plot.py index 0c3cdca4429..4d2b4b04869 100644 --- a/examples/plots/Simplified_Image_Plot.py +++ b/examples/plots/Simplified_Image_Plot.py @@ -16,12 +16,12 @@ from metpy.io import GiniFile from metpy.plots import ImagePlot, MapPanel, PanelContainer -data = xr.open_dataset(GiniFile(get_test_data('NHEM-MULTICOMP_1km_IR_20151208_2100.gini'))) +data = xr.open_dataset(GiniFile(get_test_data("NHEM-MULTICOMP_1km_IR_20151208_2100.gini"))) img = ImagePlot() img.data = data -img.field = 'IR' -img.colormap = 'Greys_r' +img.field = "IR" +img.colormap = "Greys_r" panel = MapPanel() panel.plots = [img] diff --git a/examples/plots/Skew-T_Layout.py b/examples/plots/Skew-T_Layout.py index 449d23b7756..c86dddab8e4 100644 --- a/examples/plots/Skew-T_Layout.py +++ b/examples/plots/Skew-T_Layout.py @@ -14,38 +14,43 @@ import metpy.calc as mpcalc from metpy.cbook import get_test_data -from metpy.plots import add_metpy_logo, Hodograph, SkewT +from metpy.plots import Hodograph, SkewT, add_metpy_logo from metpy.units import units ########################################### # Upper air data can be obtained using the siphon package, but for this example we will use # some of MetPy's sample data. -col_names = ['pressure', 'height', 'temperature', 'dewpoint', 'direction', 'speed'] +col_names = ["pressure", "height", "temperature", "dewpoint", "direction", "speed"] -df = pd.read_fwf(get_test_data('may4_sounding.txt', as_file_obj=False), - skiprows=5, usecols=[0, 1, 2, 3, 6, 7], names=col_names) +df = pd.read_fwf( + get_test_data("may4_sounding.txt", as_file_obj=False), + skiprows=5, + usecols=[0, 1, 2, 3, 6, 7], + names=col_names, +) # Drop any rows with all NaN values for T, Td, winds -df = df.dropna(subset=('temperature', 'dewpoint', 'direction', 'speed' - ), how='all').reset_index(drop=True) +df = df.dropna( + subset=("temperature", "dewpoint", "direction", "speed"), how="all" +).reset_index(drop=True) ########################################### # We will pull the data out of the example dataset into individual variables and # assign units. -p = df['pressure'].values * units.hPa -T = df['temperature'].values * units.degC -Td = df['dewpoint'].values * units.degC -wind_speed = df['speed'].values * units.knots -wind_dir = df['direction'].values * units.degrees +p = df["pressure"].values * units.hPa +T = df["temperature"].values * units.degC +Td = df["dewpoint"].values * units.degC +wind_speed = df["speed"].values * units.knots +wind_dir = df["direction"].values * units.degrees u, v = mpcalc.wind_components(wind_speed, wind_dir) ########################################### # Create a new figure. The dimensions here give a good aspect ratio fig = plt.figure(figsize=(9, 9)) -add_metpy_logo(fig, 630, 80, size='large') +add_metpy_logo(fig, 630, 80, size="large") # Grid for plots gs = gridspec.GridSpec(3, 3) @@ -53,8 +58,8 @@ # Plot the data using normal plotting functions, in this case using # log scaling in Y, as dictated by the typical meteorological plot -skew.plot(p, T, 'r') -skew.plot(p, Td, 'g') +skew.plot(p, T, "r") +skew.plot(p, Td, "g") skew.plot_barbs(p, u, v) skew.ax.set_ylim(1000, 100) @@ -68,7 +73,7 @@ # Create a hodograph ax = fig.add_subplot(gs[0, -1]) -h = Hodograph(ax, component_range=60.) +h = Hodograph(ax, component_range=60.0) h.add_grid(increment=20) h.plot(u, v) diff --git a/examples/plots/Station_Plot.py b/examples/plots/Station_Plot.py index e542cacec91..4142e197b73 100644 --- a/examples/plots/Station_Plot.py +++ b/examples/plots/Station_Plot.py @@ -18,7 +18,7 @@ from metpy.calc import reduce_point_density from metpy.cbook import get_test_data from metpy.io import metar -from metpy.plots import add_metpy_logo, current_weather, sky_cover, StationPlot +from metpy.plots import StationPlot, add_metpy_logo, current_weather, sky_cover ########################################### # The setup @@ -28,24 +28,26 @@ # like dealing with separating text and assembling a pandas dataframe # https://thredds-test.unidata.ucar.edu/thredds/catalog/noaaport/text/metar/catalog.html -data = metar.parse_metar_file(get_test_data('metar_20190701_1200.txt', as_file_obj=False)) +data = metar.parse_metar_file(get_test_data("metar_20190701_1200.txt", as_file_obj=False)) # Drop rows with missing winds -data = data.dropna(how='any', subset=['wind_direction', 'wind_speed']) +data = data.dropna(how="any", subset=["wind_direction", "wind_speed"]) ########################################### # This sample data has *way* too many stations to plot all of them. The number # of stations plotted will be reduced using `reduce_point_density`. # Set up the map projection -proj = ccrs.LambertConformal(central_longitude=-95, central_latitude=35, - standard_parallels=[35]) +proj = ccrs.LambertConformal( + central_longitude=-95, central_latitude=35, standard_parallels=[35] +) # Use the Cartopy map projection to transform station locations to the map and # then refine the number of stations plotted by setting a 300km radius -point_locs = proj.transform_points(ccrs.PlateCarree(), data['longitude'].values, - data['latitude'].values) -data = data[reduce_point_density(point_locs, 300000.)] +point_locs = proj.transform_points( + ccrs.PlateCarree(), data["longitude"].values, data["latitude"].values +) +data = data[reduce_point_density(point_locs, 300000.0)] ########################################### # The payoff @@ -53,11 +55,11 @@ # Change the DPI of the resulting figure. Higher DPI drastically improves the # look of the text rendering. -plt.rcParams['savefig.dpi'] = 255 +plt.rcParams["savefig.dpi"] = 255 # Create the figure and an axes set to the projection. fig = plt.figure(figsize=(20, 10)) -add_metpy_logo(fig, 1100, 300, size='large') +add_metpy_logo(fig, 1100, 300, size="large") ax = fig.add_subplot(1, 1, 1, projection=proj) # Add some various map elements to the plot to make it recognizable. @@ -77,35 +79,43 @@ # Start the station plot by specifying the axes to draw on, as well as the # lon/lat of the stations (with transform). We also the fontsize to 12 pt. -stationplot = StationPlot(ax, data['longitude'].values, data['latitude'].values, - clip_on=True, transform=ccrs.PlateCarree(), fontsize=12) +stationplot = StationPlot( + ax, + data["longitude"].values, + data["latitude"].values, + clip_on=True, + transform=ccrs.PlateCarree(), + fontsize=12, +) # Plot the temperature and dew point to the upper and lower left, respectively, of # the center point. Each one uses a different color. -stationplot.plot_parameter('NW', data['air_temperature'].values, color='red') -stationplot.plot_parameter('SW', data['dew_point_temperature'].values, - color='darkgreen') +stationplot.plot_parameter("NW", data["air_temperature"].values, color="red") +stationplot.plot_parameter("SW", data["dew_point_temperature"].values, color="darkgreen") # A more complex example uses a custom formatter to control how the sea-level pressure # values are plotted. This uses the standard trailing 3-digits of the pressure value # in tenths of millibars. -stationplot.plot_parameter('NE', data['air_pressure_at_sea_level'].values, - formatter=lambda v: format(10 * v, '.0f')[-3:]) +stationplot.plot_parameter( + "NE", + data["air_pressure_at_sea_level"].values, + formatter=lambda v: format(10 * v, ".0f")[-3:], +) # Plot the cloud cover symbols in the center location. This uses the codes made above and # uses the `sky_cover` mapper to convert these values to font codes for the # weather symbol font. -stationplot.plot_symbol('C', data['cloud_coverage'].values, sky_cover) +stationplot.plot_symbol("C", data["cloud_coverage"].values, sky_cover) # Same this time, but plot current weather to the left of center, using the # `current_weather` mapper to convert symbols to the right glyphs. -stationplot.plot_symbol('W', data['present_weather'].values, current_weather) +stationplot.plot_symbol("W", data["present_weather"].values, current_weather) # Add wind barbs -stationplot.plot_barb(data['eastward_wind'].values, data['northward_wind'].values) +stationplot.plot_barb(data["eastward_wind"].values, data["northward_wind"].values) # Also plot the actual text of the station id. Instead of cardinal directions, # plot further out by specifying a location of 2 increments in x and 0 in y. -stationplot.plot_text((2, 0), data['station_id'].values) +stationplot.plot_text((2, 0), data["station_id"].values) plt.show() diff --git a/examples/plots/Station_Plot_with_Layout.py b/examples/plots/Station_Plot_with_Layout.py index 613dda51ccf..1f18279cfe5 100644 --- a/examples/plots/Station_Plot_with_Layout.py +++ b/examples/plots/Station_Plot_with_Layout.py @@ -24,8 +24,13 @@ from metpy.calc import wind_components from metpy.cbook import get_test_data -from metpy.plots import (add_metpy_logo, simple_layout, StationPlot, - StationPlotLayout, wx_code_map) +from metpy.plots import ( + StationPlot, + StationPlotLayout, + add_metpy_logo, + simple_layout, + wx_code_map, +) from metpy.units import units ########################################### @@ -35,31 +40,89 @@ # First read in the data. We use `numpy.loadtxt` to read in the data and use a structured # `numpy.dtype` to allow different types for the various columns. This allows us to handle # the columns with string data. -with get_test_data('station_data.txt') as f: - data_arr = pd.read_csv(f, header=0, usecols=(1, 2, 3, 4, 5, 6, 7, 17, 18, 19), - names=['stid', 'lat', 'lon', 'slp', 'air_temperature', - 'cloud_fraction', 'dew_point_temperature', 'weather', - 'wind_dir', 'wind_speed'], - na_values=-99999) - - data_arr.set_index('stid', inplace=True) +with get_test_data("station_data.txt") as f: + data_arr = pd.read_csv( + f, + header=0, + usecols=(1, 2, 3, 4, 5, 6, 7, 17, 18, 19), + names=[ + "stid", + "lat", + "lon", + "slp", + "air_temperature", + "cloud_fraction", + "dew_point_temperature", + "weather", + "wind_dir", + "wind_speed", + ], + na_values=-99999, + ) + + data_arr.set_index("stid", inplace=True) ########################################### # This sample data has *way* too many stations to plot all of them. Instead, we just select # a few from around the U.S. and pull those out of the data file. # Pull out these specific stations -selected = ['OKC', 'ICT', 'GLD', 'MEM', 'BOS', 'MIA', 'MOB', 'ABQ', 'PHX', 'TTF', - 'ORD', 'BIL', 'BIS', 'CPR', 'LAX', 'ATL', 'MSP', 'SLC', 'DFW', 'NYC', 'PHL', - 'PIT', 'IND', 'OLY', 'SYR', 'LEX', 'CHS', 'TLH', 'HOU', 'GJT', 'LBB', 'LSV', - 'GRB', 'CLT', 'LNK', 'DSM', 'BOI', 'FSD', 'RAP', 'RIC', 'JAN', 'HSV', 'CRW', - 'SAT', 'BUY', '0CO', 'ZPC', 'VIH'] +selected = [ + "OKC", + "ICT", + "GLD", + "MEM", + "BOS", + "MIA", + "MOB", + "ABQ", + "PHX", + "TTF", + "ORD", + "BIL", + "BIS", + "CPR", + "LAX", + "ATL", + "MSP", + "SLC", + "DFW", + "NYC", + "PHL", + "PIT", + "IND", + "OLY", + "SYR", + "LEX", + "CHS", + "TLH", + "HOU", + "GJT", + "LBB", + "LSV", + "GRB", + "CLT", + "LNK", + "DSM", + "BOI", + "FSD", + "RAP", + "RIC", + "JAN", + "HSV", + "CRW", + "SAT", + "BUY", + "0CO", + "ZPC", + "VIH", +] # Loop over all the whitelisted sites, grab the first data, and concatenate them data_arr = data_arr.loc[selected] # Drop rows with missing winds -data_arr = data_arr.dropna(how='any', subset=['wind_dir', 'wind_speed']) +data_arr = data_arr.dropna(how="any", subset=["wind_dir", "wind_speed"]) # First, look at the names of variables that the layout is expecting: simple_layout.names() @@ -73,11 +136,11 @@ # Copy out to stage everything together. In an ideal world, this would happen on # the data reading side of things, but we're not there yet. -data['longitude'] = data_arr['lon'].values -data['latitude'] = data_arr['lat'].values -data['air_temperature'] = data_arr['air_temperature'].values * units.degC -data['dew_point_temperature'] = data_arr['dew_point_temperature'].values * units.degC -data['air_pressure_at_sea_level'] = data_arr['slp'].values * units('mbar') +data["longitude"] = data_arr["lon"].values +data["latitude"] = data_arr["lat"].values +data["air_temperature"] = data_arr["air_temperature"].values * units.degC +data["dew_point_temperature"] = data_arr["dew_point_temperature"].values * units.degC +data["air_pressure_at_sea_level"] = data_arr["slp"].values * units("mbar") ########################################### # Notice that the names (the keys) in the dictionary are the same as those that the @@ -91,24 +154,26 @@ # Get the wind components, converting from m/s to knots as will be appropriate # for the station plot -u, v = wind_components(data_arr['wind_speed'].values * units('m/s'), - data_arr['wind_dir'].values * units.degree) -data['eastward_wind'], data['northward_wind'] = u, v +u, v = wind_components( + data_arr["wind_speed"].values * units("m/s"), data_arr["wind_dir"].values * units.degree +) +data["eastward_wind"], data["northward_wind"] = u, v # Convert the fraction value into a code of 0-8, which can be used to pull out # the appropriate symbol -data['cloud_coverage'] = (8 * data_arr['cloud_fraction']).fillna(10).values.astype(int) +data["cloud_coverage"] = (8 * data_arr["cloud_fraction"]).fillna(10).values.astype(int) # Map weather strings to WMO codes, which we can use to convert to symbols # Only use the first symbol if there are multiple -wx_text = data_arr['weather'].fillna('') -data['present_weather'] = [wx_code_map[s.split()[0] if ' ' in s else s] for s in wx_text] +wx_text = data_arr["weather"].fillna("") +data["present_weather"] = [wx_code_map[s.split()[0] if " " in s else s] for s in wx_text] ########################################### # All the data wrangling is finished, just need to set up plotting and go: # Set up the map projection and set up a cartopy feature for state borders -proj = ccrs.LambertConformal(central_longitude=-95, central_latitude=35, - standard_parallels=[35]) +proj = ccrs.LambertConformal( + central_longitude=-95, central_latitude=35, standard_parallels=[35] +) ########################################### # The payoff @@ -116,11 +181,11 @@ # Change the DPI of the resulting figure. Higher DPI drastically improves the # look of the text rendering -plt.rcParams['savefig.dpi'] = 255 +plt.rcParams["savefig.dpi"] = 255 # Create the figure and an axes set to the projection fig = plt.figure(figsize=(20, 10)) -add_metpy_logo(fig, 1080, 290, size='large') +add_metpy_logo(fig, 1080, 290, size="large") ax = fig.add_subplot(1, 1, 1, projection=proj) # Add some various map elements to the plot to make it recognizable @@ -140,8 +205,9 @@ # Start the station plot by specifying the axes to draw on, as well as the # lon/lat of the stations (with transform). We also the fontsize to 12 pt. -stationplot = StationPlot(ax, data['longitude'], data['latitude'], - transform=ccrs.PlateCarree(), fontsize=12) +stationplot = StationPlot( + ax, data["longitude"], data["latitude"], transform=ccrs.PlateCarree(), fontsize=12 +) # The layout knows where everything should go, and things are standardized using # the names of variables. So the layout pulls arrays out of `data` and plots them @@ -156,17 +222,18 @@ # Just winds, temps, and dewpoint, with colors. Dewpoint and temp will be plotted # out to Farenheit tenths. Extra data will be ignored custom_layout = StationPlotLayout() -custom_layout.add_barb('eastward_wind', 'northward_wind', units='knots') -custom_layout.add_value('NW', 'air_temperature', fmt='.1f', units='degF', color='darkred') -custom_layout.add_value('SW', 'dew_point_temperature', fmt='.1f', units='degF', - color='darkgreen') +custom_layout.add_barb("eastward_wind", "northward_wind", units="knots") +custom_layout.add_value("NW", "air_temperature", fmt=".1f", units="degF", color="darkred") +custom_layout.add_value( + "SW", "dew_point_temperature", fmt=".1f", units="degF", color="darkgreen" +) # Also, we'll add a field that we don't have in our dataset. This will be ignored -custom_layout.add_value('E', 'precipitation', fmt='0.2f', units='inch', color='blue') +custom_layout.add_value("E", "precipitation", fmt="0.2f", units="inch", color="blue") # Create the figure and an axes set to the projection fig = plt.figure(figsize=(20, 10)) -add_metpy_logo(fig, 1080, 290, size='large') +add_metpy_logo(fig, 1080, 290, size="large") ax = fig.add_subplot(1, 1, 1, projection=proj) # Add some various map elements to the plot to make it recognizable @@ -186,8 +253,9 @@ # Start the station plot by specifying the axes to draw on, as well as the # lon/lat of the stations (with transform). We also the fontsize to 12 pt. -stationplot = StationPlot(ax, data['longitude'], data['latitude'], - transform=ccrs.PlateCarree(), fontsize=12) +stationplot = StationPlot( + ax, data["longitude"], data["latitude"], transform=ccrs.PlateCarree(), fontsize=12 +) # The layout knows where everything should go, and things are standardized using # the names of variables. So the layout pulls arrays out of `data` and plots them diff --git a/examples/plots/US_Counties.py b/examples/plots/US_Counties.py index 30168861299..e6ea1e6ab5e 100644 --- a/examples/plots/US_Counties.py +++ b/examples/plots/US_Counties.py @@ -21,6 +21,6 @@ ax2 = fig.add_subplot(1, 3, 2, projection=proj) ax3 = fig.add_subplot(1, 3, 3, projection=proj) -for scale, axis in zip(['20m', '5m', '500k'], [ax1, ax2, ax3]): +for scale, axis in zip(["20m", "5m", "500k"], [ax1, ax2, ax3]): axis.set_extent([270.25, 270.9, 38.15, 38.75], ccrs.Geodetic()) axis.add_feature(USCOUNTIES.with_scale(scale)) diff --git a/examples/plots/surface_declarative.py b/examples/plots/surface_declarative.py index 72556fc1b01..44aad87459e 100644 --- a/examples/plots/surface_declarative.py +++ b/examples/plots/surface_declarative.py @@ -27,8 +27,11 @@ # Python script. The data are pre-processed to determine sky cover and weather symbols from # text output. -data = pd.read_csv(get_test_data('SFC_obs.csv', as_file_obj=False), - infer_datetime_format=True, parse_dates=['valid']) +data = pd.read_csv( + get_test_data("SFC_obs.csv", as_file_obj=False), + infer_datetime_format=True, + parse_dates=["valid"], +) ######################################## # **Plotting the data** @@ -42,20 +45,25 @@ obs.time = datetime(1993, 3, 12, 13) obs.time_window = timedelta(minutes=15) obs.level = None -obs.fields = ['tmpf', 'dwpf', 'emsl', 'cloud_cover', 'wxsym'] -obs.locations = ['NW', 'SW', 'NE', 'C', 'W'] -obs.colors = ['red', 'green', 'black', 'black', 'blue'] -obs.formats = [None, None, lambda v: format(10 * v, '.0f')[-3:], 'sky_cover', - 'current_weather'] -obs.vector_field = ('uwind', 'vwind') +obs.fields = ["tmpf", "dwpf", "emsl", "cloud_cover", "wxsym"] +obs.locations = ["NW", "SW", "NE", "C", "W"] +obs.colors = ["red", "green", "black", "black", "blue"] +obs.formats = [ + None, + None, + lambda v: format(10 * v, ".0f")[-3:], + "sky_cover", + "current_weather", +] +obs.vector_field = ("uwind", "vwind") obs.reduce_points = 1 # Add map features for the particular panel panel = mpplots.MapPanel() panel.layout = (1, 1, 1) -panel.area = 'ga' +panel.area = "ga" panel.projection = ccrs.PlateCarree() -panel.layers = ['coastline', 'borders', 'states'] +panel.layers = ["coastline", "borders", "states"] panel.plots = [obs] # Collecting panels for complete figure diff --git a/examples/plots/upperair_declarative.py b/examples/plots/upperair_declarative.py index ffe0753a1ff..e2111000dce 100644 --- a/examples/plots/upperair_declarative.py +++ b/examples/plots/upperair_declarative.py @@ -19,7 +19,6 @@ import metpy.plots as mpplots from metpy.units import units - ######################################## # **Getting the data** # @@ -27,7 +26,7 @@ # (https://mesonet.agron.iastate.edu/archive/raob/) available through a Siphon method. # The data are pre-processed to attach latitude/longitude locations for each RAOB site. -data = pd.read_csv(get_test_data('UPA_obs.csv', as_file_obj=False)) +data = pd.read_csv(get_test_data("UPA_obs.csv", as_file_obj=False)) ######################################## # **Plotting the data** @@ -39,18 +38,18 @@ obs.data = data obs.time = datetime(1993, 3, 14, 0) obs.level = 500 * units.hPa -obs.fields = ['temperature', 'dewpoint', 'height'] -obs.locations = ['NW', 'SW', 'NE'] -obs.formats = [None, None, lambda v: format(v, '.0f')[:3]] -obs.vector_field = ('u_wind', 'v_wind') +obs.fields = ["temperature", "dewpoint", "height"] +obs.locations = ["NW", "SW", "NE"] +obs.formats = [None, None, lambda v: format(v, ".0f")[:3]] +obs.vector_field = ("u_wind", "v_wind") obs.reduce_points = 0 # Add map features for the particular panel panel = mpplots.MapPanel() panel.layout = (1, 1, 1) panel.area = (-124, -72, 20, 53) -panel.projection = 'lcc' -panel.layers = ['coastline', 'borders', 'states', 'land', 'ocean'] +panel.projection = "lcc" +panel.layers = ["coastline", "borders", "states", "land", "ocean"] panel.plots = [obs] # Collecting panels for complete figure diff --git a/examples/sigma_to_pressure_interpolation.py b/examples/sigma_to_pressure_interpolation.py index f86706a87d3..78b5523be11 100644 --- a/examples/sigma_to_pressure_interpolation.py +++ b/examples/sigma_to_pressure_interpolation.py @@ -28,18 +28,18 @@ # University Department of Geography and Meteorology. -data = Dataset(get_test_data('wrf_example.nc', False)) -lat = data.variables['lat'][:] -lon = data.variables['lon'][:] -time = data.variables['time'] +data = Dataset(get_test_data("wrf_example.nc", False)) +lat = data.variables["lat"][:] +lon = data.variables["lon"][:] +time = data.variables["time"] vtimes = num2date(time[:], time.units) -temperature = units.Quantity(data.variables['temperature'][:], 'degC') -pres = units.Quantity(data.variables['pressure'][:], 'Pa') -hgt = units.Quantity(data.variables['height'][:], 'meter') +temperature = units.Quantity(data.variables["temperature"][:], "degC") +pres = units.Quantity(data.variables["pressure"][:], "Pa") +hgt = units.Quantity(data.variables["height"][:], "meter") #################################### # Array of desired pressure levels -plevs = [700.] * units.hPa +plevs = [700.0] * units.hPa ##################################### # **Interpolate The Data** @@ -64,33 +64,48 @@ # Create the figure and grid for subplots fig = plt.figure(figsize=(17, 12)) -add_metpy_logo(fig, 470, 320, size='large') +add_metpy_logo(fig, 470, 320, size="large") # Plot 700 hPa ax = plt.subplot(111, projection=crs) -ax.add_feature(cfeature.COASTLINE.with_scale('50m'), linewidth=0.75) +ax.add_feature(cfeature.COASTLINE.with_scale("50m"), linewidth=0.75) ax.add_feature(cfeature.STATES, linewidth=0.5) # Plot the heights -cs = ax.contour(lon, lat, height[FH, 0, :, :], transform=ccrs.PlateCarree(), - colors='k', linewidths=1.0, linestyles='solid') -cs.clabel(fontsize=10, inline=1, inline_spacing=7, fmt='%i', rightside_up=True, - use_clabeltext=True) +cs = ax.contour( + lon, + lat, + height[FH, 0, :, :], + transform=ccrs.PlateCarree(), + colors="k", + linewidths=1.0, + linestyles="solid", +) +cs.clabel( + fontsize=10, inline=1, inline_spacing=7, fmt="%i", rightside_up=True, use_clabeltext=True +) # Contour the temperature -cf = ax.contourf(lon, lat, temp[FH, 0, :, :], range(-20, 20, 1), cmap=plt.cm.RdBu_r, - transform=ccrs.PlateCarree()) -cb = fig.colorbar(cf, orientation='horizontal', aspect=65, shrink=0.5, pad=0.05, - extendrect='True') -cb.set_label('Celsius', size='x-large') +cf = ax.contourf( + lon, + lat, + temp[FH, 0, :, :], + range(-20, 20, 1), + cmap=plt.cm.RdBu_r, + transform=ccrs.PlateCarree(), +) +cb = fig.colorbar( + cf, orientation="horizontal", aspect=65, shrink=0.5, pad=0.05, extendrect="True" +) +cb.set_label("Celsius", size="x-large") ax.set_extent([-106.5, -90.4, 34.5, 46.75], crs=ccrs.PlateCarree()) # Make the axis title -ax.set_title(f'{plevs[0]:~.0f} Heights (m) and Temperature (C)', loc='center', fontsize=10) +ax.set_title(f"{plevs[0]:~.0f} Heights (m) and Temperature (C)", loc="center", fontsize=10) # Set the figure title -fig.suptitle(f'WRF-ARW Forecast VALID: {vtimes[FH]} UTC', fontsize=14) +fig.suptitle(f"WRF-ARW Forecast VALID: {vtimes[FH]} UTC", fontsize=14) add_timestamp(ax, vtimes[FH], y=0.02, high_contrast=True) plt.show() diff --git a/pyproject.toml b/pyproject.toml index 1f96918e1cd..eb5f57b3532 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,3 +4,6 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] version_scheme = "post-release" + +[tool.black] +line-length = 95 diff --git a/setup.cfg b/setup.cfg index ecdec2cf20d..2622d88e19c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -105,3 +105,14 @@ max-line-length = 95 match = (?!(test_|setup)).*\.py match-dir = (?!(build|docs|examples|tutorials|_nexrad_msgs))[^.].* convention = numpy + +[isort] +multi_line_output = 3 +include_trailing_comma = True +force_grid_wrap = 0 +use_parentheses = True +ensure_newline_before_comments = True +line_length = 95 +known_first_party = ["metpy"] +force_sort_within_sections = true +src_paths=src,ci,docs,examples,tests,tools,tutorials diff --git a/setup.py b/setup.py index a16a12cb384..cf368601e00 100644 --- a/setup.py +++ b/setup.py @@ -15,9 +15,11 @@ but it is no longer maintained. Python {py} detected. - """.format(py='.'.join([str(v) for v in sys.version_info[:3]])) + """.format( + py=".".join([str(v) for v in sys.version_info[:3]]) + ) print(error) # noqa: T001 sys.exit(1) -setup(use_scm_version={'version_scheme': 'post-release'}) +setup(use_scm_version={"version_scheme": "post-release"}) diff --git a/src/metpy/__init__.py b/src/metpy/__init__.py index ee1199205ec..bc196a81421 100644 --- a/src/metpy/__init__.py +++ b/src/metpy/__init__.py @@ -25,13 +25,15 @@ - Install an older version of MetPy: $ pip install 'metpy=0.11.1' - """) + """ + ) # Must occur before below imports -warnings.filterwarnings('ignore', 'numpy.dtype size changed') -os.environ['PINT_ARRAY_PROTOCOL_FALLBACK'] = '0' +warnings.filterwarnings("ignore", "numpy.dtype size changed") +os.environ["PINT_ARRAY_PROTOCOL_FALLBACK"] = "0" from ._version import get_version # noqa: E402 from .xarray import * # noqa: F401, F403, E402 + __version__ = get_version() del get_version diff --git a/src/metpy/_vendor/xarray.py b/src/metpy/_vendor/xarray.py index ebbdc402ce1..8b8525f8ec2 100644 --- a/src/metpy/_vendor/xarray.py +++ b/src/metpy/_vendor/xarray.py @@ -48,25 +48,28 @@ def expanded_indexer(key, ndim): else: new_key.append(k) if len(new_key) > ndim: - raise IndexError('too many indices') + raise IndexError("too many indices") new_key.extend((ndim - len(new_key)) * [slice(None)]) return tuple(new_key) def is_dict_like(value): """Check if value is dict-like.""" - return hasattr(value, 'keys') and hasattr(value, '__getitem__') + return hasattr(value, "keys") and hasattr(value, "__getitem__") def either_dict_or_kwargs(pos_kwargs, kw_kwargs, func_name): """Ensure dict-like argument from either positional or keyword arguments.""" if pos_kwargs is not None: if not is_dict_like(pos_kwargs): - raise ValueError('the first argument to .{} must be a ' - 'dictionary'.format(func_name)) + raise ValueError( + "the first argument to .{} must be a " "dictionary".format(func_name) + ) if kw_kwargs: - raise ValueError('cannot specify both keyword and positional arguments to ' - '.{}'.format(func_name)) + raise ValueError( + "cannot specify both keyword and positional arguments to " + ".{}".format(func_name) + ) return pos_kwargs else: return kw_kwargs diff --git a/src/metpy/_version.py b/src/metpy/_version.py index 73d7e36e0f5..a6165bfaaf6 100644 --- a/src/metpy/_version.py +++ b/src/metpy/_version.py @@ -12,15 +12,20 @@ def get_version(): """ try: from setuptools_scm import get_version - return get_version(root='../..', relative_to=__file__, - version_scheme='post-release', local_scheme='dirty-tag') + + return get_version( + root="../..", + relative_to=__file__, + version_scheme="post-release", + local_scheme="dirty-tag", + ) except (ImportError, LookupError): try: - from importlib.metadata import version, PackageNotFoundError + from importlib.metadata import PackageNotFoundError, version except ImportError: # Can remove when we require Python > 3.7 - from importlib_metadata import version, PackageNotFoundError + from importlib_metadata import PackageNotFoundError, version try: return version(__package__) except PackageNotFoundError: - return 'Unknown' + return "Unknown" diff --git a/src/metpy/calc/basic.py b/src/metpy/calc/basic.py index 719e2ed1cfd..98828887c84 100644 --- a/src/metpy/calc/basic.py +++ b/src/metpy/calc/basic.py @@ -23,13 +23,13 @@ exporter = Exporter(globals()) # The following variables are constants for a standard atmosphere -t0 = 288. * units.kelvin +t0 = 288.0 * units.kelvin p0 = 1013.25 * units.hPa @exporter.export -@preprocess_and_wrap(wrap_like='u') -@check_units('[speed]', '[speed]') +@preprocess_and_wrap(wrap_like="u") +@check_units("[speed]", "[speed]") def wind_speed(u, v): r"""Compute the wind speed from u and v-components. @@ -55,9 +55,9 @@ def wind_speed(u, v): @exporter.export -@preprocess_and_wrap(wrap_like='u') -@check_units('[speed]', '[speed]') -def wind_direction(u, v, convention='from'): +@preprocess_and_wrap(wrap_like="u") +@check_units("[speed]", "[speed]") +def wind_direction(u, v, convention="from"): r"""Compute the wind direction from u and v-components. Parameters @@ -87,30 +87,30 @@ def wind_direction(u, v, convention='from'): of 0. """ - wdir = 90. * units.deg - np.arctan2(-v, -u) + wdir = 90.0 * units.deg - np.arctan2(-v, -u) origshape = wdir.shape wdir = np.atleast_1d(wdir) # Handle oceanographic convection - if convention == 'to': + if convention == "to": wdir -= 180 * units.deg - elif convention not in ('to', 'from'): + elif convention not in ("to", "from"): raise ValueError('Invalid kwarg for "convention". Valid options are "from" or "to".') mask = wdir <= 0 if np.any(mask): - wdir[mask] += 360. * units.deg + wdir[mask] += 360.0 * units.deg # avoid unintended modification of `pint.Quantity` by direct use of magnitude - calm_mask = (np.asarray(u.magnitude) == 0.) & (np.asarray(v.magnitude) == 0.) + calm_mask = (np.asarray(u.magnitude) == 0.0) & (np.asarray(v.magnitude) == 0.0) # np.any check required for legacy numpy which treats 0-d False boolean index as zero if np.any(calm_mask): - wdir[calm_mask] = 0. * units.deg - return wdir.reshape(origshape).to('degrees') + wdir[calm_mask] = 0.0 * units.deg + return wdir.reshape(origshape).to("degrees") @exporter.export -@preprocess_and_wrap(wrap_like=('speed', 'speed')) -@check_units('[speed]') +@preprocess_and_wrap(wrap_like=("speed", "speed")) +@check_units("[speed]") def wind_components(speed, wind_direction): r"""Calculate the U, V wind vector components from the speed and direction. @@ -147,8 +147,8 @@ def wind_components(speed, wind_direction): @exporter.export -@preprocess_and_wrap(wrap_like='temperature') -@check_units(temperature='[temperature]', speed='[speed]') +@preprocess_and_wrap(wrap_like="temperature") +@check_units(temperature="[temperature]", speed="[speed]") def windchill(temperature, speed, face_level_winds=False, mask_undefined=True): r"""Calculate the Wind Chill Temperature Index (WCTI). @@ -194,10 +194,14 @@ def windchill(temperature, speed, face_level_winds=False, mask_undefined=True): # noinspection PyAugmentAssignment speed = speed * 1.5 - temp_limit, speed_limit = 10. * units.degC, 3 * units.mph - speed_factor = speed.to('km/hr').magnitude ** 0.16 - wcti = units.Quantity((0.6215 + 0.3965 * speed_factor) * temperature.to('degC').magnitude - - 11.37 * speed_factor + 13.12, units.degC).to(temperature.units) + temp_limit, speed_limit = 10.0 * units.degC, 3 * units.mph + speed_factor = speed.to("km/hr").magnitude ** 0.16 + wcti = units.Quantity( + (0.6215 + 0.3965 * speed_factor) * temperature.to("degC").magnitude + - 11.37 * speed_factor + + 13.12, + units.degC, + ).to(temperature.units) # See if we need to mask any undefined values if mask_undefined: @@ -209,8 +213,8 @@ def windchill(temperature, speed, face_level_winds=False, mask_undefined=True): @exporter.export -@preprocess_and_wrap(wrap_like='temperature') -@check_units('[temperature]') +@preprocess_and_wrap(wrap_like="temperature") +@check_units("[temperature]") def heat_index(temperature, relative_humidity, mask_undefined=True): r"""Calculate the Heat Index from the current temperature and relative humidity. @@ -247,9 +251,9 @@ def heat_index(temperature, relative_humidity, mask_undefined=True): temperature = np.atleast_1d(temperature) relative_humidity = np.atleast_1d(relative_humidity) # assign units to relative_humidity if they currently are not present - if not hasattr(relative_humidity, 'units'): + if not hasattr(relative_humidity, "units"): relative_humidity = relative_humidity * units.dimensionless - delta = temperature.to(units.degF) - 0. * units.degF + delta = temperature.to(units.degF) - 0.0 * units.degF rh2 = relative_humidity * relative_humidity delta2 = delta * delta @@ -257,29 +261,31 @@ def heat_index(temperature, relative_humidity, mask_undefined=True): a = -10.3 * units.degF + 1.1 * delta + 4.7 * units.delta_degF * relative_humidity # More refined Heat Index -- constants converted for relative_humidity in [0, 1] - b = (-42.379 * units.degF - + 2.04901523 * delta - + 1014.333127 * units.delta_degF * relative_humidity - - 22.475541 * delta * relative_humidity - - 6.83783e-3 / units.delta_degF * delta2 - - 5.481717e2 * units.delta_degF * rh2 - + 1.22874e-1 / units.delta_degF * delta2 * relative_humidity - + 8.5282 * delta * rh2 - - 1.99e-2 / units.delta_degF * delta2 * rh2) + b = ( + -42.379 * units.degF + + 2.04901523 * delta + + 1014.333127 * units.delta_degF * relative_humidity + - 22.475541 * delta * relative_humidity + - 6.83783e-3 / units.delta_degF * delta2 + - 5.481717e2 * units.delta_degF * rh2 + + 1.22874e-1 / units.delta_degF * delta2 * relative_humidity + + 8.5282 * delta * rh2 + - 1.99e-2 / units.delta_degF * delta2 * rh2 + ) # Create return heat index hi = np.full(np.shape(temperature), np.nan) * units.degF # Retain masked status of temperature with resulting heat index - if hasattr(temperature, 'mask'): + if hasattr(temperature, "mask"): hi = masked_array(hi) # If T <= 40F, Heat Index is T - sel = (temperature <= 40. * units.degF) + sel = temperature <= 40.0 * units.degF if np.any(sel): hi[sel] = temperature[sel].to(units.degF) # If a < 79F and hi is unset, Heat Index is a - sel = (a < 79. * units.degF) & np.isnan(hi) + sel = (a < 79.0 * units.degF) & np.isnan(hi) if np.any(sel): hi[sel] = a[sel] @@ -289,36 +295,51 @@ def heat_index(temperature, relative_humidity, mask_undefined=True): hi[sel] = b[sel] # Adjustment for RH <= 13% and 80F <= T <= 112F - sel = ((relative_humidity <= 13. * units.percent) & (temperature >= 80. * units.degF) - & (temperature <= 112. * units.degF)) + sel = ( + (relative_humidity <= 13.0 * units.percent) + & (temperature >= 80.0 * units.degF) + & (temperature <= 112.0 * units.degF) + ) if np.any(sel): - rh15adj = ((13. - relative_humidity[sel] * 100.) / 4. - * np.sqrt((17. * units.delta_degF - - np.abs(delta[sel] - 95. * units.delta_degF)) - / 17. * units.delta_degF)) + rh15adj = ( + (13.0 - relative_humidity[sel] * 100.0) + / 4.0 + * np.sqrt( + (17.0 * units.delta_degF - np.abs(delta[sel] - 95.0 * units.delta_degF)) + / 17.0 + * units.delta_degF + ) + ) hi[sel] = hi[sel] - rh15adj # Adjustment for RH > 85% and 80F <= T <= 87F - sel = ((relative_humidity > 85. * units.percent) & (temperature >= 80. * units.degF) - & (temperature <= 87. * units.degF)) + sel = ( + (relative_humidity > 85.0 * units.percent) + & (temperature >= 80.0 * units.degF) + & (temperature <= 87.0 * units.degF) + ) if np.any(sel): - rh85adj = 0.02 * (relative_humidity[sel] * 100. - 85.) * (87. * units.delta_degF - - delta[sel]) + rh85adj = ( + 0.02 + * (relative_humidity[sel] * 100.0 - 85.0) + * (87.0 * units.delta_degF - delta[sel]) + ) hi[sel] = hi[sel] + rh85adj # See if we need to mask any undefined values if mask_undefined: - mask = np.array(temperature < 80. * units.degF) + mask = np.array(temperature < 80.0 * units.degF) if mask.any(): hi = masked_array(hi, mask=mask) return hi @exporter.export -@preprocess_and_wrap(wrap_like='temperature') -@check_units(temperature='[temperature]', speed='[speed]') -def apparent_temperature(temperature, relative_humidity, speed, face_level_winds=False, - mask_undefined=True): +@preprocess_and_wrap(wrap_like="temperature") +@check_units(temperature="[temperature]", speed="[speed]") +def apparent_temperature( + temperature, relative_humidity, speed, face_level_winds=False, mask_undefined=True +): r"""Calculate the current apparent temperature. Calculates the current apparent temperature based on the wind chill or heat index @@ -364,19 +385,24 @@ def apparent_temperature(temperature, relative_humidity, speed, face_level_winds speed = np.atleast_1d(speed) # NB: mask_defined=True is needed to know where computed values exist - wind_chill_temperature = windchill(temperature, speed, face_level_winds=face_level_winds, - mask_undefined=True).to(temperature.units) + wind_chill_temperature = windchill( + temperature, speed, face_level_winds=face_level_winds, mask_undefined=True + ).to(temperature.units) - heat_index_temperature = heat_index(temperature, relative_humidity, - mask_undefined=True).to(temperature.units) + heat_index_temperature = heat_index( + temperature, relative_humidity, mask_undefined=True + ).to(temperature.units) # Combine the heat index and wind chill arrays (no point has a value in both) # NB: older numpy.ma.where does not return a masked array app_temperature = masked_array( - np.ma.where(masked_array(wind_chill_temperature).mask, - heat_index_temperature.to(temperature.units), - wind_chill_temperature.to(temperature.units) - ), temperature.units) + np.ma.where( + masked_array(wind_chill_temperature).mask, + heat_index_temperature.to(temperature.units), + wind_chill_temperature.to(temperature.units), + ), + temperature.units, + ) # If mask_undefined is False, then set any masked values to the temperature if not mask_undefined: @@ -384,7 +410,7 @@ def apparent_temperature(temperature, relative_humidity, speed, face_level_winds # If no values are masked and provided temperature does not have a mask # we should return a non-masked array - if not np.any(app_temperature.mask) and not hasattr(temperature, 'mask'): + if not np.any(app_temperature.mask) and not hasattr(temperature, "mask"): app_temperature = np.array(app_temperature.m) * temperature.units if is_not_scalar: @@ -394,8 +420,8 @@ def apparent_temperature(temperature, relative_humidity, speed, face_level_winds @exporter.export -@preprocess_and_wrap(wrap_like='pressure') -@check_units('[pressure]') +@preprocess_and_wrap(wrap_like="pressure") +@check_units("[pressure]") def pressure_to_height_std(pressure): r"""Convert pressure data to height using the U.S. standard atmosphere [NOAA1976]_. @@ -416,14 +442,15 @@ def pressure_to_height_std(pressure): .. math:: Z = \frac{T_0}{\Gamma}[1-\frac{p}{p_0}^\frac{R\Gamma}{g}] """ - gamma = 6.5 * units('K/km') - return (t0 / gamma) * (1 - (pressure / p0).to('dimensionless')**( - mpconsts.Rd * gamma / mpconsts.g)) + gamma = 6.5 * units("K/km") + return (t0 / gamma) * ( + 1 - (pressure / p0).to("dimensionless") ** (mpconsts.Rd * gamma / mpconsts.g) + ) @exporter.export -@preprocess_and_wrap(wrap_like='height') -@check_units('[length]') +@preprocess_and_wrap(wrap_like="height") +@check_units("[length]") def height_to_geopotential(height): r"""Compute geopotential for a given height above sea level. @@ -479,7 +506,7 @@ def height_to_geopotential(height): @exporter.export -@preprocess_and_wrap(wrap_like='geopotential') +@preprocess_and_wrap(wrap_like="geopotential") def geopotential_to_height(geopotential): r"""Compute height above sea level from a given geopotential. @@ -539,8 +566,8 @@ def geopotential_to_height(geopotential): @exporter.export -@preprocess_and_wrap(wrap_like='height') -@check_units('[length]') +@preprocess_and_wrap(wrap_like="height") +@check_units("[length]") def height_to_pressure_std(height): r"""Convert height data to pressures using the U.S. standard atmosphere [NOAA1976]_. @@ -561,12 +588,12 @@ def height_to_pressure_std(height): .. math:: p = p_0 e^{\frac{g}{R \Gamma} \text{ln}(1-\frac{Z \Gamma}{T_0})} """ - gamma = 6.5 * units('K/km') + gamma = 6.5 * units("K/km") return p0 * (1 - (gamma / t0) * height) ** (mpconsts.g / (mpconsts.Rd * gamma)) @exporter.export -@preprocess_and_wrap(wrap_like='latitude') +@preprocess_and_wrap(wrap_like="latitude") def coriolis_parameter(latitude): r"""Calculate the coriolis parameter at each point. @@ -584,12 +611,12 @@ def coriolis_parameter(latitude): """ latitude = _check_radians(latitude, max_radians=np.pi / 2) - return (2. * mpconsts.omega * np.sin(latitude)).to('1/s') + return (2.0 * mpconsts.omega * np.sin(latitude)).to("1/s") @exporter.export -@preprocess_and_wrap(wrap_like='pressure') -@check_units('[pressure]', '[length]') +@preprocess_and_wrap(wrap_like="pressure") +@check_units("[pressure]", "[length]") def add_height_to_pressure(pressure, height): r"""Calculate the pressure at a certain height above another pressure level. @@ -617,8 +644,8 @@ def add_height_to_pressure(pressure, height): @exporter.export -@preprocess_and_wrap(wrap_like='height') -@check_units('[length]', '[pressure]') +@preprocess_and_wrap(wrap_like="height") +@check_units("[length]", "[pressure]") def add_pressure_to_height(height, pressure): r"""Calculate the height at a certain pressure above another height. @@ -646,8 +673,8 @@ def add_pressure_to_height(height, pressure): @exporter.export -@preprocess_and_wrap(wrap_like='sigma') -@check_units('[dimensionless]', '[pressure]', '[pressure]') +@preprocess_and_wrap(wrap_like="sigma") +@check_units("[dimensionless]", "[pressure]", "[pressure]") def sigma_to_pressure(sigma, pressure_sfc, pressure_top): r"""Calculate pressure from sigma values. @@ -680,16 +707,16 @@ def sigma_to_pressure(sigma, pressure_sfc, pressure_top): """ if np.any(sigma < 0) or np.any(sigma > 1): - raise ValueError('Sigma values should be bounded by 0 and 1') + raise ValueError("Sigma values should be bounded by 0 and 1") if pressure_sfc.magnitude < 0 or pressure_top.magnitude < 0: - raise ValueError('Pressure input should be non-negative') + raise ValueError("Pressure input should be non-negative") return sigma * (pressure_sfc - pressure_top) + pressure_top @exporter.export -@preprocess_and_wrap(wrap_like='scalar_grid', match_unit=True, to_magnitude=True) +@preprocess_and_wrap(wrap_like="scalar_grid", match_unit=True, to_magnitude=True) def smooth_gaussian(scalar_grid, n): """Filter with normal distribution of weights. @@ -781,7 +808,7 @@ def smooth_gaussian(scalar_grid, n): @exporter.export -@preprocess_and_wrap(wrap_like='scalar_grid', match_unit=True, to_magnitude=True) +@preprocess_and_wrap(wrap_like="scalar_grid", match_unit=True, to_magnitude=True) def smooth_window(scalar_grid, window, passes=1, normalize_weights=True): """Filter with an arbitrary window smoother. @@ -823,6 +850,7 @@ def smooth_window(scalar_grid, window, passes=1, normalize_weights=True): smooth_rectangular, smooth_circular, smooth_n_point, smooth_gaussian """ + def _pad(n): # Return number of entries to pad given length along dimension. return (n - 1) // 2 @@ -842,7 +870,7 @@ def _trailing_dims(indexer): # Verify that shape in all dimensions is odd (need to have a neighboorhood around a # central point) if any((size % 2 == 0) for size in window.shape): - raise ValueError('The shape of the smoothing window must be odd in all dimensions.') + raise ValueError("The shape of the smoothing window must be odd in all dimensions.") # Optionally normalize the supplied weighting window if normalize_weights: @@ -858,16 +886,18 @@ def _trailing_dims(indexer): # Index for full array elements, offset by the weight index def offset_full_index(weight_index): - return _trailing_dims(_offset(_pad(n), weight_index[i] - _pad(n)) - for i, n in enumerate(weights.shape)) + return _trailing_dims( + _offset(_pad(n), weight_index[i] - _pad(n)) for i, n in enumerate(weights.shape) + ) # TODO: this is not lazy-loading/dask compatible, as it "densifies" the data data = np.array(scalar_grid) for _ in range(passes): # Set values corresponding to smoothing weights by summing over each weight and # applying offsets in needed dimensions - data[inner_full_index] = sum(weights[index] * data[offset_full_index(index)] - for index in weight_indexes) + data[inner_full_index] = sum( + weights[index] * data[offset_full_index(index)] for index in weight_indexes + ) return data @@ -994,23 +1024,23 @@ def smooth_n_point(scalar_grid, n=5, passes=1): """ if n == 9: - weights = np.array([[0.0625, 0.125, 0.0625], - [0.125, 0.25, 0.125], - [0.0625, 0.125, 0.0625]]) + weights = np.array( + [[0.0625, 0.125, 0.0625], [0.125, 0.25, 0.125], [0.0625, 0.125, 0.0625]] + ) elif n == 5: - weights = np.array([[0., 0.125, 0.], - [0.125, 0.5, 0.125], - [0., 0.125, 0.]]) + weights = np.array([[0.0, 0.125, 0.0], [0.125, 0.5, 0.125], [0.0, 0.125, 0.0]]) else: - raise ValueError('The number of points to use in the smoothing ' - 'calculation must be either 5 or 9.') + raise ValueError( + "The number of points to use in the smoothing " + "calculation must be either 5 or 9." + ) return smooth_window(scalar_grid, window=weights, passes=passes, normalize_weights=False) @exporter.export -@preprocess_and_wrap(wrap_like='altimeter_value') -@check_units('[pressure]', '[length]') +@preprocess_and_wrap(wrap_like="altimeter_value") +@check_units("[pressure]", "[length]") def altimeter_to_station_pressure(altimeter_value, height): r"""Convert the altimeter measurement to station pressure. @@ -1080,19 +1110,19 @@ def altimeter_to_station_pressure(altimeter_value, height): """ # Gamma Value for this case - gamma = 0.0065 * units('K/m') + gamma = 0.0065 * units("K/m") # N-Value n = (mpconsts.Rd * gamma / mpconsts.g).to_base_units() - return ((altimeter_value ** n - - ((p0.to(altimeter_value.units) ** n * gamma * height) / t0)) ** (1 / n) - + 0.3 * units.hPa) + return ( + altimeter_value ** n - ((p0.to(altimeter_value.units) ** n * gamma * height) / t0) + ) ** (1 / n) + 0.3 * units.hPa @exporter.export -@preprocess_and_wrap(wrap_like='altimeter_value') -@check_units('[pressure]', '[length]', '[temperature]') +@preprocess_and_wrap(wrap_like="altimeter_value") +@check_units("[pressure]", "[length]", "[temperature]") def altimeter_to_sea_level_pressure(altimeter_value, height, temperature): r"""Convert the altimeter setting to sea-level pressure. @@ -1179,10 +1209,12 @@ def _check_radians(value, max_radians=2 * np.pi): """ try: - value = value.to('radians').m + value = value.to("radians").m except AttributeError: pass if np.any(np.greater(np.abs(value), max_radians)): - warnings.warn('Input over {} radians. ' - 'Ensure proper units are given.'.format(np.nanmax(max_radians))) + warnings.warn( + "Input over {} radians. " + "Ensure proper units are given.".format(np.nanmax(max_radians)) + ) return value diff --git a/src/metpy/calc/cross_sections.py b/src/metpy/calc/cross_sections.py index bbae1520762..4c0fd300210 100644 --- a/src/metpy/calc/cross_sections.py +++ b/src/metpy/calc/cross_sections.py @@ -10,11 +10,11 @@ import numpy as np import xarray as xr -from .basic import coriolis_parameter -from .tools import first_derivative from ..package_tools import Exporter from ..units import units from ..xarray import check_axis, check_matching_coordinates +from .basic import coriolis_parameter +from .tools import first_derivative exporter = Exporter(globals()) @@ -34,7 +34,7 @@ def distances_from_cross_section(cross): A tuple of the x and y distances as DataArrays """ - if check_axis(cross.metpy.x, 'longitude') and check_axis(cross.metpy.y, 'latitude'): + if check_axis(cross.metpy.x, "longitude") and check_axis(cross.metpy.y, "latitude"): # Use pyproj to obtain x and y distances from pyproj import Geod @@ -42,10 +42,12 @@ def distances_from_cross_section(cross): lon = cross.metpy.x lat = cross.metpy.y - forward_az, _, distance = g.inv(lon[0].values * np.ones_like(lon), - lat[0].values * np.ones_like(lat), - lon.values, - lat.values) + forward_az, _, distance = g.inv( + lon[0].values * np.ones_like(lon), + lat[0].values * np.ones_like(lat), + lon.values, + lat.values, + ) x = distance * np.sin(np.deg2rad(forward_az)) y = distance * np.cos(np.deg2rad(forward_az)) @@ -53,14 +55,14 @@ def distances_from_cross_section(cross): x = xr.DataArray(x * units.meter, coords=lon.coords, dims=lon.dims) y = xr.DataArray(y * units.meter, coords=lat.coords, dims=lat.dims) - elif check_axis(cross.metpy.x, 'x') and check_axis(cross.metpy.y, 'y'): + elif check_axis(cross.metpy.x, "x") and check_axis(cross.metpy.y, "y"): # Simply return what we have x = cross.metpy.x y = cross.metpy.y else: - raise AttributeError('Sufficient horizontal coordinates not defined.') + raise AttributeError("Sufficient horizontal coordinates not defined.") return x, y @@ -80,19 +82,20 @@ def latitude_from_cross_section(cross): """ y = cross.metpy.y - if check_axis(y, 'latitude'): + if check_axis(y, "latitude"): return y else: import cartopy.crs as ccrs - latitude = ccrs.Geodetic().transform_points(cross.metpy.cartopy_crs, - cross.metpy.x.values, - y.values)[..., 1] + + latitude = ccrs.Geodetic().transform_points( + cross.metpy.cartopy_crs, cross.metpy.x.values, y.values + )[..., 1] latitude = xr.DataArray(latitude * units.degrees_north, coords=y.coords, dims=y.dims) return latitude @exporter.export -def unit_vectors_from_cross_section(cross, index='index'): +def unit_vectors_from_cross_section(cross, index="index"): r"""Calculate the unit tanget and unit normal vectors from a cross-section. Given a path described parametrically by :math:`\vec{l}(i) = (x(i), y(i))`, we can find @@ -131,7 +134,7 @@ def unit_vectors_from_cross_section(cross, index='index'): @exporter.export @check_matching_coordinates -def cross_section_components(data_x, data_y, index='index'): +def cross_section_components(data_x, data_y, index="index"): r"""Obtain the tangential and normal components of a cross-section of a vector field. Parameters @@ -170,7 +173,7 @@ def cross_section_components(data_x, data_y, index='index'): @exporter.export @check_matching_coordinates -def normal_component(data_x, data_y, index='index'): +def normal_component(data_x, data_y, index="index"): r"""Obtain the normal component of a cross-section of a vector field. Parameters @@ -203,15 +206,15 @@ def normal_component(data_x, data_y, index='index'): component_norm = data_x * unit_norm[0] + data_y * unit_norm[1] # Reattach only reliable attributes after operation - if 'grid_mapping' in data_x.attrs: - component_norm.attrs['grid_mapping'] = data_x.attrs['grid_mapping'] + if "grid_mapping" in data_x.attrs: + component_norm.attrs["grid_mapping"] = data_x.attrs["grid_mapping"] return component_norm @exporter.export @check_matching_coordinates -def tangential_component(data_x, data_y, index='index'): +def tangential_component(data_x, data_y, index="index"): r"""Obtain the tangential component of a cross-section of a vector field. Parameters @@ -244,15 +247,15 @@ def tangential_component(data_x, data_y, index='index'): component_tang = data_x * unit_tang[0] + data_y * unit_tang[1] # Reattach only reliable attributes after operation - if 'grid_mapping' in data_x.attrs: - component_tang.attrs['grid_mapping'] = data_x.attrs['grid_mapping'] + if "grid_mapping" in data_x.attrs: + component_tang.attrs["grid_mapping"] = data_x.attrs["grid_mapping"] return component_tang @exporter.export @check_matching_coordinates -def absolute_momentum(u, v, index='index'): +def absolute_momentum(u, v, index="index"): r"""Calculate cross-sectional absolute momentum (also called pseudoangular momentum). As given in [Schultz1999]_, absolute momentum (also called pseudoangular momentum) is @@ -286,15 +289,15 @@ def absolute_momentum(u, v, index='index'): """ # Get the normal component of the wind - norm_wind = normal_component(u, v, index=index).metpy.convert_units('m/s') + norm_wind = normal_component(u, v, index=index).metpy.convert_units("m/s") # Get other pieces of calculation (all as ndarrays matching shape of norm_wind) latitude = latitude_from_cross_section(norm_wind) _, latitude = xr.broadcast(norm_wind, latitude) f = coriolis_parameter(np.deg2rad(latitude.values)) x, y = distances_from_cross_section(norm_wind) - x = x.metpy.convert_units('meters') - y = y.metpy.convert_units('meters') + x = x.metpy.convert_units("meters") + y = y.metpy.convert_units("meters") _, x, y = xr.broadcast(norm_wind, x, y) distance = np.hypot(x.metpy.quantify(), y.metpy.quantify()) diff --git a/src/metpy/calc/indices.py b/src/metpy/calc/indices.py index 1fda8c50b36..e90cfbc640f 100644 --- a/src/metpy/calc/indices.py +++ b/src/metpy/calc/indices.py @@ -4,19 +4,19 @@ """Contains calculation of various derived indices.""" import numpy as np -from .thermo import mixing_ratio, saturation_vapor_pressure -from .tools import _remove_nans, get_layer from .. import constants as mpconsts from ..package_tools import Exporter from ..units import check_units, concatenate, units from ..xarray import preprocess_and_wrap +from .thermo import mixing_ratio, saturation_vapor_pressure +from .tools import _remove_nans, get_layer exporter = Exporter(globals()) @exporter.export -@preprocess_and_wrap(wrap_like='dewpoint') -@check_units('[pressure]', '[temperature]', bottom='[pressure]', top='[pressure]') +@preprocess_and_wrap(wrap_like="dewpoint") +@check_units("[pressure]", "[temperature]", bottom="[pressure]", top="[pressure]") def precipitable_water(pressure, dewpoint, *, bottom=None, top=None): r"""Calculate precipitable water through the depth of a sounding. @@ -66,20 +66,24 @@ def precipitable_water(pressure, dewpoint, *, bottom=None, top=None): if bottom is None: bottom = np.nanmax(pressure.magnitude) * pressure.units - pres_layer, dewpoint_layer = get_layer(pressure, dewpoint, bottom=bottom, - depth=bottom - top) + pres_layer, dewpoint_layer = get_layer( + pressure, dewpoint, bottom=bottom, depth=bottom - top + ) w = mixing_ratio(saturation_vapor_pressure(dewpoint_layer), pres_layer) # Since pressure is in decreasing order, pw will be the opposite sign of that expected. - pw = -1. * (np.trapz(w.magnitude, pres_layer.magnitude) * (w.units * pres_layer.units) - / (mpconsts.g * mpconsts.rho_l)) - return pw.to('millimeters') + pw = -1.0 * ( + np.trapz(w.magnitude, pres_layer.magnitude) + * (w.units * pres_layer.units) + / (mpconsts.g * mpconsts.rho_l) + ) + return pw.to("millimeters") @exporter.export @preprocess_and_wrap() -@check_units('[pressure]') +@check_units("[pressure]") def mean_pressure_weighted(pressure, *args, height=None, bottom=None, depth=None): r"""Calculate pressure-weighted mean of an arbitrary variable through a layer. @@ -115,16 +119,14 @@ def mean_pressure_weighted(pressure, *args, height=None, bottom=None, depth=None """ ret = [] # Returned variable means in layer - layer_arg = get_layer(pressure, *args, height=height, - bottom=bottom, depth=depth) + layer_arg = get_layer(pressure, *args, height=height, bottom=bottom, depth=depth) layer_p = layer_arg[0] layer_arg = layer_arg[1:] # Taking the integral of the weights (pressure) to feed into the weighting # function. Said integral works out to this function: - pres_int = 0.5 * (layer_p[-1].magnitude**2 - layer_p[0].magnitude**2) + pres_int = 0.5 * (layer_p[-1].magnitude ** 2 - layer_p[0].magnitude ** 2) for i, datavar in enumerate(args): - arg_mean = np.trapz((layer_arg[i] * layer_p).magnitude, - x=layer_p.magnitude) / pres_int + arg_mean = np.trapz((layer_arg[i] * layer_p).magnitude, x=layer_p.magnitude) / pres_int ret.append(arg_mean * datavar.units) return ret @@ -132,7 +134,7 @@ def mean_pressure_weighted(pressure, *args, height=None, bottom=None, depth=None @exporter.export @preprocess_and_wrap() -@check_units('[pressure]', '[speed]', '[speed]', '[length]') +@check_units("[pressure]", "[speed]", "[speed]", "[length]") def bunkers_storm_motion(pressure, u, v, height): r"""Calculate the Bunkers right-mover and left-mover storm motions and sfc-6km mean flow. @@ -166,17 +168,26 @@ def bunkers_storm_motion(pressure, u, v, height): """ # mean wind from sfc-6km - wind_mean = concatenate(mean_pressure_weighted(pressure, u, v, height=height, - depth=6000 * units('meter'))) + wind_mean = concatenate( + mean_pressure_weighted(pressure, u, v, height=height, depth=6000 * units("meter")) + ) # mean wind from sfc-500m - wind_500m = concatenate(mean_pressure_weighted(pressure, u, v, height=height, - depth=500 * units('meter'))) + wind_500m = concatenate( + mean_pressure_weighted(pressure, u, v, height=height, depth=500 * units("meter")) + ) # mean wind from 5.5-6km - wind_5500m = concatenate(mean_pressure_weighted(pressure, u, v, height=height, - depth=500 * units('meter'), - bottom=height[0] + 5500 * units('meter'))) + wind_5500m = concatenate( + mean_pressure_weighted( + pressure, + u, + v, + height=height, + depth=500 * units("meter"), + bottom=height[0] + 5500 * units("meter"), + ) + ) # Calculate the shear vector from sfc-500m to 5.5-6km shear = wind_5500m - wind_500m @@ -185,7 +196,7 @@ def bunkers_storm_motion(pressure, u, v, height): # multiply by the deviaton empirically calculated in Bunkers (2000) (7.5 m/s) shear_cross = concatenate([shear[1], -shear[0]]) shear_mag = np.hypot(*(arg.magnitude for arg in shear)) * shear.units - rdev = shear_cross * (7.5 * units('m/s').to(u.units) / shear_mag) + rdev = shear_cross * (7.5 * units("m/s").to(u.units) / shear_mag) # Add the deviations to the layer average wind to get the RM motion right_mover = wind_mean + rdev @@ -198,7 +209,7 @@ def bunkers_storm_motion(pressure, u, v, height): @exporter.export @preprocess_and_wrap() -@check_units('[pressure]', '[speed]', '[speed]') +@check_units("[pressure]", "[speed]", "[speed]") def bulk_shear(pressure, u, v, height=None, bottom=None, depth=None): r"""Calculate bulk shear through a layer. @@ -236,8 +247,7 @@ def bulk_shear(pressure, u, v, height=None, bottom=None, depth=None): Quantities even when given xarray DataArray profiles. """ - _, u_layer, v_layer = get_layer(pressure, u, v, height=height, - bottom=bottom, depth=depth) + _, u_layer, v_layer = get_layer(pressure, u, v, height=height, bottom=bottom, depth=depth) u_shr = u_layer[-1] - u_layer[0] v_shr = v_layer[-1] - v_layer[0] @@ -246,8 +256,8 @@ def bulk_shear(pressure, u, v, height=None, bottom=None, depth=None): @exporter.export -@preprocess_and_wrap(wrap_like='mucape') -@check_units('[energy] / [mass]', '[speed] * [speed]', '[speed]') +@preprocess_and_wrap(wrap_like="mucape") +@check_units("[energy] / [mass]", "[speed] * [speed]", "[speed]") def supercell_composite(mucape, effective_storm_helicity, effective_shear): r"""Calculate the supercell composite parameter. @@ -278,18 +288,20 @@ def supercell_composite(mucape, effective_storm_helicity, effective_shear): supercell composite """ - effective_shear = np.clip(np.atleast_1d(effective_shear), None, 20 * units('m/s')) - effective_shear[effective_shear < 10 * units('m/s')] = 0 * units('m/s') - effective_shear = effective_shear / (20 * units('m/s')) + effective_shear = np.clip(np.atleast_1d(effective_shear), None, 20 * units("m/s")) + effective_shear[effective_shear < 10 * units("m/s")] = 0 * units("m/s") + effective_shear = effective_shear / (20 * units("m/s")) - return ((mucape / (1000 * units('J/kg'))) - * (effective_storm_helicity / (50 * units('m^2/s^2'))) - * effective_shear).to('dimensionless') + return ( + (mucape / (1000 * units("J/kg"))) + * (effective_storm_helicity / (50 * units("m^2/s^2"))) + * effective_shear + ).to("dimensionless") @exporter.export -@preprocess_and_wrap(wrap_like='sbcape') -@check_units('[energy] / [mass]', '[length]', '[speed] * [speed]', '[speed]') +@preprocess_and_wrap(wrap_like="sbcape") +@check_units("[energy] / [mass]", "[length]", "[speed] * [speed]", "[speed]") def significant_tornado(sbcape, surface_based_lcl_height, storm_helicity_1km, shear_6km): r"""Calculate the significant tornado parameter (fixed layer). @@ -326,24 +338,28 @@ def significant_tornado(sbcape, surface_based_lcl_height, storm_helicity_1km, sh significant tornado parameter """ - surface_based_lcl_height = np.clip(np.atleast_1d(surface_based_lcl_height), - 1000 * units.m, 2000 * units.m) + surface_based_lcl_height = np.clip( + np.atleast_1d(surface_based_lcl_height), 1000 * units.m, 2000 * units.m + ) surface_based_lcl_height[surface_based_lcl_height > 2000 * units.m] = 0 * units.m - surface_based_lcl_height = ((2000. * units.m - surface_based_lcl_height) - / (1000. * units.m)) - shear_6km = np.clip(np.atleast_1d(shear_6km), None, 30 * units('m/s')) - shear_6km[shear_6km < 12.5 * units('m/s')] = 0 * units('m/s') - shear_6km /= 20 * units('m/s') + surface_based_lcl_height = (2000.0 * units.m - surface_based_lcl_height) / ( + 1000.0 * units.m + ) + shear_6km = np.clip(np.atleast_1d(shear_6km), None, 30 * units("m/s")) + shear_6km[shear_6km < 12.5 * units("m/s")] = 0 * units("m/s") + shear_6km /= 20 * units("m/s") - return ((sbcape / (1500. * units('J/kg'))) - * surface_based_lcl_height - * (storm_helicity_1km / (150. * units('m^2/s^2'))) - * shear_6km) + return ( + (sbcape / (1500.0 * units("J/kg"))) + * surface_based_lcl_height + * (storm_helicity_1km / (150.0 * units("m^2/s^2"))) + * shear_6km + ) @exporter.export @preprocess_and_wrap() -@check_units('[pressure]', '[speed]', '[speed]', '[length]', '[speed]', '[speed]') +@check_units("[pressure]", "[speed]", "[speed]", "[length]", "[speed]", "[speed]") def critical_angle(pressure, u, v, height, u_storm, v_storm): r"""Calculate the critical angle. @@ -382,10 +398,10 @@ def critical_angle(pressure, u, v, height, u_storm, v_storm): """ # Convert everything to m/s - u = u.to('m/s') - v = v.to('m/s') - u_storm = u_storm.to('m/s') - v_storm = v_storm.to('m/s') + u = u.to("m/s") + v = v.to("m/s") + u_storm = u_storm.to("m/s") + v_storm = v_storm.to("m/s") sort_inds = np.argsort(pressure[::-1]) pressure = pressure[sort_inds] @@ -394,7 +410,7 @@ def critical_angle(pressure, u, v, height, u_storm, v_storm): v = v[sort_inds] # Calculate sfc-500m shear vector - shr5 = bulk_shear(pressure, u, v, height=height, depth=500 * units('meter')) + shr5 = bulk_shear(pressure, u, v, height=height, depth=500 * units("meter")) # Make everything relative to the sfc wind orientation umn = u_storm - u[0] @@ -403,6 +419,6 @@ def critical_angle(pressure, u, v, height, u_storm, v_storm): vshr = np.asarray([shr5[0].magnitude, shr5[1].magnitude]) vsm = np.asarray([umn.magnitude, vmn.magnitude]) angle_c = np.dot(vshr, vsm) / (np.linalg.norm(vshr) * np.linalg.norm(vsm)) - critical_angle = np.arccos(angle_c) * units('radian') + critical_angle = np.arccos(angle_c) * units("radian") - return critical_angle.to('degrees') + return critical_angle.to("degrees") diff --git a/src/metpy/calc/kinematics.py b/src/metpy/calc/kinematics.py index 1ee602fe85f..135699d67e4 100644 --- a/src/metpy/calc/kinematics.py +++ b/src/metpy/calc/kinematics.py @@ -5,19 +5,19 @@ import numpy as np from . import coriolis_parameter -from .tools import first_derivative, get_layer_heights, gradient from .. import constants as mpconsts from ..package_tools import Exporter from ..units import check_units, units from ..xarray import add_grid_arguments_from_xarray, preprocess_and_wrap +from .tools import first_derivative, get_layer_heights, gradient exporter = Exporter(globals()) @exporter.export @add_grid_arguments_from_xarray -@preprocess_and_wrap(wrap_like='u') -@check_units('[speed]', '[speed]', dx='[length]', dy='[length]') +@preprocess_and_wrap(wrap_like="u") +@check_units("[speed]", "[speed]", dx="[length]", dy="[length]") def vorticity(u, v, *, dx=None, dy=None, x_dim=-1, y_dim=-2): r"""Calculate the vertical vorticity of the horizontal wind. @@ -59,8 +59,8 @@ def vorticity(u, v, *, dx=None, dy=None, x_dim=-1, y_dim=-2): @exporter.export @add_grid_arguments_from_xarray -@preprocess_and_wrap(wrap_like='u') -@check_units(dx='[length]', dy='[length]') +@preprocess_and_wrap(wrap_like="u") +@check_units(dx="[length]", dy="[length]") def divergence(u, v, *, dx=None, dy=None, x_dim=-1, y_dim=-2): r"""Calculate the horizontal divergence of a vector. @@ -102,8 +102,8 @@ def divergence(u, v, *, dx=None, dy=None, x_dim=-1, y_dim=-2): @exporter.export @add_grid_arguments_from_xarray -@preprocess_and_wrap(wrap_like='u') -@check_units('[speed]', '[speed]', '[length]', '[length]') +@preprocess_and_wrap(wrap_like="u") +@check_units("[speed]", "[speed]", "[length]", "[length]") def shearing_deformation(u, v, dx=None, dy=None, x_dim=-1, y_dim=-2): r"""Calculate the shearing deformation of the horizontal wind. @@ -145,8 +145,8 @@ def shearing_deformation(u, v, dx=None, dy=None, x_dim=-1, y_dim=-2): @exporter.export @add_grid_arguments_from_xarray -@preprocess_and_wrap(wrap_like='u') -@check_units('[speed]', '[speed]', '[length]', '[length]') +@preprocess_and_wrap(wrap_like="u") +@check_units("[speed]", "[speed]", "[length]", "[length]") def stretching_deformation(u, v, dx=None, dy=None, x_dim=-1, y_dim=-2): r"""Calculate the stretching deformation of the horizontal wind. @@ -188,8 +188,8 @@ def stretching_deformation(u, v, dx=None, dy=None, x_dim=-1, y_dim=-2): @exporter.export @add_grid_arguments_from_xarray -@preprocess_and_wrap(wrap_like='u') -@check_units('[speed]', '[speed]', '[length]', '[length]') +@preprocess_and_wrap(wrap_like="u") +@check_units("[speed]", "[speed]", "[length]", "[length]") def total_deformation(u, v, dx=None, dy=None, x_dim=-1, y_dim=-2): r"""Calculate the horizontal total deformation of the horizontal wind. @@ -231,11 +231,11 @@ def total_deformation(u, v, dx=None, dy=None, x_dim=-1, y_dim=-2): """ dudy, dudx = gradient(u, deltas=(dy, dx), axes=(y_dim, x_dim)) dvdy, dvdx = gradient(v, deltas=(dy, dx), axes=(y_dim, x_dim)) - return np.sqrt((dvdx + dudy)**2 + (dudx - dvdy)**2) + return np.sqrt((dvdx + dudy) ** 2 + (dudx - dvdy) ** 2) @exporter.export -@preprocess_and_wrap(wrap_like='scalar', broadcast=('scalar', 'u', 'v', 'w')) +@preprocess_and_wrap(wrap_like="scalar", broadcast=("scalar", "u", "v", "w")) def advection( scalar, u=None, @@ -288,11 +288,7 @@ def advection( """ return -sum( wind * first_derivative(scalar, axis=axis, delta=delta) - for wind, delta, axis in ( - (u, dx, x_dim), - (v, dy, y_dim), - (w, dz, vertical_dim) - ) + for wind, delta, axis in ((u, dx, x_dim), (v, dy, y_dim), (w, dz, vertical_dim)) if wind is not None ) @@ -300,10 +296,9 @@ def advection( @exporter.export @add_grid_arguments_from_xarray @preprocess_and_wrap( - wrap_like='potential_temperature', - broadcast=('potential_temperature', 'u', 'v') + wrap_like="potential_temperature", broadcast=("potential_temperature", "u", "v") ) -@check_units('[temperature]', '[speed]', '[speed]', '[length]', '[length]') +@check_units("[temperature]", "[speed]", "[speed]", "[length]", "[length]") def frontogenesis(potential_temperature, u, v, dx=None, dy=None, x_dim=-1, y_dim=-2): r"""Calculate the 2D kinematic frontogenesis of a temperature field. @@ -357,7 +352,7 @@ def frontogenesis(potential_temperature, u, v, dx=None, dy=None, x_dim=-1, y_dim ddx_thta = first_derivative(potential_temperature, delta=dx, axis=x_dim) # Compute the magnitude of the potential temperature gradient - mag_thta = np.sqrt(ddx_thta**2 + ddy_thta**2) + mag_thta = np.sqrt(ddx_thta ** 2 + ddy_thta ** 2) # Get the shearing, stretching, and total deformation of the wind field shrd = shearing_deformation(u, v, dx, dy, x_dim=x_dim, y_dim=y_dim) @@ -376,8 +371,8 @@ def frontogenesis(potential_temperature, u, v, dx=None, dy=None, x_dim=-1, y_dim @exporter.export @add_grid_arguments_from_xarray -@preprocess_and_wrap(wrap_like=('height', 'height'), broadcast=('height', 'latitude')) -@check_units(dx='[length]', dy='[length]', latitude='[dimensionless]') +@preprocess_and_wrap(wrap_like=("height", "height"), broadcast=("height", "latitude")) +@check_units(dx="[length]", dy="[length]", latitude="[dimensionless]") def geostrophic_wind(height, dx=None, dy=None, latitude=None, x_dim=-1, y_dim=-2): r"""Calculate the geostrophic wind given from the height or geopotential. @@ -412,8 +407,8 @@ def geostrophic_wind(height, dx=None, dy=None, latitude=None, x_dim=-1, y_dim=-2 """ f = coriolis_parameter(latitude) - if height.dimensionality['[length]'] == 2.0: - norm_factor = 1. / f + if height.dimensionality["[length]"] == 2.0: + norm_factor = 1.0 / f else: norm_factor = mpconsts.g / f @@ -425,15 +420,10 @@ def geostrophic_wind(height, dx=None, dy=None, latitude=None, x_dim=-1, y_dim=-2 @exporter.export @add_grid_arguments_from_xarray @preprocess_and_wrap( - wrap_like=('height', 'height'), - broadcast=('height', 'u', 'v', 'latitude') + wrap_like=("height", "height"), broadcast=("height", "u", "v", "latitude") ) @check_units( - u='[speed]', - v='[speed]', - dx='[length]', - dy='[length]', - latitude='[dimensionless]' + u="[speed]", v="[speed]", dx="[length]", dy="[length]", latitude="[dimensionless]" ) def ageostrophic_wind(height, u, v, dx=None, dy=None, latitude=None, x_dim=-1, y_dim=-2): r"""Calculate the ageostrophic wind given from the height or geopotential. @@ -473,19 +463,14 @@ def ageostrophic_wind(height, u, v, dx=None, dy=None, latitude=None, x_dim=-1, y """ u_geostrophic, v_geostrophic = geostrophic_wind( - height, - dx, - dy, - latitude, - x_dim=x_dim, - y_dim=y_dim + height, dx, dy, latitude, x_dim=x_dim, y_dim=y_dim ) return u - u_geostrophic, v - v_geostrophic @exporter.export -@preprocess_and_wrap(wrap_like='height', broadcast=('height', 'temperature')) -@check_units('[length]', '[temperature]') +@preprocess_and_wrap(wrap_like="height", broadcast=("height", "temperature")) +@check_units("[length]", "[temperature]") def montgomery_streamfunction(height, temperature): r"""Compute the Montgomery Streamfunction on isentropic surfaces. @@ -527,10 +512,25 @@ def montgomery_streamfunction(height, temperature): @exporter.export @preprocess_and_wrap() -@check_units('[length]', '[speed]', '[speed]', '[length]', - bottom='[length]', storm_u='[speed]', storm_v='[speed]') -def storm_relative_helicity(height, u, v, depth, *, bottom=0 * units.m, - storm_u=0 * units('m/s'), storm_v=0 * units('m/s')): +@check_units( + "[length]", + "[speed]", + "[speed]", + "[length]", + bottom="[length]", + storm_u="[speed]", + storm_v="[speed]", +) +def storm_relative_helicity( + height, + u, + v, + depth, + *, + bottom=0 * units.m, + storm_u=0 * units("m/s"), + storm_v=0 * units("m/s") +): # Partially adapted from similar SharpPy code r"""Calculate storm relative helicity. @@ -581,27 +581,31 @@ def storm_relative_helicity(height, u, v, depth, *, bottom=0 * units.m, storm_relative_u = u - storm_u storm_relative_v = v - storm_v - int_layers = (storm_relative_u[1:] * storm_relative_v[:-1] - - storm_relative_u[:-1] * storm_relative_v[1:]) + int_layers = ( + storm_relative_u[1:] * storm_relative_v[:-1] + - storm_relative_u[:-1] * storm_relative_v[1:] + ) # Need to manually check for masked value because sum() on masked array with non-default # mask will return a masked value rather than 0. See numpy/numpy#11736 - positive_srh = int_layers[int_layers.magnitude > 0.].sum() + positive_srh = int_layers[int_layers.magnitude > 0.0].sum() if np.ma.is_masked(positive_srh): - positive_srh = 0.0 * units('meter**2 / second**2') - negative_srh = int_layers[int_layers.magnitude < 0.].sum() + positive_srh = 0.0 * units("meter**2 / second**2") + negative_srh = int_layers[int_layers.magnitude < 0.0].sum() if np.ma.is_masked(negative_srh): - negative_srh = 0.0 * units('meter**2 / second**2') + negative_srh = 0.0 * units("meter**2 / second**2") - return (positive_srh.to('meter ** 2 / second ** 2'), - negative_srh.to('meter ** 2 / second ** 2'), - (positive_srh + negative_srh).to('meter ** 2 / second ** 2')) + return ( + positive_srh.to("meter ** 2 / second ** 2"), + negative_srh.to("meter ** 2 / second ** 2"), + (positive_srh + negative_srh).to("meter ** 2 / second ** 2"), + ) @exporter.export @add_grid_arguments_from_xarray -@preprocess_and_wrap(wrap_like='u', broadcast=('u', 'v', 'latitude')) -@check_units('[speed]', '[speed]', '[length]', '[length]') +@preprocess_and_wrap(wrap_like="u", broadcast=("u", "v", "latitude")) +@check_units("[speed]", "[speed]", "[length]", "[length]") def absolute_vorticity(u, v, dx=None, dy=None, latitude=None, x_dim=-1, y_dim=-2): """Calculate the absolute vorticity of the horizontal wind. @@ -644,11 +648,18 @@ def absolute_vorticity(u, v, dx=None, dy=None, latitude=None, x_dim=-1, y_dim=-2 @exporter.export @add_grid_arguments_from_xarray @preprocess_and_wrap( - wrap_like='potential_temperature', - broadcast=('potential_temperature', 'pressure', 'u', 'v', 'latitude') + wrap_like="potential_temperature", + broadcast=("potential_temperature", "pressure", "u", "v", "latitude"), +) +@check_units( + "[temperature]", + "[pressure]", + "[speed]", + "[speed]", + "[length]", + "[length]", + "[dimensionless]", ) -@check_units('[temperature]', '[pressure]', '[speed]', '[speed]', - '[length]', '[length]', '[dimensionless]') def potential_vorticity_baroclinic( potential_temperature, pressure, @@ -659,7 +670,7 @@ def potential_vorticity_baroclinic( latitude=None, x_dim=-1, y_dim=-2, - vertical_dim=-3 + vertical_dim=-3, ): r"""Calculate the baroclinic potential vorticity. @@ -726,15 +737,16 @@ def potential_vorticity_baroclinic( or np.shape(pressure)[vertical_dim] < 3 or np.shape(potential_temperature)[vertical_dim] != np.shape(pressure)[vertical_dim] ): - raise ValueError('Length of potential temperature along the vertical axis ' - '{} must be at least 3.'.format(vertical_dim)) + raise ValueError( + "Length of potential temperature along the vertical axis " + "{} must be at least 3.".format(vertical_dim) + ) avor = absolute_vorticity(u, v, dx, dy, latitude, x_dim=x_dim, y_dim=y_dim) dthtadp = first_derivative(potential_temperature, x=pressure, axis=vertical_dim) - if ( - (np.shape(potential_temperature)[y_dim] == 1) - and (np.shape(potential_temperature)[x_dim] == 1) + if (np.shape(potential_temperature)[y_dim] == 1) and ( + np.shape(potential_temperature)[x_dim] == 1 ): dthtady = 0 * units.K / units.m # axis=y_dim only has one dimension dthtadx = 0 * units.K / units.m # axis=x_dim only has one dimension @@ -744,24 +756,17 @@ def potential_vorticity_baroclinic( dudp = first_derivative(u, x=pressure, axis=vertical_dim) dvdp = first_derivative(v, x=pressure, axis=vertical_dim) - return (-mpconsts.g * (dudp * dthtady - dvdp * dthtadx - + avor * dthtadp)).to(units.kelvin * units.meter**2 - / (units.second * units.kilogram)) + return (-mpconsts.g * (dudp * dthtady - dvdp * dthtadx + avor * dthtadp)).to( + units.kelvin * units.meter ** 2 / (units.second * units.kilogram) + ) @exporter.export @add_grid_arguments_from_xarray -@preprocess_and_wrap(wrap_like='height', broadcast=('height', 'u', 'v', 'latitude')) -@check_units('[length]', '[speed]', '[speed]', '[length]', '[length]', '[dimensionless]') +@preprocess_and_wrap(wrap_like="height", broadcast=("height", "u", "v", "latitude")) +@check_units("[length]", "[speed]", "[speed]", "[length]", "[length]", "[dimensionless]") def potential_vorticity_barotropic( - height, - u, - v, - dx=None, - dy=None, - latitude=None, - x_dim=-1, - y_dim=-2 + height, u, v, dx=None, dy=None, latitude=None, x_dim=-1, y_dim=-2 ): r"""Calculate the barotropic (Rossby) potential vorticity. @@ -803,27 +808,19 @@ def potential_vorticity_barotropic( """ avor = absolute_vorticity(u, v, dx, dy, latitude, x_dim=x_dim, y_dim=y_dim) - return (avor / height).to('meter**-1 * second**-1') + return (avor / height).to("meter**-1 * second**-1") @exporter.export @add_grid_arguments_from_xarray @preprocess_and_wrap( - wrap_like=('u', 'u'), - broadcast=('u', 'v', 'u_geostrophic', 'v_geostrophic', 'latitude') + wrap_like=("u", "u"), broadcast=("u", "v", "u_geostrophic", "v_geostrophic", "latitude") +) +@check_units( + "[speed]", "[speed]", "[speed]", "[speed]", "[length]", "[length]", "[dimensionless]" ) -@check_units('[speed]', '[speed]', '[speed]', '[speed]', '[length]', '[length]', - '[dimensionless]') def inertial_advective_wind( - u, - v, - u_geostrophic, - v_geostrophic, - dx=None, - dy=None, - latitude=None, - x_dim=-1, - y_dim=-2 + u, v, u_geostrophic, v_geostrophic, dx=None, dy=None, latitude=None, x_dim=-1, y_dim=-2 ): r"""Calculate the inertial advective wind. @@ -897,20 +894,11 @@ def inertial_advective_wind( @exporter.export @add_grid_arguments_from_xarray @preprocess_and_wrap( - wrap_like=('u', 'u'), - broadcast=('u', 'v', 'temperature', 'pressure', 'static_stability') + wrap_like=("u", "u"), broadcast=("u", "v", "temperature", "pressure", "static_stability") ) -@check_units('[speed]', '[speed]', '[temperature]', '[pressure]', '[length]', '[length]') +@check_units("[speed]", "[speed]", "[temperature]", "[pressure]", "[length]", "[length]") def q_vector( - u, - v, - temperature, - pressure, - dx=None, - dy=None, - static_stability=1, - x_dim=-1, - y_dim=-2 + u, v, temperature, pressure, dx=None, dy=None, static_stability=1, x_dim=-1, y_dim=-2 ): r"""Calculate Q-vector at a given pressure level using the u, v winds and temperature. diff --git a/src/metpy/calc/thermo.py b/src/metpy/calc/thermo.py index 99b82295599..2dac32e4f0d 100644 --- a/src/metpy/calc/thermo.py +++ b/src/metpy/calc/thermo.py @@ -9,14 +9,21 @@ import scipy.optimize as so import xarray as xr -from .tools import (_greater_or_close, _less_or_close, _remove_nans, find_bounding_indices, - find_intersections, first_derivative, get_layer) from .. import constants as mpconsts from ..cbook import broadcast_indices from ..interpolate.one_dimension import interpolate_1d from ..package_tools import Exporter from ..units import check_units, concatenate, units from ..xarray import add_vertical_dim_from_xarray, preprocess_and_wrap +from .tools import ( + _greater_or_close, + _less_or_close, + _remove_nans, + find_bounding_indices, + find_intersections, + first_derivative, + get_layer, +) exporter = Exporter(globals()) @@ -24,8 +31,8 @@ @exporter.export -@preprocess_and_wrap(wrap_like='temperature', broadcast=('temperature', 'dewpoint')) -@check_units('[temperature]', '[temperature]') +@preprocess_and_wrap(wrap_like="temperature", broadcast=("temperature", "dewpoint")) +@check_units("[temperature]", "[temperature]") def relative_humidity_from_dewpoint(temperature, dewpoint): r"""Calculate the relative humidity. @@ -51,12 +58,12 @@ def relative_humidity_from_dewpoint(temperature, dewpoint): """ e = saturation_vapor_pressure(dewpoint) e_s = saturation_vapor_pressure(temperature) - return (e / e_s) + return e / e_s @exporter.export -@preprocess_and_wrap(wrap_like='pressure') -@check_units('[pressure]', '[pressure]') +@preprocess_and_wrap(wrap_like="pressure") +@check_units("[pressure]", "[pressure]") def exner_function(pressure, reference_pressure=mpconsts.P0): r"""Calculate the Exner function. @@ -86,12 +93,12 @@ def exner_function(pressure, reference_pressure=mpconsts.P0): temperature_from_potential_temperature """ - return (pressure / reference_pressure).to('dimensionless')**mpconsts.kappa + return (pressure / reference_pressure).to("dimensionless") ** mpconsts.kappa @exporter.export -@preprocess_and_wrap(wrap_like='temperature', broadcast=('pressure', 'temperature')) -@check_units('[pressure]', '[temperature]') +@preprocess_and_wrap(wrap_like="temperature", broadcast=("pressure", "temperature")) +@check_units("[pressure]", "[temperature]") def potential_temperature(pressure, temperature): r"""Calculate the potential temperature. @@ -133,10 +140,9 @@ def potential_temperature(pressure, temperature): @exporter.export @preprocess_and_wrap( - wrap_like='potential_temperature', - broadcast=('pressure', 'potential_temperature') + wrap_like="potential_temperature", broadcast=("pressure", "potential_temperature") ) -@check_units('[pressure]', '[temperature]') +@check_units("[pressure]", "[temperature]") def temperature_from_potential_temperature(pressure, potential_temperature): r"""Calculate the temperature from a given potential temperature. @@ -181,10 +187,9 @@ def temperature_from_potential_temperature(pressure, potential_temperature): @exporter.export @preprocess_and_wrap( - wrap_like='temperature', - broadcast=('pressure', 'temperature', 'reference_pressure') + wrap_like="temperature", broadcast=("pressure", "temperature", "reference_pressure") ) -@check_units('[pressure]', '[temperature]', '[pressure]') +@check_units("[pressure]", "[temperature]", "[pressure]") def dry_lapse(pressure, temperature, reference_pressure=None, vertical_dim=0): r"""Calculate the temperature at a level assuming only dry processes. @@ -220,15 +225,14 @@ def dry_lapse(pressure, temperature, reference_pressure=None, vertical_dim=0): """ if reference_pressure is None: reference_pressure = pressure[0] - return temperature * (pressure / reference_pressure)**mpconsts.kappa + return temperature * (pressure / reference_pressure) ** mpconsts.kappa @exporter.export @preprocess_and_wrap( - wrap_like='temperature', - broadcast=('pressure', 'temperature', 'reference_pressure') + wrap_like="temperature", broadcast=("pressure", "temperature", "reference_pressure") ) -@check_units('[pressure]', '[temperature]', '[pressure]') +@check_units("[pressure]", "[temperature]", "[pressure]") def moist_lapse(pressure, temperature, reference_pressure=None): r"""Calculate the temperature at a level assuming liquid saturation processes. @@ -270,29 +274,34 @@ def moist_lapse(pressure, temperature, reference_pressure=None): grids). """ + def dt(t, p): t = units.Quantity(t, temperature.units) p = units.Quantity(p, pressure.units) rs = saturation_mixing_ratio(p, t) - frac = ((mpconsts.Rd * t + mpconsts.Lv * rs) - / (mpconsts.Cp_d + (mpconsts.Lv * mpconsts.Lv * rs * mpconsts.epsilon - / (mpconsts.Rd * t * t)))).to('kelvin') + frac = ( + (mpconsts.Rd * t + mpconsts.Lv * rs) + / ( + mpconsts.Cp_d + + (mpconsts.Lv * mpconsts.Lv * rs * mpconsts.epsilon / (mpconsts.Rd * t * t)) + ) + ).to("kelvin") return (frac / p).magnitude if reference_pressure is None: reference_pressure = pressure[0] - pressure = pressure.to('mbar') - reference_pressure = reference_pressure.to('mbar') + pressure = pressure.to("mbar") + reference_pressure = reference_pressure.to("mbar") temperature = np.atleast_1d(temperature) - side = 'left' + side = "left" - pres_decreasing = (pressure[0] > pressure[-1]) + pres_decreasing = pressure[0] > pressure[-1] if pres_decreasing: # Everything is easier if pressures are in increasing order pressure = pressure[::-1] - side = 'right' + side = "right" ref_pres_idx = np.searchsorted(pressure.m, reference_pressure.m, side=side) @@ -300,7 +309,7 @@ def dt(t, p): if reference_pressure > pressure.min(): # Integrate downward in pressure - pres_down = np.append(reference_pressure.m, pressure[(ref_pres_idx - 1)::-1].m) + pres_down = np.append(reference_pressure.m, pressure[(ref_pres_idx - 1) :: -1].m) trace_down = si.odeint(dt, temperature.m.squeeze(), pres_down.squeeze()) ret_temperatures = np.concatenate((ret_temperatures, trace_down[:0:-1])) @@ -318,7 +327,7 @@ def dt(t, p): @exporter.export @preprocess_and_wrap() -@check_units('[pressure]', '[temperature]', '[temperature]') +@check_units("[pressure]", "[temperature]", "[temperature]") def lcl(pressure, temperature, dewpoint, max_iters=50, eps=1e-5): r"""Calculate the lifted condensation level (LCL) using from the starting point. @@ -369,26 +378,34 @@ def lcl(pressure, temperature, dewpoint, max_iters=50, eps=1e-5): Quantities even when given xarray DataArray profiles. """ + def _lcl_iter(p, p0, w, t): - td = globals()['dewpoint'](vapor_pressure(units.Quantity(p, pressure.units), w)) - return (p0 * (td / t) ** (1. / mpconsts.kappa)).m + td = globals()["dewpoint"](vapor_pressure(units.Quantity(p, pressure.units), w)) + return (p0 * (td / t) ** (1.0 / mpconsts.kappa)).m w = mixing_ratio(saturation_vapor_pressure(dewpoint), pressure) - lcl_p = so.fixed_point(_lcl_iter, pressure.m, args=(pressure.m, w, temperature), - xtol=eps, maxiter=max_iters) + lcl_p = so.fixed_point( + _lcl_iter, pressure.m, args=(pressure.m, w, temperature), xtol=eps, maxiter=max_iters + ) # np.isclose needed if surface is LCL due to precision error with np.log in dewpoint. # Causes issues with parcel_profile_with_lcl if removed. Issue #1187 lcl_p = np.where(np.isclose(lcl_p, pressure.m), pressure.m, lcl_p) * pressure.units - return lcl_p, globals()['dewpoint'](vapor_pressure(lcl_p, w)).to(temperature.units) + return lcl_p, globals()["dewpoint"](vapor_pressure(lcl_p, w)).to(temperature.units) @exporter.export @preprocess_and_wrap() -@check_units('[pressure]', '[temperature]', '[temperature]', '[temperature]') -def lfc(pressure, temperature, dewpoint, parcel_temperature_profile=None, dewpoint_start=None, - which='top'): +@check_units("[pressure]", "[temperature]", "[temperature]", "[temperature]") +def lfc( + pressure, + temperature, + dewpoint, + parcel_temperature_profile=None, + dewpoint_start=None, + which="top", +): r"""Calculate the level of free convection (LFC). This works by finding the first intersection of the ideal parcel path and @@ -450,11 +467,21 @@ def lfc(pressure, temperature, dewpoint, parcel_temperature_profile=None, dewpoi # If that is the case, ignore that point to get the real first # intersection for the LFC calculation. Use logarithmic interpolation. if np.isclose(parcel_temperature_profile[0].to(temperature.units).m, temperature[0].m): - x, y = find_intersections(pressure[1:], parcel_temperature_profile[1:], - temperature[1:], direction='increasing', log_x=True) + x, y = find_intersections( + pressure[1:], + parcel_temperature_profile[1:], + temperature[1:], + direction="increasing", + log_x=True, + ) else: - x, y = find_intersections(pressure, parcel_temperature_profile, - temperature, direction='increasing', log_x=True) + x, y = find_intersections( + pressure, + parcel_temperature_profile, + temperature, + direction="increasing", + log_x=True, + ) # Compute LCL for this parcel for future comparisons this_lcl = lcl(pressure[0], parcel_temperature_profile[0], dewpoint_start) @@ -480,9 +507,13 @@ def lfc(pressure, temperature, dewpoint, parcel_temperature_profile=None, dewpoi idx = x < this_lcl[0] # LFC height < LCL height, so set LFC = LCL if not any(idx): - el_pres, _ = find_intersections(pressure[1:], parcel_temperature_profile[1:], - temperature[1:], direction='decreasing', - log_x=True) + el_pres, _ = find_intersections( + pressure[1:], + parcel_temperature_profile[1:], + temperature[1:], + direction="decreasing", + log_x=True, + ) if np.min(el_pres) > this_lcl[0]: x, y = np.nan * pressure.units, np.nan * temperature.units else: @@ -491,74 +522,118 @@ def lfc(pressure, temperature, dewpoint, parcel_temperature_profile=None, dewpoi # Otherwise, find all LFCs that exist above the LCL # What is returned depends on which flag as described in the docstring else: - return _multiple_el_lfc_options(x, y, idx, which, pressure, - parcel_temperature_profile, temperature, - dewpoint, intersect_type='LFC') + return _multiple_el_lfc_options( + x, + y, + idx, + which, + pressure, + parcel_temperature_profile, + temperature, + dewpoint, + intersect_type="LFC", + ) -def _multiple_el_lfc_options(intersect_pressures, intersect_temperatures, valid_x, - which, pressure, parcel_temperature_profile, temperature, - dewpoint, intersect_type): +def _multiple_el_lfc_options( + intersect_pressures, + intersect_temperatures, + valid_x, + which, + pressure, + parcel_temperature_profile, + temperature, + dewpoint, + intersect_type, +): """Choose which ELs and LFCs to return from a sounding.""" p_list, t_list = intersect_pressures[valid_x], intersect_temperatures[valid_x] - if which == 'all': + if which == "all": x, y = p_list, t_list - elif which == 'bottom': + elif which == "bottom": x, y = p_list[0], t_list[0] - elif which == 'top': + elif which == "top": x, y = p_list[-1], t_list[-1] - elif which == 'wide': - x, y = _wide_option(intersect_type, p_list, t_list, pressure, - parcel_temperature_profile, temperature) - elif which == 'most_cape': - x, y = _most_cape_option(intersect_type, p_list, t_list, pressure, temperature, - dewpoint, parcel_temperature_profile) + elif which == "wide": + x, y = _wide_option( + intersect_type, p_list, t_list, pressure, parcel_temperature_profile, temperature + ) + elif which == "most_cape": + x, y = _most_cape_option( + intersect_type, + p_list, + t_list, + pressure, + temperature, + dewpoint, + parcel_temperature_profile, + ) else: - raise ValueError('Invalid option for "which". Valid options are "top", "bottom", ' - '"wide", "most_cape", and "all".') + raise ValueError( + 'Invalid option for "which". Valid options are "top", "bottom", ' + '"wide", "most_cape", and "all".' + ) return x, y -def _wide_option(intersect_type, p_list, t_list, pressure, parcel_temperature_profile, - temperature): +def _wide_option( + intersect_type, p_list, t_list, pressure, parcel_temperature_profile, temperature +): """Calculate the LFC or EL that produces the greatest distance between these points.""" # zip the LFC and EL lists together and find greatest difference - if intersect_type == 'LFC': + if intersect_type == "LFC": # Find EL intersection pressure values lfc_p_list = p_list - el_p_list, _ = find_intersections(pressure[1:], parcel_temperature_profile[1:], - temperature[1:], direction='decreasing', - log_x=True) + el_p_list, _ = find_intersections( + pressure[1:], + parcel_temperature_profile[1:], + temperature[1:], + direction="decreasing", + log_x=True, + ) else: # intersect_type == 'EL' el_p_list = p_list # Find LFC intersection pressure values - lfc_p_list, _ = find_intersections(pressure, parcel_temperature_profile, - temperature, direction='increasing', - log_x=True) + lfc_p_list, _ = find_intersections( + pressure, + parcel_temperature_profile, + temperature, + direction="increasing", + log_x=True, + ) diff = [lfc_p.m - el_p.m for lfc_p, el_p in zip(lfc_p_list, el_p_list)] - return (p_list[np.where(diff == np.max(diff))][0], - t_list[np.where(diff == np.max(diff))][0]) + return ( + p_list[np.where(diff == np.max(diff))][0], + t_list[np.where(diff == np.max(diff))][0], + ) -def _most_cape_option(intersect_type, p_list, t_list, pressure, temperature, dewpoint, - parcel_temperature_profile): +def _most_cape_option( + intersect_type, p_list, t_list, pressure, temperature, dewpoint, parcel_temperature_profile +): """Calculate the LFC or EL that produces the most CAPE in the profile.""" # Need to loop through all possible combinations of cape, find greatest cape profile cape_list, pair_list = [], [] - for which_lfc in ['top', 'bottom']: - for which_el in ['top', 'bottom']: - cape, _ = cape_cin(pressure, temperature, dewpoint, parcel_temperature_profile, - which_lfc=which_lfc, which_el=which_el) + for which_lfc in ["top", "bottom"]: + for which_el in ["top", "bottom"]: + cape, _ = cape_cin( + pressure, + temperature, + dewpoint, + parcel_temperature_profile, + which_lfc=which_lfc, + which_el=which_el, + ) cape_list.append(cape.m) pair_list.append([which_lfc, which_el]) (lfc_chosen, el_chosen) = pair_list[np.where(cape_list == np.max(cape_list))[0][0]] - if intersect_type == 'LFC': - if lfc_chosen == 'top': + if intersect_type == "LFC": + if lfc_chosen == "top": x, y = p_list[-1], t_list[-1] else: # 'bottom' is returned x, y = p_list[0], t_list[0] else: # EL is returned - if el_chosen == 'top': + if el_chosen == "top": x, y = p_list[-1], t_list[-1] else: x, y = p_list[0], t_list[0] @@ -567,8 +642,8 @@ def _most_cape_option(intersect_type, p_list, t_list, pressure, temperature, dew @exporter.export @preprocess_and_wrap() -@check_units('[pressure]', '[temperature]', '[temperature]', '[temperature]') -def el(pressure, temperature, dewpoint, parcel_temperature_profile=None, which='top'): +@check_units("[pressure]", "[temperature]", "[temperature]", "[temperature]") +def el(pressure, temperature, dewpoint, parcel_temperature_profile=None, which="top"): r"""Calculate the equilibrium level. This works by finding the last intersection of the ideal parcel path and @@ -624,21 +699,34 @@ def el(pressure, temperature, dewpoint, parcel_temperature_profile=None, which=' # Interpolate in log space to find the appropriate pressure - units have to be stripped # and reassigned to allow np.log() to function properly. - x, y = find_intersections(pressure[1:], parcel_temperature_profile[1:], temperature[1:], - direction='decreasing', log_x=True) + x, y = find_intersections( + pressure[1:], + parcel_temperature_profile[1:], + temperature[1:], + direction="decreasing", + log_x=True, + ) lcl_p, _ = lcl(pressure[0], temperature[0], dewpoint[0]) idx = x < lcl_p if len(x) > 0 and x[-1] < lcl_p: - return _multiple_el_lfc_options(x, y, idx, which, pressure, - parcel_temperature_profile, temperature, dewpoint, - intersect_type='EL') + return _multiple_el_lfc_options( + x, + y, + idx, + which, + pressure, + parcel_temperature_profile, + temperature, + dewpoint, + intersect_type="EL", + ) else: return np.nan * pressure.units, np.nan * temperature.units @exporter.export -@preprocess_and_wrap(wrap_like='pressure') -@check_units('[pressure]', '[temperature]', '[temperature]') +@preprocess_and_wrap(wrap_like="pressure") +@check_units("[pressure]", "[temperature]", "[temperature]") def parcel_profile(pressure, temperature, dewpoint): r"""Calculate the profile a parcel takes through the atmosphere. @@ -676,7 +764,7 @@ def parcel_profile(pressure, temperature, dewpoint): @exporter.export @preprocess_and_wrap() -@check_units('[pressure]', '[temperature]', '[temperature]') +@check_units("[pressure]", "[temperature]", "[temperature]") def parcel_profile_with_lcl(pressure, temperature, dewpoint): r"""Calculate the profile a parcel takes through the atmosphere. @@ -720,8 +808,9 @@ def parcel_profile_with_lcl(pressure, temperature, dewpoint): obtain a xarray Dataset instead, use `parcel_profile_with_lcl_as_dataset` instead. """ - p_l, p_lcl, p_u, t_l, t_lcl, t_u = _parcel_profile_helper(pressure, temperature[0], - dewpoint[0]) + p_l, p_lcl, p_u, t_l, t_lcl, t_u = _parcel_profile_helper( + pressure, temperature[0], dewpoint[0] + ) new_press = concatenate((p_l, p_lcl, p_u)) prof_temp = concatenate((t_l, t_lcl, t_u)) new_temp = _insert_lcl_level(pressure, temperature, p_lcl) @@ -767,35 +856,33 @@ def parcel_profile_with_lcl_as_dataset(pressure, temperature, dewpoint): """ p, ambient_temperature, ambient_dew_point, profile_temperature = parcel_profile_with_lcl( - pressure, - temperature, - dewpoint + pressure, temperature, dewpoint ) return xr.Dataset( { - 'ambient_temperature': ( - ('isobaric',), + "ambient_temperature": ( + ("isobaric",), ambient_temperature, - {'standard_name': 'air_temperature'} + {"standard_name": "air_temperature"}, ), - 'ambient_dew_point': ( - ('isobaric',), + "ambient_dew_point": ( + ("isobaric",), ambient_dew_point, - {'standard_name': 'dew_point_temperature'} + {"standard_name": "dew_point_temperature"}, ), - 'parcel_temperature': ( - ('isobaric',), + "parcel_temperature": ( + ("isobaric",), profile_temperature, - {'long_name': 'air_temperature_of_lifted_parcel'} - ) + {"long_name": "air_temperature_of_lifted_parcel"}, + ), }, coords={ - 'isobaric': ( - 'isobaric', + "isobaric": ( + "isobaric", p.m, - {'units': str(p.units), 'standard_name': 'air_pressure'} + {"units": str(p.units), "standard_name": "air_pressure"}, ) - } + }, ) @@ -818,16 +905,28 @@ def _parcel_profile_helper(pressure, temperature, dewpoint): # If the pressure profile doesn't make it to the lcl, we can stop here if _greater_or_close(np.nanmin(pressure.m), press_lcl.m): - return (press_lower[:-1], press_lcl, units.Quantity(np.array([]), press_lower.units), - temp_lower[:-1], temp_lcl, units.Quantity(np.array([]), temp_lower.units)) + return ( + press_lower[:-1], + press_lcl, + units.Quantity(np.array([]), press_lower.units), + temp_lower[:-1], + temp_lcl, + units.Quantity(np.array([]), temp_lower.units), + ) # Find moist pseudo-adiabatic profile starting at the LCL press_upper = concatenate((press_lcl, pressure[pressure < press_lcl])) temp_upper = moist_lapse(press_upper, temp_lower[-1]).to(temp_lower.units) # Return profile pieces - return (press_lower[:-1], press_lcl, press_upper[1:], - temp_lower[:-1], temp_lcl, temp_upper[1:]) + return ( + press_lower[:-1], + press_lcl, + press_upper[1:], + temp_lower[:-1], + temp_lcl, + temp_upper[1:], + ) def _insert_lcl_level(pressure, temperature, lcl_pressure): @@ -841,8 +940,8 @@ def _insert_lcl_level(pressure, temperature, lcl_pressure): @exporter.export -@preprocess_and_wrap(wrap_like='mixing_ratio', broadcast=('pressure', 'mixing_ratio')) -@check_units('[pressure]', '[dimensionless]') +@preprocess_and_wrap(wrap_like="mixing_ratio", broadcast=("pressure", "mixing_ratio")) +@check_units("[pressure]", "[dimensionless]") def vapor_pressure(pressure, mixing_ratio): r"""Calculate water vapor (partial) pressure. @@ -878,8 +977,8 @@ def vapor_pressure(pressure, mixing_ratio): @exporter.export -@preprocess_and_wrap(wrap_like='temperature') -@check_units('[temperature]') +@preprocess_and_wrap(wrap_like="temperature") +@check_units("[temperature]") def saturation_vapor_pressure(temperature): r"""Calculate the saturation water vapor (partial) pressure. @@ -909,13 +1008,14 @@ def saturation_vapor_pressure(temperature): """ # Converted from original in terms of C to use kelvin. Using raw absolute values of C in # a formula plays havoc with units support. - return sat_pressure_0c * np.exp(17.67 * (temperature - 273.15 * units.kelvin) - / (temperature - 29.65 * units.kelvin)) + return sat_pressure_0c * np.exp( + 17.67 * (temperature - 273.15 * units.kelvin) / (temperature - 29.65 * units.kelvin) + ) @exporter.export -@preprocess_and_wrap(wrap_like='temperature', broadcast=('temperature', 'relative_humidity')) -@check_units('[temperature]', '[dimensionless]') +@preprocess_and_wrap(wrap_like="temperature", broadcast=("temperature", "relative_humidity")) +@check_units("[temperature]", "[dimensionless]") def dewpoint_from_relative_humidity(temperature, relative_humidity): r"""Calculate the ambient dewpoint given air temperature and relative humidity. @@ -937,13 +1037,13 @@ def dewpoint_from_relative_humidity(temperature, relative_humidity): """ if np.any(relative_humidity > 1.2): - warnings.warn('Relative humidity >120%, ensure proper units.') + warnings.warn("Relative humidity >120%, ensure proper units.") return dewpoint(relative_humidity * saturation_vapor_pressure(temperature)) @exporter.export -@preprocess_and_wrap(wrap_like='vapor_pressure') -@check_units('[pressure]') +@preprocess_and_wrap(wrap_like="vapor_pressure") +@check_units("[pressure]") def dewpoint(vapor_pressure): r"""Calculate the ambient dewpoint given the vapor pressure. @@ -971,12 +1071,12 @@ def dewpoint(vapor_pressure): """ val = np.log(vapor_pressure / sat_pressure_0c) - return 0. * units.degC + 243.5 * units.delta_degC * val / (17.67 - val) + return 0.0 * units.degC + 243.5 * units.delta_degC * val / (17.67 - val) @exporter.export -@preprocess_and_wrap(wrap_like='partial_press', broadcast=('partial_press', 'total_press')) -@check_units('[pressure]', '[pressure]', '[dimensionless]') +@preprocess_and_wrap(wrap_like="partial_press", broadcast=("partial_press", "total_press")) +@check_units("[pressure]", "[pressure]", "[dimensionless]") def mixing_ratio(partial_press, total_press, molecular_weight_ratio=mpconsts.epsilon): r"""Calculate the mixing ratio of a gas. @@ -1012,13 +1112,14 @@ def mixing_ratio(partial_press, total_press, molecular_weight_ratio=mpconsts.eps saturation_mixing_ratio, vapor_pressure """ - return (molecular_weight_ratio * partial_press - / (total_press - partial_press)).to('dimensionless') + return (molecular_weight_ratio * partial_press / (total_press - partial_press)).to( + "dimensionless" + ) @exporter.export -@preprocess_and_wrap(wrap_like='temperature', broadcast=('total_press', 'temperature')) -@check_units('[pressure]', '[temperature]') +@preprocess_and_wrap(wrap_like="temperature", broadcast=("total_press", "temperature")) +@check_units("[pressure]", "[temperature]") def saturation_mixing_ratio(total_press, temperature): r"""Calculate the saturation mixing ratio of water vapor. @@ -1049,10 +1150,9 @@ def saturation_mixing_ratio(total_press, temperature): @exporter.export @preprocess_and_wrap( - wrap_like='temperature', - broadcast=('pressure', 'temperature', 'dewpoint') + wrap_like="temperature", broadcast=("pressure", "temperature", "dewpoint") ) -@check_units('[pressure]', '[temperature]', '[temperature]') +@check_units("[pressure]", "[temperature]", "[temperature]") def equivalent_potential_temperature(pressure, temperature, dewpoint): r"""Calculate equivalent potential temperature. @@ -1094,19 +1194,19 @@ def equivalent_potential_temperature(pressure, temperature, dewpoint): available. """ - t = temperature.to('kelvin').magnitude - td = dewpoint.to('kelvin').magnitude + t = temperature.to("kelvin").magnitude + td = dewpoint.to("kelvin").magnitude r = saturation_mixing_ratio(pressure, dewpoint).magnitude e = saturation_vapor_pressure(dewpoint) - t_l = 56 + 1. / (1. / (td - 56) + np.log(t / td) / 800.) + t_l = 56 + 1.0 / (1.0 / (td - 56) + np.log(t / td) / 800.0) th_l = potential_temperature(pressure - e, temperature) * (t / t_l) ** (0.28 * r) - return th_l * np.exp(r * (1 + 0.448 * r) * (3036. / t_l - 1.78)) + return th_l * np.exp(r * (1 + 0.448 * r) * (3036.0 / t_l - 1.78)) @exporter.export -@preprocess_and_wrap(wrap_like='temperature', broadcast=('pressure', 'temperature')) -@check_units('[pressure]', '[temperature]') +@preprocess_and_wrap(wrap_like="temperature", broadcast=("pressure", "temperature")) +@check_units("[pressure]", "[temperature]") def saturation_equivalent_potential_temperature(pressure, temperature): r"""Calculate saturation equivalent potential temperature. @@ -1160,20 +1260,20 @@ def saturation_equivalent_potential_temperature(pressure, temperature): available. """ - t = temperature.to('kelvin').magnitude - p = pressure.to('hPa').magnitude - e = saturation_vapor_pressure(temperature).to('hPa').magnitude + t = temperature.to("kelvin").magnitude + p = pressure.to("hPa").magnitude + e = saturation_vapor_pressure(temperature).to("hPa").magnitude r = saturation_mixing_ratio(pressure, temperature).magnitude th_l = t * (1000 / (p - e)) ** mpconsts.kappa - th_es = th_l * np.exp((3036. / t - 1.78) * r * (1 + 0.448 * r)) + th_es = th_l * np.exp((3036.0 / t - 1.78) * r * (1 + 0.448 * r)) return units.Quantity(th_es, units.kelvin) @exporter.export -@preprocess_and_wrap(wrap_like='temperature', broadcast=('temperature', 'mixing_ratio')) -@check_units('[temperature]', '[dimensionless]', '[dimensionless]') +@preprocess_and_wrap(wrap_like="temperature", broadcast=("temperature", "mixing_ratio")) +@check_units("[temperature]", "[dimensionless]", "[dimensionless]") def virtual_temperature(temperature, mixing_ratio, molecular_weight_ratio=mpconsts.epsilon): r"""Calculate virtual temperature. @@ -1201,18 +1301,19 @@ def virtual_temperature(temperature, mixing_ratio, molecular_weight_ratio=mpcons .. math:: T_v = T \frac{\text{w} + \epsilon}{\epsilon\,(1 + \text{w})} """ - return temperature * ((mixing_ratio + molecular_weight_ratio) - / (molecular_weight_ratio * (1 + mixing_ratio))) + return temperature * ( + (mixing_ratio + molecular_weight_ratio) / (molecular_weight_ratio * (1 + mixing_ratio)) + ) @exporter.export @preprocess_and_wrap( - wrap_like='temperature', - broadcast=('pressure', 'temperature', 'mixing_ratio') + wrap_like="temperature", broadcast=("pressure", "temperature", "mixing_ratio") ) -@check_units('[pressure]', '[temperature]', '[dimensionless]', '[dimensionless]') -def virtual_potential_temperature(pressure, temperature, mixing_ratio, - molecular_weight_ratio=mpconsts.epsilon): +@check_units("[pressure]", "[temperature]", "[dimensionless]", "[dimensionless]") +def virtual_potential_temperature( + pressure, temperature, mixing_ratio, molecular_weight_ratio=mpconsts.epsilon +): r"""Calculate virtual potential temperature. This calculation must be given an air parcel's pressure, temperature, and mixing ratio. @@ -1247,10 +1348,9 @@ def virtual_potential_temperature(pressure, temperature, mixing_ratio, @exporter.export @preprocess_and_wrap( - wrap_like='temperature', - broadcast=('pressure', 'temperature', 'mixing_ratio') + wrap_like="temperature", broadcast=("pressure", "temperature", "mixing_ratio") ) -@check_units('[pressure]', '[temperature]', '[dimensionless]', '[dimensionless]') +@check_units("[pressure]", "[temperature]", "[dimensionless]", "[dimensionless]") def density(pressure, temperature, mixing_ratio, molecular_weight_ratio=mpconsts.epsilon): r"""Calculate density. @@ -1286,12 +1386,13 @@ def density(pressure, temperature, mixing_ratio, molecular_weight_ratio=mpconsts @exporter.export @preprocess_and_wrap( - wrap_like='dry_bulb_temperature', - broadcast=('pressure', 'dry_bulb_temperature', 'wet_bulb_temperature') + wrap_like="dry_bulb_temperature", + broadcast=("pressure", "dry_bulb_temperature", "wet_bulb_temperature"), ) -@check_units('[pressure]', '[temperature]', '[temperature]') -def relative_humidity_wet_psychrometric(pressure, dry_bulb_temperature, wet_bulb_temperature, - **kwargs): +@check_units("[pressure]", "[temperature]", "[temperature]") +def relative_humidity_wet_psychrometric( + pressure, dry_bulb_temperature, wet_bulb_temperature, **kwargs +): r"""Calculate the relative humidity with wet bulb and dry bulb temperatures. This uses a psychrometric relationship as outlined in [WMO8]_, with @@ -1324,19 +1425,23 @@ def relative_humidity_wet_psychrometric(pressure, dry_bulb_temperature, wet_bulb psychrometric_vapor_pressure_wet, saturation_vapor_pressure """ - return (psychrometric_vapor_pressure_wet(pressure, dry_bulb_temperature, - wet_bulb_temperature, **kwargs) - / saturation_vapor_pressure(dry_bulb_temperature)) + return psychrometric_vapor_pressure_wet( + pressure, dry_bulb_temperature, wet_bulb_temperature, **kwargs + ) / saturation_vapor_pressure(dry_bulb_temperature) @exporter.export @preprocess_and_wrap( - wrap_like='dry_bulb_temperature', - broadcast=('pressure', 'dry_bulb_temperature', 'wet_bulb_temperature') + wrap_like="dry_bulb_temperature", + broadcast=("pressure", "dry_bulb_temperature", "wet_bulb_temperature"), ) -@check_units('[pressure]', '[temperature]', '[temperature]') -def psychrometric_vapor_pressure_wet(pressure, dry_bulb_temperature, wet_bulb_temperature, - psychrometer_coefficient=6.21e-4 / units.kelvin): +@check_units("[pressure]", "[temperature]", "[temperature]") +def psychrometric_vapor_pressure_wet( + pressure, + dry_bulb_temperature, + wet_bulb_temperature, + psychrometer_coefficient=6.21e-4 / units.kelvin, +): r"""Calculate the vapor pressure with wet bulb and dry bulb temperatures. This uses a psychrometric relationship as outlined in [WMO8]_, with @@ -1378,16 +1483,18 @@ def psychrometric_vapor_pressure_wet(pressure, dry_bulb_temperature, wet_bulb_te saturation_vapor_pressure """ - return (saturation_vapor_pressure(wet_bulb_temperature) - psychrometer_coefficient - * pressure * (dry_bulb_temperature - wet_bulb_temperature).to('kelvin')) + return saturation_vapor_pressure( + wet_bulb_temperature + ) - psychrometer_coefficient * pressure * (dry_bulb_temperature - wet_bulb_temperature).to( + "kelvin" + ) @exporter.export @preprocess_and_wrap( - wrap_like='temperature', - broadcast=('pressure', 'temperature', 'relative_humidity') + wrap_like="temperature", broadcast=("pressure", "temperature", "relative_humidity") ) -@check_units('[pressure]', '[temperature]', '[dimensionless]') +@check_units("[pressure]", "[temperature]", "[dimensionless]") def mixing_ratio_from_relative_humidity(pressure, temperature, relative_humidity): r"""Calculate the mixing ratio from relative humidity, temperature, and pressure. @@ -1421,16 +1528,16 @@ def mixing_ratio_from_relative_humidity(pressure, temperature, relative_humidity relative_humidity_from_mixing_ratio, saturation_mixing_ratio """ - return (relative_humidity - * saturation_mixing_ratio(pressure, temperature)).to('dimensionless') + return (relative_humidity * saturation_mixing_ratio(pressure, temperature)).to( + "dimensionless" + ) @exporter.export @preprocess_and_wrap( - wrap_like='temperature', - broadcast=('pressure', 'temperature', 'mixing_ratio') + wrap_like="temperature", broadcast=("pressure", "temperature", "mixing_ratio") ) -@check_units('[pressure]', '[temperature]', '[dimensionless]') +@check_units("[pressure]", "[temperature]", "[dimensionless]") def relative_humidity_from_mixing_ratio(pressure, temperature, mixing_ratio): r"""Calculate the relative humidity from mixing ratio, temperature, and pressure. @@ -1467,8 +1574,8 @@ def relative_humidity_from_mixing_ratio(pressure, temperature, mixing_ratio): @exporter.export -@preprocess_and_wrap(wrap_like='specific_humidity') -@check_units('[dimensionless]') +@preprocess_and_wrap(wrap_like="specific_humidity") +@check_units("[dimensionless]") def mixing_ratio_from_specific_humidity(specific_humidity): r"""Calculate the mixing ratio from specific humidity. @@ -1497,15 +1604,15 @@ def mixing_ratio_from_specific_humidity(specific_humidity): """ try: - specific_humidity = specific_humidity.to('dimensionless') + specific_humidity = specific_humidity.to("dimensionless") except AttributeError: pass return specific_humidity / (1 - specific_humidity) @exporter.export -@preprocess_and_wrap(wrap_like='mixing_ratio') -@check_units('[dimensionless]') +@preprocess_and_wrap(wrap_like="mixing_ratio") +@check_units("[dimensionless]") def specific_humidity_from_mixing_ratio(mixing_ratio): r"""Calculate the specific humidity from the mixing ratio. @@ -1534,7 +1641,7 @@ def specific_humidity_from_mixing_ratio(mixing_ratio): """ try: - mixing_ratio = mixing_ratio.to('dimensionless') + mixing_ratio = mixing_ratio.to("dimensionless") except AttributeError: pass return mixing_ratio / (1 + mixing_ratio) @@ -1542,10 +1649,9 @@ def specific_humidity_from_mixing_ratio(mixing_ratio): @exporter.export @preprocess_and_wrap( - wrap_like='temperature', - broadcast=('pressure', 'temperature', 'specific_humidity') + wrap_like="temperature", broadcast=("pressure", "temperature", "specific_humidity") ) -@check_units('[pressure]', '[temperature]', '[dimensionless]') +@check_units("[pressure]", "[temperature]", "[dimensionless]") def relative_humidity_from_specific_humidity(pressure, temperature, specific_humidity): r"""Calculate the relative humidity from specific humidity, temperature, and pressure. @@ -1578,15 +1684,17 @@ def relative_humidity_from_specific_humidity(pressure, temperature, specific_hum relative_humidity_from_mixing_ratio """ - return (mixing_ratio_from_specific_humidity(specific_humidity) - / saturation_mixing_ratio(pressure, temperature)) + return mixing_ratio_from_specific_humidity(specific_humidity) / saturation_mixing_ratio( + pressure, temperature + ) @exporter.export @preprocess_and_wrap() -@check_units('[pressure]', '[temperature]', '[temperature]', '[temperature]') -def cape_cin(pressure, temperature, dewpoint, parcel_profile, which_lfc='bottom', - which_el='top'): +@check_units("[pressure]", "[temperature]", "[temperature]", "[temperature]") +def cape_cin( + pressure, temperature, dewpoint, parcel_profile, which_lfc="bottom", which_el="top" +): r"""Calculate CAPE and CIN. Calculate the convective available potential energy (CAPE) and convective inhibition (CIN) @@ -1648,21 +1756,32 @@ def cape_cin(pressure, temperature, dewpoint, parcel_profile, which_lfc='bottom' lfc, el """ - pressure, temperature, dewpoint, parcel_profile = _remove_nans(pressure, temperature, - dewpoint, parcel_profile) + pressure, temperature, dewpoint, parcel_profile = _remove_nans( + pressure, temperature, dewpoint, parcel_profile + ) # Calculate LFC limit of integration - lfc_pressure, _ = lfc(pressure, temperature, dewpoint, - parcel_temperature_profile=parcel_profile, which=which_lfc) + lfc_pressure, _ = lfc( + pressure, + temperature, + dewpoint, + parcel_temperature_profile=parcel_profile, + which=which_lfc, + ) # If there is no LFC, no need to proceed. if np.isnan(lfc_pressure): - return 0 * units('J/kg'), 0 * units('J/kg') + return 0 * units("J/kg"), 0 * units("J/kg") else: lfc_pressure = lfc_pressure.magnitude # Calculate the EL limit of integration - el_pressure, _ = el(pressure, temperature, dewpoint, - parcel_temperature_profile=parcel_profile, which=which_el) + el_pressure, _ = el( + pressure, + temperature, + dewpoint, + parcel_temperature_profile=parcel_profile, + which=which_el, + ) # No EL and we use the top reading of the sounding. if np.isnan(el_pressure): @@ -1681,20 +1800,22 @@ def cape_cin(pressure, temperature, dewpoint, parcel_profile, which_lfc='bottom' p_mask = _less_or_close(x.m, lfc_pressure) & _greater_or_close(x.m, el_pressure) x_clipped = x[p_mask].magnitude y_clipped = y[p_mask].magnitude - cape = (mpconsts.Rd - * (np.trapz(y_clipped, np.log(x_clipped)) * units.degK)).to(units('J/kg')) + cape = (mpconsts.Rd * (np.trapz(y_clipped, np.log(x_clipped)) * units.degK)).to( + units("J/kg") + ) # CIN # Only use data between the surface and LFC for calculation p_mask = _greater_or_close(x.m, lfc_pressure) x_clipped = x[p_mask].magnitude y_clipped = y[p_mask].magnitude - cin = (mpconsts.Rd - * (np.trapz(y_clipped, np.log(x_clipped)) * units.degK)).to(units('J/kg')) + cin = (mpconsts.Rd * (np.trapz(y_clipped, np.log(x_clipped)) * units.degK)).to( + units("J/kg") + ) # Set CIN to 0 if it's returned as a positive value (#1190) - if cin > 0 * units('J/kg'): - cin = 0 * units('J/kg') + if cin > 0 * units("J/kg"): + cin = 0 * units("J/kg") return cape, cin @@ -1738,9 +1859,10 @@ def _find_append_zero_crossings(x, y): @exporter.export @preprocess_and_wrap() -@check_units('[pressure]', '[temperature]', '[temperature]') -def most_unstable_parcel(pressure, temperature, dewpoint, height=None, - bottom=None, depth=300 * units.hPa): +@check_units("[pressure]", "[temperature]", "[temperature]") +def most_unstable_parcel( + pressure, temperature, dewpoint, height=None, bottom=None, depth=300 * units.hPa +): """ Determine the most unstable parcel in a layer. @@ -1782,8 +1904,15 @@ def most_unstable_parcel(pressure, temperature, dewpoint, height=None, Quantities even when given xarray DataArray profiles. """ - p_layer, t_layer, td_layer = get_layer(pressure, temperature, dewpoint, bottom=bottom, - depth=depth, height=height, interpolate=False) + p_layer, t_layer, td_layer = get_layer( + pressure, + temperature, + dewpoint, + bottom=bottom, + depth=depth, + height=height, + interpolate=False, + ) theta_e = equivalent_potential_temperature(p_layer, t_layer, td_layer) max_idx = np.argmax(theta_e) return p_layer[max_idx], t_layer[max_idx], td_layer[max_idx], max_idx @@ -1792,10 +1921,19 @@ def most_unstable_parcel(pressure, temperature, dewpoint, height=None, @exporter.export @add_vertical_dim_from_xarray @preprocess_and_wrap() -@check_units('[temperature]', '[pressure]', '[temperature]') -def isentropic_interpolation(levels, pressure, temperature, *args, vertical_dim=0, - temperature_out=False, max_iters=50, eps=1e-6, - bottom_up_search=True, **kwargs): +@check_units("[temperature]", "[pressure]", "[temperature]") +def isentropic_interpolation( + levels, + pressure, + temperature, + *args, + vertical_dim=0, + temperature_out=False, + max_iters=50, + eps=1e-6, + bottom_up_search=True, + **kwargs +): r"""Interpolate data in isobaric coordinates to isentropic coordinates. Parameters @@ -1858,8 +1996,8 @@ def _isen_iter(iter_log_p, isentlevs_nd, ka, a, b, pok): ndim = temperature.ndim # Convert units - pres = pressure.to('hPa') - temperature = temperature.to('kelvin') + pres = pressure.to("hPa") + temperature = temperature.to("kelvin") slices = [np.newaxis] * ndim slices[vertical_dim] = slice(None) @@ -1873,7 +2011,7 @@ def _isen_iter(iter_log_p, isentlevs_nd, ka, a, b, pok): levs = pres[sorter] tmpk = temperature[sorter] - levels = np.asarray(levels.m_as('kelvin')).reshape(-1) + levels = np.asarray(levels.m_as("kelvin")).reshape(-1) isentlevels = levels[np.argsort(levels)] # Make the desired isentropic levels the same shape as temperature @@ -1882,14 +2020,14 @@ def _isen_iter(iter_log_p, isentlevs_nd, ka, a, b, pok): isentlevs_nd = np.broadcast_to(isentlevels[slices], shape) # exponent to Poisson's Equation, which is imported above - ka = mpconsts.kappa.m_as('dimensionless') + ka = mpconsts.kappa.m_as("dimensionless") # calculate theta for each point pres_theta = potential_temperature(levs, tmpk) # Raise error if input theta level is larger than pres_theta max if np.max(pres_theta.m) < np.max(levels): - raise ValueError('Input theta level out of data bounds') + raise ValueError("Input theta level out of data bounds") # Find log of pressure to implement assumption of linear temperature dependence on # ln(p) @@ -1899,8 +2037,9 @@ def _isen_iter(iter_log_p, isentlevs_nd, ka, a, b, pok): pok = mpconsts.P0 ** ka # index values for each point for the pressure level nearest to the desired theta level - above, below, good = find_bounding_indices(pres_theta.m, levels, vertical_dim, - from_below=bottom_up_search) + above, below, good = find_bounding_indices( + pres_theta.m, levels, vertical_dim, from_below=bottom_up_search + ) # calculate constants for the interpolation a = (tmpk.m[above] - tmpk.m[below]) / (log_p[above] - log_p[below]) @@ -1914,9 +2053,13 @@ def _isen_iter(iter_log_p, isentlevs_nd, ka, a, b, pok): good &= ~np.isnan(a) # iterative interpolation using scipy.optimize.fixed_point and _isen_iter defined above - log_p_solved = so.fixed_point(_isen_iter, isentprs[good], - args=(isentlevs_nd[good], ka, a[good], b[good], pok.m), - xtol=eps, maxiter=max_iters) + log_p_solved = so.fixed_point( + _isen_iter, + isentprs[good], + args=(isentlevs_nd[good], ka, a[good], b[good], pok.m), + xtol=eps, + maxiter=max_iters, + ) # get back pressure from log p isentprs[good] = np.exp(log_p_solved) @@ -1933,8 +2076,13 @@ def _isen_iter(iter_log_p, isentlevs_nd, ka, a, b, pok): # do an interpolation for each additional argument if args: - others = interpolate_1d(isentlevels, pres_theta.m, *(arr[sorter] for arr in args), - axis=vertical_dim, return_list_always=True) + others = interpolate_1d( + isentlevels, + pres_theta.m, + *(arr[sorter] for arr in args), + axis=vertical_dim, + return_list_always=True + ) ret.extend(others) return ret @@ -1942,12 +2090,7 @@ def _isen_iter(iter_log_p, isentlevs_nd, ka, a, b, pok): @exporter.export def isentropic_interpolation_as_dataset( - levels, - temperature, - *args, - max_iters=50, - eps=1e-6, - bottom_up_search=True + levels, temperature, *args, max_iters=50, eps=1e-6, bottom_up_search=True ): r"""Interpolate xarray data in isobaric coords to isentropic coords, returning a Dataset. @@ -2000,7 +2143,7 @@ def isentropic_interpolation_as_dataset( all_args[0].metpy.vertical, all_args[0].metpy.unit_array, *(arg.metpy.unit_array for arg in all_args[1:]), - vertical_dim=all_args[0].metpy.find_axis_number('vertical'), + vertical_dim=all_args[0].metpy.find_axis_number("vertical"), temperature_out=True, max_iters=max_iters, eps=eps, @@ -2008,53 +2151,36 @@ def isentropic_interpolation_as_dataset( ) # Reconstruct coordinates and dims (add isentropic levels, remove isobaric levels) - vertical_dim = all_args[0].metpy.find_axis_name('vertical') + vertical_dim = all_args[0].metpy.find_axis_name("vertical") new_coords = { - 'isentropic_level': xr.DataArray( + "isentropic_level": xr.DataArray( levels.m, - dims=('isentropic_level',), - coords={'isentropic_level': levels.m}, - name='isentropic_level', - attrs={ - 'units': str(levels.units), - 'positive': 'up' - } + dims=("isentropic_level",), + coords={"isentropic_level": levels.m}, + name="isentropic_level", + attrs={"units": str(levels.units), "positive": "up"}, ), - **{ - key: value - for key, value in all_args[0].coords.items() - if key != vertical_dim - } + **{key: value for key, value in all_args[0].coords.items() if key != vertical_dim}, } - new_dims = [ - dim if dim != vertical_dim else 'isentropic_level' for dim in all_args[0].dims - ] + new_dims = [dim if dim != vertical_dim else "isentropic_level" for dim in all_args[0].dims] # Build final dataset from interpolated Quantities and original DataArrays return xr.Dataset( { - 'pressure': ( - new_dims, - ret[0], - {'standard_name': 'air_pressure'} - ), - 'temperature': ( - new_dims, - ret[1], - {'standard_name': 'air_temperature'} - ), + "pressure": (new_dims, ret[0], {"standard_name": "air_pressure"}), + "temperature": (new_dims, ret[1], {"standard_name": "air_temperature"}), **{ all_args[i].name: (new_dims, ret[i + 1], all_args[i].attrs) for i in range(1, len(all_args)) - } + }, }, - coords=new_coords + coords=new_coords, ) @exporter.export @preprocess_and_wrap() -@check_units('[pressure]', '[temperature]', '[temperature]') +@check_units("[pressure]", "[temperature]", "[temperature]") def surface_based_cape_cin(pressure, temperature, dewpoint): r"""Calculate surface-based CAPE and CIN. @@ -2099,7 +2225,7 @@ def surface_based_cape_cin(pressure, temperature, dewpoint): @exporter.export @preprocess_and_wrap() -@check_units('[pressure]', '[temperature]', '[temperature]') +@check_units("[pressure]", "[temperature]", "[temperature]") def most_unstable_cape_cin(pressure, temperature, dewpoint, **kwargs): r"""Calculate most unstable CAPE/CIN. @@ -2140,15 +2266,15 @@ def most_unstable_cape_cin(pressure, temperature, dewpoint, **kwargs): """ pressure, temperature, dewpoint = _remove_nans(pressure, temperature, dewpoint) _, _, _, parcel_idx = most_unstable_parcel(pressure, temperature, dewpoint, **kwargs) - p, t, td, mu_profile = parcel_profile_with_lcl(pressure[parcel_idx:], - temperature[parcel_idx:], - dewpoint[parcel_idx:]) + p, t, td, mu_profile = parcel_profile_with_lcl( + pressure[parcel_idx:], temperature[parcel_idx:], dewpoint[parcel_idx:] + ) return cape_cin(p, t, td, mu_profile) @exporter.export @preprocess_and_wrap() -@check_units('[pressure]', '[temperature]', '[temperature]') +@check_units("[pressure]", "[temperature]", "[temperature]") def mixed_layer_cape_cin(pressure, temperature, dewpoint, **kwargs): r"""Calculate mixed-layer CAPE and CIN. @@ -2188,9 +2314,10 @@ def mixed_layer_cape_cin(pressure, temperature, dewpoint, **kwargs): Quantities even when given xarray DataArray profiles. """ - depth = kwargs.get('depth', 100 * units.hPa) - parcel_pressure, parcel_temp, parcel_dewpoint = mixed_parcel(pressure, temperature, - dewpoint, **kwargs) + depth = kwargs.get("depth", 100 * units.hPa) + parcel_pressure, parcel_temp, parcel_dewpoint = mixed_parcel( + pressure, temperature, dewpoint, **kwargs + ) # Remove values below top of mixed layer and add in the mixed layer values pressure_prof = pressure[pressure < (pressure[0] - depth)] @@ -2206,9 +2333,17 @@ def mixed_layer_cape_cin(pressure, temperature, dewpoint, **kwargs): @exporter.export @preprocess_and_wrap() -@check_units('[pressure]', '[temperature]', '[temperature]') -def mixed_parcel(pressure, temperature, dewpoint, parcel_start_pressure=None, - height=None, bottom=None, depth=100 * units.hPa, interpolate=True): +@check_units("[pressure]", "[temperature]", "[temperature]") +def mixed_parcel( + pressure, + temperature, + dewpoint, + parcel_start_pressure=None, + height=None, + bottom=None, + depth=100 * units.hPa, + interpolate=True, +): r"""Calculate the properties of a parcel mixed from a layer. Determines the properties of an air parcel that is the result of complete mixing of a @@ -2260,9 +2395,15 @@ def mixed_parcel(pressure, temperature, dewpoint, parcel_start_pressure=None, mixing_ratio = saturation_mixing_ratio(pressure, dewpoint) # Mix the variables over the layer - mean_theta, mean_mixing_ratio = mixed_layer(pressure, theta, mixing_ratio, bottom=bottom, - height=height, depth=depth, - interpolate=interpolate) + mean_theta, mean_mixing_ratio = mixed_layer( + pressure, + theta, + mixing_ratio, + bottom=bottom, + height=height, + depth=depth, + interpolate=interpolate, + ) # Convert back to temperature mean_temperature = mean_theta * exner_function(parcel_start_pressure) @@ -2272,17 +2413,21 @@ def mixed_parcel(pressure, temperature, dewpoint, parcel_start_pressure=None, # Using globals() here allows us to keep the dewpoint parameter but still call the # function of the same name. - mean_dewpoint = globals()['dewpoint'](mean_vapor_pressure) + mean_dewpoint = globals()["dewpoint"](mean_vapor_pressure) - return (parcel_start_pressure, mean_temperature.to(temperature.units), - mean_dewpoint.to(dewpoint.units)) + return ( + parcel_start_pressure, + mean_temperature.to(temperature.units), + mean_dewpoint.to(dewpoint.units), + ) @exporter.export @preprocess_and_wrap() -@check_units('[pressure]') -def mixed_layer(pressure, *args, height=None, bottom=None, depth=100 * units.hPa, - interpolate=True): +@check_units("[pressure]") +def mixed_layer( + pressure, *args, height=None, bottom=None, depth=100 * units.hPa, interpolate=True +): r"""Mix variable(s) over a layer, yielding a mass-weighted average. This function will integrate a data variable with respect to pressure and determine the @@ -2317,22 +2462,26 @@ def mixed_layer(pressure, *args, height=None, bottom=None, depth=100 * units.hPa Quantities even when given xarray DataArray profiles. """ - layer = get_layer(pressure, *args, height=height, bottom=bottom, - depth=depth, interpolate=interpolate) + layer = get_layer( + pressure, *args, height=height, bottom=bottom, depth=depth, interpolate=interpolate + ) p_layer = layer[0] datavars_layer = layer[1:] ret = [] for datavar_layer in datavars_layer: actual_depth = abs(p_layer[0] - p_layer[-1]) - ret.append((-1. / actual_depth.m) * np.trapz(datavar_layer.m, p_layer.m) - * datavar_layer.units) + ret.append( + (-1.0 / actual_depth.m) + * np.trapz(datavar_layer.m, p_layer.m) + * datavar_layer.units + ) return ret @exporter.export -@preprocess_and_wrap(wrap_like='temperature', broadcast=('height', 'temperature')) -@check_units('[length]', '[temperature]') +@preprocess_and_wrap(wrap_like="temperature", broadcast=("height", "temperature")) +@check_units("[length]", "[temperature]") def dry_static_energy(height, temperature): r"""Calculate the dry static energy of parcels. @@ -2359,15 +2508,14 @@ def dry_static_energy(height, temperature): The dry static energy """ - return (mpconsts.g * height + mpconsts.Cp_d * temperature).to('kJ/kg') + return (mpconsts.g * height + mpconsts.Cp_d * temperature).to("kJ/kg") @exporter.export @preprocess_and_wrap( - wrap_like='temperature', - broadcast=('height', 'temperature', 'specific_humidity') + wrap_like="temperature", broadcast=("height", "temperature", "specific_humidity") ) -@check_units('[length]', '[temperature]', '[dimensionless]') +@check_units("[length]", "[temperature]", "[dimensionless]") def moist_static_energy(height, temperature, specific_humidity): r"""Calculate the moist static energy of parcels. @@ -2397,15 +2545,23 @@ def moist_static_energy(height, temperature, specific_humidity): The moist static energy """ - return (dry_static_energy(height, temperature) - + mpconsts.Lv * specific_humidity.to('dimensionless')).to('kJ/kg') + return ( + dry_static_energy(height, temperature) + + mpconsts.Lv * specific_humidity.to("dimensionless") + ).to("kJ/kg") @exporter.export @preprocess_and_wrap() -@check_units('[pressure]', '[temperature]') -def thickness_hydrostatic(pressure, temperature, mixing_ratio=None, - molecular_weight_ratio=mpconsts.epsilon, bottom=None, depth=None): +@check_units("[pressure]", "[temperature]") +def thickness_hydrostatic( + pressure, + temperature, + mixing_ratio=None, + molecular_weight_ratio=mpconsts.epsilon, + bottom=None, + depth=None, +): r"""Calculate the thickness of a layer via the hypsometric equation. This thickness calculation uses the pressure and temperature profiles (and optionally @@ -2461,27 +2617,35 @@ def thickness_hydrostatic(pressure, temperature, mixing_ratio=None, layer_p, layer_virttemp = pressure, temperature else: layer_p = pressure - layer_virttemp = virtual_temperature(temperature, mixing_ratio, - molecular_weight_ratio) + layer_virttemp = virtual_temperature( + temperature, mixing_ratio, molecular_weight_ratio + ) else: if mixing_ratio is None: - layer_p, layer_virttemp = get_layer(pressure, temperature, bottom=bottom, - depth=depth) + layer_p, layer_virttemp = get_layer( + pressure, temperature, bottom=bottom, depth=depth + ) else: - layer_p, layer_temp, layer_w = get_layer(pressure, temperature, mixing_ratio, - bottom=bottom, depth=depth) + layer_p, layer_temp, layer_w = get_layer( + pressure, temperature, mixing_ratio, bottom=bottom, depth=depth + ) layer_virttemp = virtual_temperature(layer_temp, layer_w, molecular_weight_ratio) # Take the integral (with unit handling) and return the result in meters - return (- mpconsts.Rd / mpconsts.g * np.trapz( - layer_virttemp.m_as('K'), x=np.log(layer_p.m_as('hPa'))) * units.K).to('m') + return ( + -mpconsts.Rd + / mpconsts.g + * np.trapz(layer_virttemp.m_as("K"), x=np.log(layer_p.m_as("hPa"))) + * units.K + ).to("m") @exporter.export @preprocess_and_wrap() -@check_units('[pressure]', '[temperature]') -def thickness_hydrostatic_from_relative_humidity(pressure, temperature, relative_humidity, - bottom=None, depth=None): +@check_units("[pressure]", "[temperature]") +def thickness_hydrostatic_from_relative_humidity( + pressure, temperature, relative_humidity, bottom=None, depth=None +): r"""Calculate the thickness of a layer given pressure, temperature and relative humidity. Similar to ``thickness_hydrostatic``, this thickness calculation uses the pressure, @@ -2532,14 +2696,15 @@ def thickness_hydrostatic_from_relative_humidity(pressure, temperature, relative """ mixing = mixing_ratio_from_relative_humidity(pressure, temperature, relative_humidity) - return thickness_hydrostatic(pressure, temperature, mixing_ratio=mixing, bottom=bottom, - depth=depth) + return thickness_hydrostatic( + pressure, temperature, mixing_ratio=mixing, bottom=bottom, depth=depth + ) @exporter.export @add_vertical_dim_from_xarray -@preprocess_and_wrap(wrap_like='height', broadcast=('height', 'potential_temperature')) -@check_units('[length]', '[temperature]') +@preprocess_and_wrap(wrap_like="height", broadcast=("height", "potential_temperature")) +@check_units("[length]", "[temperature]") def brunt_vaisala_frequency_squared(height, potential_temperature, vertical_dim=0): r"""Calculate the square of the Brunt-Vaisala frequency. @@ -2574,20 +2739,20 @@ def brunt_vaisala_frequency_squared(height, potential_temperature, vertical_dim= """ # Ensure validity of temperature units - potential_temperature = potential_temperature.to('K') + potential_temperature = potential_temperature.to("K") # Calculate and return the square of Brunt-Vaisala frequency - return mpconsts.g / potential_temperature * first_derivative( - potential_temperature, - x=height, - axis=vertical_dim + return ( + mpconsts.g + / potential_temperature + * first_derivative(potential_temperature, x=height, axis=vertical_dim) ) @exporter.export @add_vertical_dim_from_xarray -@preprocess_and_wrap(wrap_like='height', broadcast=('height', 'potential_temperature')) -@check_units('[length]', '[temperature]') +@preprocess_and_wrap(wrap_like="height", broadcast=("height", "potential_temperature")) +@check_units("[length]", "[temperature]") def brunt_vaisala_frequency(height, potential_temperature, vertical_dim=0): r"""Calculate the Brunt-Vaisala frequency. @@ -2623,8 +2788,9 @@ def brunt_vaisala_frequency(height, potential_temperature, vertical_dim=0): brunt_vaisala_frequency_squared, brunt_vaisala_period, potential_temperature """ - bv_freq_squared = brunt_vaisala_frequency_squared(height, potential_temperature, - vertical_dim=vertical_dim) + bv_freq_squared = brunt_vaisala_frequency_squared( + height, potential_temperature, vertical_dim=vertical_dim + ) bv_freq_squared[bv_freq_squared.magnitude < 0] = np.nan return np.sqrt(bv_freq_squared) @@ -2632,8 +2798,8 @@ def brunt_vaisala_frequency(height, potential_temperature, vertical_dim=0): @exporter.export @add_vertical_dim_from_xarray -@preprocess_and_wrap(wrap_like='height', broadcast=('height', 'potential_temperature')) -@check_units('[length]', '[temperature]') +@preprocess_and_wrap(wrap_like="height", broadcast=("height", "potential_temperature")) +@check_units("[length]", "[temperature]") def brunt_vaisala_period(height, potential_temperature, vertical_dim=0): r"""Calculate the Brunt-Vaisala period. @@ -2668,8 +2834,9 @@ def brunt_vaisala_period(height, potential_temperature, vertical_dim=0): brunt_vaisala_frequency, brunt_vaisala_frequency_squared, potential_temperature """ - bv_freq_squared = brunt_vaisala_frequency_squared(height, potential_temperature, - vertical_dim=vertical_dim) + bv_freq_squared = brunt_vaisala_frequency_squared( + height, potential_temperature, vertical_dim=vertical_dim + ) bv_freq_squared[bv_freq_squared.magnitude <= 0] = np.nan return 2 * np.pi / np.sqrt(bv_freq_squared) @@ -2677,10 +2844,9 @@ def brunt_vaisala_period(height, potential_temperature, vertical_dim=0): @exporter.export @preprocess_and_wrap( - wrap_like='temperature', - broadcast=('pressure', 'temperature', 'dewpoint') + wrap_like="temperature", broadcast=("pressure", "temperature", "dewpoint") ) -@check_units('[pressure]', '[temperature]', '[temperature]') +@check_units("[pressure]", "[temperature]", "[temperature]") def wet_bulb_temperature(pressure, temperature, dewpoint): """Calculate the wet-bulb temperature using Normand's rule. @@ -2712,22 +2878,25 @@ def wet_bulb_temperature(pressure, temperature, dewpoint): caution on large arrays. """ - if not hasattr(pressure, 'shape'): + if not hasattr(pressure, "shape"): pressure = np.atleast_1d(pressure) temperature = np.atleast_1d(temperature) dewpoint = np.atleast_1d(dewpoint) - it = np.nditer([pressure, temperature, dewpoint, None], - op_dtypes=['float', 'float', 'float', 'float'], - flags=['buffered']) + it = np.nditer( + [pressure, temperature, dewpoint, None], + op_dtypes=["float", "float", "float", "float"], + flags=["buffered"], + ) for press, temp, dewp, ret in it: press = press * pressure.units temp = temp * temperature.units dewp = dewp * dewpoint.units lcl_pressure, lcl_temperature = lcl(press, temp, dewp) - moist_adiabat_temperatures = moist_lapse(concatenate([lcl_pressure, press]), - lcl_temperature) + moist_adiabat_temperatures = moist_lapse( + concatenate([lcl_pressure, press]), lcl_temperature + ) ret[...] = moist_adiabat_temperatures[-1].magnitude # If we started with a scalar, return a scalar @@ -2738,8 +2907,8 @@ def wet_bulb_temperature(pressure, temperature, dewpoint): @exporter.export @add_vertical_dim_from_xarray -@preprocess_and_wrap(wrap_like='temperature', broadcast=('pressure', 'temperature')) -@check_units('[pressure]', '[temperature]') +@preprocess_and_wrap(wrap_like="temperature", broadcast=("pressure", "temperature")) +@check_units("[pressure]", "[temperature]") def static_stability(pressure, temperature, vertical_dim=0): r"""Calculate the static stability within a vertical profile. @@ -2765,19 +2934,19 @@ def static_stability(pressure, temperature, vertical_dim=0): """ theta = potential_temperature(pressure, temperature) - return - mpconsts.Rd * temperature / pressure * first_derivative( - np.log(theta.m_as('K')), - x=pressure, - axis=vertical_dim + return ( + -mpconsts.Rd + * temperature + / pressure + * first_derivative(np.log(theta.m_as("K")), x=pressure, axis=vertical_dim) ) @exporter.export @preprocess_and_wrap( - wrap_like='temperature', - broadcast=('pressure', 'temperature', 'specific_humdiity') + wrap_like="temperature", broadcast=("pressure", "temperature", "specific_humdiity") ) -@check_units('[pressure]', '[temperature]', '[dimensionless]') +@check_units("[pressure]", "[temperature]", "[dimensionless]") def dewpoint_from_specific_humidity(pressure, temperature, specific_humidity): r"""Calculate the dewpoint from specific humidity, temperature, and pressure. @@ -2800,14 +2969,15 @@ def dewpoint_from_specific_humidity(pressure, temperature, specific_humidity): relative_humidity_from_mixing_ratio, dewpoint_from_relative_humidity """ - return dewpoint_from_relative_humidity(temperature, - relative_humidity_from_specific_humidity( - pressure, temperature, specific_humidity)) + return dewpoint_from_relative_humidity( + temperature, + relative_humidity_from_specific_humidity(pressure, temperature, specific_humidity), + ) @exporter.export -@preprocess_and_wrap(wrap_like='w', broadcast=('w', 'pressure', 'temperature')) -@check_units('[length]/[time]', '[pressure]', '[temperature]') +@preprocess_and_wrap(wrap_like="w", broadcast=("w", "pressure", "temperature")) +@check_units("[length]/[time]", "[pressure]", "[temperature]") def vertical_velocity_pressure(w, pressure, temperature, mixing_ratio=0): r"""Calculate omega from w assuming hydrostatic conditions. @@ -2845,15 +3015,14 @@ def vertical_velocity_pressure(w, pressure, temperature, mixing_ratio=0): """ rho = density(pressure, temperature, mixing_ratio) - return (-mpconsts.g * rho * w).to('Pa/s') + return (-mpconsts.g * rho * w).to("Pa/s") @exporter.export @preprocess_and_wrap( - wrap_like='omega', - broadcast=('omega', 'pressure', 'temperature', 'mixing_ratio') + wrap_like="omega", broadcast=("omega", "pressure", "temperature", "mixing_ratio") ) -@check_units('[pressure]/[time]', '[pressure]', '[temperature]') +@check_units("[pressure]/[time]", "[pressure]", "[temperature]") def vertical_velocity(omega, pressure, temperature, mixing_ratio=0): r"""Calculate w from omega assuming hydrostatic conditions. @@ -2894,12 +3063,12 @@ def vertical_velocity(omega, pressure, temperature, mixing_ratio=0): """ rho = density(pressure, temperature, mixing_ratio) - return (omega / (- mpconsts.g * rho)).to('m/s') + return (omega / (-mpconsts.g * rho)).to("m/s") @exporter.export -@preprocess_and_wrap(wrap_like='dewpoint', broadcast=('dewpoint', 'pressure')) -@check_units('[pressure]', '[temperature]') +@preprocess_and_wrap(wrap_like="dewpoint", broadcast=("dewpoint", "pressure")) +@check_units("[pressure]", "[temperature]") def specific_humidity_from_dewpoint(pressure, dewpoint): r"""Calculate the specific humidity from the dewpoint temperature and pressure. @@ -2927,7 +3096,7 @@ def specific_humidity_from_dewpoint(pressure, dewpoint): @exporter.export @preprocess_and_wrap() -@check_units('[pressure]', '[temperature]', '[temperature]') +@check_units("[pressure]", "[temperature]", "[temperature]") def lifted_index(pressure, temperature, parcel_profile): """Calculate Lifted Index from the pressure temperature and parcel profile. @@ -2971,10 +3140,9 @@ def lifted_index(pressure, temperature, parcel_profile): @exporter.export @add_vertical_dim_from_xarray @preprocess_and_wrap( - wrap_like='potential_temperature', - broadcast=('height', 'potential_temperature', 'u', 'v') + wrap_like="potential_temperature", broadcast=("height", "potential_temperature", "u", "v") ) -@check_units('[length]', '[temperature]', '[speed]', '[speed]') +@check_units("[length]", "[temperature]", "[speed]", "[speed]") def gradient_richardson_number(height, potential_temperature, u, v, vertical_dim=0): r"""Calculate the gradient (or flux) Richardson number. diff --git a/src/metpy/calc/tools.py b/src/metpy/calc/tools.py index 049316746cb..a28ee8f4a30 100644 --- a/src/metpy/calc/tools.py +++ b/src/metpy/calc/tools.py @@ -19,14 +19,26 @@ exporter = Exporter(globals()) -UND = 'UND' -UND_ANGLE = -999. +UND = "UND" +UND_ANGLE = -999.0 DIR_STRS = ( - 'N', 'NNE', 'NE', 'ENE', - 'E', 'ESE', 'SE', 'SSE', - 'S', 'SSW', 'SW', 'WSW', - 'W', 'WNW', 'NW', 'NNW', - UND + "N", + "NNE", + "NE", + "ENE", + "E", + "ESE", + "SE", + "SSE", + "S", + "SSW", + "SW", + "WSW", + "W", + "WNW", + "NW", + "NNW", + UND, ) # note the order matters! MAX_DEGREE_ANGLE = 360 * units.degree @@ -84,15 +96,15 @@ def nearest_intersection_idx(a, b): # Determine the point just before the intersection of the lines # Will return multiple points for multiple intersections - sign_change_idx, = np.nonzero(np.diff(np.sign(difference))) + (sign_change_idx,) = np.nonzero(np.diff(np.sign(difference))) return sign_change_idx @exporter.export @preprocess_and_wrap() -@units.wraps(('=A', '=B'), ('=A', '=B', '=B', None, None)) -def find_intersections(x, a, b, direction='all', log_x=False): +@units.wraps(("=A", "=B"), ("=A", "=B", "=B", None, None)) +def find_intersections(x, a, b, direction="all", log_x=False): """Calculate the best estimate of intersection. Calculates the best estimates of the intersection of two y-value @@ -168,17 +180,17 @@ def find_intersections(x, a, b, direction='all', log_x=False): intersect_x = np.exp(intersect_x) # Check for duplicates - duplicate_mask = (np.ediff1d(intersect_x, to_end=1) != 0) + duplicate_mask = np.ediff1d(intersect_x, to_end=1) != 0 # Make a mask based on the direction of sign change desired - if direction == 'increasing': + if direction == "increasing": mask = sign_change > 0 - elif direction == 'decreasing': + elif direction == "decreasing": mask = sign_change < 0 - elif direction == 'all': + elif direction == "all": return intersect_x[duplicate_mask], intersect_y[duplicate_mask] else: - raise ValueError(f'Unknown option for direction: {direction}') + raise ValueError(f"Unknown option for direction: {direction}") return intersect_x[mask & duplicate_mask], intersect_y[mask & duplicate_mask] @@ -227,7 +239,7 @@ def _delete_masked_points(*arrs): arrays with masked elements removed """ - if any(hasattr(a, 'mask') for a in arrs): + if any(hasattr(a, "mask") for a in arrs): keep = ~functools.reduce(np.logical_or, (np.ma.getmaskarray(a) for a in arrs)) return tuple(ma.asarray(a[keep]) for a in arrs) else: @@ -271,11 +283,11 @@ def reduce_point_density(points, radius, priority=None): """ # Handle input with units. Assume meters if units are not specified - if hasattr(radius, 'units'): - radius = radius.to('m').m + if hasattr(radius, "units"): + radius = radius.to("m").m - if hasattr(points, 'units'): - points = points.to('m').m + if hasattr(points, "units"): + points = points.to("m").m # Handle 1D input if points.ndim < 2: @@ -337,6 +349,7 @@ def _get_bound_pressure_height(pressure, bound, height=None, interpolate=True): """ # avoid circular import if basic.py ever imports something from tools.py from .basic import height_to_pressure_std, pressure_to_height_std + # Make sure pressure is monotonically decreasing sort_inds = np.argsort(pressure)[::-1] pressure = pressure[sort_inds] @@ -344,7 +357,7 @@ def _get_bound_pressure_height(pressure, bound, height=None, interpolate=True): height = height[sort_inds] # Bound is given in pressure - if bound.dimensionality == {'[length]': -1.0, '[mass]': 1.0, '[time]': -2.0}: + if bound.dimensionality == {"[length]": -1.0, "[mass]": 1.0, "[time]": -2.0}: # If the bound is in the pressure data, we know the pressure bound exactly if bound in pressure: bound_pressure = bound @@ -371,7 +384,7 @@ def _get_bound_pressure_height(pressure, bound, height=None, interpolate=True): bound_height = pressure_to_height_std(bound_pressure) # Bound is given in height - elif bound.dimensionality == {'[length]': 1.0}: + elif bound.dimensionality == {"[length]": 1.0}: # If there is height data, see if we have the bound or need to interpolate/find nearest if height is not None: if bound in height: # Bound is in the height data @@ -384,9 +397,12 @@ def _get_bound_pressure_height(pressure, bound, height=None, interpolate=True): # Need to cast back to the input type since interp (up to at least numpy # 1.13 always returns float64. This can cause upstream users problems, # resulting in something like np.append() to upcast. - bound_pressure = (np.interp(np.atleast_1d(bound.m), height.m, - pressure.m).astype(np.result_type(bound)) - * pressure.units) + bound_pressure = ( + np.interp(np.atleast_1d(bound.m), height.m, pressure.m).astype( + np.result_type(bound) + ) + * pressure.units + ) else: idx = (np.abs(height - bound)).argmin() bound_pressure = pressure[idx] @@ -403,24 +419,26 @@ def _get_bound_pressure_height(pressure, bound, height=None, interpolate=True): # Bound has invalid units else: - raise ValueError('Bound must be specified in units of length or pressure.') + raise ValueError("Bound must be specified in units of length or pressure.") # If the bound is out of the range of the data, we shouldn't extrapolate - if not (_greater_or_close(bound_pressure, np.nanmin(pressure.m) * pressure.units) - and _less_or_close(bound_pressure, np.nanmax(pressure.m) * pressure.units)): - raise ValueError('Specified bound is outside pressure range.') - if height is not None and not (_less_or_close(bound_height, - np.nanmax(height.m) * height.units) - and _greater_or_close(bound_height, - np.nanmin(height.m) * height.units)): - raise ValueError('Specified bound is outside height range.') + if not ( + _greater_or_close(bound_pressure, np.nanmin(pressure.m) * pressure.units) + and _less_or_close(bound_pressure, np.nanmax(pressure.m) * pressure.units) + ): + raise ValueError("Specified bound is outside pressure range.") + if height is not None and not ( + _less_or_close(bound_height, np.nanmax(height.m) * height.units) + and _greater_or_close(bound_height, np.nanmin(height.m) * height.units) + ): + raise ValueError("Specified bound is outside height range.") return bound_pressure, bound_height @exporter.export @preprocess_and_wrap() -@check_units('[length]') +@check_units("[length]") def get_layer_heights(height, depth, *args, bottom=None, interpolate=True, with_agl=False): """Return an atmospheric layer from upper air data with the requested bottom and depth. @@ -458,7 +476,7 @@ def get_layer_heights(height, depth, *args, bottom=None, interpolate=True, with_ # Make sure pressure and datavars are the same length for datavar in args: if len(height) != len(datavar): - raise ValueError('Height and data variables must have the same length.') + raise ValueError("Height and data variables must have the same length.") # If we want things in AGL, subtract the minimum height from all height values if with_agl: @@ -490,11 +508,13 @@ def get_layer_heights(height, depth, *args, bottom=None, interpolate=True, with_ if interpolate: # If we don't have the bottom or top requested, append them if top not in heights_interp: - heights_interp = units.Quantity(np.sort(np.append(heights_interp.m, top.m)), - height.units) + heights_interp = units.Quantity( + np.sort(np.append(heights_interp.m, top.m)), height.units + ) if bottom not in heights_interp: - heights_interp = units.Quantity(np.sort(np.append(heights_interp.m, bottom.m)), - height.units) + heights_interp = units.Quantity( + np.sort(np.append(heights_interp.m, bottom.m)), height.units + ) ret.append(heights_interp) @@ -515,9 +535,10 @@ def get_layer_heights(height, depth, *args, bottom=None, interpolate=True, with_ @exporter.export @preprocess_and_wrap() -@check_units('[pressure]') -def get_layer(pressure, *args, height=None, bottom=None, depth=100 * units.hPa, - interpolate=True): +@check_units("[pressure]") +def get_layer( + pressure, *args, height=None, bottom=None, depth=100 * units.hPa, interpolate=True +): r"""Return an atmospheric layer from upper air data with the requested bottom and depth. This function will subset an upper air dataset to contain only the specified layer. The @@ -563,26 +584,27 @@ def get_layer(pressure, *args, height=None, bottom=None, depth=100 * units.hPa, # Make sure pressure and datavars are the same length for datavar in args: if len(pressure) != len(datavar): - raise ValueError('Pressure and data variables must have the same length.') + raise ValueError("Pressure and data variables must have the same length.") # If the bottom is not specified, make it the surface pressure if bottom is None: bottom = np.nanmax(pressure.m) * pressure.units - bottom_pressure, bottom_height = _get_bound_pressure_height(pressure, bottom, - height=height, - interpolate=interpolate) + bottom_pressure, bottom_height = _get_bound_pressure_height( + pressure, bottom, height=height, interpolate=interpolate + ) # Calculate the top if whatever units depth is in - if depth.dimensionality == {'[length]': -1.0, '[mass]': 1.0, '[time]': -2.0}: + if depth.dimensionality == {"[length]": -1.0, "[mass]": 1.0, "[time]": -2.0}: top = bottom_pressure - depth - elif depth.dimensionality == {'[length]': 1}: + elif depth.dimensionality == {"[length]": 1}: top = bottom_height + depth else: - raise ValueError('Depth must be specified in units of length or pressure') + raise ValueError("Depth must be specified in units of length or pressure") - top_pressure, _ = _get_bound_pressure_height(pressure, top, height=height, - interpolate=interpolate) + top_pressure, _ = _get_bound_pressure_height( + pressure, top, height=height, interpolate=interpolate + ) ret = [] # returned data variables in layer @@ -591,8 +613,9 @@ def get_layer(pressure, *args, height=None, bottom=None, depth=100 * units.hPa, pressure = pressure[sort_inds] # Mask based on top and bottom pressure - inds = (_less_or_close(pressure, bottom_pressure) - & _greater_or_close(pressure, top_pressure)) + inds = _less_or_close(pressure, bottom_pressure) & _greater_or_close( + pressure, top_pressure + ) p_interp = pressure[inds] # Interpolate pressures at bounds if necessary and sort @@ -755,9 +778,13 @@ def _less_or_close(a, value, **kwargs): def make_take(ndims, slice_dim): """Generate a take function to index in a particular dimension.""" + def take(indexer): - return tuple(indexer if slice_dim % ndims == i else slice(None) # noqa: S001 - for i in range(ndims)) + return tuple( + indexer if slice_dim % ndims == i else slice(None) # noqa: S001 + for i in range(ndims) + ) + return take @@ -801,7 +828,7 @@ def lat_lon_grid_deltas(longitude, latitude, x_dim=-1, y_dim=-2, **kwargs): # Inputs must be the same number of dimensions if latitude.ndim != longitude.ndim: - raise ValueError('Latitude and longitude must have the same number of dimensions.') + raise ValueError("Latitude and longitude must have the same number of dimensions.") # If we were given 1D arrays, make a mesh grid if latitude.ndim < 2: @@ -809,8 +836,8 @@ def lat_lon_grid_deltas(longitude, latitude, x_dim=-1, y_dim=-2, **kwargs): # pyproj requires ndarrays, not Quantities try: - longitude = np.asarray(longitude.m_as('degrees')) - latitude = np.asarray(latitude.m_as('degrees')) + longitude = np.asarray(longitude.m_as("degrees")) + latitude = np.asarray(latitude.m_as("degrees")) except AttributeError: longitude = np.asarray(longitude) latitude = np.asarray(latitude) @@ -819,23 +846,27 @@ def lat_lon_grid_deltas(longitude, latitude, x_dim=-1, y_dim=-2, **kwargs): take_y = make_take(latitude.ndim, y_dim) take_x = make_take(latitude.ndim, x_dim) - geod_args = {'ellps': 'sphere'} + geod_args = {"ellps": "sphere"} if kwargs: geod_args = kwargs g = Geod(**geod_args) - forward_az, _, dy = g.inv(longitude[take_y(slice(None, -1))], - latitude[take_y(slice(None, -1))], - longitude[take_y(slice(1, None))], - latitude[take_y(slice(1, None))]) - dy[(forward_az < -90.) | (forward_az > 90.)] *= -1 - - forward_az, _, dx = g.inv(longitude[take_x(slice(None, -1))], - latitude[take_x(slice(None, -1))], - longitude[take_x(slice(1, None))], - latitude[take_x(slice(1, None))]) - dx[(forward_az < 0.) | (forward_az > 180.)] *= -1 + forward_az, _, dy = g.inv( + longitude[take_y(slice(None, -1))], + latitude[take_y(slice(None, -1))], + longitude[take_y(slice(1, None))], + latitude[take_y(slice(1, None))], + ) + dy[(forward_az < -90.0) | (forward_az > 90.0)] *= -1 + + forward_az, _, dx = g.inv( + longitude[take_x(slice(None, -1))], + latitude[take_x(slice(None, -1))], + longitude[take_x(slice(1, None))], + latitude[take_x(slice(1, None))], + ) + dx[(forward_az < 0.0) | (forward_az > 180.0)] *= -1 return dx * units.meter, dy * units.meter @@ -872,7 +903,7 @@ def azimuth_range_to_lat_lon(azimuths, ranges, center_lon, center_lat, **kwargs) """ from pyproj import Geod - geod_args = {'ellps': 'sphere'} + geod_args = {"ellps": "sphere"} if kwargs: geod_args = kwargs g = Geod(**geod_args) @@ -891,38 +922,42 @@ def xarray_derivative_wrap(func): This will automatically determine if the coordinates can be pulled directly from the DataArray, or if a call to lat_lon_grid_deltas is needed. """ + @functools.wraps(func) def wrapper(f, **kwargs): - if 'x' in kwargs or 'delta' in kwargs: + if "x" in kwargs or "delta" in kwargs: # Use the usual DataArray to pint.Quantity preprocessing wrapper return preprocess_and_wrap()(func)(f, **kwargs) elif isinstance(f, xr.DataArray): # Get axis argument, defaulting to first dimension - axis = f.metpy.find_axis_name(kwargs.get('axis', 0)) + axis = f.metpy.find_axis_name(kwargs.get("axis", 0)) # Initialize new kwargs with the axis number - new_kwargs = {'axis': f.get_axis_num(axis)} + new_kwargs = {"axis": f.get_axis_num(axis)} - if check_axis(f[axis], 'time'): + if check_axis(f[axis], "time"): # Time coordinate, need to get time deltas - new_kwargs['delta'] = f[axis].metpy.time_deltas - elif check_axis(f[axis], 'longitude'): + new_kwargs["delta"] = f[axis].metpy.time_deltas + elif check_axis(f[axis], "longitude"): # Longitude coordinate, need to get grid deltas - new_kwargs['delta'], _ = grid_deltas_from_dataarray(f) - elif check_axis(f[axis], 'latitude'): + new_kwargs["delta"], _ = grid_deltas_from_dataarray(f) + elif check_axis(f[axis], "latitude"): # Latitude coordinate, need to get grid deltas - _, new_kwargs['delta'] = grid_deltas_from_dataarray(f) + _, new_kwargs["delta"] = grid_deltas_from_dataarray(f) else: # General coordinate, use as is - new_kwargs['x'] = f[axis].metpy.unit_array + new_kwargs["x"] = f[axis].metpy.unit_array # Calculate and return result as a DataArray result = func(f.metpy.unit_array, **new_kwargs) return xr.DataArray(result, coords=f.coords, dims=f.dims) else: # Error - raise ValueError('Must specify either "x" or "delta" for value positions when "f" ' - 'is not a DataArray.') + raise ValueError( + 'Must specify either "x" or "delta" for value positions when "f" ' + "is not a DataArray." + ) + return wrapper @@ -983,9 +1018,11 @@ def first_derivative(f, axis=None, x=None, delta=None): combined_delta = delta[delta_slice0] + delta[delta_slice1] delta_diff = delta[delta_slice1] - delta[delta_slice0] - center = (- delta[delta_slice1] / (combined_delta * delta[delta_slice0]) * f[slice0] - + delta_diff / (delta[delta_slice0] * delta[delta_slice1]) * f[slice1] - + delta[delta_slice0] / (combined_delta * delta[delta_slice1]) * f[slice2]) + center = ( + -delta[delta_slice1] / (combined_delta * delta[delta_slice0]) * f[slice0] + + delta_diff / (delta[delta_slice0] * delta[delta_slice1]) * f[slice1] + + delta[delta_slice0] / (combined_delta * delta[delta_slice1]) * f[slice2] + ) # Fill in "left" edge with forward difference slice0 = take(slice(None, 1)) @@ -996,9 +1033,11 @@ def first_derivative(f, axis=None, x=None, delta=None): combined_delta = delta[delta_slice0] + delta[delta_slice1] big_delta = combined_delta + delta[delta_slice0] - left = (- big_delta / (combined_delta * delta[delta_slice0]) * f[slice0] - + combined_delta / (delta[delta_slice0] * delta[delta_slice1]) * f[slice1] - - delta[delta_slice0] / (combined_delta * delta[delta_slice1]) * f[slice2]) + left = ( + -big_delta / (combined_delta * delta[delta_slice0]) * f[slice0] + + combined_delta / (delta[delta_slice0] * delta[delta_slice1]) * f[slice1] + - delta[delta_slice0] / (combined_delta * delta[delta_slice1]) * f[slice2] + ) # Now the "right" edge with backward difference slice0 = take(slice(-3, -2)) @@ -1009,9 +1048,11 @@ def first_derivative(f, axis=None, x=None, delta=None): combined_delta = delta[delta_slice0] + delta[delta_slice1] big_delta = combined_delta + delta[delta_slice1] - right = (delta[delta_slice1] / (combined_delta * delta[delta_slice0]) * f[slice0] - - combined_delta / (delta[delta_slice0] * delta[delta_slice1]) * f[slice1] - + big_delta / (combined_delta * delta[delta_slice1]) * f[slice2]) + right = ( + delta[delta_slice1] / (combined_delta * delta[delta_slice0]) * f[slice0] + - combined_delta / (delta[delta_slice0] * delta[delta_slice1]) * f[slice1] + + big_delta / (combined_delta * delta[delta_slice1]) * f[slice2] + ) return concatenate((left, center, right), axis=axis) @@ -1072,9 +1113,11 @@ def second_derivative(f, axis=None, x=None, delta=None): delta_slice1 = take(slice(1, None)) combined_delta = delta[delta_slice0] + delta[delta_slice1] - center = 2 * (f[slice0] / (combined_delta * delta[delta_slice0]) - - f[slice1] / (delta[delta_slice0] * delta[delta_slice1]) - + f[slice2] / (combined_delta * delta[delta_slice1])) + center = 2 * ( + f[slice0] / (combined_delta * delta[delta_slice0]) + - f[slice1] / (delta[delta_slice0] * delta[delta_slice1]) + + f[slice2] / (combined_delta * delta[delta_slice1]) + ) # Fill in "left" edge slice0 = take(slice(None, 1)) @@ -1084,9 +1127,11 @@ def second_derivative(f, axis=None, x=None, delta=None): delta_slice1 = take(slice(1, 2)) combined_delta = delta[delta_slice0] + delta[delta_slice1] - left = 2 * (f[slice0] / (combined_delta * delta[delta_slice0]) - - f[slice1] / (delta[delta_slice0] * delta[delta_slice1]) - + f[slice2] / (combined_delta * delta[delta_slice1])) + left = 2 * ( + f[slice0] / (combined_delta * delta[delta_slice0]) + - f[slice1] / (delta[delta_slice0] * delta[delta_slice1]) + + f[slice2] / (combined_delta * delta[delta_slice1]) + ) # Now the "right" edge slice0 = take(slice(-3, -2)) @@ -1096,9 +1141,11 @@ def second_derivative(f, axis=None, x=None, delta=None): delta_slice1 = take(slice(-1, None)) combined_delta = delta[delta_slice0] + delta[delta_slice1] - right = 2 * (f[slice0] / (combined_delta * delta[delta_slice0]) - - f[slice1] / (delta[delta_slice0] * delta[delta_slice1]) - + f[slice2] / (combined_delta * delta[delta_slice1])) + right = 2 * ( + f[slice0] / (combined_delta * delta[delta_slice0]) + - f[slice1] / (delta[delta_slice0] * delta[delta_slice1]) + + f[slice2] / (combined_delta * delta[delta_slice1]) + ) return concatenate((left, center, right), axis=axis) @@ -1152,8 +1199,10 @@ def gradient(f, axes=None, coordinates=None, deltas=None): """ pos_kwarg, positions, axes = _process_gradient_args(f, axes, coordinates, deltas) - return tuple(first_derivative(f, axis=axis, **{pos_kwarg: positions[ind]}) - for ind, axis in enumerate(axes)) + return tuple( + first_derivative(f, axis=axis, **{pos_kwarg: positions[ind]}) + for ind, axis in enumerate(axes) + ) @exporter.export @@ -1203,8 +1252,10 @@ def laplacian(f, axes=None, coordinates=None, deltas=None): """ pos_kwarg, positions, axes = _process_gradient_args(f, axes, coordinates, deltas) - derivs = [second_derivative(f, axis=axis, **{pos_kwarg: positions[ind]}) - for ind, axis in enumerate(axes)] + derivs = [ + second_derivative(f, axis=axis, **{pos_kwarg: positions[ind]}) + for ind, axis in enumerate(axes) + ] return sum(derivs) @@ -1227,25 +1278,30 @@ def _process_gradient_args(f, axes, coordinates, deltas): def _check_length(positions): if axes_given and len(positions) < len(axes): - raise ValueError('Length of "coordinates" or "deltas" cannot be less than that ' - 'of "axes".') + raise ValueError( + 'Length of "coordinates" or "deltas" cannot be less than that ' 'of "axes".' + ) elif not axes_given and len(positions) != len(axes): - raise ValueError('Length of "coordinates" or "deltas" must match the number of ' - 'dimensions of "f" when "axes" is not given.') + raise ValueError( + 'Length of "coordinates" or "deltas" must match the number of ' + 'dimensions of "f" when "axes" is not given.' + ) if deltas is not None: if coordinates is not None: raise ValueError('Cannot specify both "coordinates" and "deltas".') _check_length(deltas) - return 'delta', deltas, axes + return "delta", deltas, axes elif coordinates is not None: _check_length(coordinates) - return 'x', coordinates, axes + return "x", coordinates, axes elif isinstance(f, xr.DataArray): - return 'pass', axes, axes # only the axis argument matters + return "pass", axes, axes # only the axis argument matters else: - raise ValueError('Must specify either "coordinates" or "deltas" for value positions ' - 'when "f" is not a DataArray.') + raise ValueError( + 'Must specify either "coordinates" or "deltas" for value positions ' + 'when "f" is not a DataArray.' + ) def _process_deriv_args(f, axis, x, delta): @@ -1254,7 +1310,7 @@ def _process_deriv_args(f, axis, x, delta): axis = normalize_axis_index(axis if axis is not None else 0, n) if f.shape[axis] < 3: - raise ValueError('f must have at least 3 point along the desired axis.') + raise ValueError("f must have at least 3 point along the desired axis.") if delta is not None: if x is not None: @@ -1264,9 +1320,9 @@ def _process_deriv_args(f, axis, x, delta): if delta.size == 1: diff_size = list(f.shape) diff_size[axis] -= 1 - delta_units = getattr(delta, 'units', None) + delta_units = getattr(delta, "units", None) delta = np.broadcast_to(delta, diff_size, subok=True) - if not hasattr(delta, 'units') and delta_units is not None: + if not hasattr(delta, "units") and delta_units is not None: delta = delta * delta_units else: delta = _broadcast_to_axis(delta, axis, n) @@ -1280,7 +1336,7 @@ def _process_deriv_args(f, axis, x, delta): @exporter.export -@preprocess_and_wrap(wrap_like='input_dir') +@preprocess_and_wrap(wrap_like="input_dir") def parse_angle(input_dir): """Calculate the meteorological angle from directional text. @@ -1301,10 +1357,10 @@ def parse_angle(input_dir): if isinstance(input_dir, str): # abb_dirs = abbrieviated directions abb_dirs = _clean_direction([_abbrieviate_direction(input_dir)]) - elif hasattr(input_dir, '__len__'): # handle np.array, pd.Series, list, and array-like - input_dir_str = ','.join(_clean_direction(input_dir, preprocess=True)) + elif hasattr(input_dir, "__len__"): # handle np.array, pd.Series, list, and array-like + input_dir_str = ",".join(_clean_direction(input_dir, preprocess=True)) abb_dir_str = _abbrieviate_direction(input_dir_str) - abb_dirs = _clean_direction(abb_dir_str.split(',')) + abb_dirs = _clean_direction(abb_dir_str.split(",")) else: # handle unrecognizable scalar return np.nan @@ -1314,25 +1370,23 @@ def parse_angle(input_dir): def _clean_direction(dir_list, preprocess=False): """Handle None if preprocess, else handles anything not in DIR_STRS.""" if preprocess: # primarily to remove None from list so ','.join works - return [UND if not isinstance(the_dir, str) else the_dir - for the_dir in dir_list] + return [UND if not isinstance(the_dir, str) else the_dir for the_dir in dir_list] else: # remove extraneous abbrieviated directions - return [UND if the_dir not in DIR_STRS else the_dir - for the_dir in dir_list] + return [UND if the_dir not in DIR_STRS else the_dir for the_dir in dir_list] def _abbrieviate_direction(ext_dir_str): """Convert extended (non-abbrievated) directions to abbrieviation.""" - return (ext_dir_str - .upper() - .replace('_', '') - .replace('-', '') - .replace(' ', '') - .replace('NORTH', 'N') - .replace('EAST', 'E') - .replace('SOUTH', 'S') - .replace('WEST', 'W') - ) + return ( + ext_dir_str.upper() + .replace("_", "") + .replace("-", "") + .replace(" ", "") + .replace("NORTH", "N") + .replace("EAST", "E") + .replace("SOUTH", "S") + .replace("WEST", "W") + ) @exporter.export @@ -1364,7 +1418,7 @@ def angle_to_direction(input_angle, full=False, level=3): except AttributeError: # no units associated origin_units = units.degree - if not hasattr(input_angle, '__len__') or isinstance(input_angle, str): + if not hasattr(input_angle, "__len__") or isinstance(input_angle, str): input_angle = [input_angle] scalar = True else: @@ -1373,7 +1427,7 @@ def angle_to_direction(input_angle, full=False, level=3): # clean any numeric strings, negatives, and None # does not handle strings with alphabet input_angle = np.array(input_angle).astype(float) - with np.errstate(invalid='ignore'): # warns about the np.nan + with np.errstate(invalid="ignore"): # warns about the np.nan input_angle[np.where(input_angle < 0)] = np.nan input_angle = input_angle * origin_units @@ -1389,12 +1443,14 @@ def angle_to_direction(input_angle, full=False, level=3): elif level == 1: nskip = 4 else: - err_msg = 'Level of complexity cannot be less than 1 or greater than 3!' + err_msg = "Level of complexity cannot be less than 1 or greater than 3!" raise ValueError(err_msg) - angle_dict = {i * BASE_DEGREE_MULTIPLIER.m * nskip: dir_str - for i, dir_str in enumerate(DIR_STRS[::nskip])} - angle_dict[MAX_DEGREE_ANGLE.m] = 'N' # handle edge case of 360. + angle_dict = { + i * BASE_DEGREE_MULTIPLIER.m * nskip: dir_str + for i, dir_str in enumerate(DIR_STRS[::nskip]) + } + angle_dict[MAX_DEGREE_ANGLE.m] = "N" # handle edge case of 360. angle_dict[UND_ANGLE] = UND # round to the nearest angles for dict lookup @@ -1409,19 +1465,18 @@ def angle_to_direction(input_angle, full=False, level=3): # ['N', 'N', 'NE', 'NE', 'E', 'E', 'SE', 'SE', # 'S', 'S', 'SW', 'SW', 'W', 'W', 'NW', 'NW'] - multiplier = np.round( - (norm_angles / BASE_DEGREE_MULTIPLIER / nskip) - 0.001).m - round_angles = (multiplier * BASE_DEGREE_MULTIPLIER.m * nskip) + multiplier = np.round((norm_angles / BASE_DEGREE_MULTIPLIER / nskip) - 0.001).m + round_angles = multiplier * BASE_DEGREE_MULTIPLIER.m * nskip round_angles[np.where(np.isnan(round_angles))] = UND_ANGLE dir_str_arr = itemgetter(*round_angles)(angle_dict) # for array if full: - dir_str_arr = ','.join(dir_str_arr) + dir_str_arr = ",".join(dir_str_arr) dir_str_arr = _unabbrieviate_direction(dir_str_arr) if not scalar: - dir_str = dir_str_arr.split(',') + dir_str = dir_str_arr.split(",") else: - dir_str = dir_str_arr.replace(',', ' ') + dir_str = dir_str_arr.replace(",", " ") else: dir_str = dir_str_arr @@ -1430,15 +1485,15 @@ def angle_to_direction(input_angle, full=False, level=3): def _unabbrieviate_direction(abb_dir_str): """Convert abbrieviated directions to non-abbrieviated direction.""" - return (abb_dir_str - .upper() - .replace(UND, 'Undefined ') - .replace('N', 'North ') - .replace('E', 'East ') - .replace('S', 'South ') - .replace('W', 'West ') - .replace(' ,', ',') - ).strip() + return ( + abb_dir_str.upper() + .replace(UND, "Undefined ") + .replace("N", "North ") + .replace("E", "East ") + .replace("S", "South ") + .replace("W", "West ") + .replace(" ,", ",") + ).strip() def _remove_nans(*variables): diff --git a/src/metpy/calc/turbulence.py b/src/metpy/calc/turbulence.py index 461ba0eeba1..b14c6fd9ed1 100644 --- a/src/metpy/calc/turbulence.py +++ b/src/metpy/calc/turbulence.py @@ -5,15 +5,15 @@ import numpy as np -from .tools import make_take from ..package_tools import Exporter from ..xarray import preprocess_and_wrap +from .tools import make_take exporter = Exporter(globals()) @exporter.export -@preprocess_and_wrap(wrap_like='ts') +@preprocess_and_wrap(wrap_like="ts") def get_perturbation(ts, axis=-1): r"""Compute the perturbation from the mean of a time series. @@ -46,7 +46,7 @@ def get_perturbation(ts, axis=-1): @exporter.export -@preprocess_and_wrap(wrap_like='u') +@preprocess_and_wrap(wrap_like="u") def tke(u, v, w, perturbation=False, axis=-1): r"""Compute turbulence kinetic energy. @@ -112,7 +112,7 @@ def tke(u, v, w, perturbation=False, axis=-1): @exporter.export -@preprocess_and_wrap(wrap_like='vel') +@preprocess_and_wrap(wrap_like="vel") def kinematic_flux(vel, b, perturbation=False, axis=-1): r"""Compute the kinematic flux from two time series. @@ -181,7 +181,7 @@ def kinematic_flux(vel, b, perturbation=False, axis=-1): @exporter.export -@preprocess_and_wrap(wrap_like='u') +@preprocess_and_wrap(wrap_like="u") def friction_velocity(u, w, v=None, perturbation=False, axis=-1): r"""Compute the friction velocity from the time series of velocity components. diff --git a/src/metpy/cbook.py b/src/metpy/cbook.py index dc0678354c6..66dd0fc1943 100644 --- a/src/metpy/cbook.py +++ b/src/metpy/cbook.py @@ -12,22 +12,23 @@ from . import __version__ POOCH = pooch.create( - path=pooch.os_cache('metpy'), - base_url='https://github.com/Unidata/MetPy/raw/{version}/staticdata/', - version='v' + __version__, - version_dev='master') + path=pooch.os_cache("metpy"), + base_url="https://github.com/Unidata/MetPy/raw/{version}/staticdata/", + version="v" + __version__, + version_dev="master", +) # Check if we have the data available directly from a git checkout, either from the # TEST_DATA_DIR variable, or looking relative to the path of this module's file. Use this # to override Pooch's path. -dev_data_path = os.environ.get('TEST_DATA_DIR', Path(__file__).parents[2] / 'staticdata') +dev_data_path = os.environ.get("TEST_DATA_DIR", Path(__file__).parents[2] / "staticdata") if Path(dev_data_path).exists(): POOCH.path = dev_data_path -POOCH.load_registry(Path(__file__).parent / 'static-data-manifest.txt') +POOCH.load_registry(Path(__file__).parent / "static-data-manifest.txt") -def get_test_data(fname, as_file_obj=True, mode='rb'): +def get_test_data(fname, as_file_obj=True, mode="rb"): """Access a file from MetPy's collection of test data.""" path = POOCH.fetch(fname) # If we want a file object, open it, trying to guess whether this should be binary mode @@ -63,9 +64,11 @@ def register(self, name): A decorator that takes a function and will register it under the name. """ + def dec(func): self._registry[name] = func return func + return dec def __getitem__(self, name): @@ -90,4 +93,4 @@ def broadcast_indices(x, minv, ndim, axis): return tuple(ret) -__all__ = ('Registry', 'broadcast_indices', 'get_test_data') +__all__ = ("Registry", "broadcast_indices", "get_test_data") diff --git a/src/metpy/constants.py b/src/metpy/constants.py index 0007e970884..1c873ffee56 100644 --- a/src/metpy/constants.py +++ b/src/metpy/constants.py @@ -69,51 +69,52 @@ # Export all the variables defined in this block with exporter: # Earth - earth_gravity = g = units.Quantity(1.0, units.gravity).to('m / s^2') + earth_gravity = g = units.Quantity(1.0, units.gravity).to("m / s^2") # Taken from GEMPAK constants Re = earth_avg_radius = 6.3712e6 * units.m - G = gravitational_constant = (units.Quantity(1, units. - newtonian_constant_of_gravitation) - .to('m^3 / kg / s^2')) + G = gravitational_constant = units.Quantity(1, units.newtonian_constant_of_gravitation).to( + "m^3 / kg / s^2" + ) omega = earth_avg_angular_vel = 2 * units.pi / units.sidereal_day d = earth_sfc_avg_dist_sun = 1.496e11 * units.m - S = earth_solar_irradiance = units.Quantity(1.368e3, 'W / m^2') + S = earth_solar_irradiance = units.Quantity(1.368e3, "W / m^2") delta = earth_max_declination = 23.45 * units.deg earth_orbit_eccentricity = 0.0167 earth_mass = me = 5.9722e24 * units.kg # molar gas constant - R = units.Quantity(1.0, units.R).to('J / K / mol') + R = units.Quantity(1.0, units.R).to("J / K / mol") # # Water # # From: https://pubchem.ncbi.nlm.nih.gov/compound/water - Mw = water_molecular_weight = units.Quantity(18.01528, 'g / mol') + Mw = water_molecular_weight = units.Quantity(18.01528, "g / mol") Rv = water_gas_constant = R / Mw # Nominal density of liquid water at 0C - rho_l = density_water = units.Quantity(1e3, 'kg / m^3') - Cp_v = wv_specific_heat_press = units.Quantity(1952., 'm^2 / s^2 / K') - Cv_v = wv_specific_heat_vol = units.Quantity(1463., 'm^2 / s^2 / K') - Cp_l = water_specific_heat = units.Quantity(4218., 'm^2 / s^2 / K') # at 0C - Lv = water_heat_vaporization = units.Quantity(2.501e6, 'm^2 / s^2') # at 0C - Lf = water_heat_fusion = units.Quantity(3.34e5, 'm^2 / s^2') # at 0C - Cp_i = ice_specific_heat = units.Quantity(2106, 'm^2 / s^2 / K') # at 0C - rho_i = density_ice = units.Quantity(917, 'kg / m^3') # at 0C + rho_l = density_water = units.Quantity(1e3, "kg / m^3") + Cp_v = wv_specific_heat_press = units.Quantity(1952.0, "m^2 / s^2 / K") + Cv_v = wv_specific_heat_vol = units.Quantity(1463.0, "m^2 / s^2 / K") + Cp_l = water_specific_heat = units.Quantity(4218.0, "m^2 / s^2 / K") # at 0C + Lv = water_heat_vaporization = units.Quantity(2.501e6, "m^2 / s^2") # at 0C + Lf = water_heat_fusion = units.Quantity(3.34e5, "m^2 / s^2") # at 0C + Cp_i = ice_specific_heat = units.Quantity(2106, "m^2 / s^2 / K") # at 0C + rho_i = density_ice = units.Quantity(917, "kg / m^3") # at 0C # Dry air -- standard atmosphere - Md = dry_air_molecular_weight = units.Quantity(28.9644, 'g / mol') + Md = dry_air_molecular_weight = units.Quantity(28.9644, "g / mol") Rd = dry_air_gas_constant = R / Md dry_air_spec_heat_ratio = 1.4 - Cp_d = dry_air_spec_heat_press = units.Quantity(1005, 'm^2 / s^2 / K') # Bolton 1980 + Cp_d = dry_air_spec_heat_press = units.Quantity(1005, "m^2 / s^2 / K") # Bolton 1980 Cv_d = dry_air_spec_heat_vol = Cp_d / dry_air_spec_heat_ratio - rho_d = dry_air_density_stp = ((1000. * units.mbar) - / (Rd * 273.15 * units.K)).to('kg / m^3') + rho_d = dry_air_density_stp = ((1000.0 * units.mbar) / (Rd * 273.15 * units.K)).to( + "kg / m^3" + ) # General meteorology constants - P0 = pot_temp_ref_press = 1000. * units.mbar - kappa = poisson_exponent = (Rd / Cp_d).to('dimensionless') + P0 = pot_temp_ref_press = 1000.0 * units.mbar + kappa = poisson_exponent = (Rd / Cp_d).to("dimensionless") gamma_d = dry_adiabatic_lapse_rate = g / Cp_d - epsilon = molecular_weight_ratio = (Mw / Md).to('dimensionless') + epsilon = molecular_weight_ratio = (Mw / Md).to("dimensionless") del Exporter diff --git a/src/metpy/deprecation.py b/src/metpy/deprecation.py index 942f6fb369c..e08de4ae24e 100644 --- a/src/metpy/deprecation.py +++ b/src/metpy/deprecation.py @@ -122,25 +122,30 @@ class MetpyDeprecationWarning(UserWarning): metpyDeprecation = MetpyDeprecationWarning # noqa: N816 -def _generate_deprecation_message(since, message='', name='', - alternative='', pending=False, - obj_type='attribute', - addendum=''): +def _generate_deprecation_message( + since, + message="", + name="", + alternative="", + pending=False, + obj_type="attribute", + addendum="", +): if not message: if pending: - message = ( - 'The {} {} will be deprecated in a ' - 'future version.'.format(name, obj_type)) + message = "The {} {} will be deprecated in a " "future version.".format( + name, obj_type + ) else: - message = ( - 'The {} {} was deprecated in version ' - '{}.'.format(name, obj_type, since)) + message = "The {} {} was deprecated in version " "{}.".format( + name, obj_type, since + ) - altmessage = '' + altmessage = "" if alternative: - altmessage = f' Use {alternative} instead.' + altmessage = f" Use {alternative} instead." message = message + altmessage @@ -150,8 +155,15 @@ def _generate_deprecation_message(since, message='', name='', return message -def warn_deprecated(since, message='', name='', alternative='', pending=False, - obj_type='attribute', addendum=''): +def warn_deprecated( + since, + message="", + name="", + alternative="", + pending=False, + obj_type="attribute", + addendum="", +): """Display deprecation warning in a standard way. Parameters @@ -194,14 +206,16 @@ def warn_deprecated(since, message='', name='', alternative='', pending=False, obj_type='module') """ - message = _generate_deprecation_message(since, message, name, alternative, - pending, obj_type) + message = _generate_deprecation_message( + since, message, name, alternative, pending, obj_type + ) warnings.warn(message, metpyDeprecation, stacklevel=1) -def deprecated(since, message='', name='', alternative='', pending=False, - obj_type=None, addendum=''): +def deprecated( + since, message="", name="", alternative="", pending=False, obj_type=None, addendum="" +): """Mark a function or a class as deprecated. Parameters @@ -250,23 +264,31 @@ def the_function_to_deprecate(): pass """ - def deprecate(obj, message=message, name=name, alternative=alternative, - pending=pending, addendum=addendum): + + def deprecate( + obj, + message=message, + name=name, + alternative=alternative, + pending=pending, + addendum=addendum, + ): import textwrap if not name: name = obj.__name__ if isinstance(obj, type): - obj_type = 'class' + obj_type = "class" old_doc = obj.__doc__ func = obj.__init__ def finalize(wrapper, new_doc): obj.__init__ = wrapper return obj + else: - obj_type = 'function' + obj_type = "function" func = obj old_doc = func.__doc__ @@ -275,22 +297,21 @@ def finalize(wrapper, new_doc): # wrapper.__doc__ = new_doc return wrapper - message = _generate_deprecation_message(since, message, name, - alternative, pending, - obj_type, addendum) + message = _generate_deprecation_message( + since, message, name, alternative, pending, obj_type, addendum + ) def wrapper(*args, **kwargs): warnings.warn(message, metpyDeprecation, stacklevel=2) return func(*args, **kwargs) - old_doc = textwrap.dedent(old_doc or '').strip('\n') + old_doc = textwrap.dedent(old_doc or "").strip("\n") message = message.strip() - new_doc = ('\n.. deprecated:: {}' - '\n {}\n\n'.format(since, message) + old_doc) + new_doc = "\n.. deprecated:: {}" "\n {}\n\n".format(since, message) + old_doc if not old_doc: # This is to prevent a spurious 'unexected unindent' warning from # docutils when the original docstring was blank. - new_doc += r'\ ' + new_doc += r"\ " return finalize(wrapper, new_doc) diff --git a/src/metpy/interpolate/geometry.py b/src/metpy/interpolate/geometry.py index 3acc03b2d24..05fc104aece 100644 --- a/src/metpy/interpolate/geometry.py +++ b/src/metpy/interpolate/geometry.py @@ -220,7 +220,7 @@ def circumcenter(pt0, pt1, pt2): ac_x_diff = a_x - c_x ba_x_diff = b_x - a_x - d_div = (a_x * bc_y_diff + b_x * ca_y_diff + c_x * ab_y_diff) + d_div = a_x * bc_y_diff + b_x * ca_y_diff + c_x * ab_y_diff if d_div == 0: raise ZeroDivisionError diff --git a/src/metpy/interpolate/grid.py b/src/metpy/interpolate/grid.py index 1e1ed468893..d6c0d195d84 100644 --- a/src/metpy/interpolate/grid.py +++ b/src/metpy/interpolate/grid.py @@ -5,10 +5,13 @@ import numpy as np -from .points import (interpolate_to_points, inverse_distance_to_points, - natural_neighbor_to_points) from ..package_tools import Exporter from ..pandas import preprocess_pandas +from .points import ( + interpolate_to_points, + inverse_distance_to_points, + natural_neighbor_to_points, +) exporter = Exporter(globals()) @@ -33,8 +36,8 @@ def generate_grid(horiz_dim, bbox): """ x_steps, y_steps = get_xy_steps(bbox, horiz_dim) - grid_x = np.linspace(bbox['west'], bbox['east'], x_steps) - grid_y = np.linspace(bbox['south'], bbox['north'], y_steps) + grid_x = np.linspace(bbox["west"], bbox["east"], x_steps) + grid_y = np.linspace(bbox["south"], bbox["north"], y_steps) gx, gy = np.meshgrid(grid_x, grid_y) @@ -74,8 +77,8 @@ def get_xy_range(bbox): Range in meters in y dimension. """ - x_range = bbox['east'] - bbox['west'] - y_range = bbox['north'] - bbox['south'] + x_range = bbox["east"] - bbox["west"] + y_range = bbox["north"] - bbox["south"] return x_range, y_range @@ -126,7 +129,7 @@ def get_boundary_coords(x, y, spatial_pad=0): north = np.max(y) + spatial_pad south = np.min(y) - spatial_pad - return {'west': west, 'south': south, 'east': east, 'north': north} + return {"west": west, "south": south, "east": east, "north": north} @exporter.export @@ -168,8 +171,18 @@ def natural_neighbor_to_grid(xp, yp, variable, grid_x, grid_y): @exporter.export -def inverse_distance_to_grid(xp, yp, variable, grid_x, grid_y, r, gamma=None, kappa=None, - min_neighbors=3, kind='cressman'): +def inverse_distance_to_grid( + xp, + yp, + variable, + grid_x, + grid_y, + r, + gamma=None, + kappa=None, + min_neighbors=3, + kind="cressman", +): r"""Generate an inverse distance interpolation of the given points to a regular grid. Values are assigned to the given grid using inverse distance weighting based on either @@ -215,17 +228,35 @@ def inverse_distance_to_grid(xp, yp, variable, grid_x, grid_y, r, gamma=None, ka # Handle grid-to-points conversion, and use function from `interpolation` points_obs = list(zip(xp, yp)) points_grid = generate_grid_coords(grid_x, grid_y) - img = inverse_distance_to_points(points_obs, variable, points_grid, r, gamma=gamma, - kappa=kappa, min_neighbors=min_neighbors, kind=kind) + img = inverse_distance_to_points( + points_obs, + variable, + points_grid, + r, + gamma=gamma, + kappa=kappa, + min_neighbors=min_neighbors, + kind=kind, + ) return img.reshape(grid_x.shape) @exporter.export @preprocess_pandas -def interpolate_to_grid(x, y, z, interp_type='linear', hres=50000, - minimum_neighbors=3, gamma=0.25, kappa_star=5.052, - search_radius=None, rbf_func='linear', rbf_smooth=0, - boundary_coords=None): +def interpolate_to_grid( + x, + y, + z, + interp_type="linear", + hres=50000, + minimum_neighbors=3, + gamma=0.25, + kappa_star=5.052, + search_radius=None, + rbf_func="linear", + rbf_smooth=0, + boundary_coords=None, +): r"""Interpolate given (x,y), observation (z) pairs to a grid based on given parameters. Parameters @@ -297,10 +328,18 @@ def interpolate_to_grid(x, y, z, interp_type='linear', hres=50000, # Handle grid-to-points conversion, and use function from `interpolation` points_obs = np.array(list(zip(x, y))) points_grid = generate_grid_coords(grid_x, grid_y) - img = interpolate_to_points(points_obs, z, points_grid, interp_type=interp_type, - minimum_neighbors=minimum_neighbors, gamma=gamma, - kappa_star=kappa_star, search_radius=search_radius, - rbf_func=rbf_func, rbf_smooth=rbf_smooth) + img = interpolate_to_points( + points_obs, + z, + points_grid, + interp_type=interp_type, + minimum_neighbors=minimum_neighbors, + gamma=gamma, + kappa_star=kappa_star, + search_radius=search_radius, + rbf_func=rbf_func, + rbf_smooth=rbf_smooth, + ) return grid_x, grid_y, img.reshape(grid_x.shape) @@ -342,18 +381,22 @@ def interpolate_to_isosurface(level_var, interp_var, level, bottom_up_search=Tru """ from ..calc import find_bounding_indices + # Find index values above and below desired interpolated surface value - above, below, good = find_bounding_indices(level_var, [level], axis=0, - from_below=bottom_up_search) + above, below, good = find_bounding_indices( + level_var, [level], axis=0, from_below=bottom_up_search + ) # Linear interpolation of variable to interpolated surface value - interp_level = (((level - level_var[above]) / (level_var[below] - level_var[above])) - * (interp_var[below] - interp_var[above])) + interp_var[above] + interp_level = ( + ((level - level_var[above]) / (level_var[below] - level_var[above])) + * (interp_var[below] - interp_var[above]) + ) + interp_var[above] # Handle missing values and instances where no values for surface exist above and below interp_level[~good] = np.nan - minvar = (np.min(level_var, axis=0) >= level) - maxvar = (np.max(level_var, axis=0) <= level) + minvar = np.min(level_var, axis=0) >= level + maxvar = np.max(level_var, axis=0) <= level interp_level[0][minvar] = interp_var[-1][minvar] interp_level[0][maxvar] = interp_var[0][maxvar] return interp_level.squeeze() diff --git a/src/metpy/interpolate/one_dimension.py b/src/metpy/interpolate/one_dimension.py index 2d76d4be3fe..79c534027d5 100644 --- a/src/metpy/interpolate/one_dimension.py +++ b/src/metpy/interpolate/one_dimension.py @@ -15,7 +15,7 @@ @exporter.export @preprocess_and_wrap() -def interpolate_nans_1d(x, y, kind='linear'): +def interpolate_nans_1d(x, y, kind="linear"): """Interpolate NaN values in y. Interpolate NaN values in the y dimension. Works with unsorted x values. @@ -39,12 +39,12 @@ def interpolate_nans_1d(x, y, kind='linear'): x = x[x_sort_args] y = y[x_sort_args] nans = np.isnan(y) - if kind == 'linear': + if kind == "linear": y[nans] = np.interp(x[nans], x[~nans], y[~nans]) - elif kind == 'log': + elif kind == "log": y[nans] = np.interp(np.log(x[nans]), np.log(x[~nans]), y[~nans]) else: - raise ValueError(f'Unknown option for kind: {kind}') + raise ValueError(f"Unknown option for kind: {kind}") return y[x_sort_args] @@ -129,12 +129,12 @@ def interpolate_1d(x, xp, *args, axis=0, fill_value=np.nan, return_list_always=F # If fill_value is none and data is out of bounds, raise value error if ((np.max(minv) == xp.shape[axis]) or (np.min(minv) == 0)) and fill_value is None: - raise ValueError('Interpolation point out of data bounds encountered') + raise ValueError("Interpolation point out of data bounds encountered") # Warn if interpolated values are outside data bounds, will make these the values # at end of data range. if np.max(minv) == xp.shape[axis]: - warnings.warn('Interpolation point out of data bounds encountered') + warnings.warn("Interpolation point out of data bounds encountered") minv2[minv == xp.shape[axis]] = xp.shape[axis] - 1 if np.min(minv) == 0: minv2[minv == 0] = 1 @@ -144,7 +144,7 @@ def interpolate_1d(x, xp, *args, axis=0, fill_value=np.nan, return_list_always=F below = broadcast_indices(xp, minv2 - 1, ndim, axis) if np.any(x_array < xp[below]): - warnings.warn('Interpolation point out of data bounds encountered') + warnings.warn("Interpolation point out of data bounds encountered") # Create empty output list ret = [] @@ -154,8 +154,9 @@ def interpolate_1d(x, xp, *args, axis=0, fill_value=np.nan, return_list_always=F # Var needs to be on the *left* of the multiply to ensure that if it's a pint # Quantity, it gets to control the operation--at least until we make sure # masked arrays and pint play together better. See https://github.com/hgrecco/pint#633 - var_interp = var[below] + (var[above] - var[below]) * ((x_array - xp[below]) - / (xp[above] - xp[below])) + var_interp = var[below] + (var[above] - var[below]) * ( + (x_array - xp[below]) / (xp[above] - xp[below]) + ) # Set points out of bounds to fill value. var_interp[minv == xp.shape[axis]] = fill_value @@ -232,7 +233,7 @@ def _strip_matching_units(*args): Replaces `@units.wraps(None, ('=A', '=A'))`, which breaks with `*args` handling for pint>=0.9. """ - if all(hasattr(arr, 'units') for arr in args): + if all(hasattr(arr, "units") for arr in args): return [arr.to(args[0].units).magnitude for arr in args] else: return args diff --git a/src/metpy/interpolate/points.py b/src/metpy/interpolate/points.py index 086fc5ddf86..86154817e7b 100644 --- a/src/metpy/interpolate/points.py +++ b/src/metpy/interpolate/points.py @@ -6,8 +6,8 @@ import logging import numpy as np -from scipy.interpolate import griddata, Rbf -from scipy.spatial import cKDTree, ConvexHull, Delaunay, qhull +from scipy.interpolate import Rbf, griddata +from scipy.spatial import ConvexHull, Delaunay, cKDTree, qhull from . import geometry, tools from ..package_tools import Exporter @@ -152,9 +152,11 @@ def natural_neighbor_point(xp, yp, variable, grid_loc, tri, neighbors, circumcen area_list.append(cur_area * value[0]) except (ZeroDivisionError, qhull.QhullError) as e: - message = ('Error during processing of a grid. ' - 'Interpolation will continue but be mindful ' - f'of errors in output. {e}') + message = ( + "Error during processing of a grid. " + "Interpolation will continue but be mindful " + f"of errors in output. {e}" + ) log.warning(message) return np.nan @@ -204,15 +206,23 @@ def natural_neighbor_to_points(points, values, xi): if len(neighbors) > 0: points_transposed = np.array(points).transpose() - img[ind] = natural_neighbor_point(points_transposed[0], points_transposed[1], - values, xi[grid], tri, neighbors, circumcenters) + img[ind] = natural_neighbor_point( + points_transposed[0], + points_transposed[1], + values, + xi[grid], + tri, + neighbors, + circumcenters, + ) return img @exporter.export -def inverse_distance_to_points(points, values, xi, r, gamma=None, kappa=None, min_neighbors=3, - kind='cressman'): +def inverse_distance_to_points( + points, values, xi, r, gamma=None, kappa=None, min_neighbors=3, kind="cressman" +): r"""Generate an inverse distance weighting interpolation to the given points. Values are assigned to the given interpolation points based on either [Cressman1959]_ or @@ -264,21 +274,30 @@ def inverse_distance_to_points(points, values, xi, r, gamma=None, kappa=None, mi values_subset = values[matches] dists = geometry.dist_2(grid[0], grid[1], x1, y1) - if kind == 'cressman': + if kind == "cressman": img[idx] = cressman_point(dists, values_subset, r) - elif kind == 'barnes': + elif kind == "barnes": img[idx] = barnes_point(dists, values_subset, kappa, gamma) else: - raise ValueError(f'{kind} interpolation not supported.') + raise ValueError(f"{kind} interpolation not supported.") return img @exporter.export -def interpolate_to_points(points, values, xi, interp_type='linear', minimum_neighbors=3, - gamma=0.25, kappa_star=5.052, search_radius=None, rbf_func='linear', - rbf_smooth=0): +def interpolate_to_points( + points, + values, + xi, + interp_type="linear", + minimum_neighbors=3, + gamma=0.25, + kappa_star=5.052, + search_radius=None, + rbf_func="linear", + rbf_smooth=0, +): r"""Interpolate unstructured point data to the given points. This function interpolates the given `values` valid at `points` to the points `xi`. This is @@ -342,40 +361,59 @@ def interpolate_to_points(points, values, xi, interp_type='linear', minimum_neig """ # If this is a type that `griddata` handles, hand it along to `griddata` - if interp_type in ['linear', 'nearest', 'cubic']: + if interp_type in ["linear", "nearest", "cubic"]: return griddata(points, values, xi, method=interp_type) # If this is natural neighbor, hand it along to `natural_neighbor` - elif interp_type == 'natural_neighbor': + elif interp_type == "natural_neighbor": return natural_neighbor_to_points(points, values, xi) # If this is Barnes/Cressman, determine search_radios and hand it along to # `inverse_distance` - elif interp_type in ['cressman', 'barnes']: + elif interp_type in ["cressman", "barnes"]: ave_spacing = tools.average_spacing(points) if search_radius is None: search_radius = 5 * ave_spacing - if interp_type == 'cressman': - return inverse_distance_to_points(points, values, xi, search_radius, - min_neighbors=minimum_neighbors, - kind=interp_type) + if interp_type == "cressman": + return inverse_distance_to_points( + points, + values, + xi, + search_radius, + min_neighbors=minimum_neighbors, + kind=interp_type, + ) else: kappa = tools.calc_kappa(ave_spacing, kappa_star) - return inverse_distance_to_points(points, values, xi, search_radius, gamma, kappa, - min_neighbors=minimum_neighbors, - kind=interp_type) + return inverse_distance_to_points( + points, + values, + xi, + search_radius, + gamma, + kappa, + min_neighbors=minimum_neighbors, + kind=interp_type, + ) # If this is radial basis function, make the interpolator and apply it - elif interp_type == 'rbf': + elif interp_type == "rbf": points_transposed = np.array(points).transpose() xi_transposed = np.array(xi).transpose() - rbfi = Rbf(points_transposed[0], points_transposed[1], values, function=rbf_func, - smooth=rbf_smooth) + rbfi = Rbf( + points_transposed[0], + points_transposed[1], + values, + function=rbf_func, + smooth=rbf_smooth, + ) return rbfi(xi_transposed[0], xi_transposed[1]) else: - raise ValueError('Interpolation option not available. ' - 'Try: linear, nearest, cubic, natural_neighbor, ' - 'barnes, cressman, rbf') + raise ValueError( + "Interpolation option not available. " + "Try: linear, nearest, cubic, natural_neighbor, " + "barnes, cressman, rbf" + ) diff --git a/src/metpy/interpolate/slices.py b/src/metpy/interpolate/slices.py index 053644fb2ce..d145717f78e 100644 --- a/src/metpy/interpolate/slices.py +++ b/src/metpy/interpolate/slices.py @@ -14,7 +14,7 @@ @exporter.export -def interpolate_to_slice(data, points, interp_type='linear'): +def interpolate_to_slice(data, points, interp_type="linear"): r"""Obtain an interpolated slice through data using xarray. Utilizing the interpolation functionality in `xarray`, this function takes a slice the @@ -44,22 +44,26 @@ def interpolate_to_slice(data, points, interp_type='linear'): """ try: - x, y = data.metpy.coordinates('x', 'y') + x, y = data.metpy.coordinates("x", "y") except AttributeError: - raise ValueError('Required coordinate information not available. Verify that ' - 'your data has been parsed by MetPy with proper x and y ' - 'dimension coordinates.') - - data_sliced = data.interp({ - x.name: xr.DataArray(points[:, 0], dims='index', attrs=x.attrs), - y.name: xr.DataArray(points[:, 1], dims='index', attrs=y.attrs) - }, method=interp_type) - data_sliced.coords['index'] = range(len(points)) + raise ValueError( + "Required coordinate information not available. Verify that " + "your data has been parsed by MetPy with proper x and y " + "dimension coordinates." + ) + + data_sliced = data.interp( + { + x.name: xr.DataArray(points[:, 0], dims="index", attrs=x.attrs), + y.name: xr.DataArray(points[:, 1], dims="index", attrs=y.attrs), + }, + method=interp_type, + ) + data_sliced.coords["index"] = range(len(points)) # Bug in xarray: interp strips units - if ( - isinstance(data.data, units.Quantity) - and not isinstance(data_sliced.data, units.Quantity) + if isinstance(data.data, units.Quantity) and not isinstance( + data_sliced.data, units.Quantity ): data_sliced.data = units.Quantity(data_sliced.data, data.data.units) @@ -102,18 +106,20 @@ def geodesic(crs, start, end, steps): # Geod.npts only gives points *in between* the start and end, and we want to include # the endpoints. g = Geod(crs.proj4_init) - geodesic = np.concatenate([ - np.array(start[::-1])[None], - np.array(g.npts(start[1], start[0], end[1], end[0], steps - 2)), - np.array(end[::-1])[None] - ]).transpose() + geodesic = np.concatenate( + [ + np.array(start[::-1])[None], + np.array(g.npts(start[1], start[0], end[1], end[0], steps - 2)), + np.array(end[::-1])[None], + ] + ).transpose() points = crs.transform_points(ccrs.Geodetic(), *geodesic)[:, :2] return points @exporter.export -def cross_section(data, start, end, steps=100, interp_type='linear'): +def cross_section(data, start, end, steps=100, interp_type="linear"): r"""Obtain an interpolated cross-sectional slice through gridded data. Utilizing the interpolation functionality in `xarray`, this function takes a vertical @@ -152,8 +158,9 @@ def cross_section(data, start, end, steps=100, interp_type='linear'): """ if isinstance(data, xr.Dataset): # Recursively apply to dataset - return data.map(cross_section, True, (start, end), steps=steps, - interp_type=interp_type) + return data.map( + cross_section, True, (start, end), steps=steps, interp_type=interp_type + ) elif data.ndim == 0: # This has no dimensions, so it is likely a projection variable. In any case, there # are no data here to take the cross section with. Therefore, do nothing. @@ -165,17 +172,19 @@ def cross_section(data, start, end, steps=100, interp_type='linear'): crs_data = data.metpy.cartopy_crs x = data.metpy.x except AttributeError: - raise ValueError('Data missing required coordinate information. Verify that ' - 'your data have been parsed by MetPy with proper x and y ' - 'dimension coordinates and added crs coordinate of the ' - 'correct projection for each variable.') + raise ValueError( + "Data missing required coordinate information. Verify that " + "your data have been parsed by MetPy with proper x and y " + "dimension coordinates and added crs coordinate of the " + "correct projection for each variable." + ) # Get the geodesic points_cross = geodesic(crs_data, start, end, steps) # Patch points_cross to match given longitude range, whether [0, 360) or (-180, 180] - if check_axis(x, 'longitude') and (x > 180).any(): - points_cross[points_cross[:, 0] < 0, 0] += 360. + if check_axis(x, "longitude") and (x > 180).any(): + points_cross[points_cross[:, 0] < 0, 0] += 360.0 # Return the interpolated data return interpolate_to_slice(data, points_cross, interp_type=interp_type) diff --git a/src/metpy/interpolate/tools.py b/src/metpy/interpolate/tools.py index 4ceee152f3f..1732935aa11 100644 --- a/src/metpy/interpolate/tools.py +++ b/src/metpy/interpolate/tools.py @@ -26,7 +26,7 @@ def calc_kappa(spacing, kappa_star=5.052): kappa: float """ - return kappa_star * (2.0 * spacing / np.pi)**2 + return kappa_star * (2.0 * spacing / np.pi) ** 2 def average_spacing(points): diff --git a/src/metpy/io/_nexrad_msgs/msg18.py b/src/metpy/io/_nexrad_msgs/msg18.py index 8c81a5c2dba..edba4f753ef 100644 --- a/src/metpy/io/_nexrad_msgs/msg18.py +++ b/src/metpy/io/_nexrad_msgs/msg18.py @@ -4,746 +4,750 @@ # flake8: noqa # Generated file -- do not modify -descriptions = {"ADAP_FILE_NAME": "NAME OF ADAPTATION DATA FILE", - "ADAP_FORMAT": "FORMAT OF ADAPTATION DATA FILE", - "ADAP_REVISION": "REVISION NUMBER OF ADAPTATION DATA FILE", - "ADAP_DATE": "LAST MODIFIED DATE ADAPTATION DATA FILE", - "ADAP_TIME": "LAST MODIFIED TIME OF ADAPTATION DATA FILE", - "K1": "AZIMUTH POSITION GAIN FACTOR (K1)", - "AZ_LAT": "LATENCY OF DCU AZIMUTH MEASUREMENT (s)", - "K3": "ELEVATION POSITION GAIN FACTOR (K3)", - "EL_LAT": "LATENCY OF DCU ELEVATION MEASUREMENT (s)", - "PARKAZ": "PEDESTAL PARK POSITION IN AZIMUTH (deg)", - "PARKEL": "PEDESTAL PARK POSITION IN ELEVATION (deg)", - "A_FUEL_CONV_0": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (0% HGT) (%)", - "A_FUEL_CONV_1": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (10% HGT) (%)", - "A_FUEL_CONV_2": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (20% HGT) (%)", - "A_FUEL_CONV_3": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (30% HGT) (%)", - "A_FUEL_CONV_4": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (40% HGT) (%)", - "A_FUEL_CONV_5": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (50% HGT) (%)", - "A_FUEL_CONV_6": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (60% HGT) (%)", - "A_FUEL_CONV_7": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (70% HGT) (%)", - "A_FUEL_CONV_8": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (80% HGT) (%)", - "A_FUEL_CONV_9": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (90% HGT) (%)", - "A_FUEL_CONV_10": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (100% HGT) (%)", - "A_MIN_SHELTER_TEMP": "MINIMUM EQUIPMENT SHELTER ALARM TEMPERATURE (deg C)", - "A_MAX_SHELTER_TEMP": "MAXIMUM EQUIPMENT SHELTER ALARM TEMPERATURE (deg C)", - "A_MIN_SHELTER_AC_TEMP_DIFF": "MINIMUM A/C DISCHARGE AIR TEMPERATURE DIFFERENTIAL (deg C)", - "A_MAX_XMTR_AIR_TEMP": "MAXIMUM TRANSMITTER LEAVING AIR ALARM TEMPERATURE (deg C)", - "A_MAX_RAD_TEMP": "MAXIMUM RADOME ALARM TEMPERATURE (deg C)", - "A_MAX_RAD_TEMP_RISE": "MAXIMUM RADOME MINUS AMBIENT TEMPERATURE DIFFERENCE (deg C)", - "PED_28V_REG_LIM": "PEDESTAL +28 VOLT POWER SUPPLY TOLERANCE (%)", - "PED_5V_REG_LIM": "PEDESTAL +5 VOLT POWER SUPPLY TOLERANCE (%)", - "PED_15V_REG_LIM": "PEDESTAL +/- 15 VOLT POWER SUPPLY TOLERANCE (%)", - "A_MIN_GEN_ROOM_TEMP": "MINIMUM GENERATOR SHELTER ALARM TEMPERATURE (deg C)", - "A_MAX_GEN_ROOM_TEMP": "MAXIMUM GENERATOR SHELTER ALARM TEMPERATURE (deg C)", - "DAU_5V_REG_LIM": "DAU +5 VOLT POWER SUPPLY TOLERANCE (%)", - "DAU_15V_REG_LIM": "DAU +/- 15 VOLT POWER SUPPLY TOLERANCE (%)", - "DAU_28V_REG_LIM": "DAU +28 VOLT POWER (%)", - "EN_5V_REG_LIM": "ENCODER +5 VOLT POWER SUPPLY TOLERANCE (%)", - "EN_5V_NOM_VOLTS": "ENCODER +5 VOLT POWER SUPPLY NOMINAL VOLTAGE (V)", - "RPG_CO_LOCATED": "RPG CO-LOCATED", - "SPEC_FILTER_INSTALLED": "TRANSMITTER SPECTRUM FILTER INSTALLED", - "TPS_INSTALLED": "TRANSITION POWER SOURCE INSTALLED", - "RMS_INSTALLED": "FAA RMS INSTALLED", - "A_HVDL_TST_INT": "PERFORMANCE TEST INTERVAL (h)", - "A_RPG_LT_INT": "RPG LOOP TEST INTERVAL (min)", - "A_MIN_STAB_UTIL_PWR_TIME": "REQUIRED INTERVAL TIME FOR STABLE UTILITY POWER (min)", - "A_GEN_AUTO_EXER_INTERVAL": "MAXIMUM GENERATOR AUTOMATIC EXERCISE INTERVAL (h)", - "A_UTIL_PWR_SW_REQ_INTERVAL": "RECOMMENDED SWITCH TO UTILITY POWER TIME INTERVAL (min)", - "A_LOW_FUEL_LEVEL": "LOW FUEL TANK WARNING LEVEL (%)", - "CONFIG_CHAN_NUMBER": "CONFIGURATION CHANNEL NUMBER", - "A_RPG_LINK_TYPE": "RPG WIDEBAND LINK TYPE (0 = DIRECT, 1 = MICROWAVE, 2 = FIBER OPTIC)", - "REDUNDANT_CHAN_CONFIG": "REDUNDANT CHANNEL CONFIGURATION (1 = SINGLE CHAN, 2 = FAA, 3 = NWS REDUNDANT)", - "ATTEN_TABLE_0": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (0dB) (dB)", - "ATTEN_TABLE_1": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (1dB) (dB)", - "ATTEN_TABLE_2": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (2dB) (dB)", - "ATTEN_TABLE_3": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (3dB) (dB)", - "ATTEN_TABLE_4": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (4dB) (dB)", - "ATTEN_TABLE_5": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (5dB) (dB)", - "ATTEN_TABLE_6": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (6dB) (dB)", - "ATTEN_TABLE_7": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (7dB) (dB)", - "ATTEN_TABLE_8": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (8dB) (dB)", - "ATTEN_TABLE_9": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (9dB) (dB)", - "ATTEN_TABLE_10": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (10dB) (dB)", - "ATTEN_TABLE_11": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (11dB) (dB)", - "ATTEN_TABLE_12": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (12dB) (dB)", - "ATTEN_TABLE_13": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (13dB) (dB)", - "ATTEN_TABLE_14": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (14dB) (dB)", - "ATTEN_TABLE_15": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (15dB) (dB)", - "ATTEN_TABLE_16": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (16dB) (dB)", - "ATTEN_TABLE_17": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (17dB) (dB)", - "ATTEN_TABLE_18": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (18dB) (dB)", - "ATTEN_TABLE_19": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (19dB) (dB)", - "ATTEN_TABLE_20": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (20dB) (dB)", - "ATTEN_TABLE_21": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (21dB) (dB)", - "ATTEN_TABLE_22": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (22dB) (dB)", - "ATTEN_TABLE_23": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (23dB) (dB)", - "ATTEN_TABLE_24": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (24dB) (dB)", - "ATTEN_TABLE_25": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (25dB) (dB)", - "ATTEN_TABLE_26": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (26dB) (dB)", - "ATTEN_TABLE_27": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (27dB) (dB)", - "ATTEN_TABLE_28": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (28dB) (dB)", - "ATTEN_TABLE_29": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (29dB) (dB)", - "ATTEN_TABLE_30": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (30dB) (dB)", - "ATTEN_TABLE_31": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (31dB) (dB)", - "ATTEN_TABLE_32": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (32dB) (dB)", - "ATTEN_TABLE_33": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (33dB) (dB)", - "ATTEN_TABLE_34": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (34dB) (dB)", - "ATTEN_TABLE_35": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (35dB) (dB)", - "ATTEN_TABLE_36": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (36dB) (dB)", - "ATTEN_TABLE_37": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (37dB) (dB)", - "ATTEN_TABLE_38": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (38dB) (dB)", - "ATTEN_TABLE_39": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (39dB) (dB)", - "ATTEN_TABLE_40": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (40dB) (dB)", - "ATTEN_TABLE_41": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (41dB) (dB)", - "ATTEN_TABLE_42": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (42dB) (dB)", - "ATTEN_TABLE_43": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (43dB) (dB)", - "ATTEN_TABLE_44": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (44dB) (dB)", - "ATTEN_TABLE_45": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (45dB) (dB)", - "ATTEN_TABLE_46": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (46dB) (dB)", - "ATTEN_TABLE_47": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (47dB) (dB)", - "ATTEN_TABLE_48": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (48dB) (dB)", - "ATTEN_TABLE_49": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (49dB) (dB)", - "ATTEN_TABLE_50": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (50dB) (dB)", - "ATTEN_TABLE_51": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (51dB) (dB)", - "ATTEN_TABLE_52": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (52dB) (dB)", - "ATTEN_TABLE_53": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (53dB) (dB)", - "ATTEN_TABLE_54": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (54dB) (dB)", - "ATTEN_TABLE_55": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (55dB) (dB)", - "ATTEN_TABLE_56": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (56dB) (dB)", - "ATTEN_TABLE_57": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (57dB) (dB)", - "ATTEN_TABLE_58": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (58dB) (dB)", - "ATTEN_TABLE_59": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (59dB) (dB)", - "ATTEN_TABLE_60": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (60dB) (dB)", - "ATTEN_TABLE_61": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (61dB) (dB)", - "ATTEN_TABLE_62": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (62dB) (dB)", - "ATTEN_TABLE_63": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (63dB) (dB)", - "ATTEN_TABLE_64": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (64dB) (dB)", - "ATTEN_TABLE_65": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (65dB) (dB)", - "ATTEN_TABLE_66": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (66dB) (dB)", - "ATTEN_TABLE_67": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (67dB) (dB)", - "ATTEN_TABLE_68": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (68dB) (dB)", - "ATTEN_TABLE_69": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (69dB) (dB)", - "ATTEN_TABLE_70": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (70dB) (dB)", - "ATTEN_TABLE_71": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (71dB) (dB)", - "ATTEN_TABLE_72": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (72dB) (dB)", - "ATTEN_TABLE_73": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (73dB) (dB)", - "ATTEN_TABLE_74": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (74dB) (dB)", - "ATTEN_TABLE_75": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (75dB) (dB)", - "ATTEN_TABLE_76": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (76dB) (dB)", - "ATTEN_TABLE_77": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (77dB) (dB)", - "ATTEN_TABLE_78": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (78dB) (dB)", - "ATTEN_TABLE_79": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (79dB) (dB)", - "ATTEN_TABLE_80": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (80dB) (dB)", - "ATTEN_TABLE_81": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (81dB) (dB)", - "ATTEN_TABLE_82": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (82dB) (dB)", - "ATTEN_TABLE_83": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (83dB) (dB)", - "ATTEN_TABLE_84": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (84dB) (dB)", - "ATTEN_TABLE_85": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (85dB) (dB)", - "ATTEN_TABLE_86": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (86dB) (dB)", - "ATTEN_TABLE_87": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (87dB) (dB)", - "ATTEN_TABLE_88": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (88dB) (dB)", - "ATTEN_TABLE_89": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (89dB) (dB)", - "ATTEN_TABLE_90": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (90dB) (dB)", - "ATTEN_TABLE_91": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (91dB) (dB)", - "ATTEN_TABLE_92": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (92dB) (dB)", - "ATTEN_TABLE_93": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (93dB) (dB)", - "ATTEN_TABLE_94": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (94dB) (dB)", - "ATTEN_TABLE_95": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (95dB) (dB)", - "ATTEN_TABLE_96": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (96dB) (dB)", - "ATTEN_TABLE_97": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (97dB) (dB)", - "ATTEN_TABLE_98": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (98dB) (dB)", - "ATTEN_TABLE_99": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (99dB) (dB)", - "ATTEN_TABLE_100": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (100dB) (dB)", - "ATTEN_TABLE_101": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (101dB) (dB)", - "ATTEN_TABLE_102": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (102dB) (dB)", - "ATTEN_TABLE_103": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (103dB) (dB)", - "PATH_LOSSES_7": "PATH LOSS - VERTICAL IF HELIAX TO 4AT16 (dB)", - "PATH_LOSSES_11": "PATH LOSS - 4AT14 TO RF LOG AMP 4A29J1 (dB)", - "PATH_LOSSES_13": "PATH LOSS - 2A9A9 RF DELAY LINE (dB)", - "PATH_LOSSES_14": "PATH LOSS - 4AT13 TO RF LOG AMP 4A29J1 (dB)", - "PATH_LOSSES_19": "PATH LOSS - 4AT15 TO RF LOG AMP 4A29J1 (dB)", - "PATH_LOSSES_28": "PATH LOSS - HORIZONTAL IF HELIAX TO 4AT17 (dB)", - "PATH_LOSSES_30": "PATH LOSS - A5 ELEVATION ROTARY JOINT (dB)", - "PATH_LOSSES_31": "PATH LOSS - ELEVATION ROTARY JOINT TO ANTENNA (dB)", - "PATH_LOSSES_32": "PATH LOSS - WG02 HARMONIC FILTER (dB)", - "PATH_LOSSES_33": "PATH LOSS - WAVEGUIDE KLYSTRON T0 SWITCH (dB)", - "PATH_LOSSES_34": "PATH LOSS - 2A1A4 WAVEGUIDE CHANNEL AZIMUTH ROTARY JOINT (dB)", - "PATH_LOSSES_35": "PATH LOSS - WG06 SPECTRUM FILTER (dB)", - "PATH_LOSSES_36": "PATH LOSS - COAX TRANSMITTER RF DRIVE TO 4AT15 (dB)", - "PATH_LOSSES_37": "PATH LOSS - WAVEGUIDE SWITCH TO AZIMUTH ROTARY JOINT (dB)", - "PATH_LOSSES_38": "PATH LOSS - WAVEGUIDE SWITCH (dB)", - "PATH_LOSSES_39": "PATH LOSS - WG04 CIRCULATOR (dB)", - "PATH_LOSSES_40": "PATH LOSS - A6 ARC DETECTOR (dB)", - "PATH_LOSSES_41": "PATH LOSS - 1DC1 TRANSMITTER COUPLER STRAIGHT THRU (dB)", - "PATH_LOSSES_42": "PATH LOSS - 1DC1 TRANSMITTER COUPLER COUPLING (dB)", - "PATH_LOSSES_43": "PATH LOSS - A33 PAD (dB)", - "PATH_LOSSES_44": "PATH LOSS - COAX TRANSMITTER RF SAMPLE TO A33 PAD (dB)", - "PATH_LOSSES_45": "PATH LOSS - A20J1_4 POWER SPLITTER (dB)", - "PATH_LOSSES_46": "PATH LOSS - A20J1_3 POWER SPLITTER (dB)", - "PATH_LOSSES_47": "PATH LOSS - A20J1_2 POWER SPLITTER (dB)", - "H_COUPLER_CW_LOSS": "RF PALLET HORIZONTAL COUPLER TEST SIGNAL LOSS (dB)", - "V_COUPLER_XMT_LOSS": "RF PALLET VERTICAL COUPLER TRANSMITTER LOSS (dB)", - "PATH_LOSSES_50": "PATH LOSS - WAVEGUIDE AZIMUTH JOINT TO ELEVATION JOINT (dB)", - "PATH_LOSSES_52": "PATH LOSS - 1AT4 TRANSMITTER COUPLER PAD (dB)", - "V_COUPLER_CW_LOSS": "RF PALLET VERTICAL COUPLER TEST SIGNAL LOSS (dB)", - "PATH_LOSSES_54": "PATH LOSS - 4AT13 PAD (dB)", - "PATH_LOSSES_58": "PATH LOSS - 4AT17 ATTENUATOR (dB)", - "PATH_LOSSES_59": "PATH LOSS - IFD IF ANTI-ALIAS FILTER (dB)", - "PATH_LOSSES_60": "PATH LOSS - A20J1_5 POWER SPLITTER (dB)", - "PATH_LOSSES_61": "PATH LOSS - AT5 50dB ATTENUATOR (dB)", - "PATH_LOSSES_63": "PATH LOSS - A39 RF_IF BURST MIXER (dB)", - "PATH_LOSSES_64": "PATH LOSS - AR1 BURST IF AMPLIFIER (dB)", - "PATH_LOSSES_65": "PATH LOSS - IFD BURST ANTIALIAS FILTER (dB)", - "PATH_LOSSES_66": "PATH LOSS - DC3 J1_3 6dB COUPLER, THROUGH (dB)", - "PATH_LOSSES_67": "PATH LOSS - DC3 J1_2 6DB COUPLER, COUPLED (dB)", - "PATH_LOSSES_68": "PATH LOSS - AT2+AT3 26dB COHO ATTENUATOR (dB)", - "PATH_LOSSES_69": "PATH LOSS - 4DC3J1 TO 4AT14 (dB)", - "CHAN_CAL_DIFF": "NONCONTROLLING CHANNEL CALIBRATION DIFFERENCE (dB)", - "LOG_AMP_FACTOR_1": "RF DETECTOR LOG AMPLIFIER SCALE FACTOR FOR CONVERTING RECEIVER TEST DATA (V/dBm)", - "LOG_AMP_FACTOR_2": "RF DETECTOR LOG AMPLIFIER BIAS FOR CONVERTING RECEIVER TEST DATA (V)", - "V_TS_CW": "AME VERTICAL TEST SIGNAL POWER (dBm)", - "RNSCALE_0": "RECEIVER NOISE NORMALIZATION (-1.0 deg to -0.5 deg)", - "RNSCALE_1": "RECEIVER NOISE NORMALIZATION (-0.5 deg to 0.0 deg)", - "RNSCALE_2": "RECEIVER NOISE NORMALIZATION (0.0 deg to 0.5 deg)", - "RNSCALE_3": "RECEIVER NOISE NORMALIZATION (0.5 deg to 1.0 deg)", - "RNSCALE_4": "RECEIVER NOISE NORMALIZATION (1.0 deg to 1.5 deg)", - "RNSCALE_5": "RECEIVER NOISE NORMALIZATION (1.5 deg to 2.0 deg)", - "RNSCALE_6": "RECEIVER NOISE NORMALIZATION (2.0 deg to 2.5 deg)", - "RNSCALE_7": "RECEIVER NOISE NORMALIZATION (2.5 deg to 3.0 deg)", - "RNSCALE_8": "RECEIVER NOISE NORMALIZATION (3.0 deg to 3.5 deg)", - "RNSCALE_9": "RECEIVER NOISE NORMALIZATION (3.5 deg to 4.0 deg)", - "RNSCALE_10": "RECEIVER NOISE NORMALIZATION (4.0 deg to 4.5 deg)", - "RNSCALE_11": "RECEIVER NOISE NORMALIZATION (4.5 deg to 5.0 deg)", - "RNSCALE_12": "RECEIVER NOISE NORMALIZATION (> 5.0 deg)", - "ATMOS_0": "TWO WAY ATMOSPHERIC LOSS/KM (-1.0 deg to -0.5 deg) (dB/km)", - "ATMOS_1": "TWO WAY ATMOSPHERIC LOSS/KM (-0.5 deg to 0.0 deg) (dB/km)", - "ATMOS_2": "TWO WAY ATMOSPHERIC LOSS/KM (0.0 deg to 0.5 deg) (dB/km)", - "ATMOS_3": "TWO WAY ATMOSPHERIC LOSS/KM (0.5 deg to 1.0 deg) (dB/km)", - "ATMOS_4": "TWO WAY ATMOSPHERIC LOSS/KM (1.0 deg to 1.5 deg) (dB/km)", - "ATMOS_5": "TWO WAY ATMOSPHERIC LOSS/KM (1.5 deg to 2.0 deg) (dB/km)", - "ATMOS_6": "TWO WAY ATMOSPHERIC LOSS/KM (2.0 deg to 2.5 deg) (dB/km)", - "ATMOS_7": "TWO WAY ATMOSPHERIC LOSS/KM (2.5 deg to 3.0 deg) (dB/km)", - "ATMOS_8": "TWO WAY ATMOSPHERIC LOSS/KM (3.0 deg to 3.5 deg) (dB/km)", - "ATMOS_9": "TWO WAY ATMOSPHERIC LOSS/KM (3.5 deg to 4.0 deg) (dB/km)", - "ATMOS_10": "TWO WAY ATMOSPHERIC LOSS/KM (4.0 deg to 4.5 deg) (dB/km)", - "ATMOS_11": "TWO WAY ATMOSPHERIC LOSS/KM (4.5 deg to 5.0 deg) (dB/km)", - "ATMOS_12": "TWO WAY ATMOSPHERIC LOSS/KM (> 5.0 deg) (dB/km)", - "EL_INDEX_0": "BYPASS MAP GENERATION ELEVATION ANGLE (0) (deg)", - "EL_INDEX_1": "BYPASS MAP GENERATION ELEVATION ANGLE (1) (deg)", - "EL_INDEX_2": "BYPASS MAP GENERATION ELEVATION ANGLE (2) (deg)", - "EL_INDEX_3": "BYPASS MAP GENERATION ELEVATION ANGLE (3) (deg)", - "EL_INDEX_4": "BYPASS MAP GENERATION ELEVATION ANGLE (4) (deg)", - "EL_INDEX_5": "BYPASS MAP GENERATION ELEVATION ANGLE (5) (deg)", - "EL_INDEX_6": "BYPASS MAP GENERATION ELEVATION ANGLE (6) (deg)", - "EL_INDEX_7": "BYPASS MAP GENERATION ELEVATION ANGLE (7) (deg)", - "EL_INDEX_8": "BYPASS MAP GENERATION ELEVATION ANGLE (8) (deg)", - "EL_INDEX_9": "BYPASS MAP GENERATION ELEVATION ANGLE (9) (deg)", - "EL_INDEX_10": "BYPASS MAP GENERATION ELEVATION ANGLE (10) (deg)", - "EL_INDEX_11": "BYPASS MAP GENERATION ELEVATION ANGLE (11) (deg)", - "TFREQ_MHZ": "TRANSMITTER FREQUENCY (MHz)", - "BASE_DATA_TCN": "POINT CLUTTER SUPPRESSION THRESHOLD (TCN) (dB)", - "REFL_DATA_TOVER": "RANGE UNFOLDING OVERLAY THRESHOLD (TOVER) (dB)", - "TAR_H_DBZ0_LP": "HORIZONTAL TARGET SYSTEM CALIBRATION (dBZ0) FOR LONG PULSE (dBZ)", - "TAR_V_DBZ0_LP": "VERTICAL TARGET SYSTEM CALIBRATION (dBZ0) FOR LONG PULSE (dBZ)", - "INIT_PHI_DP": "INITIAL SYSTEM DIFFERENTIAL PHASE (deg)", - "NORM_INIT_PHI_DP": "NORMALIZED INITIAL SYSTEM DIFFERENTIAL PHASE (deg)", - "LX_LP": "MATCHED FILTER LOSS FOR LONG PULSE (dB)", - "LX_SP": "MATCHED FILTER LOSS FOR SHORT PULSE (dB)", - "METEOR_PARAM": "/K/**2 HYDROMETEOR REFRACTIVITY FACTOR", - "BEAMWIDTH": "ANTENNA BEAMWIDTH (deg)", - "ANTENNA_GAIN": "ANTENNA GAIN INCLUDING RADOME (dB)", - "VEL_MAINT_LIMIT": "VELOCITY CHECK DELTA MAINTENANCE LIMIT (m/s)", - "WTH_MAINT_LIMIT": "SPECTRUM WIDTH CHECK DELTA MAINTENANCE LIMIT (m/s)", - "VEL_DEGRAD_LIMIT": "VELOCITY CHECK DELTA DEGRADE LIMIT (m/s)", - "WTH_DEGRAD_LIMIT": "SPECTRUM WIDTH CHECK DELTA DEGRADE LIMIT (m/s)", - "H_NOISETEMP_DGRAD_LIMIT": "HORIZONTAL SYSTEM NOISE TEMP DEGRADE LIMIT (K)", - "H_NOISETEMP_MAINT_LIMIT": "HORIZONTAL SYSTEM NOISE TEMP MAINTENANCE LIMIT (K)", - "V_NOISETEMP_DGRAD_LIMIT": "VERTICAL SYSTEM NOISE TEMP DEGRADE LIMIT (K)", - "V_NOISETEMP_MAINT_LIMIT": "VERTICAL SYSTEM NOISE TEMP MAINTENANCE LIMIT (K)", - "KLY_DEGRADE_LIMIT": "KLYS TRON OUTPUT TARGET CONSISTENCY DEGRADE LIMIT (dB)", - "TS_COHO": "COHO POWER AT A1J4 (dBm)", - "H_TS_CW": "AME HORIZONTAL TEST SIGNAL POWER (dBm)", - "TS_RF_SP": "RF DRIVE TEST SIGNAL SHORT PULSE AT 3A5J4 (dBm)", - "TS_RF_LP": "RF DRIVE TEST SIGNAL LONG PULSE AT 3A5J4 (dBm)", - "TS_STALO": "STALO POWER AT A1J2 (dBm)", - "AME_H_NOISE_ENR": "AME NOISE SOURCE HORIZONTAL EXCESS NOISE RATIO (dB)", - "XMTR_PEAK_PWR_HIGH_LIMIT": "MAXIMUM TRANSMITTER PEAK POWER ALARM LEVEL (kW)", - "XMTR_PEAK_PWR_LOW_LIMIT": "MINIMUM TRANSMITTER PEAK POWER ALARM LEVEL (kW)", - "H_DBZ0_DELTA_LIMIT": "DIFFERENCE BETWEEN COMPUTED AND TARGET HORIZONTAL DBZ0 LIMIT (dB)", - "THRESHOLD1": "BYPASS MAP GENERATOR NOISE THRESHOLD (dB)", - "THRESHOLD2": "BYPASS MAP GENERATOR REJECTION RATIO THRESHOLD (dB)", - "CLUT_SUPP_DGRAD_LIM": "CLUTTER SUPPRESSION DEGRADE LIMIT (dB)", - "CLUT_SUPP_MAINT_LIM": "CLUTTER SUPPRESSION MAINTENANCE LIMIT (dB)", - "RANGE0_VALUE": "TRUE RANGE AT START OF FIRST RANGE BIN (km)", - "XMTR_PWR_MTR_SCALE": "SCALE FACTOR USED TO CONVERT TRANSMITTER POWER BYTE DATA TO WATTS (W (4))", - "V_DBZ0_DELTA_LIMIT": "DIFFERENCE BETWEEN COMPUTED AND TARGET VERTICAL DBZ0 LIMIT (dB)", - "TAR_H_DBZ0_SP": "HORIZONTAL TARGET SYSTEM CALIBRATION (dBZ0) FOR SHORT PULSE (dBZ)", - "TAR_V_DBZ0_SP": "VERTICAL TARGET SYSTEM CALIBRATION (DBZ0) FOR SHORT PULSE (dBZ)", - "DELTAPRF": "SITE PRF SET (A=1, B=2, C=3, D=4, E=5)", - "TAU_SP": "PULSE WIDTH OF TRANSMITTER OUTPUT IN SHORT PULSE (nsec)", - "TAU_LP": "PULSE WIDTH OF TRANSMITTER OUTPUT IN LONG PULSE (nsec)", - "NC_DEAD_VALUE": "NUMBER OF 1/4 KM BINS OF CORRUPTED DATA AT END OF SWEEP", - "TAU_RF_SP": "RF DRIVE PULSE WIDTH IN SHORT PULSE (nsec)", - "TAU_RF_LP": "RF DRIVE PULSE WIDTH IN LONG PULSE MODE (nsec)", - "SEG1LIM": "CLUTTER MAP BOUNDARY ELEVATION BETWEEN SEGMENTS 1 & 2 (deg)", - "SLATSEC": "SITE LATITUDE - SECONDS (s)", - "SLONSEC": "SITE LONGITUDE - SECONDS (s)", - "SLATDEG": "SITE LATITUDE - DEGREES (deg)", - "SLATMIN": "SITE LATITUDE - MINUTES (min)", - "SLONDEG": "SITE LONGITUDE - DEGREES (deg)", - "SLONMIN": "SITE LONGITUDE - MINUTES (min)", - "SLATDIR": "SITE LATITUDE - DIRECTION", - "SLONDIR": "SITE LONGITUDE - DIRECTION", - "VCPAT11": "VOLUME COVERAGE PATTERN NUMBER 11 DEFINITION", - "VCPAT21": "VOLUME COVERAGE PATTERN NUMBER 21 DEFINITION", - "VCPAT31": "VOLUME COVERAGE PATTERN NUMBER 31 DEFINITION", - "VCPAT32": "VOLUME COVERAGE PATTERN NUMBER 32 DEFINITION", - "VCPAT300": "VOLUME COVERAGE PATTERN NUMBER 300 DEFINITION", - "VCPAT301": "VOLUME COVERAGE PATTERN NUMBER 301 DEFINITION", - "AZ_CORRECTION_FACTOR": "AZIMUTH BORESIGHT CORRECTION FACTOR (deg)", - "EL_CORRECTION_FACTOR": "ELEVATION BORESIGHT CORRECTION FACTOR (deg)", - "SITE_NAME": "SITE NAME DESIGNATION", - "ANT_MANUAL_SETUP_IELMIN": "MINIMUM ELEVATION ANGLE (deg)", - "ANT_MANUAL_SETUP_IELMAX": "MAXIMUM ELEVATION ANGLE (deg)", - "ANT_MANUAL_SETUP_FAZVELMAX": "MAXIMUM AZIMUTH VELOCITY (deg/s)", - "ANT_MANUAL_SETUP_FELVELMAX": "MAXIMUM ELEVATION VELOCITY (deg/s)", - "ANT_MANUAL_SETUP_IGND_HGT": "SITE GROUND HEIGHT (ABOVE SEA LEVEL) (m)", - "ANT_MANUAL_SETUP_IRAD_HGT": "SITE RADAR HEIGHT (ABOVE GROUND) (m)", - "RVP8NV_IWAVEGUIDE_LENGTH": "WAVEGUIDE LENGTH (m)", - "VEL_DATA_TOVER": "VELOCITY UNFOLDING OVERLAY THRESHOLD (dB)", - "WIDTH_DATA_TOVER": "WIDTH UNFOLDING OVERLAY THRESHOLD (dB)", - "DOPPLER_RANGE_START": "START RANGE FOR FIRST DOPPLER RADIAL (km)", - "MAX_EL_INDEX": "THE MAXIMUM INDEX FOR THE EL_INDEX PARAMETERS", - "SEG2LIM": "CLUTTER MAP BOUNDARY ELEVATION BETWEEN SEGMENTS 2 & 3. (deg)", - "SEG3LIM": "CLUTTER MAP BOUNDARY ELEVATION BETWEEN SEGMENTS 3 & 4. (deg)", - "SEG4LIM": "CLUTTER MAP BOUNDARY ELEVATION BETWEEN SEGMENTS 4 & 5. (deg)", - "NBR_EL_SEGMENTS": "NUMBER OF ELEVATION SEGMENTS IN ORDA CLUTTER MAP.", - "H_NOISE_LONG": "HORIZONTAL RECEIVER NOISE LONG PULSE (dBm)", - "ANT_NOISE_TEMP": "ANTENNA NOISE TEMPERATURE (K)", - "H_NOISE_SHORT": "HORIZONTAL RECEIVER NOISE SHORT PULSE (dBm)", - "H_NOISE_TOLERANCE": "HORIZONTAL RECEIVER NOISE TOLERANCE (dB)", - "MIN_H_DYN_RANGE": "MINIMUM HORIZONTAL DYNAMIC RANGE (dB)", - "GEN_INSTALLED": "AUXILIARY GENERATOR INSTALLED (FAA ONLY)", - "GEN_EXERCISE": "AUXILIARY GENERATOR AUTOMATIC EXERCISE ENABLED (FAA ONLY)", - "V_NOISE_TOLERANCE": "VERTICAL RECEIVER NOISE TOLERANCE (dB)", - "MIN_V_DYN_RANGE": "MINIMUM VERTICAL DYNAMIC RANGE (dB)", - "ZDR_BIAS_DGRAD_LIM": "SYSTEM DIFFERENTIAL REFLECTIVITY BIAS DEGRADE LIMIT (dB)", - "V_NOISE_LONG": "VERTICAL RECEIVER NOISE LONG PULSE (dBm)", - "V_NOISE_SHORT": "VERTICAL RECEIVER NOISE SHORT PULSE (dBm)", - "ZDR_DATA_TOVER": "ZDR UNFOLDING OVERLAY THRESHOLD (dB)", - "PHI_DATA_TOVER": "PHI UNFOLDING OVERLAY THRESHOLD (dB)", - "RHO_DATA_TOVER": "RHO UNFOLDING OVERLAY THRESHOLD (dB)", - "STALO_POWER_DGRAD_LIMIT": "STALO POWER DEGRADE LIMIT (V)", - "STALO_POWER_MAINT_LIMIT": "STALO POWER MAINTENANCE LIMIT (V)", - "MIN_H_PWR_SENSE": "MINIMUM HORIZONTAL POWER SENSE (dBm)", - "MIN_V_PWR_SENSE": "MINIMUM VERTICAL POWER SENSE (dBm)", - "H_PWR_SENSE_OFFSET": "HORIZONTAL POWER SENSE CALIBRATION OFFSET (dB)", - "V_PWR_SENSE_OFFSET": "VERTICAL POWER SENSE CALIBRATION OFFSET (dB)", - "PS_GAIN_REF": "RF PALLET BIAS ERROR (dB)", - "RF_PALLET_BROAD_LOSS": "RF PALLET BROADBAND LOSS (dB)", - "ZDR_CHECK_THRESHOLD": "ZDR CHECK FAILURE THRESHOLD (dB)", - "PHI_CHECK_THRESHOLD": "PHI CHECK FAILURE THRESHOLD (deg)", - "RHO_CHECK_THRESHOLD": "RHO CHECK FAILURE THRESHOLD (ratio)", - "AME_PS_TOLERANCE": "AME POWER SUPPLY TOLERANCE (%)", - "AME_MAX_TEMP": "MAXIMUM AME INTERNAL ALARM TEMPERATURE (deg C)", - "AME_MIN_TEMP": "MINIMUM AME INTERNAL ALARM TEMPERATURE (deg C)", - "RCVR_MOD_MAX_TEMP": "MAXIMUM AME RECEIVER MODULE ALARM TEMPERATURE (deg C)", - "RCVR_MOD_MIN_TEMP": "MINIMUM AME RECEIVER MODULE ALARM TEMPERATURE (deg C)", - "BITE_MOD_MAX_TEMP": "MAXIMUM AME BITE MODULE ALARM TEMPERATURE (deg C)", - "BITE_MOD_MIN_TEMP": "MINIMUM AME BITE MODULE ALARM TEMPERATURE (deg C)", - "DEFAULT_POLARIZATION": "DEFAULT (H+V) MICROWAVE ASSEMBLY PHASE SHIFTER POSITION", - "TR_LIMIT_DGRAD_LIMIT": "TR LIMITER DEGRADE LIMIT (V)", - "TR_LIMIT_FAIL_LIMIT": "TR LIMITER FAILURE LIMIT (V)", - "AME_CURRENT_TOLERANCE": "AME PELTIER CURRENT TOLERANCE (%)", - "H_ONLY_POLARIZATION": "HORIZONTAL (H ONLY) MICROWAVE ASSEMBLY PHASE SHIFTER POSITION", - "V_ONLY_POLARIZATION": "VERTICAL (V ONLY) MICROWAVE ASSEMBLY PHASE SHIFTER POSITION", - "REFLECTOR_BIAS": "ANTENNA REFLECTOR BIAS (dB)", - "A_MIN_SHELTER_TEMP_WARN": "LOW EQUIPMENT SHELTER TEMPERATURE WARNING LIMIT (deg C)"} +descriptions = { + "ADAP_FILE_NAME": "NAME OF ADAPTATION DATA FILE", + "ADAP_FORMAT": "FORMAT OF ADAPTATION DATA FILE", + "ADAP_REVISION": "REVISION NUMBER OF ADAPTATION DATA FILE", + "ADAP_DATE": "LAST MODIFIED DATE ADAPTATION DATA FILE", + "ADAP_TIME": "LAST MODIFIED TIME OF ADAPTATION DATA FILE", + "K1": "AZIMUTH POSITION GAIN FACTOR (K1)", + "AZ_LAT": "LATENCY OF DCU AZIMUTH MEASUREMENT (s)", + "K3": "ELEVATION POSITION GAIN FACTOR (K3)", + "EL_LAT": "LATENCY OF DCU ELEVATION MEASUREMENT (s)", + "PARKAZ": "PEDESTAL PARK POSITION IN AZIMUTH (deg)", + "PARKEL": "PEDESTAL PARK POSITION IN ELEVATION (deg)", + "A_FUEL_CONV_0": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (0% HGT) (%)", + "A_FUEL_CONV_1": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (10% HGT) (%)", + "A_FUEL_CONV_2": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (20% HGT) (%)", + "A_FUEL_CONV_3": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (30% HGT) (%)", + "A_FUEL_CONV_4": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (40% HGT) (%)", + "A_FUEL_CONV_5": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (50% HGT) (%)", + "A_FUEL_CONV_6": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (60% HGT) (%)", + "A_FUEL_CONV_7": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (70% HGT) (%)", + "A_FUEL_CONV_8": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (80% HGT) (%)", + "A_FUEL_CONV_9": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (90% HGT) (%)", + "A_FUEL_CONV_10": "GENERATOR FUEL LEVEL HEIGHT/CAPACITY CONVERSION (100% HGT) (%)", + "A_MIN_SHELTER_TEMP": "MINIMUM EQUIPMENT SHELTER ALARM TEMPERATURE (deg C)", + "A_MAX_SHELTER_TEMP": "MAXIMUM EQUIPMENT SHELTER ALARM TEMPERATURE (deg C)", + "A_MIN_SHELTER_AC_TEMP_DIFF": "MINIMUM A/C DISCHARGE AIR TEMPERATURE DIFFERENTIAL (deg C)", + "A_MAX_XMTR_AIR_TEMP": "MAXIMUM TRANSMITTER LEAVING AIR ALARM TEMPERATURE (deg C)", + "A_MAX_RAD_TEMP": "MAXIMUM RADOME ALARM TEMPERATURE (deg C)", + "A_MAX_RAD_TEMP_RISE": "MAXIMUM RADOME MINUS AMBIENT TEMPERATURE DIFFERENCE (deg C)", + "PED_28V_REG_LIM": "PEDESTAL +28 VOLT POWER SUPPLY TOLERANCE (%)", + "PED_5V_REG_LIM": "PEDESTAL +5 VOLT POWER SUPPLY TOLERANCE (%)", + "PED_15V_REG_LIM": "PEDESTAL +/- 15 VOLT POWER SUPPLY TOLERANCE (%)", + "A_MIN_GEN_ROOM_TEMP": "MINIMUM GENERATOR SHELTER ALARM TEMPERATURE (deg C)", + "A_MAX_GEN_ROOM_TEMP": "MAXIMUM GENERATOR SHELTER ALARM TEMPERATURE (deg C)", + "DAU_5V_REG_LIM": "DAU +5 VOLT POWER SUPPLY TOLERANCE (%)", + "DAU_15V_REG_LIM": "DAU +/- 15 VOLT POWER SUPPLY TOLERANCE (%)", + "DAU_28V_REG_LIM": "DAU +28 VOLT POWER (%)", + "EN_5V_REG_LIM": "ENCODER +5 VOLT POWER SUPPLY TOLERANCE (%)", + "EN_5V_NOM_VOLTS": "ENCODER +5 VOLT POWER SUPPLY NOMINAL VOLTAGE (V)", + "RPG_CO_LOCATED": "RPG CO-LOCATED", + "SPEC_FILTER_INSTALLED": "TRANSMITTER SPECTRUM FILTER INSTALLED", + "TPS_INSTALLED": "TRANSITION POWER SOURCE INSTALLED", + "RMS_INSTALLED": "FAA RMS INSTALLED", + "A_HVDL_TST_INT": "PERFORMANCE TEST INTERVAL (h)", + "A_RPG_LT_INT": "RPG LOOP TEST INTERVAL (min)", + "A_MIN_STAB_UTIL_PWR_TIME": "REQUIRED INTERVAL TIME FOR STABLE UTILITY POWER (min)", + "A_GEN_AUTO_EXER_INTERVAL": "MAXIMUM GENERATOR AUTOMATIC EXERCISE INTERVAL (h)", + "A_UTIL_PWR_SW_REQ_INTERVAL": "RECOMMENDED SWITCH TO UTILITY POWER TIME INTERVAL (min)", + "A_LOW_FUEL_LEVEL": "LOW FUEL TANK WARNING LEVEL (%)", + "CONFIG_CHAN_NUMBER": "CONFIGURATION CHANNEL NUMBER", + "A_RPG_LINK_TYPE": "RPG WIDEBAND LINK TYPE (0 = DIRECT, 1 = MICROWAVE, 2 = FIBER OPTIC)", + "REDUNDANT_CHAN_CONFIG": "REDUNDANT CHANNEL CONFIGURATION (1 = SINGLE CHAN, 2 = FAA, 3 = NWS REDUNDANT)", + "ATTEN_TABLE_0": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (0dB) (dB)", + "ATTEN_TABLE_1": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (1dB) (dB)", + "ATTEN_TABLE_2": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (2dB) (dB)", + "ATTEN_TABLE_3": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (3dB) (dB)", + "ATTEN_TABLE_4": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (4dB) (dB)", + "ATTEN_TABLE_5": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (5dB) (dB)", + "ATTEN_TABLE_6": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (6dB) (dB)", + "ATTEN_TABLE_7": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (7dB) (dB)", + "ATTEN_TABLE_8": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (8dB) (dB)", + "ATTEN_TABLE_9": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (9dB) (dB)", + "ATTEN_TABLE_10": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (10dB) (dB)", + "ATTEN_TABLE_11": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (11dB) (dB)", + "ATTEN_TABLE_12": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (12dB) (dB)", + "ATTEN_TABLE_13": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (13dB) (dB)", + "ATTEN_TABLE_14": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (14dB) (dB)", + "ATTEN_TABLE_15": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (15dB) (dB)", + "ATTEN_TABLE_16": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (16dB) (dB)", + "ATTEN_TABLE_17": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (17dB) (dB)", + "ATTEN_TABLE_18": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (18dB) (dB)", + "ATTEN_TABLE_19": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (19dB) (dB)", + "ATTEN_TABLE_20": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (20dB) (dB)", + "ATTEN_TABLE_21": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (21dB) (dB)", + "ATTEN_TABLE_22": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (22dB) (dB)", + "ATTEN_TABLE_23": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (23dB) (dB)", + "ATTEN_TABLE_24": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (24dB) (dB)", + "ATTEN_TABLE_25": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (25dB) (dB)", + "ATTEN_TABLE_26": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (26dB) (dB)", + "ATTEN_TABLE_27": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (27dB) (dB)", + "ATTEN_TABLE_28": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (28dB) (dB)", + "ATTEN_TABLE_29": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (29dB) (dB)", + "ATTEN_TABLE_30": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (30dB) (dB)", + "ATTEN_TABLE_31": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (31dB) (dB)", + "ATTEN_TABLE_32": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (32dB) (dB)", + "ATTEN_TABLE_33": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (33dB) (dB)", + "ATTEN_TABLE_34": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (34dB) (dB)", + "ATTEN_TABLE_35": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (35dB) (dB)", + "ATTEN_TABLE_36": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (36dB) (dB)", + "ATTEN_TABLE_37": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (37dB) (dB)", + "ATTEN_TABLE_38": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (38dB) (dB)", + "ATTEN_TABLE_39": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (39dB) (dB)", + "ATTEN_TABLE_40": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (40dB) (dB)", + "ATTEN_TABLE_41": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (41dB) (dB)", + "ATTEN_TABLE_42": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (42dB) (dB)", + "ATTEN_TABLE_43": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (43dB) (dB)", + "ATTEN_TABLE_44": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (44dB) (dB)", + "ATTEN_TABLE_45": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (45dB) (dB)", + "ATTEN_TABLE_46": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (46dB) (dB)", + "ATTEN_TABLE_47": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (47dB) (dB)", + "ATTEN_TABLE_48": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (48dB) (dB)", + "ATTEN_TABLE_49": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (49dB) (dB)", + "ATTEN_TABLE_50": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (50dB) (dB)", + "ATTEN_TABLE_51": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (51dB) (dB)", + "ATTEN_TABLE_52": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (52dB) (dB)", + "ATTEN_TABLE_53": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (53dB) (dB)", + "ATTEN_TABLE_54": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (54dB) (dB)", + "ATTEN_TABLE_55": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (55dB) (dB)", + "ATTEN_TABLE_56": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (56dB) (dB)", + "ATTEN_TABLE_57": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (57dB) (dB)", + "ATTEN_TABLE_58": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (58dB) (dB)", + "ATTEN_TABLE_59": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (59dB) (dB)", + "ATTEN_TABLE_60": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (60dB) (dB)", + "ATTEN_TABLE_61": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (61dB) (dB)", + "ATTEN_TABLE_62": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (62dB) (dB)", + "ATTEN_TABLE_63": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (63dB) (dB)", + "ATTEN_TABLE_64": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (64dB) (dB)", + "ATTEN_TABLE_65": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (65dB) (dB)", + "ATTEN_TABLE_66": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (66dB) (dB)", + "ATTEN_TABLE_67": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (67dB) (dB)", + "ATTEN_TABLE_68": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (68dB) (dB)", + "ATTEN_TABLE_69": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (69dB) (dB)", + "ATTEN_TABLE_70": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (70dB) (dB)", + "ATTEN_TABLE_71": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (71dB) (dB)", + "ATTEN_TABLE_72": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (72dB) (dB)", + "ATTEN_TABLE_73": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (73dB) (dB)", + "ATTEN_TABLE_74": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (74dB) (dB)", + "ATTEN_TABLE_75": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (75dB) (dB)", + "ATTEN_TABLE_76": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (76dB) (dB)", + "ATTEN_TABLE_77": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (77dB) (dB)", + "ATTEN_TABLE_78": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (78dB) (dB)", + "ATTEN_TABLE_79": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (79dB) (dB)", + "ATTEN_TABLE_80": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (80dB) (dB)", + "ATTEN_TABLE_81": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (81dB) (dB)", + "ATTEN_TABLE_82": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (82dB) (dB)", + "ATTEN_TABLE_83": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (83dB) (dB)", + "ATTEN_TABLE_84": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (84dB) (dB)", + "ATTEN_TABLE_85": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (85dB) (dB)", + "ATTEN_TABLE_86": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (86dB) (dB)", + "ATTEN_TABLE_87": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (87dB) (dB)", + "ATTEN_TABLE_88": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (88dB) (dB)", + "ATTEN_TABLE_89": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (89dB) (dB)", + "ATTEN_TABLE_90": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (90dB) (dB)", + "ATTEN_TABLE_91": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (91dB) (dB)", + "ATTEN_TABLE_92": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (92dB) (dB)", + "ATTEN_TABLE_93": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (93dB) (dB)", + "ATTEN_TABLE_94": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (94dB) (dB)", + "ATTEN_TABLE_95": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (95dB) (dB)", + "ATTEN_TABLE_96": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (96dB) (dB)", + "ATTEN_TABLE_97": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (97dB) (dB)", + "ATTEN_TABLE_98": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (98dB) (dB)", + "ATTEN_TABLE_99": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (99dB) (dB)", + "ATTEN_TABLE_100": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (100dB) (dB)", + "ATTEN_TABLE_101": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (101dB) (dB)", + "ATTEN_TABLE_102": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (102dB) (dB)", + "ATTEN_TABLE_103": "TEST SIGNAL ATTENUATOR INSERTION LOSSES (103dB) (dB)", + "PATH_LOSSES_7": "PATH LOSS - VERTICAL IF HELIAX TO 4AT16 (dB)", + "PATH_LOSSES_11": "PATH LOSS - 4AT14 TO RF LOG AMP 4A29J1 (dB)", + "PATH_LOSSES_13": "PATH LOSS - 2A9A9 RF DELAY LINE (dB)", + "PATH_LOSSES_14": "PATH LOSS - 4AT13 TO RF LOG AMP 4A29J1 (dB)", + "PATH_LOSSES_19": "PATH LOSS - 4AT15 TO RF LOG AMP 4A29J1 (dB)", + "PATH_LOSSES_28": "PATH LOSS - HORIZONTAL IF HELIAX TO 4AT17 (dB)", + "PATH_LOSSES_30": "PATH LOSS - A5 ELEVATION ROTARY JOINT (dB)", + "PATH_LOSSES_31": "PATH LOSS - ELEVATION ROTARY JOINT TO ANTENNA (dB)", + "PATH_LOSSES_32": "PATH LOSS - WG02 HARMONIC FILTER (dB)", + "PATH_LOSSES_33": "PATH LOSS - WAVEGUIDE KLYSTRON T0 SWITCH (dB)", + "PATH_LOSSES_34": "PATH LOSS - 2A1A4 WAVEGUIDE CHANNEL AZIMUTH ROTARY JOINT (dB)", + "PATH_LOSSES_35": "PATH LOSS - WG06 SPECTRUM FILTER (dB)", + "PATH_LOSSES_36": "PATH LOSS - COAX TRANSMITTER RF DRIVE TO 4AT15 (dB)", + "PATH_LOSSES_37": "PATH LOSS - WAVEGUIDE SWITCH TO AZIMUTH ROTARY JOINT (dB)", + "PATH_LOSSES_38": "PATH LOSS - WAVEGUIDE SWITCH (dB)", + "PATH_LOSSES_39": "PATH LOSS - WG04 CIRCULATOR (dB)", + "PATH_LOSSES_40": "PATH LOSS - A6 ARC DETECTOR (dB)", + "PATH_LOSSES_41": "PATH LOSS - 1DC1 TRANSMITTER COUPLER STRAIGHT THRU (dB)", + "PATH_LOSSES_42": "PATH LOSS - 1DC1 TRANSMITTER COUPLER COUPLING (dB)", + "PATH_LOSSES_43": "PATH LOSS - A33 PAD (dB)", + "PATH_LOSSES_44": "PATH LOSS - COAX TRANSMITTER RF SAMPLE TO A33 PAD (dB)", + "PATH_LOSSES_45": "PATH LOSS - A20J1_4 POWER SPLITTER (dB)", + "PATH_LOSSES_46": "PATH LOSS - A20J1_3 POWER SPLITTER (dB)", + "PATH_LOSSES_47": "PATH LOSS - A20J1_2 POWER SPLITTER (dB)", + "H_COUPLER_CW_LOSS": "RF PALLET HORIZONTAL COUPLER TEST SIGNAL LOSS (dB)", + "V_COUPLER_XMT_LOSS": "RF PALLET VERTICAL COUPLER TRANSMITTER LOSS (dB)", + "PATH_LOSSES_50": "PATH LOSS - WAVEGUIDE AZIMUTH JOINT TO ELEVATION JOINT (dB)", + "PATH_LOSSES_52": "PATH LOSS - 1AT4 TRANSMITTER COUPLER PAD (dB)", + "V_COUPLER_CW_LOSS": "RF PALLET VERTICAL COUPLER TEST SIGNAL LOSS (dB)", + "PATH_LOSSES_54": "PATH LOSS - 4AT13 PAD (dB)", + "PATH_LOSSES_58": "PATH LOSS - 4AT17 ATTENUATOR (dB)", + "PATH_LOSSES_59": "PATH LOSS - IFD IF ANTI-ALIAS FILTER (dB)", + "PATH_LOSSES_60": "PATH LOSS - A20J1_5 POWER SPLITTER (dB)", + "PATH_LOSSES_61": "PATH LOSS - AT5 50dB ATTENUATOR (dB)", + "PATH_LOSSES_63": "PATH LOSS - A39 RF_IF BURST MIXER (dB)", + "PATH_LOSSES_64": "PATH LOSS - AR1 BURST IF AMPLIFIER (dB)", + "PATH_LOSSES_65": "PATH LOSS - IFD BURST ANTIALIAS FILTER (dB)", + "PATH_LOSSES_66": "PATH LOSS - DC3 J1_3 6dB COUPLER, THROUGH (dB)", + "PATH_LOSSES_67": "PATH LOSS - DC3 J1_2 6DB COUPLER, COUPLED (dB)", + "PATH_LOSSES_68": "PATH LOSS - AT2+AT3 26dB COHO ATTENUATOR (dB)", + "PATH_LOSSES_69": "PATH LOSS - 4DC3J1 TO 4AT14 (dB)", + "CHAN_CAL_DIFF": "NONCONTROLLING CHANNEL CALIBRATION DIFFERENCE (dB)", + "LOG_AMP_FACTOR_1": "RF DETECTOR LOG AMPLIFIER SCALE FACTOR FOR CONVERTING RECEIVER TEST DATA (V/dBm)", + "LOG_AMP_FACTOR_2": "RF DETECTOR LOG AMPLIFIER BIAS FOR CONVERTING RECEIVER TEST DATA (V)", + "V_TS_CW": "AME VERTICAL TEST SIGNAL POWER (dBm)", + "RNSCALE_0": "RECEIVER NOISE NORMALIZATION (-1.0 deg to -0.5 deg)", + "RNSCALE_1": "RECEIVER NOISE NORMALIZATION (-0.5 deg to 0.0 deg)", + "RNSCALE_2": "RECEIVER NOISE NORMALIZATION (0.0 deg to 0.5 deg)", + "RNSCALE_3": "RECEIVER NOISE NORMALIZATION (0.5 deg to 1.0 deg)", + "RNSCALE_4": "RECEIVER NOISE NORMALIZATION (1.0 deg to 1.5 deg)", + "RNSCALE_5": "RECEIVER NOISE NORMALIZATION (1.5 deg to 2.0 deg)", + "RNSCALE_6": "RECEIVER NOISE NORMALIZATION (2.0 deg to 2.5 deg)", + "RNSCALE_7": "RECEIVER NOISE NORMALIZATION (2.5 deg to 3.0 deg)", + "RNSCALE_8": "RECEIVER NOISE NORMALIZATION (3.0 deg to 3.5 deg)", + "RNSCALE_9": "RECEIVER NOISE NORMALIZATION (3.5 deg to 4.0 deg)", + "RNSCALE_10": "RECEIVER NOISE NORMALIZATION (4.0 deg to 4.5 deg)", + "RNSCALE_11": "RECEIVER NOISE NORMALIZATION (4.5 deg to 5.0 deg)", + "RNSCALE_12": "RECEIVER NOISE NORMALIZATION (> 5.0 deg)", + "ATMOS_0": "TWO WAY ATMOSPHERIC LOSS/KM (-1.0 deg to -0.5 deg) (dB/km)", + "ATMOS_1": "TWO WAY ATMOSPHERIC LOSS/KM (-0.5 deg to 0.0 deg) (dB/km)", + "ATMOS_2": "TWO WAY ATMOSPHERIC LOSS/KM (0.0 deg to 0.5 deg) (dB/km)", + "ATMOS_3": "TWO WAY ATMOSPHERIC LOSS/KM (0.5 deg to 1.0 deg) (dB/km)", + "ATMOS_4": "TWO WAY ATMOSPHERIC LOSS/KM (1.0 deg to 1.5 deg) (dB/km)", + "ATMOS_5": "TWO WAY ATMOSPHERIC LOSS/KM (1.5 deg to 2.0 deg) (dB/km)", + "ATMOS_6": "TWO WAY ATMOSPHERIC LOSS/KM (2.0 deg to 2.5 deg) (dB/km)", + "ATMOS_7": "TWO WAY ATMOSPHERIC LOSS/KM (2.5 deg to 3.0 deg) (dB/km)", + "ATMOS_8": "TWO WAY ATMOSPHERIC LOSS/KM (3.0 deg to 3.5 deg) (dB/km)", + "ATMOS_9": "TWO WAY ATMOSPHERIC LOSS/KM (3.5 deg to 4.0 deg) (dB/km)", + "ATMOS_10": "TWO WAY ATMOSPHERIC LOSS/KM (4.0 deg to 4.5 deg) (dB/km)", + "ATMOS_11": "TWO WAY ATMOSPHERIC LOSS/KM (4.5 deg to 5.0 deg) (dB/km)", + "ATMOS_12": "TWO WAY ATMOSPHERIC LOSS/KM (> 5.0 deg) (dB/km)", + "EL_INDEX_0": "BYPASS MAP GENERATION ELEVATION ANGLE (0) (deg)", + "EL_INDEX_1": "BYPASS MAP GENERATION ELEVATION ANGLE (1) (deg)", + "EL_INDEX_2": "BYPASS MAP GENERATION ELEVATION ANGLE (2) (deg)", + "EL_INDEX_3": "BYPASS MAP GENERATION ELEVATION ANGLE (3) (deg)", + "EL_INDEX_4": "BYPASS MAP GENERATION ELEVATION ANGLE (4) (deg)", + "EL_INDEX_5": "BYPASS MAP GENERATION ELEVATION ANGLE (5) (deg)", + "EL_INDEX_6": "BYPASS MAP GENERATION ELEVATION ANGLE (6) (deg)", + "EL_INDEX_7": "BYPASS MAP GENERATION ELEVATION ANGLE (7) (deg)", + "EL_INDEX_8": "BYPASS MAP GENERATION ELEVATION ANGLE (8) (deg)", + "EL_INDEX_9": "BYPASS MAP GENERATION ELEVATION ANGLE (9) (deg)", + "EL_INDEX_10": "BYPASS MAP GENERATION ELEVATION ANGLE (10) (deg)", + "EL_INDEX_11": "BYPASS MAP GENERATION ELEVATION ANGLE (11) (deg)", + "TFREQ_MHZ": "TRANSMITTER FREQUENCY (MHz)", + "BASE_DATA_TCN": "POINT CLUTTER SUPPRESSION THRESHOLD (TCN) (dB)", + "REFL_DATA_TOVER": "RANGE UNFOLDING OVERLAY THRESHOLD (TOVER) (dB)", + "TAR_H_DBZ0_LP": "HORIZONTAL TARGET SYSTEM CALIBRATION (dBZ0) FOR LONG PULSE (dBZ)", + "TAR_V_DBZ0_LP": "VERTICAL TARGET SYSTEM CALIBRATION (dBZ0) FOR LONG PULSE (dBZ)", + "INIT_PHI_DP": "INITIAL SYSTEM DIFFERENTIAL PHASE (deg)", + "NORM_INIT_PHI_DP": "NORMALIZED INITIAL SYSTEM DIFFERENTIAL PHASE (deg)", + "LX_LP": "MATCHED FILTER LOSS FOR LONG PULSE (dB)", + "LX_SP": "MATCHED FILTER LOSS FOR SHORT PULSE (dB)", + "METEOR_PARAM": "/K/**2 HYDROMETEOR REFRACTIVITY FACTOR", + "BEAMWIDTH": "ANTENNA BEAMWIDTH (deg)", + "ANTENNA_GAIN": "ANTENNA GAIN INCLUDING RADOME (dB)", + "VEL_MAINT_LIMIT": "VELOCITY CHECK DELTA MAINTENANCE LIMIT (m/s)", + "WTH_MAINT_LIMIT": "SPECTRUM WIDTH CHECK DELTA MAINTENANCE LIMIT (m/s)", + "VEL_DEGRAD_LIMIT": "VELOCITY CHECK DELTA DEGRADE LIMIT (m/s)", + "WTH_DEGRAD_LIMIT": "SPECTRUM WIDTH CHECK DELTA DEGRADE LIMIT (m/s)", + "H_NOISETEMP_DGRAD_LIMIT": "HORIZONTAL SYSTEM NOISE TEMP DEGRADE LIMIT (K)", + "H_NOISETEMP_MAINT_LIMIT": "HORIZONTAL SYSTEM NOISE TEMP MAINTENANCE LIMIT (K)", + "V_NOISETEMP_DGRAD_LIMIT": "VERTICAL SYSTEM NOISE TEMP DEGRADE LIMIT (K)", + "V_NOISETEMP_MAINT_LIMIT": "VERTICAL SYSTEM NOISE TEMP MAINTENANCE LIMIT (K)", + "KLY_DEGRADE_LIMIT": "KLYS TRON OUTPUT TARGET CONSISTENCY DEGRADE LIMIT (dB)", + "TS_COHO": "COHO POWER AT A1J4 (dBm)", + "H_TS_CW": "AME HORIZONTAL TEST SIGNAL POWER (dBm)", + "TS_RF_SP": "RF DRIVE TEST SIGNAL SHORT PULSE AT 3A5J4 (dBm)", + "TS_RF_LP": "RF DRIVE TEST SIGNAL LONG PULSE AT 3A5J4 (dBm)", + "TS_STALO": "STALO POWER AT A1J2 (dBm)", + "AME_H_NOISE_ENR": "AME NOISE SOURCE HORIZONTAL EXCESS NOISE RATIO (dB)", + "XMTR_PEAK_PWR_HIGH_LIMIT": "MAXIMUM TRANSMITTER PEAK POWER ALARM LEVEL (kW)", + "XMTR_PEAK_PWR_LOW_LIMIT": "MINIMUM TRANSMITTER PEAK POWER ALARM LEVEL (kW)", + "H_DBZ0_DELTA_LIMIT": "DIFFERENCE BETWEEN COMPUTED AND TARGET HORIZONTAL DBZ0 LIMIT (dB)", + "THRESHOLD1": "BYPASS MAP GENERATOR NOISE THRESHOLD (dB)", + "THRESHOLD2": "BYPASS MAP GENERATOR REJECTION RATIO THRESHOLD (dB)", + "CLUT_SUPP_DGRAD_LIM": "CLUTTER SUPPRESSION DEGRADE LIMIT (dB)", + "CLUT_SUPP_MAINT_LIM": "CLUTTER SUPPRESSION MAINTENANCE LIMIT (dB)", + "RANGE0_VALUE": "TRUE RANGE AT START OF FIRST RANGE BIN (km)", + "XMTR_PWR_MTR_SCALE": "SCALE FACTOR USED TO CONVERT TRANSMITTER POWER BYTE DATA TO WATTS (W (4))", + "V_DBZ0_DELTA_LIMIT": "DIFFERENCE BETWEEN COMPUTED AND TARGET VERTICAL DBZ0 LIMIT (dB)", + "TAR_H_DBZ0_SP": "HORIZONTAL TARGET SYSTEM CALIBRATION (dBZ0) FOR SHORT PULSE (dBZ)", + "TAR_V_DBZ0_SP": "VERTICAL TARGET SYSTEM CALIBRATION (DBZ0) FOR SHORT PULSE (dBZ)", + "DELTAPRF": "SITE PRF SET (A=1, B=2, C=3, D=4, E=5)", + "TAU_SP": "PULSE WIDTH OF TRANSMITTER OUTPUT IN SHORT PULSE (nsec)", + "TAU_LP": "PULSE WIDTH OF TRANSMITTER OUTPUT IN LONG PULSE (nsec)", + "NC_DEAD_VALUE": "NUMBER OF 1/4 KM BINS OF CORRUPTED DATA AT END OF SWEEP", + "TAU_RF_SP": "RF DRIVE PULSE WIDTH IN SHORT PULSE (nsec)", + "TAU_RF_LP": "RF DRIVE PULSE WIDTH IN LONG PULSE MODE (nsec)", + "SEG1LIM": "CLUTTER MAP BOUNDARY ELEVATION BETWEEN SEGMENTS 1 & 2 (deg)", + "SLATSEC": "SITE LATITUDE - SECONDS (s)", + "SLONSEC": "SITE LONGITUDE - SECONDS (s)", + "SLATDEG": "SITE LATITUDE - DEGREES (deg)", + "SLATMIN": "SITE LATITUDE - MINUTES (min)", + "SLONDEG": "SITE LONGITUDE - DEGREES (deg)", + "SLONMIN": "SITE LONGITUDE - MINUTES (min)", + "SLATDIR": "SITE LATITUDE - DIRECTION", + "SLONDIR": "SITE LONGITUDE - DIRECTION", + "VCPAT11": "VOLUME COVERAGE PATTERN NUMBER 11 DEFINITION", + "VCPAT21": "VOLUME COVERAGE PATTERN NUMBER 21 DEFINITION", + "VCPAT31": "VOLUME COVERAGE PATTERN NUMBER 31 DEFINITION", + "VCPAT32": "VOLUME COVERAGE PATTERN NUMBER 32 DEFINITION", + "VCPAT300": "VOLUME COVERAGE PATTERN NUMBER 300 DEFINITION", + "VCPAT301": "VOLUME COVERAGE PATTERN NUMBER 301 DEFINITION", + "AZ_CORRECTION_FACTOR": "AZIMUTH BORESIGHT CORRECTION FACTOR (deg)", + "EL_CORRECTION_FACTOR": "ELEVATION BORESIGHT CORRECTION FACTOR (deg)", + "SITE_NAME": "SITE NAME DESIGNATION", + "ANT_MANUAL_SETUP_IELMIN": "MINIMUM ELEVATION ANGLE (deg)", + "ANT_MANUAL_SETUP_IELMAX": "MAXIMUM ELEVATION ANGLE (deg)", + "ANT_MANUAL_SETUP_FAZVELMAX": "MAXIMUM AZIMUTH VELOCITY (deg/s)", + "ANT_MANUAL_SETUP_FELVELMAX": "MAXIMUM ELEVATION VELOCITY (deg/s)", + "ANT_MANUAL_SETUP_IGND_HGT": "SITE GROUND HEIGHT (ABOVE SEA LEVEL) (m)", + "ANT_MANUAL_SETUP_IRAD_HGT": "SITE RADAR HEIGHT (ABOVE GROUND) (m)", + "RVP8NV_IWAVEGUIDE_LENGTH": "WAVEGUIDE LENGTH (m)", + "VEL_DATA_TOVER": "VELOCITY UNFOLDING OVERLAY THRESHOLD (dB)", + "WIDTH_DATA_TOVER": "WIDTH UNFOLDING OVERLAY THRESHOLD (dB)", + "DOPPLER_RANGE_START": "START RANGE FOR FIRST DOPPLER RADIAL (km)", + "MAX_EL_INDEX": "THE MAXIMUM INDEX FOR THE EL_INDEX PARAMETERS", + "SEG2LIM": "CLUTTER MAP BOUNDARY ELEVATION BETWEEN SEGMENTS 2 & 3. (deg)", + "SEG3LIM": "CLUTTER MAP BOUNDARY ELEVATION BETWEEN SEGMENTS 3 & 4. (deg)", + "SEG4LIM": "CLUTTER MAP BOUNDARY ELEVATION BETWEEN SEGMENTS 4 & 5. (deg)", + "NBR_EL_SEGMENTS": "NUMBER OF ELEVATION SEGMENTS IN ORDA CLUTTER MAP.", + "H_NOISE_LONG": "HORIZONTAL RECEIVER NOISE LONG PULSE (dBm)", + "ANT_NOISE_TEMP": "ANTENNA NOISE TEMPERATURE (K)", + "H_NOISE_SHORT": "HORIZONTAL RECEIVER NOISE SHORT PULSE (dBm)", + "H_NOISE_TOLERANCE": "HORIZONTAL RECEIVER NOISE TOLERANCE (dB)", + "MIN_H_DYN_RANGE": "MINIMUM HORIZONTAL DYNAMIC RANGE (dB)", + "GEN_INSTALLED": "AUXILIARY GENERATOR INSTALLED (FAA ONLY)", + "GEN_EXERCISE": "AUXILIARY GENERATOR AUTOMATIC EXERCISE ENABLED (FAA ONLY)", + "V_NOISE_TOLERANCE": "VERTICAL RECEIVER NOISE TOLERANCE (dB)", + "MIN_V_DYN_RANGE": "MINIMUM VERTICAL DYNAMIC RANGE (dB)", + "ZDR_BIAS_DGRAD_LIM": "SYSTEM DIFFERENTIAL REFLECTIVITY BIAS DEGRADE LIMIT (dB)", + "V_NOISE_LONG": "VERTICAL RECEIVER NOISE LONG PULSE (dBm)", + "V_NOISE_SHORT": "VERTICAL RECEIVER NOISE SHORT PULSE (dBm)", + "ZDR_DATA_TOVER": "ZDR UNFOLDING OVERLAY THRESHOLD (dB)", + "PHI_DATA_TOVER": "PHI UNFOLDING OVERLAY THRESHOLD (dB)", + "RHO_DATA_TOVER": "RHO UNFOLDING OVERLAY THRESHOLD (dB)", + "STALO_POWER_DGRAD_LIMIT": "STALO POWER DEGRADE LIMIT (V)", + "STALO_POWER_MAINT_LIMIT": "STALO POWER MAINTENANCE LIMIT (V)", + "MIN_H_PWR_SENSE": "MINIMUM HORIZONTAL POWER SENSE (dBm)", + "MIN_V_PWR_SENSE": "MINIMUM VERTICAL POWER SENSE (dBm)", + "H_PWR_SENSE_OFFSET": "HORIZONTAL POWER SENSE CALIBRATION OFFSET (dB)", + "V_PWR_SENSE_OFFSET": "VERTICAL POWER SENSE CALIBRATION OFFSET (dB)", + "PS_GAIN_REF": "RF PALLET BIAS ERROR (dB)", + "RF_PALLET_BROAD_LOSS": "RF PALLET BROADBAND LOSS (dB)", + "ZDR_CHECK_THRESHOLD": "ZDR CHECK FAILURE THRESHOLD (dB)", + "PHI_CHECK_THRESHOLD": "PHI CHECK FAILURE THRESHOLD (deg)", + "RHO_CHECK_THRESHOLD": "RHO CHECK FAILURE THRESHOLD (ratio)", + "AME_PS_TOLERANCE": "AME POWER SUPPLY TOLERANCE (%)", + "AME_MAX_TEMP": "MAXIMUM AME INTERNAL ALARM TEMPERATURE (deg C)", + "AME_MIN_TEMP": "MINIMUM AME INTERNAL ALARM TEMPERATURE (deg C)", + "RCVR_MOD_MAX_TEMP": "MAXIMUM AME RECEIVER MODULE ALARM TEMPERATURE (deg C)", + "RCVR_MOD_MIN_TEMP": "MINIMUM AME RECEIVER MODULE ALARM TEMPERATURE (deg C)", + "BITE_MOD_MAX_TEMP": "MAXIMUM AME BITE MODULE ALARM TEMPERATURE (deg C)", + "BITE_MOD_MIN_TEMP": "MINIMUM AME BITE MODULE ALARM TEMPERATURE (deg C)", + "DEFAULT_POLARIZATION": "DEFAULT (H+V) MICROWAVE ASSEMBLY PHASE SHIFTER POSITION", + "TR_LIMIT_DGRAD_LIMIT": "TR LIMITER DEGRADE LIMIT (V)", + "TR_LIMIT_FAIL_LIMIT": "TR LIMITER FAILURE LIMIT (V)", + "AME_CURRENT_TOLERANCE": "AME PELTIER CURRENT TOLERANCE (%)", + "H_ONLY_POLARIZATION": "HORIZONTAL (H ONLY) MICROWAVE ASSEMBLY PHASE SHIFTER POSITION", + "V_ONLY_POLARIZATION": "VERTICAL (V ONLY) MICROWAVE ASSEMBLY PHASE SHIFTER POSITION", + "REFLECTOR_BIAS": "ANTENNA REFLECTOR BIAS (dB)", + "A_MIN_SHELTER_TEMP_WARN": "LOW EQUIPMENT SHELTER TEMPERATURE WARNING LIMIT (deg C)", +} -fields = [("ADAP_FILE_NAME", "12s"), - ("ADAP_FORMAT", "4s"), - ("ADAP_REVISION", "4s"), - ("ADAP_DATE", "12s"), - ("ADAP_TIME", "12s"), - ("K1", "f"), - ("AZ_LAT", "f"), - ("K3", "f"), - ("EL_LAT", "f"), - ("PARKAZ", "f"), - ("PARKEL", "f"), - ("A_FUEL_CONV_0", "f"), - ("A_FUEL_CONV_1", "f"), - ("A_FUEL_CONV_2", "f"), - ("A_FUEL_CONV_3", "f"), - ("A_FUEL_CONV_4", "f"), - ("A_FUEL_CONV_5", "f"), - ("A_FUEL_CONV_6", "f"), - ("A_FUEL_CONV_7", "f"), - ("A_FUEL_CONV_8", "f"), - ("A_FUEL_CONV_9", "f"), - ("A_FUEL_CONV_10", "f"), - ("A_MIN_SHELTER_TEMP", "f"), - ("A_MAX_SHELTER_TEMP", "f"), - ("A_MIN_SHELTER_AC_TEMP_DIFF", "f"), - ("A_MAX_XMTR_AIR_TEMP", "f"), - ("A_MAX_RAD_TEMP", "f"), - ("A_MAX_RAD_TEMP_RISE", "f"), - ("PED_28V_REG_LIM", "f"), - ("PED_5V_REG_LIM", "f"), - ("PED_15V_REG_LIM", "f"), - ("A_MIN_GEN_ROOM_TEMP", "f"), - ("A_MAX_GEN_ROOM_TEMP", "f"), - ("DAU_5V_REG_LIM", "f"), - ("DAU_15V_REG_LIM", "f"), - ("DAU_28V_REG_LIM", "f"), - ("EN_5V_REG_LIM", "f"), - ("EN_5V_NOM_VOLTS", "f"), - ("RPG_CO_LOCATED", "4s"), - ("SPEC_FILTER_INSTALLED", "4s"), - ("TPS_INSTALLED", "4s"), - ("RMS_INSTALLED", "4s"), - ("A_HVDL_TST_INT", "L"), - ("A_RPG_LT_INT", "L"), - ("A_MIN_STAB_UTIL_PWR_TIME", "L"), - ("A_GEN_AUTO_EXER_INTERVAL", "L"), - ("A_UTIL_PWR_SW_REQ_INTERVAL", "L"), - ("A_LOW_FUEL_LEVEL", "f"), - ("CONFIG_CHAN_NUMBER", "L"), - ("A_RPG_LINK_TYPE", "L"), - ("REDUNDANT_CHAN_CONFIG", "L"), - ("ATTEN_TABLE_0", "f"), - ("ATTEN_TABLE_1", "f"), - ("ATTEN_TABLE_2", "f"), - ("ATTEN_TABLE_3", "f"), - ("ATTEN_TABLE_4", "f"), - ("ATTEN_TABLE_5", "f"), - ("ATTEN_TABLE_6", "f"), - ("ATTEN_TABLE_7", "f"), - ("ATTEN_TABLE_8", "f"), - ("ATTEN_TABLE_9", "f"), - ("ATTEN_TABLE_10", "f"), - ("ATTEN_TABLE_11", "f"), - ("ATTEN_TABLE_12", "f"), - ("ATTEN_TABLE_13", "f"), - ("ATTEN_TABLE_14", "f"), - ("ATTEN_TABLE_15", "f"), - ("ATTEN_TABLE_16", "f"), - ("ATTEN_TABLE_17", "f"), - ("ATTEN_TABLE_18", "f"), - ("ATTEN_TABLE_19", "f"), - ("ATTEN_TABLE_20", "f"), - ("ATTEN_TABLE_21", "f"), - ("ATTEN_TABLE_22", "f"), - ("ATTEN_TABLE_23", "f"), - ("ATTEN_TABLE_24", "f"), - ("ATTEN_TABLE_25", "f"), - ("ATTEN_TABLE_26", "f"), - ("ATTEN_TABLE_27", "f"), - ("ATTEN_TABLE_28", "f"), - ("ATTEN_TABLE_29", "f"), - ("ATTEN_TABLE_30", "f"), - ("ATTEN_TABLE_31", "f"), - ("ATTEN_TABLE_32", "f"), - ("ATTEN_TABLE_33", "f"), - ("ATTEN_TABLE_34", "f"), - ("ATTEN_TABLE_35", "f"), - ("ATTEN_TABLE_36", "f"), - ("ATTEN_TABLE_37", "f"), - ("ATTEN_TABLE_38", "f"), - ("ATTEN_TABLE_39", "f"), - ("ATTEN_TABLE_40", "f"), - ("ATTEN_TABLE_41", "f"), - ("ATTEN_TABLE_42", "f"), - ("ATTEN_TABLE_43", "f"), - ("ATTEN_TABLE_44", "f"), - ("ATTEN_TABLE_45", "f"), - ("ATTEN_TABLE_46", "f"), - ("ATTEN_TABLE_47", "f"), - ("ATTEN_TABLE_48", "f"), - ("ATTEN_TABLE_49", "f"), - ("ATTEN_TABLE_50", "f"), - ("ATTEN_TABLE_51", "f"), - ("ATTEN_TABLE_52", "f"), - ("ATTEN_TABLE_53", "f"), - ("ATTEN_TABLE_54", "f"), - ("ATTEN_TABLE_55", "f"), - ("ATTEN_TABLE_56", "f"), - ("ATTEN_TABLE_57", "f"), - ("ATTEN_TABLE_58", "f"), - ("ATTEN_TABLE_59", "f"), - ("ATTEN_TABLE_60", "f"), - ("ATTEN_TABLE_61", "f"), - ("ATTEN_TABLE_62", "f"), - ("ATTEN_TABLE_63", "f"), - ("ATTEN_TABLE_64", "f"), - ("ATTEN_TABLE_65", "f"), - ("ATTEN_TABLE_66", "f"), - ("ATTEN_TABLE_67", "f"), - ("ATTEN_TABLE_68", "f"), - ("ATTEN_TABLE_69", "f"), - ("ATTEN_TABLE_70", "f"), - ("ATTEN_TABLE_71", "f"), - ("ATTEN_TABLE_72", "f"), - ("ATTEN_TABLE_73", "f"), - ("ATTEN_TABLE_74", "f"), - ("ATTEN_TABLE_75", "f"), - ("ATTEN_TABLE_76", "f"), - ("ATTEN_TABLE_77", "f"), - ("ATTEN_TABLE_78", "f"), - ("ATTEN_TABLE_79", "f"), - ("ATTEN_TABLE_80", "f"), - ("ATTEN_TABLE_81", "f"), - ("ATTEN_TABLE_82", "f"), - ("ATTEN_TABLE_83", "f"), - ("ATTEN_TABLE_84", "f"), - ("ATTEN_TABLE_85", "f"), - ("ATTEN_TABLE_86", "f"), - ("ATTEN_TABLE_87", "f"), - ("ATTEN_TABLE_88", "f"), - ("ATTEN_TABLE_89", "f"), - ("ATTEN_TABLE_90", "f"), - ("ATTEN_TABLE_91", "f"), - ("ATTEN_TABLE_92", "f"), - ("ATTEN_TABLE_93", "f"), - ("ATTEN_TABLE_94", "f"), - ("ATTEN_TABLE_95", "f"), - ("ATTEN_TABLE_96", "f"), - ("ATTEN_TABLE_97", "f"), - ("ATTEN_TABLE_98", "f"), - ("ATTEN_TABLE_99", "f"), - ("ATTEN_TABLE_100", "f"), - ("ATTEN_TABLE_101", "f"), - ("ATTEN_TABLE_102", "f"), - ("ATTEN_TABLE_103", "f"), - (None, "24x"), - ("PATH_LOSSES_7", "f"), - (None, "12x"), - ("PATH_LOSSES_11", "f"), - (None, "4x"), - ("PATH_LOSSES_13", "f"), - ("PATH_LOSSES_14", "f"), - (None, "16x"), - ("PATH_LOSSES_19", "f"), - (None, "32x"), - ("PATH_LOSSES_28", "f"), - (None, "4x"), - ("PATH_LOSSES_30", "f"), - ("PATH_LOSSES_31", "f"), - ("PATH_LOSSES_32", "f"), - ("PATH_LOSSES_33", "f"), - ("PATH_LOSSES_34", "f"), - ("PATH_LOSSES_35", "f"), - ("PATH_LOSSES_36", "f"), - ("PATH_LOSSES_37", "f"), - ("PATH_LOSSES_38", "f"), - ("PATH_LOSSES_39", "f"), - ("PATH_LOSSES_40", "f"), - ("PATH_LOSSES_41", "f"), - ("PATH_LOSSES_42", "f"), - ("PATH_LOSSES_43", "f"), - ("PATH_LOSSES_44", "f"), - ("PATH_LOSSES_45", "f"), - ("PATH_LOSSES_46", "f"), - ("PATH_LOSSES_47", "f"), - ("H_COUPLER_CW_LOSS", "f"), - ("V_COUPLER_XMT_LOSS", "f"), - ("PATH_LOSSES_50", "f"), - (None, "4x"), - ("PATH_LOSSES_52", "f"), - ("V_COUPLER_CW_LOSS", "f"), - ("PATH_LOSSES_54", "f"), - (None, "4x"), - (None, "4x"), - (None, "4x"), - ("PATH_LOSSES_58", "f"), - ("PATH_LOSSES_59", "f"), - ("PATH_LOSSES_60", "f"), - ("PATH_LOSSES_61", "f"), - (None, "4x"), - ("PATH_LOSSES_63", "f"), - ("PATH_LOSSES_64", "f"), - ("PATH_LOSSES_65", "f"), - ("PATH_LOSSES_66", "f"), - ("PATH_LOSSES_67", "f"), - ("PATH_LOSSES_68", "f"), - ("PATH_LOSSES_69", "f"), - ("CHAN_CAL_DIFF", "f"), - (None, "4x"), - ("LOG_AMP_FACTOR_1", "f"), - ("LOG_AMP_FACTOR_2", "f"), - ("V_TS_CW", "f"), - ("RNSCALE_0", "f"), - ("RNSCALE_1", "f"), - ("RNSCALE_2", "f"), - ("RNSCALE_3", "f"), - ("RNSCALE_4", "f"), - ("RNSCALE_5", "f"), - ("RNSCALE_6", "f"), - ("RNSCALE_7", "f"), - ("RNSCALE_8", "f"), - ("RNSCALE_9", "f"), - ("RNSCALE_10", "f"), - ("RNSCALE_11", "f"), - ("RNSCALE_12", "f"), - ("ATMOS_0", "f"), - ("ATMOS_1", "f"), - ("ATMOS_2", "f"), - ("ATMOS_3", "f"), - ("ATMOS_4", "f"), - ("ATMOS_5", "f"), - ("ATMOS_6", "f"), - ("ATMOS_7", "f"), - ("ATMOS_8", "f"), - ("ATMOS_9", "f"), - ("ATMOS_10", "f"), - ("ATMOS_11", "f"), - ("ATMOS_12", "f"), - ("EL_INDEX_0", "f"), - ("EL_INDEX_1", "f"), - ("EL_INDEX_2", "f"), - ("EL_INDEX_3", "f"), - ("EL_INDEX_4", "f"), - ("EL_INDEX_5", "f"), - ("EL_INDEX_6", "f"), - ("EL_INDEX_7", "f"), - ("EL_INDEX_8", "f"), - ("EL_INDEX_9", "f"), - ("EL_INDEX_10", "f"), - ("EL_INDEX_11", "f"), - ("TFREQ_MHZ", "L"), - ("BASE_DATA_TCN", "f"), - ("REFL_DATA_TOVER", "f"), - ("TAR_H_DBZ0_LP", "f"), - ("TAR_V_DBZ0_LP", "f"), - ("INIT_PHI_DP", "L"), - ("NORM_INIT_PHI_DP", "L"), - ("LX_LP", "f"), - ("LX_SP", "f"), - ("METEOR_PARAM", "f"), - ("BEAMWIDTH", "f"), - ("ANTENNA_GAIN", "f"), - (None, "4x"), - ("VEL_MAINT_LIMIT", "f"), - ("WTH_MAINT_LIMIT", "f"), - ("VEL_DEGRAD_LIMIT", "f"), - ("WTH_DEGRAD_LIMIT", "f"), - ("H_NOISETEMP_DGRAD_LIMIT", "f"), - ("H_NOISETEMP_MAINT_LIMIT", "f"), - ("V_NOISETEMP_DGRAD_LIMIT", "f"), - ("V_NOISETEMP_MAINT_LIMIT", "f"), - ("KLY_DEGRADE_LIMIT", "f"), - ("TS_COHO", "f"), - ("H_TS_CW", "f"), - ("TS_RF_SP", "f"), - ("TS_RF_LP", "f"), - ("TS_STALO", "f"), - ("AME_H_NOISE_ENR", "f"), - ("XMTR_PEAK_PWR_HIGH_LIMIT", "f"), - ("XMTR_PEAK_PWR_LOW_LIMIT", "f"), - ("H_DBZ0_DELTA_LIMIT", "f"), - ("THRESHOLD1", "f"), - ("THRESHOLD2", "f"), - ("CLUT_SUPP_DGRAD_LIM", "f"), - ("CLUT_SUPP_MAINT_LIM", "f"), - ("RANGE0_VALUE", "f"), - ("XMTR_PWR_MTR_SCALE", "f"), - ("V_DBZ0_DELTA_LIMIT", "f"), - ("TAR_H_DBZ0_SP", "f"), - ("TAR_V_DBZ0_SP", "f"), - ("DELTAPRF", "L"), - (None, "4x"), - (None, "4x"), - ("TAU_SP", "L"), - ("TAU_LP", "L"), - ("NC_DEAD_VALUE", "L"), - ("TAU_RF_SP", "L"), - ("TAU_RF_LP", "L"), - ("SEG1LIM", "f"), - ("SLATSEC", "f"), - ("SLONSEC", "f"), - (None, "4x"), - ("SLATDEG", "L"), - ("SLATMIN", "L"), - ("SLONDEG", "L"), - ("SLONMIN", "L"), - ("SLATDIR", "4s"), - ("SLONDIR", "4s"), - (None, "4x"), - ("VCPAT11", "1172s"), - ("VCPAT21", "1172s"), - ("VCPAT31", "1172s"), - ("VCPAT32", "1172s"), - ("VCPAT300", "1172s"), - ("VCPAT301", "1172s"), - ("AZ_CORRECTION_FACTOR", "f"), - ("EL_CORRECTION_FACTOR", "f"), - ("SITE_NAME", "4s"), - ("ANT_MANUAL_SETUP_IELMIN", "l"), - ("ANT_MANUAL_SETUP_IELMAX", "L"), - ("ANT_MANUAL_SETUP_FAZVELMAX", "L"), - ("ANT_MANUAL_SETUP_FELVELMAX", "L"), - ("ANT_MANUAL_SETUP_IGND_HGT", "L"), - ("ANT_MANUAL_SETUP_IRAD_HGT", "L"), - (None, "300x"), - ("RVP8NV_IWAVEGUIDE_LENGTH", "L"), - (None, "44x"), - ("VEL_DATA_TOVER", "f"), - ("WIDTH_DATA_TOVER", "f"), - (None, "12x"), - ("DOPPLER_RANGE_START", "f"), - ("MAX_EL_INDEX", "L"), - ("SEG2LIM", "f"), - ("SEG3LIM", "f"), - ("SEG4LIM", "f"), - ("NBR_EL_SEGMENTS", "L"), - ("H_NOISE_LONG", "f"), - ("ANT_NOISE_TEMP", "f"), - ("H_NOISE_SHORT", "f"), - ("H_NOISE_TOLERANCE", "f"), - ("MIN_H_DYN_RANGE", "f"), - ("GEN_INSTALLED", "4s"), - ("GEN_EXERCISE", "4s"), - ("V_NOISE_TOLERANCE", "f"), - ("MIN_V_DYN_RANGE", "f"), - ("ZDR_BIAS_DGRAD_LIM", "f"), - (None, "4x"), - (None, "4x"), - (None, "8x"), - ("V_NOISE_LONG", "f"), - ("V_NOISE_SHORT", "f"), - ("ZDR_DATA_TOVER", "f"), - ("PHI_DATA_TOVER", "f"), - ("RHO_DATA_TOVER", "f"), - ("STALO_POWER_DGRAD_LIMIT", "f"), - ("STALO_POWER_MAINT_LIMIT", "f"), - ("MIN_H_PWR_SENSE", "f"), - ("MIN_V_PWR_SENSE", "f"), - ("H_PWR_SENSE_OFFSET", "f"), - ("V_PWR_SENSE_OFFSET", "f"), - ("PS_GAIN_REF", "f"), - ("RF_PALLET_BROAD_LOSS", "f"), - ("ZDR_CHECK_THRESHOLD", "f"), - ("PHI_CHECK_THRESHOLD", "f"), - ("RHO_CHECK_THRESHOLD", "f"), - (None, "52x"), - ("AME_PS_TOLERANCE", "f"), - ("AME_MAX_TEMP", "f"), - ("AME_MIN_TEMP", "f"), - ("RCVR_MOD_MAX_TEMP", "f"), - ("RCVR_MOD_MIN_TEMP", "f"), - ("BITE_MOD_MAX_TEMP", "f"), - ("BITE_MOD_MIN_TEMP", "f"), - ("DEFAULT_POLARIZATION", "L"), - ("TR_LIMIT_DGRAD_LIMIT", "f"), - ("TR_LIMIT_FAIL_LIMIT", "f"), - (None, "4x"), - (None, "4x"), - ("AME_CURRENT_TOLERANCE", "f"), - ("H_ONLY_POLARIZATION", "L"), - ("V_ONLY_POLARIZATION", "L"), - (None, "8x"), - ("REFLECTOR_BIAS", "f"), - ("A_MIN_SHELTER_TEMP_WARN", "f"), - (None, "432x")] +fields = [ + ("ADAP_FILE_NAME", "12s"), + ("ADAP_FORMAT", "4s"), + ("ADAP_REVISION", "4s"), + ("ADAP_DATE", "12s"), + ("ADAP_TIME", "12s"), + ("K1", "f"), + ("AZ_LAT", "f"), + ("K3", "f"), + ("EL_LAT", "f"), + ("PARKAZ", "f"), + ("PARKEL", "f"), + ("A_FUEL_CONV_0", "f"), + ("A_FUEL_CONV_1", "f"), + ("A_FUEL_CONV_2", "f"), + ("A_FUEL_CONV_3", "f"), + ("A_FUEL_CONV_4", "f"), + ("A_FUEL_CONV_5", "f"), + ("A_FUEL_CONV_6", "f"), + ("A_FUEL_CONV_7", "f"), + ("A_FUEL_CONV_8", "f"), + ("A_FUEL_CONV_9", "f"), + ("A_FUEL_CONV_10", "f"), + ("A_MIN_SHELTER_TEMP", "f"), + ("A_MAX_SHELTER_TEMP", "f"), + ("A_MIN_SHELTER_AC_TEMP_DIFF", "f"), + ("A_MAX_XMTR_AIR_TEMP", "f"), + ("A_MAX_RAD_TEMP", "f"), + ("A_MAX_RAD_TEMP_RISE", "f"), + ("PED_28V_REG_LIM", "f"), + ("PED_5V_REG_LIM", "f"), + ("PED_15V_REG_LIM", "f"), + ("A_MIN_GEN_ROOM_TEMP", "f"), + ("A_MAX_GEN_ROOM_TEMP", "f"), + ("DAU_5V_REG_LIM", "f"), + ("DAU_15V_REG_LIM", "f"), + ("DAU_28V_REG_LIM", "f"), + ("EN_5V_REG_LIM", "f"), + ("EN_5V_NOM_VOLTS", "f"), + ("RPG_CO_LOCATED", "4s"), + ("SPEC_FILTER_INSTALLED", "4s"), + ("TPS_INSTALLED", "4s"), + ("RMS_INSTALLED", "4s"), + ("A_HVDL_TST_INT", "L"), + ("A_RPG_LT_INT", "L"), + ("A_MIN_STAB_UTIL_PWR_TIME", "L"), + ("A_GEN_AUTO_EXER_INTERVAL", "L"), + ("A_UTIL_PWR_SW_REQ_INTERVAL", "L"), + ("A_LOW_FUEL_LEVEL", "f"), + ("CONFIG_CHAN_NUMBER", "L"), + ("A_RPG_LINK_TYPE", "L"), + ("REDUNDANT_CHAN_CONFIG", "L"), + ("ATTEN_TABLE_0", "f"), + ("ATTEN_TABLE_1", "f"), + ("ATTEN_TABLE_2", "f"), + ("ATTEN_TABLE_3", "f"), + ("ATTEN_TABLE_4", "f"), + ("ATTEN_TABLE_5", "f"), + ("ATTEN_TABLE_6", "f"), + ("ATTEN_TABLE_7", "f"), + ("ATTEN_TABLE_8", "f"), + ("ATTEN_TABLE_9", "f"), + ("ATTEN_TABLE_10", "f"), + ("ATTEN_TABLE_11", "f"), + ("ATTEN_TABLE_12", "f"), + ("ATTEN_TABLE_13", "f"), + ("ATTEN_TABLE_14", "f"), + ("ATTEN_TABLE_15", "f"), + ("ATTEN_TABLE_16", "f"), + ("ATTEN_TABLE_17", "f"), + ("ATTEN_TABLE_18", "f"), + ("ATTEN_TABLE_19", "f"), + ("ATTEN_TABLE_20", "f"), + ("ATTEN_TABLE_21", "f"), + ("ATTEN_TABLE_22", "f"), + ("ATTEN_TABLE_23", "f"), + ("ATTEN_TABLE_24", "f"), + ("ATTEN_TABLE_25", "f"), + ("ATTEN_TABLE_26", "f"), + ("ATTEN_TABLE_27", "f"), + ("ATTEN_TABLE_28", "f"), + ("ATTEN_TABLE_29", "f"), + ("ATTEN_TABLE_30", "f"), + ("ATTEN_TABLE_31", "f"), + ("ATTEN_TABLE_32", "f"), + ("ATTEN_TABLE_33", "f"), + ("ATTEN_TABLE_34", "f"), + ("ATTEN_TABLE_35", "f"), + ("ATTEN_TABLE_36", "f"), + ("ATTEN_TABLE_37", "f"), + ("ATTEN_TABLE_38", "f"), + ("ATTEN_TABLE_39", "f"), + ("ATTEN_TABLE_40", "f"), + ("ATTEN_TABLE_41", "f"), + ("ATTEN_TABLE_42", "f"), + ("ATTEN_TABLE_43", "f"), + ("ATTEN_TABLE_44", "f"), + ("ATTEN_TABLE_45", "f"), + ("ATTEN_TABLE_46", "f"), + ("ATTEN_TABLE_47", "f"), + ("ATTEN_TABLE_48", "f"), + ("ATTEN_TABLE_49", "f"), + ("ATTEN_TABLE_50", "f"), + ("ATTEN_TABLE_51", "f"), + ("ATTEN_TABLE_52", "f"), + ("ATTEN_TABLE_53", "f"), + ("ATTEN_TABLE_54", "f"), + ("ATTEN_TABLE_55", "f"), + ("ATTEN_TABLE_56", "f"), + ("ATTEN_TABLE_57", "f"), + ("ATTEN_TABLE_58", "f"), + ("ATTEN_TABLE_59", "f"), + ("ATTEN_TABLE_60", "f"), + ("ATTEN_TABLE_61", "f"), + ("ATTEN_TABLE_62", "f"), + ("ATTEN_TABLE_63", "f"), + ("ATTEN_TABLE_64", "f"), + ("ATTEN_TABLE_65", "f"), + ("ATTEN_TABLE_66", "f"), + ("ATTEN_TABLE_67", "f"), + ("ATTEN_TABLE_68", "f"), + ("ATTEN_TABLE_69", "f"), + ("ATTEN_TABLE_70", "f"), + ("ATTEN_TABLE_71", "f"), + ("ATTEN_TABLE_72", "f"), + ("ATTEN_TABLE_73", "f"), + ("ATTEN_TABLE_74", "f"), + ("ATTEN_TABLE_75", "f"), + ("ATTEN_TABLE_76", "f"), + ("ATTEN_TABLE_77", "f"), + ("ATTEN_TABLE_78", "f"), + ("ATTEN_TABLE_79", "f"), + ("ATTEN_TABLE_80", "f"), + ("ATTEN_TABLE_81", "f"), + ("ATTEN_TABLE_82", "f"), + ("ATTEN_TABLE_83", "f"), + ("ATTEN_TABLE_84", "f"), + ("ATTEN_TABLE_85", "f"), + ("ATTEN_TABLE_86", "f"), + ("ATTEN_TABLE_87", "f"), + ("ATTEN_TABLE_88", "f"), + ("ATTEN_TABLE_89", "f"), + ("ATTEN_TABLE_90", "f"), + ("ATTEN_TABLE_91", "f"), + ("ATTEN_TABLE_92", "f"), + ("ATTEN_TABLE_93", "f"), + ("ATTEN_TABLE_94", "f"), + ("ATTEN_TABLE_95", "f"), + ("ATTEN_TABLE_96", "f"), + ("ATTEN_TABLE_97", "f"), + ("ATTEN_TABLE_98", "f"), + ("ATTEN_TABLE_99", "f"), + ("ATTEN_TABLE_100", "f"), + ("ATTEN_TABLE_101", "f"), + ("ATTEN_TABLE_102", "f"), + ("ATTEN_TABLE_103", "f"), + (None, "24x"), + ("PATH_LOSSES_7", "f"), + (None, "12x"), + ("PATH_LOSSES_11", "f"), + (None, "4x"), + ("PATH_LOSSES_13", "f"), + ("PATH_LOSSES_14", "f"), + (None, "16x"), + ("PATH_LOSSES_19", "f"), + (None, "32x"), + ("PATH_LOSSES_28", "f"), + (None, "4x"), + ("PATH_LOSSES_30", "f"), + ("PATH_LOSSES_31", "f"), + ("PATH_LOSSES_32", "f"), + ("PATH_LOSSES_33", "f"), + ("PATH_LOSSES_34", "f"), + ("PATH_LOSSES_35", "f"), + ("PATH_LOSSES_36", "f"), + ("PATH_LOSSES_37", "f"), + ("PATH_LOSSES_38", "f"), + ("PATH_LOSSES_39", "f"), + ("PATH_LOSSES_40", "f"), + ("PATH_LOSSES_41", "f"), + ("PATH_LOSSES_42", "f"), + ("PATH_LOSSES_43", "f"), + ("PATH_LOSSES_44", "f"), + ("PATH_LOSSES_45", "f"), + ("PATH_LOSSES_46", "f"), + ("PATH_LOSSES_47", "f"), + ("H_COUPLER_CW_LOSS", "f"), + ("V_COUPLER_XMT_LOSS", "f"), + ("PATH_LOSSES_50", "f"), + (None, "4x"), + ("PATH_LOSSES_52", "f"), + ("V_COUPLER_CW_LOSS", "f"), + ("PATH_LOSSES_54", "f"), + (None, "4x"), + (None, "4x"), + (None, "4x"), + ("PATH_LOSSES_58", "f"), + ("PATH_LOSSES_59", "f"), + ("PATH_LOSSES_60", "f"), + ("PATH_LOSSES_61", "f"), + (None, "4x"), + ("PATH_LOSSES_63", "f"), + ("PATH_LOSSES_64", "f"), + ("PATH_LOSSES_65", "f"), + ("PATH_LOSSES_66", "f"), + ("PATH_LOSSES_67", "f"), + ("PATH_LOSSES_68", "f"), + ("PATH_LOSSES_69", "f"), + ("CHAN_CAL_DIFF", "f"), + (None, "4x"), + ("LOG_AMP_FACTOR_1", "f"), + ("LOG_AMP_FACTOR_2", "f"), + ("V_TS_CW", "f"), + ("RNSCALE_0", "f"), + ("RNSCALE_1", "f"), + ("RNSCALE_2", "f"), + ("RNSCALE_3", "f"), + ("RNSCALE_4", "f"), + ("RNSCALE_5", "f"), + ("RNSCALE_6", "f"), + ("RNSCALE_7", "f"), + ("RNSCALE_8", "f"), + ("RNSCALE_9", "f"), + ("RNSCALE_10", "f"), + ("RNSCALE_11", "f"), + ("RNSCALE_12", "f"), + ("ATMOS_0", "f"), + ("ATMOS_1", "f"), + ("ATMOS_2", "f"), + ("ATMOS_3", "f"), + ("ATMOS_4", "f"), + ("ATMOS_5", "f"), + ("ATMOS_6", "f"), + ("ATMOS_7", "f"), + ("ATMOS_8", "f"), + ("ATMOS_9", "f"), + ("ATMOS_10", "f"), + ("ATMOS_11", "f"), + ("ATMOS_12", "f"), + ("EL_INDEX_0", "f"), + ("EL_INDEX_1", "f"), + ("EL_INDEX_2", "f"), + ("EL_INDEX_3", "f"), + ("EL_INDEX_4", "f"), + ("EL_INDEX_5", "f"), + ("EL_INDEX_6", "f"), + ("EL_INDEX_7", "f"), + ("EL_INDEX_8", "f"), + ("EL_INDEX_9", "f"), + ("EL_INDEX_10", "f"), + ("EL_INDEX_11", "f"), + ("TFREQ_MHZ", "L"), + ("BASE_DATA_TCN", "f"), + ("REFL_DATA_TOVER", "f"), + ("TAR_H_DBZ0_LP", "f"), + ("TAR_V_DBZ0_LP", "f"), + ("INIT_PHI_DP", "L"), + ("NORM_INIT_PHI_DP", "L"), + ("LX_LP", "f"), + ("LX_SP", "f"), + ("METEOR_PARAM", "f"), + ("BEAMWIDTH", "f"), + ("ANTENNA_GAIN", "f"), + (None, "4x"), + ("VEL_MAINT_LIMIT", "f"), + ("WTH_MAINT_LIMIT", "f"), + ("VEL_DEGRAD_LIMIT", "f"), + ("WTH_DEGRAD_LIMIT", "f"), + ("H_NOISETEMP_DGRAD_LIMIT", "f"), + ("H_NOISETEMP_MAINT_LIMIT", "f"), + ("V_NOISETEMP_DGRAD_LIMIT", "f"), + ("V_NOISETEMP_MAINT_LIMIT", "f"), + ("KLY_DEGRADE_LIMIT", "f"), + ("TS_COHO", "f"), + ("H_TS_CW", "f"), + ("TS_RF_SP", "f"), + ("TS_RF_LP", "f"), + ("TS_STALO", "f"), + ("AME_H_NOISE_ENR", "f"), + ("XMTR_PEAK_PWR_HIGH_LIMIT", "f"), + ("XMTR_PEAK_PWR_LOW_LIMIT", "f"), + ("H_DBZ0_DELTA_LIMIT", "f"), + ("THRESHOLD1", "f"), + ("THRESHOLD2", "f"), + ("CLUT_SUPP_DGRAD_LIM", "f"), + ("CLUT_SUPP_MAINT_LIM", "f"), + ("RANGE0_VALUE", "f"), + ("XMTR_PWR_MTR_SCALE", "f"), + ("V_DBZ0_DELTA_LIMIT", "f"), + ("TAR_H_DBZ0_SP", "f"), + ("TAR_V_DBZ0_SP", "f"), + ("DELTAPRF", "L"), + (None, "4x"), + (None, "4x"), + ("TAU_SP", "L"), + ("TAU_LP", "L"), + ("NC_DEAD_VALUE", "L"), + ("TAU_RF_SP", "L"), + ("TAU_RF_LP", "L"), + ("SEG1LIM", "f"), + ("SLATSEC", "f"), + ("SLONSEC", "f"), + (None, "4x"), + ("SLATDEG", "L"), + ("SLATMIN", "L"), + ("SLONDEG", "L"), + ("SLONMIN", "L"), + ("SLATDIR", "4s"), + ("SLONDIR", "4s"), + (None, "4x"), + ("VCPAT11", "1172s"), + ("VCPAT21", "1172s"), + ("VCPAT31", "1172s"), + ("VCPAT32", "1172s"), + ("VCPAT300", "1172s"), + ("VCPAT301", "1172s"), + ("AZ_CORRECTION_FACTOR", "f"), + ("EL_CORRECTION_FACTOR", "f"), + ("SITE_NAME", "4s"), + ("ANT_MANUAL_SETUP_IELMIN", "l"), + ("ANT_MANUAL_SETUP_IELMAX", "L"), + ("ANT_MANUAL_SETUP_FAZVELMAX", "L"), + ("ANT_MANUAL_SETUP_FELVELMAX", "L"), + ("ANT_MANUAL_SETUP_IGND_HGT", "L"), + ("ANT_MANUAL_SETUP_IRAD_HGT", "L"), + (None, "300x"), + ("RVP8NV_IWAVEGUIDE_LENGTH", "L"), + (None, "44x"), + ("VEL_DATA_TOVER", "f"), + ("WIDTH_DATA_TOVER", "f"), + (None, "12x"), + ("DOPPLER_RANGE_START", "f"), + ("MAX_EL_INDEX", "L"), + ("SEG2LIM", "f"), + ("SEG3LIM", "f"), + ("SEG4LIM", "f"), + ("NBR_EL_SEGMENTS", "L"), + ("H_NOISE_LONG", "f"), + ("ANT_NOISE_TEMP", "f"), + ("H_NOISE_SHORT", "f"), + ("H_NOISE_TOLERANCE", "f"), + ("MIN_H_DYN_RANGE", "f"), + ("GEN_INSTALLED", "4s"), + ("GEN_EXERCISE", "4s"), + ("V_NOISE_TOLERANCE", "f"), + ("MIN_V_DYN_RANGE", "f"), + ("ZDR_BIAS_DGRAD_LIM", "f"), + (None, "4x"), + (None, "4x"), + (None, "8x"), + ("V_NOISE_LONG", "f"), + ("V_NOISE_SHORT", "f"), + ("ZDR_DATA_TOVER", "f"), + ("PHI_DATA_TOVER", "f"), + ("RHO_DATA_TOVER", "f"), + ("STALO_POWER_DGRAD_LIMIT", "f"), + ("STALO_POWER_MAINT_LIMIT", "f"), + ("MIN_H_PWR_SENSE", "f"), + ("MIN_V_PWR_SENSE", "f"), + ("H_PWR_SENSE_OFFSET", "f"), + ("V_PWR_SENSE_OFFSET", "f"), + ("PS_GAIN_REF", "f"), + ("RF_PALLET_BROAD_LOSS", "f"), + ("ZDR_CHECK_THRESHOLD", "f"), + ("PHI_CHECK_THRESHOLD", "f"), + ("RHO_CHECK_THRESHOLD", "f"), + (None, "52x"), + ("AME_PS_TOLERANCE", "f"), + ("AME_MAX_TEMP", "f"), + ("AME_MIN_TEMP", "f"), + ("RCVR_MOD_MAX_TEMP", "f"), + ("RCVR_MOD_MIN_TEMP", "f"), + ("BITE_MOD_MAX_TEMP", "f"), + ("BITE_MOD_MIN_TEMP", "f"), + ("DEFAULT_POLARIZATION", "L"), + ("TR_LIMIT_DGRAD_LIMIT", "f"), + ("TR_LIMIT_FAIL_LIMIT", "f"), + (None, "4x"), + (None, "4x"), + ("AME_CURRENT_TOLERANCE", "f"), + ("H_ONLY_POLARIZATION", "L"), + ("V_ONLY_POLARIZATION", "L"), + (None, "8x"), + ("REFLECTOR_BIAS", "f"), + ("A_MIN_SHELTER_TEMP_WARN", "f"), + (None, "432x"), +] diff --git a/src/metpy/io/_nexrad_msgs/msg3.py b/src/metpy/io/_nexrad_msgs/msg3.py index 39c87d0a9c9..22f28a508d9 100644 --- a/src/metpy/io/_nexrad_msgs/msg3.py +++ b/src/metpy/io/_nexrad_msgs/msg3.py @@ -4,401 +4,405 @@ # flake8: noqa # Generated file -- do not modify -descriptions = {"T1_Output_Frames": "The number of octets received on interface, including frame octets (octet)", - "T1_Input_Frames": "The number of octets sent on interface, including frame octets (octet)", - "Router_Memory_Used": "Bytes currently in use by applications on managed device (byte)", - "Router_Memory_Free": "Bytes currently free on managed device (byte)", - "Router_Memory_Utilization": "%", - "CSU_Loss_of_Signal": "Number of times Loss of Signal event detected", - "CSU_Loss_of_Frames": "Number of times Loss of Frames event detected", - "CSU_Yellow_Alarms": "Number of times Resource Availability Indication (RAI) (yellow) alarm received.", - "CSU_Blue_Alarms": "Number of times Alarm Indication Signal (AIS) (blue) alarm received.", - "CSU_24hr_Errored_Seconds": "Number of errored seconds in previous 24 hours. (s)", - "CSU_24hr_Severely_Errored_Seconds": "Number of severely errored seconds in previous 24 hours. (s)", - "CSU_24hr_Severely_Errored_Framing_Seconds": "Number of severely errored framing seconds in previous 24 hours. (s)", - "CSU_24hr_Unavailable_Seconds": "Number of unavailable seconds in previous 24 hours. (s)", - "CSU_24hr_Controlled_Slip_Seconds": "Number of controlled slip seconds in previous 24 hours. (s)", - "CSU_24hr_Path_Coding_Violations": "Number of path coding violations in previous 24 hours.", - "CSU_24hr_Line_Errored_Seconds": "Number of line errored seconds in previous 24 hours. (s)", - "CSU_24hr_Bursty_Errored_Seconds": "Number of bursty errored seconds in previous 24 hours. (s)", - "CSU_24hr_Degraded_Minutes": "Number of degraded minutes in previous 24 hours. (min)", - "LAN_Switch_Memory_Used": "Bytes currently in use by applications on this device (byte)", - "LAN_Switch_Memory_Free": "Bytes currently free on this device (byte)", - "LAN_Switch_Memory_Utilization": "%", - "NTP_Rejected_Packets": "Number of packets rejected by NTP application layer", - "NTP_Estimated_Time_Error": "Current estimated time error of the time server (usec)", - "GPS_Satellites": "Current number of GPS satellites used in position and time fix calculation", - "GPS_Max_Signal_Strength": "Strongest signal strength of all tracking satellites as seen by receiver (dB)", - "IPC_Status": "Status of the communications between channels on a redundant system. N/A on a Single channel system.", - "Commanded_Channel_Control": "Indicates which channel the RDA has commanded to be the controlling channel. This is not necessarily the channel which is in control.", - "DAU_Test_0": "Tests the performance of the DAU A/D Mutiplexer with a known low voltage input.", - "DAU_Test_1": "Tests the performance of the DAU A/D Mutiplexer with a known medium voltage input.", - "DAU_Test_2": "Tests the performance of the DAU A/D Mutiplexer with a known high voltage input.", - "AME_Internal_Temperature": "deg C", - "AME_Receiver_Module_Temperature": "deg C", - "AME_BITE_CAL_Module_Temperature": "deg C", - "AME_Peltier_Pulse_Width_Modulation": "%", - "AME_pos_3_3V_PS_Voltage": "V", - "AME_pos_5V_PS_Voltage": "V", - "AME_pos_6_5V_PS_Voltage": "V", - "AME_pos_15V_PS_Voltage": "V", - "AME_pos_48V_PS_Voltage": "V", - "AME_STALO_Power": "V", - "Peltier_Current": "A", - "ADC_Calibration_Reference_Voltage": "V", - "AME_Peltier_Inside_Fan_Current": "A", - "AME_Peltier_Outside_Fan_Current": "A", - "Horizontal_TR_Limiter_Voltage": "V", - "Vertical_TR_Limiter_Voltage": "V", - "ADC_Calibration_Offset_Voltage": "mV", - "UPS_Time_on_Battery": "s", - "UPS_Battery_Temperature": "deg C", - "UPS_Output_Voltage": "V", - "UPS_Output_Frequency": "Hz", - "UPS_Output_Current": "A", - "Power_Administrator_Load": "A", - "Transmitter_RF_Power__Sensor": "mW", - "Horizontal_XMTR_Peak_Power": "kW", - "XMTR_Peak_Power": "kW", - "Vertical_XMTR_Peak_Power": "kW", - "XMTR_RF_Avg_Power": "W", - "Receiver_Bias": "dB", - "Transmit_Imbalance": "dB", - "Equipment_Shelter_Temperature": "deg C", - "Outside_Ambient_Temperature": "deg C", - "Transmitter_Leaving_Air_Temp": "deg C", - "AC_Unit__1_Discharge_Air_Temp": "deg C", - "Generator_Shelter_Temperature": "deg C", - "Radome_Air_Temperature": "deg C", - "AC_Unit__2_Discharge_Air_Temp": "deg C", - "DAU_pos_15v_PS": "V", - "DAU_neg_15v_PS": "V", - "DAU_pos_28v_PS": "V", - "DAU_pos_5v_PS": "V", - "Converted_Generator_Fuel_Level": "%", - "pos_28v_PS": "V", - "Pedestal_pos_15v_PS": "V", - "Encoder_pos_5v_PS": "V", - "Pedestal_pos_5v_PS": "V", - "Pedestal_neg_15v_PS": "V", - "Horizontal_Short_Pulse_Noise": "dBm", - "Horizontal_Long_Pulse_Noise": "dBm", - "Horizontal_Noise_Temperature": "K", - "Vertical_Noise_Long_Pulse": "dBm", - "Vertical_Noise_Temperature": "K", - "Horizontal_Dynamic_Range": "dB", - "Horizontal_Delta_dBZ0": "dB", - "Vertical_Delta_dBZ0": "dB", - "KD_Peak_Measured": "dBm", - "Short_Pulse__Horizontal_dBZ0": "dBZ", - "Long_Pulse__Horizontal_dBZ0": "dBZ", - "Horizontal_I0": "dBm", - "Vertical_I0": "dBm", - "Vertical_Dynamic_Range": "dB", - "Short_Pulse__Vertical_dBZ0": "dBZ", - "Long_Pulse__Vertical_dBZ0": "dBZ", - "Horizontal_Power_Sense": "dBm", - "Vertical_Power_Sense": "dBm", - "ZDR_Bias": "dB", - "Clutter_Suppression_Delta": "dB", - "Clutter_Suppression_Unfiltered_Power": "dBZ", - "Clutter_Suppression_Filtered_Power": "dBZ", - "Transmit_Burst_Power": "dBm", - "Transmit_Burst_Phase": "deg"} +descriptions = { + "T1_Output_Frames": "The number of octets received on interface, including frame octets (octet)", + "T1_Input_Frames": "The number of octets sent on interface, including frame octets (octet)", + "Router_Memory_Used": "Bytes currently in use by applications on managed device (byte)", + "Router_Memory_Free": "Bytes currently free on managed device (byte)", + "Router_Memory_Utilization": "%", + "CSU_Loss_of_Signal": "Number of times Loss of Signal event detected", + "CSU_Loss_of_Frames": "Number of times Loss of Frames event detected", + "CSU_Yellow_Alarms": "Number of times Resource Availability Indication (RAI) (yellow) alarm received.", + "CSU_Blue_Alarms": "Number of times Alarm Indication Signal (AIS) (blue) alarm received.", + "CSU_24hr_Errored_Seconds": "Number of errored seconds in previous 24 hours. (s)", + "CSU_24hr_Severely_Errored_Seconds": "Number of severely errored seconds in previous 24 hours. (s)", + "CSU_24hr_Severely_Errored_Framing_Seconds": "Number of severely errored framing seconds in previous 24 hours. (s)", + "CSU_24hr_Unavailable_Seconds": "Number of unavailable seconds in previous 24 hours. (s)", + "CSU_24hr_Controlled_Slip_Seconds": "Number of controlled slip seconds in previous 24 hours. (s)", + "CSU_24hr_Path_Coding_Violations": "Number of path coding violations in previous 24 hours.", + "CSU_24hr_Line_Errored_Seconds": "Number of line errored seconds in previous 24 hours. (s)", + "CSU_24hr_Bursty_Errored_Seconds": "Number of bursty errored seconds in previous 24 hours. (s)", + "CSU_24hr_Degraded_Minutes": "Number of degraded minutes in previous 24 hours. (min)", + "LAN_Switch_Memory_Used": "Bytes currently in use by applications on this device (byte)", + "LAN_Switch_Memory_Free": "Bytes currently free on this device (byte)", + "LAN_Switch_Memory_Utilization": "%", + "NTP_Rejected_Packets": "Number of packets rejected by NTP application layer", + "NTP_Estimated_Time_Error": "Current estimated time error of the time server (usec)", + "GPS_Satellites": "Current number of GPS satellites used in position and time fix calculation", + "GPS_Max_Signal_Strength": "Strongest signal strength of all tracking satellites as seen by receiver (dB)", + "IPC_Status": "Status of the communications between channels on a redundant system. N/A on a Single channel system.", + "Commanded_Channel_Control": "Indicates which channel the RDA has commanded to be the controlling channel. This is not necessarily the channel which is in control.", + "DAU_Test_0": "Tests the performance of the DAU A/D Mutiplexer with a known low voltage input.", + "DAU_Test_1": "Tests the performance of the DAU A/D Mutiplexer with a known medium voltage input.", + "DAU_Test_2": "Tests the performance of the DAU A/D Mutiplexer with a known high voltage input.", + "AME_Internal_Temperature": "deg C", + "AME_Receiver_Module_Temperature": "deg C", + "AME_BITE_CAL_Module_Temperature": "deg C", + "AME_Peltier_Pulse_Width_Modulation": "%", + "AME_pos_3_3V_PS_Voltage": "V", + "AME_pos_5V_PS_Voltage": "V", + "AME_pos_6_5V_PS_Voltage": "V", + "AME_pos_15V_PS_Voltage": "V", + "AME_pos_48V_PS_Voltage": "V", + "AME_STALO_Power": "V", + "Peltier_Current": "A", + "ADC_Calibration_Reference_Voltage": "V", + "AME_Peltier_Inside_Fan_Current": "A", + "AME_Peltier_Outside_Fan_Current": "A", + "Horizontal_TR_Limiter_Voltage": "V", + "Vertical_TR_Limiter_Voltage": "V", + "ADC_Calibration_Offset_Voltage": "mV", + "UPS_Time_on_Battery": "s", + "UPS_Battery_Temperature": "deg C", + "UPS_Output_Voltage": "V", + "UPS_Output_Frequency": "Hz", + "UPS_Output_Current": "A", + "Power_Administrator_Load": "A", + "Transmitter_RF_Power__Sensor": "mW", + "Horizontal_XMTR_Peak_Power": "kW", + "XMTR_Peak_Power": "kW", + "Vertical_XMTR_Peak_Power": "kW", + "XMTR_RF_Avg_Power": "W", + "Receiver_Bias": "dB", + "Transmit_Imbalance": "dB", + "Equipment_Shelter_Temperature": "deg C", + "Outside_Ambient_Temperature": "deg C", + "Transmitter_Leaving_Air_Temp": "deg C", + "AC_Unit__1_Discharge_Air_Temp": "deg C", + "Generator_Shelter_Temperature": "deg C", + "Radome_Air_Temperature": "deg C", + "AC_Unit__2_Discharge_Air_Temp": "deg C", + "DAU_pos_15v_PS": "V", + "DAU_neg_15v_PS": "V", + "DAU_pos_28v_PS": "V", + "DAU_pos_5v_PS": "V", + "Converted_Generator_Fuel_Level": "%", + "pos_28v_PS": "V", + "Pedestal_pos_15v_PS": "V", + "Encoder_pos_5v_PS": "V", + "Pedestal_pos_5v_PS": "V", + "Pedestal_neg_15v_PS": "V", + "Horizontal_Short_Pulse_Noise": "dBm", + "Horizontal_Long_Pulse_Noise": "dBm", + "Horizontal_Noise_Temperature": "K", + "Vertical_Noise_Long_Pulse": "dBm", + "Vertical_Noise_Temperature": "K", + "Horizontal_Dynamic_Range": "dB", + "Horizontal_Delta_dBZ0": "dB", + "Vertical_Delta_dBZ0": "dB", + "KD_Peak_Measured": "dBm", + "Short_Pulse__Horizontal_dBZ0": "dBZ", + "Long_Pulse__Horizontal_dBZ0": "dBZ", + "Horizontal_I0": "dBm", + "Vertical_I0": "dBm", + "Vertical_Dynamic_Range": "dB", + "Short_Pulse__Vertical_dBZ0": "dBZ", + "Long_Pulse__Vertical_dBZ0": "dBZ", + "Horizontal_Power_Sense": "dBm", + "Vertical_Power_Sense": "dBm", + "ZDR_Bias": "dB", + "Clutter_Suppression_Delta": "dB", + "Clutter_Suppression_Unfiltered_Power": "dBZ", + "Clutter_Suppression_Filtered_Power": "dBZ", + "Transmit_Burst_Power": "dBm", + "Transmit_Burst_Phase": "deg", +} -fields = [(None, "2x"), - ("Loop_Back_Test_Status", "H"), - ("T1_Output_Frames", "L"), - ("T1_Input_Frames", "L"), - ("Router_Memory_Used", "L"), - ("Router_Memory_Free", "L"), - ("Router_Memory_Utilization", "H"), - (None, "2x"), - ("CSU_Loss_of_Signal", "L"), - ("CSU_Loss_of_Frames", "L"), - ("CSU_Yellow_Alarms", "L"), - ("CSU_Blue_Alarms", "L"), - ("CSU_24hr_Errored_Seconds", "L"), - ("CSU_24hr_Severely_Errored_Seconds", "L"), - ("CSU_24hr_Severely_Errored_Framing_Seconds", "L"), - ("CSU_24hr_Unavailable_Seconds", "L"), - ("CSU_24hr_Controlled_Slip_Seconds", "L"), - ("CSU_24hr_Path_Coding_Violations", "L"), - ("CSU_24hr_Line_Errored_Seconds", "L"), - ("CSU_24hr_Bursty_Errored_Seconds", "L"), - ("CSU_24hr_Degraded_Minutes", "L"), - ("LAN_Switch_Memory_Used", "L"), - ("LAN_Switch_Memory_Free", "L"), - ("LAN_Switch_Memory_Utilization", "H"), - (None, "2x"), - ("NTP_Rejected_Packets", "L"), - ("NTP_Estimated_Time_Error", "l"), - ("GPS_Satellites", "l"), - ("GPS_Max_Signal_Strength", "l"), - ("IPC_Status", "H"), - ("Commanded_Channel_Control", "H"), - ("DAU_Test_0", "H"), - ("DAU_Test_1", "H"), - ("DAU_Test_2", "H"), - ("AME_Polarization", "H"), - ("AME_Internal_Temperature", "f"), - ("AME_Receiver_Module_Temperature", "f"), - ("AME_BITE_CAL_Module_Temperature", "f"), - ("AME_Peltier_Pulse_Width_Modulation", "H"), - ("AME_Peltier_Status", "H"), - ("AME_A_D_Converter_Status", "H"), - ("AME_State", "H"), - ("AME_pos_3_3V_PS_Voltage", "f"), - ("AME_pos_5V_PS_Voltage", "f"), - ("AME_pos_6_5V_PS_Voltage", "f"), - ("AME_pos_15V_PS_Voltage", "f"), - ("AME_pos_48V_PS_Voltage", "f"), - ("AME_STALO_Power", "f"), - ("Peltier_Current", "f"), - ("ADC_Calibration_Reference_Voltage", "f"), - ("AME_Mode", "H"), - ("AME_Peltier_Mode", "H"), - ("AME_Peltier_Inside_Fan_Current", "f"), - ("AME_Peltier_Outside_Fan_Current", "f"), - ("Horizontal_TR_Limiter_Voltage", "f"), - ("Vertical_TR_Limiter_Voltage", "f"), - ("ADC_Calibration_Offset_Voltage", "f"), - ("ADC_Calibration_Gain_Correction", "f"), - ("Power_UPS_Battery_Status", "L"), - ("UPS_Time_on_Battery", "L"), - ("UPS_Battery_Temperature", "f"), - ("UPS_Output_Voltage", "f"), - ("UPS_Output_Frequency", "f"), - ("UPS_Output_Current", "f"), - ("Power_Administrator_Load", "f"), - (None, "48x"), - ("pos_5_VDC_PS", "H"), - ("pos_15_VDC_PS", "H"), - ("pos_28_VDC_PS", "H"), - ("neg_15_VDC_PS", "H"), - ("pos_45_VDC_PS", "H"), - ("Filament_PS_Voltage", "H"), - ("Vacuum_Pump_PS_Voltage", "H"), - ("Focus_Coil_PS_Voltage", "H"), - ("Filament_PS", "H"), - ("Klystron_Warmup", "H"), - ("Transmitter_Available", "H"), - ("WG_Switch_Position", "H"), - ("WG_PFN_Transfer_Interlock", "H"), - ("Maintenance_Mode", "H"), - ("Maintenance_Required", "H"), - ("PFN_Switch_Position", "H"), - ("Modulator_Overload", "H"), - ("Modulator_Inv_Current", "H"), - ("Modulator_Switch_Fail", "H"), - ("Main_Power_Voltage", "H"), - ("Charging_System_Fail", "H"), - ("Inverse_Diode_Current", "H"), - ("Trigger_Amplifier", "H"), - ("Circulator_Temperature", "H"), - ("Spectrum_Filter_Pressure", "H"), - ("WG_ARC_VSWR", "H"), - ("Cabinet_Interlock", "H"), - ("Cabinet_Air_Temperature", "H"), - ("Cabinet_Airflow", "H"), - ("Klystron_Current", "H"), - ("Klystron_Filament_Current", "H"), - ("Klystron_Vacion_Current", "H"), - ("Klystron_Air_Temperature", "H"), - ("Klystron_Airflow", "H"), - ("Modulator_Switch_Maintenance", "H"), - ("Post_Charge_Regulator_Maintenance", "H"), - ("WG_Pressure_Humidity", "H"), - ("Transmitter_Overvoltage", "H"), - ("Transmitter_Overcurrent", "H"), - ("Focus_Coil_Current", "H"), - ("Focus_Coil_Airflow", "H"), - ("Oil_Temperature", "H"), - ("PRF_Limit", "H"), - ("Transmitter_Oil_Level", "H"), - ("Transmitter_Battery_Charging", "H"), - ("High_Voltage__HV__Status", "H"), - ("Transmitter_Recycling_Summary", "H"), - ("Transmitter_Inoperable", "H"), - ("Transmitter_Air_Filter", "H"), - ("Zero_Test_Bit_0", "H"), - ("Zero_Test_Bit_1", "H"), - ("Zero_Test_Bit_2", "H"), - ("Zero_Test_Bit_3", "H"), - ("Zero_Test_Bit_4", "H"), - ("Zero_Test_Bit_5", "H"), - ("Zero_Test_Bit_6", "H"), - ("Zero_Test_Bit_7", "H"), - ("One_Test_Bit_0", "H"), - ("One_Test_Bit_1", "H"), - ("One_Test_Bit_2", "H"), - ("One_Test_Bit_3", "H"), - ("One_Test_Bit_4", "H"), - ("One_Test_Bit_5", "H"), - ("One_Test_Bit_6", "H"), - ("One_Test_Bit_7", "H"), - ("XMTR_DAU_Interface", "H"), - ("Transmitter_Summary_Status", "H"), - (None, "2x"), - ("Transmitter_RF_Power__Sensor", "f"), - ("Horizontal_XMTR_Peak_Power", "f"), - ("XMTR_Peak_Power", "f"), - ("Vertical_XMTR_Peak_Power", "f"), - ("XMTR_RF_Avg_Power", "f"), - ("XMTR_Power_Meter_Zero", "H"), - (None, "2x"), - ("XMTR_Recycle_Count", "L"), - ("Receiver_Bias", "f"), - ("Transmit_Imbalance", "f"), - (None, "12x"), - ("AC_Unit__1_Compressor_Shut_off", "H"), - ("AC_Unit__2_Compressor_Shut_off", "H"), - ("Generator_Maintenance_Required", "H"), - ("Generator_Battery_Voltage", "H"), - ("Generator_Engine", "H"), - ("Generator_Volt_Frequency", "H"), - ("Power_Source", "H"), - ("Transitional_Power_Source__TPS", "H"), - ("Generator_Auto_Run_Off_Switch", "H"), - ("Aircraft_Hazard_Lighting", "H"), - ("DAU_UART", "H"), - (None, "20x"), - ("Equipment_Shelter_Fire_Detection_System", "H"), - ("Equipment_Shelter_Fire_Smoke", "H"), - ("Generator_Shelter_Fire_Smoke", "H"), - ("Utility_Voltage_Frequency", "H"), - ("Site_Security_Alarm", "H"), - ("Security_Equipment", "H"), - ("Security_System", "H"), - ("Receiver_Connected_to_Antenna", "H"), - ("Radome_Hatch", "H"), - ("AC_Unit__1_Filter_Dirty", "H"), - ("AC_Unit__2_Filter_Dirty", "H"), - ("Equipment_Shelter_Temperature", "f"), - ("Outside_Ambient_Temperature", "f"), - ("Transmitter_Leaving_Air_Temp", "f"), - ("AC_Unit__1_Discharge_Air_Temp", "f"), - ("Generator_Shelter_Temperature", "f"), - ("Radome_Air_Temperature", "f"), - ("AC_Unit__2_Discharge_Air_Temp", "f"), - ("DAU_pos_15v_PS", "f"), - ("DAU_neg_15v_PS", "f"), - ("DAU_pos_28v_PS", "f"), - ("DAU_pos_5v_PS", "f"), - ("Converted_Generator_Fuel_Level", "H"), - (None, "14x"), - ("pos_28v_PS", "f"), - ("Pedestal_pos_15v_PS", "f"), - ("Encoder_pos_5v_PS", "f"), - ("Pedestal_pos_5v_PS", "f"), - ("Pedestal_neg_15v_PS", "f"), - ("pos_150V_Overvoltage", "H"), - ("pos_150V_Undervoltage", "H"), - ("Elevation_Servo_Amp_Inhibit", "H"), - ("Elevation_Servo_Amp_Short_Circuit", "H"), - ("Elevation_Servo_Amp_Overtemp", "H"), - ("Elevation_Motor_Overtemp", "H"), - ("Elevation_Stow_Pin", "H"), - ("Elevation_PCU_Parity", "H"), - ("Elevation_Dead_Limit", "H"), - ("Elevation_pos_Normal_Limit", "H"), - ("Elevation_neg_Normal_Limit", "H"), - ("Elevation_Encoder_Light", "H"), - ("Elevation_Gearbox_Oil", "H"), - ("Elevation_Handwheel", "H"), - ("Elevation_Amp_PS", "H"), - ("Azimuth_Servo_Amp_Inhibit", "H"), - ("Azimuth_Servo_Amp_Short_Circuit", "H"), - ("Azimuth_Servo_Amp_Overtemp", "H"), - ("Azimuth_Motor_Overtemp", "H"), - ("Azimuth_Stow_Pin", "H"), - ("Azimuth_PCU_Parity", "H"), - ("Azimuth_Encoder_Light", "H"), - ("Azimuth_Gearbox_Oil", "H"), - ("Azimuth_Bull_Gear_Oil", "H"), - ("Azimuth_Handwheel", "H"), - ("Azimuth_Servo_Amp_PS", "H"), - ("Servo", "H"), - ("Pedestal_Interlock_Switch", "H"), - (None, "2x"), - (None, "2x"), - ("Self_Test_1_Status", "H"), - ("Self_Test_2_Status", "H"), - ("Self_Test_2_Data", "H"), - (None, "14x"), - ("COHO_Clock", "H"), - ("Rf_Generator_Frequency_Select_Oscillator", "H"), - ("Rf_Generator_RF_STALO", "H"), - ("Rf_Generator_Phase_Shifted_COHO", "H"), - ("pos_9v_Receiver_PS", "H"), - ("pos_5v_Receiver_PS", "H"), - ("O_18v_Receiver_PS", "H"), - ("neg_9v_Receiver_PS", "H"), - ("pos_5v_Single_Channel_RDAIU_PS", "H"), - (None, "2x"), - ("Horizontal_Short_Pulse_Noise", "f"), - ("Horizontal_Long_Pulse_Noise", "f"), - ("Horizontal_Noise_Temperature", "f"), - (None, "4x"), - ("Vertical_Noise_Long_Pulse", "f"), - ("Vertical_Noise_Temperature", "f"), - ("Horizontal_Linearity", "f"), - ("Horizontal_Dynamic_Range", "f"), - ("Horizontal_Delta_dBZ0", "f"), - ("Vertical_Delta_dBZ0", "f"), - ("KD_Peak_Measured", "f"), - (None, "4x"), - ("Short_Pulse__Horizontal_dBZ0", "f"), - ("Long_Pulse__Horizontal_dBZ0", "f"), - ("Velocity__Processed", "H"), - ("Width__Processed", "H"), - ("Velocity__RF_Gen", "H"), - ("Width__RF_Gen", "H"), - ("Horizontal_I0", "f"), - ("Vertical_I0", "f"), - ("Vertical_Dynamic_Range", "f"), - ("Short_Pulse__Vertical_dBZ0", "f"), - ("Long_Pulse__Vertical_dBZ0", "f"), - (None, "4x"), - (None, "4x"), - ("Horizontal_Power_Sense", "f"), - ("Vertical_Power_Sense", "f"), - ("ZDR_Bias", "f"), - (None, "12x"), - ("Clutter_Suppression_Delta", "f"), - ("Clutter_Suppression_Unfiltered_Power", "f"), - ("Clutter_Suppression_Filtered_Power", "f"), - ("Transmit_Burst_Power", "f"), - ("Transmit_Burst_Phase", "f"), - (None, "8x"), - (None, "4x"), - ("Vertical_Linearity", "f"), - (None, "8x"), - ("State_File_Read_Status", "H"), - ("State_File_Write_Status", "H"), - ("Bypass_Map_File_Read_Status", "H"), - ("Bypass_Map_File_Write_Status", "H"), - (None, "2x"), - (None, "2x"), - ("Current_Adaptation_File_Read_Status", "H"), - ("Current_Adaptation_File_Write_Status", "H"), - ("Censor_Zone_File_Read_Status", "H"), - ("Censor_Zone_File_Write_Status", "H"), - ("Remote_VCP_File_Read_Status", "H"), - ("Remote_VCP_File_Write_Status", "H"), - ("Baseline_Adaptation_File_Read_Status", "H"), - (None, "2x"), - ("Clutter_Filter_Map_File_Read_Status", "H"), - ("Clutter_Filter_Map_File_Write_Status", "H"), - ("General_Disk_I_O_Error", "H"), - (None, "26x"), - ("DAU_Comm_Status", "H"), - ("HCI_Comm_Status", "H"), - ("Pedestal_Comm_Status", "H"), - ("Signal_Processor_Comm_Status", "H"), - ("AME_Communication_Status", "H"), - ("RMS_Link_Status", "H"), - ("RPG_Link_Status", "H"), - (None, "2x"), - ("Performance_Check_Time", "L"), - (None, "20x")] +fields = [ + (None, "2x"), + ("Loop_Back_Test_Status", "H"), + ("T1_Output_Frames", "L"), + ("T1_Input_Frames", "L"), + ("Router_Memory_Used", "L"), + ("Router_Memory_Free", "L"), + ("Router_Memory_Utilization", "H"), + (None, "2x"), + ("CSU_Loss_of_Signal", "L"), + ("CSU_Loss_of_Frames", "L"), + ("CSU_Yellow_Alarms", "L"), + ("CSU_Blue_Alarms", "L"), + ("CSU_24hr_Errored_Seconds", "L"), + ("CSU_24hr_Severely_Errored_Seconds", "L"), + ("CSU_24hr_Severely_Errored_Framing_Seconds", "L"), + ("CSU_24hr_Unavailable_Seconds", "L"), + ("CSU_24hr_Controlled_Slip_Seconds", "L"), + ("CSU_24hr_Path_Coding_Violations", "L"), + ("CSU_24hr_Line_Errored_Seconds", "L"), + ("CSU_24hr_Bursty_Errored_Seconds", "L"), + ("CSU_24hr_Degraded_Minutes", "L"), + ("LAN_Switch_Memory_Used", "L"), + ("LAN_Switch_Memory_Free", "L"), + ("LAN_Switch_Memory_Utilization", "H"), + (None, "2x"), + ("NTP_Rejected_Packets", "L"), + ("NTP_Estimated_Time_Error", "l"), + ("GPS_Satellites", "l"), + ("GPS_Max_Signal_Strength", "l"), + ("IPC_Status", "H"), + ("Commanded_Channel_Control", "H"), + ("DAU_Test_0", "H"), + ("DAU_Test_1", "H"), + ("DAU_Test_2", "H"), + ("AME_Polarization", "H"), + ("AME_Internal_Temperature", "f"), + ("AME_Receiver_Module_Temperature", "f"), + ("AME_BITE_CAL_Module_Temperature", "f"), + ("AME_Peltier_Pulse_Width_Modulation", "H"), + ("AME_Peltier_Status", "H"), + ("AME_A_D_Converter_Status", "H"), + ("AME_State", "H"), + ("AME_pos_3_3V_PS_Voltage", "f"), + ("AME_pos_5V_PS_Voltage", "f"), + ("AME_pos_6_5V_PS_Voltage", "f"), + ("AME_pos_15V_PS_Voltage", "f"), + ("AME_pos_48V_PS_Voltage", "f"), + ("AME_STALO_Power", "f"), + ("Peltier_Current", "f"), + ("ADC_Calibration_Reference_Voltage", "f"), + ("AME_Mode", "H"), + ("AME_Peltier_Mode", "H"), + ("AME_Peltier_Inside_Fan_Current", "f"), + ("AME_Peltier_Outside_Fan_Current", "f"), + ("Horizontal_TR_Limiter_Voltage", "f"), + ("Vertical_TR_Limiter_Voltage", "f"), + ("ADC_Calibration_Offset_Voltage", "f"), + ("ADC_Calibration_Gain_Correction", "f"), + ("Power_UPS_Battery_Status", "L"), + ("UPS_Time_on_Battery", "L"), + ("UPS_Battery_Temperature", "f"), + ("UPS_Output_Voltage", "f"), + ("UPS_Output_Frequency", "f"), + ("UPS_Output_Current", "f"), + ("Power_Administrator_Load", "f"), + (None, "48x"), + ("pos_5_VDC_PS", "H"), + ("pos_15_VDC_PS", "H"), + ("pos_28_VDC_PS", "H"), + ("neg_15_VDC_PS", "H"), + ("pos_45_VDC_PS", "H"), + ("Filament_PS_Voltage", "H"), + ("Vacuum_Pump_PS_Voltage", "H"), + ("Focus_Coil_PS_Voltage", "H"), + ("Filament_PS", "H"), + ("Klystron_Warmup", "H"), + ("Transmitter_Available", "H"), + ("WG_Switch_Position", "H"), + ("WG_PFN_Transfer_Interlock", "H"), + ("Maintenance_Mode", "H"), + ("Maintenance_Required", "H"), + ("PFN_Switch_Position", "H"), + ("Modulator_Overload", "H"), + ("Modulator_Inv_Current", "H"), + ("Modulator_Switch_Fail", "H"), + ("Main_Power_Voltage", "H"), + ("Charging_System_Fail", "H"), + ("Inverse_Diode_Current", "H"), + ("Trigger_Amplifier", "H"), + ("Circulator_Temperature", "H"), + ("Spectrum_Filter_Pressure", "H"), + ("WG_ARC_VSWR", "H"), + ("Cabinet_Interlock", "H"), + ("Cabinet_Air_Temperature", "H"), + ("Cabinet_Airflow", "H"), + ("Klystron_Current", "H"), + ("Klystron_Filament_Current", "H"), + ("Klystron_Vacion_Current", "H"), + ("Klystron_Air_Temperature", "H"), + ("Klystron_Airflow", "H"), + ("Modulator_Switch_Maintenance", "H"), + ("Post_Charge_Regulator_Maintenance", "H"), + ("WG_Pressure_Humidity", "H"), + ("Transmitter_Overvoltage", "H"), + ("Transmitter_Overcurrent", "H"), + ("Focus_Coil_Current", "H"), + ("Focus_Coil_Airflow", "H"), + ("Oil_Temperature", "H"), + ("PRF_Limit", "H"), + ("Transmitter_Oil_Level", "H"), + ("Transmitter_Battery_Charging", "H"), + ("High_Voltage__HV__Status", "H"), + ("Transmitter_Recycling_Summary", "H"), + ("Transmitter_Inoperable", "H"), + ("Transmitter_Air_Filter", "H"), + ("Zero_Test_Bit_0", "H"), + ("Zero_Test_Bit_1", "H"), + ("Zero_Test_Bit_2", "H"), + ("Zero_Test_Bit_3", "H"), + ("Zero_Test_Bit_4", "H"), + ("Zero_Test_Bit_5", "H"), + ("Zero_Test_Bit_6", "H"), + ("Zero_Test_Bit_7", "H"), + ("One_Test_Bit_0", "H"), + ("One_Test_Bit_1", "H"), + ("One_Test_Bit_2", "H"), + ("One_Test_Bit_3", "H"), + ("One_Test_Bit_4", "H"), + ("One_Test_Bit_5", "H"), + ("One_Test_Bit_6", "H"), + ("One_Test_Bit_7", "H"), + ("XMTR_DAU_Interface", "H"), + ("Transmitter_Summary_Status", "H"), + (None, "2x"), + ("Transmitter_RF_Power__Sensor", "f"), + ("Horizontal_XMTR_Peak_Power", "f"), + ("XMTR_Peak_Power", "f"), + ("Vertical_XMTR_Peak_Power", "f"), + ("XMTR_RF_Avg_Power", "f"), + ("XMTR_Power_Meter_Zero", "H"), + (None, "2x"), + ("XMTR_Recycle_Count", "L"), + ("Receiver_Bias", "f"), + ("Transmit_Imbalance", "f"), + (None, "12x"), + ("AC_Unit__1_Compressor_Shut_off", "H"), + ("AC_Unit__2_Compressor_Shut_off", "H"), + ("Generator_Maintenance_Required", "H"), + ("Generator_Battery_Voltage", "H"), + ("Generator_Engine", "H"), + ("Generator_Volt_Frequency", "H"), + ("Power_Source", "H"), + ("Transitional_Power_Source__TPS", "H"), + ("Generator_Auto_Run_Off_Switch", "H"), + ("Aircraft_Hazard_Lighting", "H"), + ("DAU_UART", "H"), + (None, "20x"), + ("Equipment_Shelter_Fire_Detection_System", "H"), + ("Equipment_Shelter_Fire_Smoke", "H"), + ("Generator_Shelter_Fire_Smoke", "H"), + ("Utility_Voltage_Frequency", "H"), + ("Site_Security_Alarm", "H"), + ("Security_Equipment", "H"), + ("Security_System", "H"), + ("Receiver_Connected_to_Antenna", "H"), + ("Radome_Hatch", "H"), + ("AC_Unit__1_Filter_Dirty", "H"), + ("AC_Unit__2_Filter_Dirty", "H"), + ("Equipment_Shelter_Temperature", "f"), + ("Outside_Ambient_Temperature", "f"), + ("Transmitter_Leaving_Air_Temp", "f"), + ("AC_Unit__1_Discharge_Air_Temp", "f"), + ("Generator_Shelter_Temperature", "f"), + ("Radome_Air_Temperature", "f"), + ("AC_Unit__2_Discharge_Air_Temp", "f"), + ("DAU_pos_15v_PS", "f"), + ("DAU_neg_15v_PS", "f"), + ("DAU_pos_28v_PS", "f"), + ("DAU_pos_5v_PS", "f"), + ("Converted_Generator_Fuel_Level", "H"), + (None, "14x"), + ("pos_28v_PS", "f"), + ("Pedestal_pos_15v_PS", "f"), + ("Encoder_pos_5v_PS", "f"), + ("Pedestal_pos_5v_PS", "f"), + ("Pedestal_neg_15v_PS", "f"), + ("pos_150V_Overvoltage", "H"), + ("pos_150V_Undervoltage", "H"), + ("Elevation_Servo_Amp_Inhibit", "H"), + ("Elevation_Servo_Amp_Short_Circuit", "H"), + ("Elevation_Servo_Amp_Overtemp", "H"), + ("Elevation_Motor_Overtemp", "H"), + ("Elevation_Stow_Pin", "H"), + ("Elevation_PCU_Parity", "H"), + ("Elevation_Dead_Limit", "H"), + ("Elevation_pos_Normal_Limit", "H"), + ("Elevation_neg_Normal_Limit", "H"), + ("Elevation_Encoder_Light", "H"), + ("Elevation_Gearbox_Oil", "H"), + ("Elevation_Handwheel", "H"), + ("Elevation_Amp_PS", "H"), + ("Azimuth_Servo_Amp_Inhibit", "H"), + ("Azimuth_Servo_Amp_Short_Circuit", "H"), + ("Azimuth_Servo_Amp_Overtemp", "H"), + ("Azimuth_Motor_Overtemp", "H"), + ("Azimuth_Stow_Pin", "H"), + ("Azimuth_PCU_Parity", "H"), + ("Azimuth_Encoder_Light", "H"), + ("Azimuth_Gearbox_Oil", "H"), + ("Azimuth_Bull_Gear_Oil", "H"), + ("Azimuth_Handwheel", "H"), + ("Azimuth_Servo_Amp_PS", "H"), + ("Servo", "H"), + ("Pedestal_Interlock_Switch", "H"), + (None, "2x"), + (None, "2x"), + ("Self_Test_1_Status", "H"), + ("Self_Test_2_Status", "H"), + ("Self_Test_2_Data", "H"), + (None, "14x"), + ("COHO_Clock", "H"), + ("Rf_Generator_Frequency_Select_Oscillator", "H"), + ("Rf_Generator_RF_STALO", "H"), + ("Rf_Generator_Phase_Shifted_COHO", "H"), + ("pos_9v_Receiver_PS", "H"), + ("pos_5v_Receiver_PS", "H"), + ("O_18v_Receiver_PS", "H"), + ("neg_9v_Receiver_PS", "H"), + ("pos_5v_Single_Channel_RDAIU_PS", "H"), + (None, "2x"), + ("Horizontal_Short_Pulse_Noise", "f"), + ("Horizontal_Long_Pulse_Noise", "f"), + ("Horizontal_Noise_Temperature", "f"), + (None, "4x"), + ("Vertical_Noise_Long_Pulse", "f"), + ("Vertical_Noise_Temperature", "f"), + ("Horizontal_Linearity", "f"), + ("Horizontal_Dynamic_Range", "f"), + ("Horizontal_Delta_dBZ0", "f"), + ("Vertical_Delta_dBZ0", "f"), + ("KD_Peak_Measured", "f"), + (None, "4x"), + ("Short_Pulse__Horizontal_dBZ0", "f"), + ("Long_Pulse__Horizontal_dBZ0", "f"), + ("Velocity__Processed", "H"), + ("Width__Processed", "H"), + ("Velocity__RF_Gen", "H"), + ("Width__RF_Gen", "H"), + ("Horizontal_I0", "f"), + ("Vertical_I0", "f"), + ("Vertical_Dynamic_Range", "f"), + ("Short_Pulse__Vertical_dBZ0", "f"), + ("Long_Pulse__Vertical_dBZ0", "f"), + (None, "4x"), + (None, "4x"), + ("Horizontal_Power_Sense", "f"), + ("Vertical_Power_Sense", "f"), + ("ZDR_Bias", "f"), + (None, "12x"), + ("Clutter_Suppression_Delta", "f"), + ("Clutter_Suppression_Unfiltered_Power", "f"), + ("Clutter_Suppression_Filtered_Power", "f"), + ("Transmit_Burst_Power", "f"), + ("Transmit_Burst_Phase", "f"), + (None, "8x"), + (None, "4x"), + ("Vertical_Linearity", "f"), + (None, "8x"), + ("State_File_Read_Status", "H"), + ("State_File_Write_Status", "H"), + ("Bypass_Map_File_Read_Status", "H"), + ("Bypass_Map_File_Write_Status", "H"), + (None, "2x"), + (None, "2x"), + ("Current_Adaptation_File_Read_Status", "H"), + ("Current_Adaptation_File_Write_Status", "H"), + ("Censor_Zone_File_Read_Status", "H"), + ("Censor_Zone_File_Write_Status", "H"), + ("Remote_VCP_File_Read_Status", "H"), + ("Remote_VCP_File_Write_Status", "H"), + ("Baseline_Adaptation_File_Read_Status", "H"), + (None, "2x"), + ("Clutter_Filter_Map_File_Read_Status", "H"), + ("Clutter_Filter_Map_File_Write_Status", "H"), + ("General_Disk_I_O_Error", "H"), + (None, "26x"), + ("DAU_Comm_Status", "H"), + ("HCI_Comm_Status", "H"), + ("Pedestal_Comm_Status", "H"), + ("Signal_Processor_Comm_Status", "H"), + ("AME_Communication_Status", "H"), + ("RMS_Link_Status", "H"), + ("RPG_Link_Status", "H"), + (None, "2x"), + ("Performance_Check_Time", "L"), + (None, "20x"), +] diff --git a/src/metpy/io/_tools.py b/src/metpy/io/_tools.py index 186745b2722..7bd6c2d49c9 100644 --- a/src/metpy/io/_tools.py +++ b/src/metpy/io/_tools.py @@ -16,32 +16,32 @@ log = logging.getLogger(__name__) -def open_as_needed(filename, mode='rb'): +def open_as_needed(filename, mode="rb"): """Return a file-object given either a filename or an object. Handles opening with the right class based on the file extension. """ # Handle file-like objects - if hasattr(filename, 'read'): + if hasattr(filename, "read"): # See if the file object is really gzipped or bzipped. lead = filename.read(4) # If we can seek, seek back to start, otherwise read all the data into an # in-memory file-like object. - if hasattr(filename, 'seek'): + if hasattr(filename, "seek"): filename.seek(0) else: filename = BytesIO(lead + filename.read()) # If the leading bytes match one of the signatures, pass into the appropriate class. try: - lead = lead.encode('ascii') + lead = lead.encode("ascii") except AttributeError: pass - if lead.startswith(b'\x1f\x8b'): + if lead.startswith(b"\x1f\x8b"): filename = gzip.GzipFile(fileobj=filename) - elif lead.startswith(b'BZh'): + elif lead.startswith(b"BZh"): filename = bz2.BZ2File(filename) return filename @@ -49,22 +49,22 @@ def open_as_needed(filename, mode='rb'): # This will convert pathlib.Path instances to strings filename = str(filename) - if filename.endswith('.bz2'): + if filename.endswith(".bz2"): return bz2.BZ2File(filename, mode) - elif filename.endswith('.gz'): + elif filename.endswith(".gz"): return gzip.GzipFile(filename, mode) else: - kwargs = {'errors': 'surrogateescape'} if mode != 'rb' else {} + kwargs = {"errors": "surrogateescape"} if mode != "rb" else {} return open(filename, mode, **kwargs) class NamedStruct(Struct): """Parse bytes using :class:`Struct` but provide named fields.""" - def __init__(self, info, prefmt='', tuple_name=None): + def __init__(self, info, prefmt="", tuple_name=None): """Initialize the NamedStruct.""" if tuple_name is None: - tuple_name = 'NamedStruct' + tuple_name = "NamedStruct" names, fmts = zip(*info) self.converters = {} conv_off = 0 @@ -73,8 +73,8 @@ def __init__(self, info, prefmt='', tuple_name=None): self.converters[ind - conv_off] = i[-1] elif not i[0]: # Skip items with no name conv_off += 1 - self._tuple = namedtuple(tuple_name, ' '.join(n for n in names if n)) - super().__init__(prefmt + ''.join(f for f in fmts if f)) + self._tuple = namedtuple(tuple_name, " ".join(n for n in names if n)) + super().__init__(prefmt + "".join(f for f in fmts if f)) def _create(self, items): if self.converters: @@ -112,14 +112,14 @@ def pack(self, **kwargs): class DictStruct(Struct): """Parse bytes using :class:`Struct` but provide named fields using dictionary access.""" - def __init__(self, info, prefmt=''): + def __init__(self, info, prefmt=""): """Initialize the DictStruct.""" names, formats = zip(*info) # Remove empty names self._names = [n for n in names if n] - super().__init__(prefmt + ''.join(f for f in formats if f)) + super().__init__(prefmt + "".join(f for f in formats if f)) def _create(self, items): return dict(zip(self._names, items)) @@ -146,7 +146,7 @@ def __init__(self, *args, **kwargs): def __call__(self, val): """Map an integer to the string representation.""" - return self.val_map.get(val, f'Unknown ({val})') + return self.val_map.get(val, f"Unknown ({val})") class Bits: @@ -235,7 +235,7 @@ def clear_marks(self): def splice(self, mark, newdata): """Replace the data after the marked location with the specified data.""" self.jump_to(mark) - self._data = self._data[:self._offset] + bytearray(newdata) + self._data = self._data[: self._offset] + bytearray(newdata) def read_struct(self, struct_class): """Parse and return a structure from the current buffer offset.""" @@ -252,20 +252,20 @@ def read_func(self, func, num_bytes=None): def read_ascii(self, num_bytes=None): """Return the specified bytes as ascii-formatted text.""" - return self.read(num_bytes).decode('ascii') + return self.read(num_bytes).decode("ascii") - def read_binary(self, num, item_type='B'): + def read_binary(self, num, item_type="B"): """Parse the current buffer offset as the specified code.""" - if 'B' in item_type: + if "B" in item_type: return self.read(num) - if item_type[0] in ('@', '=', '<', '>', '!'): + if item_type[0] in ("@", "=", "<", ">", "!"): order = item_type[0] item_type = item_type[1:] else: - order = '@' + order = "@" - return list(self.read_struct(Struct(order + '{:d}'.format(int(num)) + item_type))) + return list(self.read_struct(Struct(order + "{:d}".format(int(num)) + item_type))) def read_int(self, size, endian, signed): """Parse the current buffer offset as the specified integer code.""" @@ -286,9 +286,9 @@ def read(self, num_bytes=None): def get_next(self, num_bytes=None): """Get the next bytes in the buffer without modifying the offset.""" if num_bytes is None: - return self._data[self._offset:] + return self._data[self._offset :] else: - return self._data[self._offset:self._offset + num_bytes] + return self._data[self._offset : self._offset + num_bytes] def skip(self, num_bytes): """Jump the ahead the specified bytes in the buffer.""" @@ -299,7 +299,7 @@ def skip(self, num_bytes): def check_remains(self, num_bytes): """Check that the number of bytes specified remains in the buffer.""" - return len(self._data[self._offset:]) == num_bytes + return len(self._data[self._offset :]) == num_bytes def truncate(self, num_bytes): """Remove the specified number of bytes from the end of the buffer.""" @@ -315,7 +315,7 @@ def __getitem__(self, item): def __str__(self): """Return a string representation of the IOBuffer.""" - return 'Size: {} Offset: {}'.format(len(self._data), self._offset) + return "Size: {} Offset: {}".format(len(self._data), self._offset) def __len__(self): """Return the amount of data in the buffer.""" @@ -346,9 +346,9 @@ def zlib_decompress_all_frames(data): try: frames.extend(decomp.decompress(data)) data = decomp.unused_data - log.debug('Decompressed zlib frame. %d bytes remain.', len(data)) + log.debug("Decompressed zlib frame. %d bytes remain.", len(data)) except zlib.error: - log.debug('Remaining %d bytes are not zlib compressed.', len(data)) + log.debug("Remaining %d bytes are not zlib compressed.", len(data)) frames.extend(data) break return frames @@ -357,9 +357,9 @@ def zlib_decompress_all_frames(data): def bits_to_code(val): """Convert the number of bits to the proper code for unpacking.""" if val == 8: - return 'B' + return "B" elif val == 16: - return 'H' + return "H" else: log.warning('Unsupported bit size: %s. Returning "B"', val) - return 'B' + return "B" diff --git a/src/metpy/io/gini.py b/src/metpy/io/gini.py index 2f7b43a0892..2f129739d18 100644 --- a/src/metpy/io/gini.py +++ b/src/metpy/io/gini.py @@ -16,8 +16,8 @@ from xarray.backends.common import AbstractDataStore from xarray.core.utils import FrozenDict -from ._tools import Bits, IOBuffer, NamedStruct, open_as_needed, zlib_decompress_all_frames from ..package_tools import Exporter +from ._tools import Bits, IOBuffer, NamedStruct, open_as_needed, zlib_decompress_all_frames exporter = Exporter(globals()) log = logging.getLogger(__name__) @@ -37,11 +37,11 @@ def _scaled_int(s): sign = 1 - ((s[0] & 0x80) >> 6) # Combine remaining bits - int_val = (((s[0] & 0x7f) << 16) | (s[1] << 8) | s[2]) - log.debug('Source: %s Int: %x Sign: %d', ' '.join(hex(c) for c in s), int_val, sign) + int_val = ((s[0] & 0x7F) << 16) | (s[1] << 8) | s[2] + log.debug("Source: %s Int: %x Sign: %d", " ".join(hex(c) for c in s), int_val, sign) # Return scaled and with proper sign - return (sign * int_val) / 10000. + return (sign * int_val) / 10000.0 def _name_lookup(names): @@ -49,7 +49,8 @@ def _name_lookup(names): mapper = dict(zip(range(len(names)), names)) def lookup(val): - return mapper.get(val, 'UnknownValue') + return mapper.get(val, "UnknownValue") + return lookup @@ -74,62 +75,183 @@ class GiniFile(AbstractDataStore): """ missing = 255 - wmo_finder = re.compile('(T\\w{3}\\d{2})[\\s\\w\\d]+\\w*(\\w{3})\r\r\n') - - crafts = ['Unknown', 'Unknown', 'Miscellaneous', 'JERS', 'ERS/QuikSCAT', 'POES/NPOESS', - 'Composite', 'DMSP', 'GMS', 'METEOSAT', 'GOES-7', 'GOES-8', 'GOES-9', - 'GOES-10', 'GOES-11', 'GOES-12', 'GOES-13', 'GOES-14', 'GOES-15', 'GOES-16'] - - sectors = ['NH Composite', 'East CONUS', 'West CONUS', 'Alaska Regional', - 'Alaska National', 'Hawaii Regional', 'Hawaii National', 'Puerto Rico Regional', - 'Puerto Rico National', 'Supernational', 'NH Composite', 'Central CONUS', - 'East Floater', 'West Floater', 'Central Floater', 'Polar Floater'] - - channels = ['Unknown', 'Visible', 'IR (3.9 micron)', 'WV (6.5/6.7 micron)', - 'IR (11 micron)', 'IR (12 micron)', 'IR (13 micron)', 'IR (1.3 micron)', - 'Reserved', 'Reserved', 'Reserved', 'Reserved', 'Reserved', 'LI (Imager)', - 'PW (Imager)', 'Surface Skin Temp (Imager)', 'LI (Sounder)', 'PW (Sounder)', - 'Surface Skin Temp (Sounder)', 'CAPE', 'Land-sea Temp', 'WINDEX', - 'Dry Microburst Potential Index', 'Microburst Day Potential Index', - 'Convective Inhibition', 'Volcano Imagery', 'Scatterometer', 'Cloud Top', - 'Cloud Amount', 'Rainfall Rate', 'Surface Wind Speed', 'Surface Wetness', - 'Ice Concentration', 'Ice Type', 'Ice Edge', 'Cloud Water Content', - 'Surface Type', 'Snow Indicator', 'Snow/Water Content', 'Volcano Imagery', - 'Reserved', 'Sounder (14.71 micron)', 'Sounder (14.37 micron)', - 'Sounder (14.06 micron)', 'Sounder (13.64 micron)', 'Sounder (13.37 micron)', - 'Sounder (12.66 micron)', 'Sounder (12.02 micron)', 'Sounder (11.03 micron)', - 'Sounder (9.71 micron)', 'Sounder (7.43 micron)', 'Sounder (7.02 micron)', - 'Sounder (6.51 micron)', 'Sounder (4.57 micron)', 'Sounder (4.52 micron)', - 'Sounder (4.45 micron)', 'Sounder (4.13 micron)', 'Sounder (3.98 micron)', - # Percent Normal TPW found empirically from Service Change Notice 20-03 - 'Sounder (3.74 micron)', 'Sounder (Visible)', 'Percent Normal TPW'] - - prod_desc_fmt = NamedStruct([('source', 'b'), - ('creating_entity', 'b', _name_lookup(crafts)), - ('sector_id', 'b', _name_lookup(sectors)), - ('channel', 'b', _name_lookup(channels)), - ('num_records', 'H'), ('record_len', 'H'), - ('datetime', '7s', _make_datetime), - ('projection', 'b', GiniProjection), ('nx', 'H'), ('ny', 'H'), - ('la1', '3s', _scaled_int), ('lo1', '3s', _scaled_int) - ], '>', 'ProdDescStart') - - lc_ps_fmt = NamedStruct([('reserved', 'b'), ('lov', '3s', _scaled_int), - ('dx', '3s', _scaled_int), ('dy', '3s', _scaled_int), - ('proj_center', 'b')], '>', 'LambertOrPolarProjection') - - mercator_fmt = NamedStruct([('resolution', 'b'), ('la2', '3s', _scaled_int), - ('lo2', '3s', _scaled_int), ('di', 'H'), ('dj', 'H') - ], '>', 'MercatorProjection') - - prod_desc2_fmt = NamedStruct([('scanning_mode', 'b', Bits(3)), - ('lat_in', '3s', _scaled_int), ('resolution', 'b'), - ('compression', 'b'), ('version', 'b'), ('pdb_size', 'H'), - ('nav_cal', 'b')], '>', 'ProdDescEnd') - - nav_fmt = NamedStruct([('sat_lat', '3s', _scaled_int), ('sat_lon', '3s', _scaled_int), - ('sat_height', 'H'), ('ur_lat', '3s', _scaled_int), - ('ur_lon', '3s', _scaled_int)], '>', 'Navigation') + wmo_finder = re.compile("(T\\w{3}\\d{2})[\\s\\w\\d]+\\w*(\\w{3})\r\r\n") + + crafts = [ + "Unknown", + "Unknown", + "Miscellaneous", + "JERS", + "ERS/QuikSCAT", + "POES/NPOESS", + "Composite", + "DMSP", + "GMS", + "METEOSAT", + "GOES-7", + "GOES-8", + "GOES-9", + "GOES-10", + "GOES-11", + "GOES-12", + "GOES-13", + "GOES-14", + "GOES-15", + "GOES-16", + ] + + sectors = [ + "NH Composite", + "East CONUS", + "West CONUS", + "Alaska Regional", + "Alaska National", + "Hawaii Regional", + "Hawaii National", + "Puerto Rico Regional", + "Puerto Rico National", + "Supernational", + "NH Composite", + "Central CONUS", + "East Floater", + "West Floater", + "Central Floater", + "Polar Floater", + ] + + channels = [ + "Unknown", + "Visible", + "IR (3.9 micron)", + "WV (6.5/6.7 micron)", + "IR (11 micron)", + "IR (12 micron)", + "IR (13 micron)", + "IR (1.3 micron)", + "Reserved", + "Reserved", + "Reserved", + "Reserved", + "Reserved", + "LI (Imager)", + "PW (Imager)", + "Surface Skin Temp (Imager)", + "LI (Sounder)", + "PW (Sounder)", + "Surface Skin Temp (Sounder)", + "CAPE", + "Land-sea Temp", + "WINDEX", + "Dry Microburst Potential Index", + "Microburst Day Potential Index", + "Convective Inhibition", + "Volcano Imagery", + "Scatterometer", + "Cloud Top", + "Cloud Amount", + "Rainfall Rate", + "Surface Wind Speed", + "Surface Wetness", + "Ice Concentration", + "Ice Type", + "Ice Edge", + "Cloud Water Content", + "Surface Type", + "Snow Indicator", + "Snow/Water Content", + "Volcano Imagery", + "Reserved", + "Sounder (14.71 micron)", + "Sounder (14.37 micron)", + "Sounder (14.06 micron)", + "Sounder (13.64 micron)", + "Sounder (13.37 micron)", + "Sounder (12.66 micron)", + "Sounder (12.02 micron)", + "Sounder (11.03 micron)", + "Sounder (9.71 micron)", + "Sounder (7.43 micron)", + "Sounder (7.02 micron)", + "Sounder (6.51 micron)", + "Sounder (4.57 micron)", + "Sounder (4.52 micron)", + "Sounder (4.45 micron)", + "Sounder (4.13 micron)", + "Sounder (3.98 micron)", + # Percent Normal TPW found empirically from Service Change Notice 20-03 + "Sounder (3.74 micron)", + "Sounder (Visible)", + "Percent Normal TPW", + ] + + prod_desc_fmt = NamedStruct( + [ + ("source", "b"), + ("creating_entity", "b", _name_lookup(crafts)), + ("sector_id", "b", _name_lookup(sectors)), + ("channel", "b", _name_lookup(channels)), + ("num_records", "H"), + ("record_len", "H"), + ("datetime", "7s", _make_datetime), + ("projection", "b", GiniProjection), + ("nx", "H"), + ("ny", "H"), + ("la1", "3s", _scaled_int), + ("lo1", "3s", _scaled_int), + ], + ">", + "ProdDescStart", + ) + + lc_ps_fmt = NamedStruct( + [ + ("reserved", "b"), + ("lov", "3s", _scaled_int), + ("dx", "3s", _scaled_int), + ("dy", "3s", _scaled_int), + ("proj_center", "b"), + ], + ">", + "LambertOrPolarProjection", + ) + + mercator_fmt = NamedStruct( + [ + ("resolution", "b"), + ("la2", "3s", _scaled_int), + ("lo2", "3s", _scaled_int), + ("di", "H"), + ("dj", "H"), + ], + ">", + "MercatorProjection", + ) + + prod_desc2_fmt = NamedStruct( + [ + ("scanning_mode", "b", Bits(3)), + ("lat_in", "3s", _scaled_int), + ("resolution", "b"), + ("compression", "b"), + ("version", "b"), + ("pdb_size", "H"), + ("nav_cal", "b"), + ], + ">", + "ProdDescEnd", + ) + + nav_fmt = NamedStruct( + [ + ("sat_lat", "3s", _scaled_int), + ("sat_lon", "3s", _scaled_int), + ("sat_height", "H"), + ("ur_lat", "3s", _scaled_int), + ("ur_lon", "3s", _scaled_int), + ], + ">", + "Navigation", + ) def __init__(self, filename): r"""Create an instance of `GiniFile`. @@ -150,18 +272,18 @@ def __init__(self, filename): self._buffer = IOBuffer.fromfile(fobj) # Pop off the WMO header if we find it - self.wmo_code = '' + self.wmo_code = "" self._process_wmo_header() - log.debug('First wmo code: %s', self.wmo_code) + log.debug("First wmo code: %s", self.wmo_code) # Decompress the data if necessary, and if so, pop off new header - log.debug('Length before decompression: %s', len(self._buffer)) + log.debug("Length before decompression: %s", len(self._buffer)) self._buffer = IOBuffer(self._buffer.read_func(zlib_decompress_all_frames)) - log.debug('Length after decompression: %s', len(self._buffer)) + log.debug("Length after decompression: %s", len(self._buffer)) # Process WMO header inside compressed data if necessary self._process_wmo_header() - log.debug('2nd wmo code: %s', self.wmo_code) + log.debug("2nd wmo code: %s", self.wmo_code) # Read product description start start = self._buffer.set_mark() @@ -176,13 +298,15 @@ def __init__(self, filename): self.proj_info = None # Handle projection-dependent parts - if self.prod_desc.projection in (GiniProjection.lambert_conformal, - GiniProjection.polar_stereographic): + if self.prod_desc.projection in ( + GiniProjection.lambert_conformal, + GiniProjection.polar_stereographic, + ): self.proj_info = self._buffer.read_struct(self.lc_ps_fmt) elif self.prod_desc.projection == GiniProjection.mercator: self.proj_info = self._buffer.read_struct(self.mercator_fmt) else: - log.warning('Unknown projection: %d', self.prod_desc.projection) + log.warning("Unknown projection: %d", self.prod_desc.projection) log.debug(self.proj_info) # Read the rest of the guaranteed product description block (PDB) @@ -193,15 +317,15 @@ def __init__(self, filename): if self.prod_desc2.nav_cal not in (0, -128): # TODO: See how GEMPAK/MCIDAS parses # Only warn if there actually seems to be useful navigation data - if self._buffer.get_next(self.nav_fmt.size) != b'\x00' * self.nav_fmt.size: - log.warning('Navigation/Calibration unhandled: %d', self.prod_desc2.nav_cal) + if self._buffer.get_next(self.nav_fmt.size) != b"\x00" * self.nav_fmt.size: + log.warning("Navigation/Calibration unhandled: %d", self.prod_desc2.nav_cal) if self.prod_desc2.nav_cal in (1, 2): self.navigation = self._buffer.read_struct(self.nav_fmt) log.debug(self.navigation) # Catch bad PDB with size set to 0 if self.prod_desc2.pdb_size == 0: - log.warning('Adjusting bad PDB size from 0 to 512.') + log.warning("Adjusting bad PDB size from 0 to 512.") self.prod_desc2 = self.prod_desc2._replace(pdb_size=512) # Jump past the remaining empty bytes in the product description block @@ -212,25 +336,27 @@ def __init__(self, filename): # Check for end marker end = self._buffer.read(self.prod_desc.record_len) - if end != b''.join(repeat(b'\xff\x00', self.prod_desc.record_len // 2)): - log.warning('End marker not as expected: %s', end) + if end != b"".join(repeat(b"\xff\x00", self.prod_desc.record_len // 2)): + log.warning("End marker not as expected: %s", end) # Check to ensure that we processed all of the data if not self._buffer.at_end(): if not blob: - log.debug('No data read yet, trying to decompress remaining data as an image.') + log.debug("No data read yet, trying to decompress remaining data as an image.") from matplotlib.image import imread - blob = (imread(BytesIO(self._buffer.read())) * 255).astype('uint8') + + blob = (imread(BytesIO(self._buffer.read())) * 255).astype("uint8") else: - log.warning('Leftover unprocessed data beyond EOF marker: %s', - self._buffer.get_next(10)) + log.warning( + "Leftover unprocessed data beyond EOF marker: %s", + self._buffer.get_next(10), + ) - self.data = np.array(blob).reshape((self.prod_desc.ny, - self.prod_desc.nx)) + self.data = np.array(blob).reshape((self.prod_desc.ny, self.prod_desc.nx)) def _process_wmo_header(self): """Read off the WMO header from the file, if necessary.""" - data = self._buffer.get_next(64).decode('utf-8', 'ignore') + data = self._buffer.get_next(64).decode("utf-8", "ignore") match = self.wmo_finder.search(data) if match: self.wmo_code = match.groups()[0] @@ -239,45 +365,52 @@ def _process_wmo_header(self): def __str__(self): """Return a string representation of the product.""" - parts = [self.__class__.__name__ + ': {0.creating_entity} {0.sector_id} {0.channel}', - 'Time: {0.datetime}', 'Size: {0.ny}x{0.nx}', - 'Projection: {0.projection.name}', - 'Lower Left Corner (Lon, Lat): ({0.lo1}, {0.la1})', - 'Resolution: {1.resolution}km'] - return '\n\t'.join(parts).format(self.prod_desc, self.prod_desc2) + parts = [ + self.__class__.__name__ + ": {0.creating_entity} {0.sector_id} {0.channel}", + "Time: {0.datetime}", + "Size: {0.ny}x{0.nx}", + "Projection: {0.projection.name}", + "Lower Left Corner (Lon, Lat): ({0.lo1}, {0.la1})", + "Resolution: {1.resolution}km", + ] + return "\n\t".join(parts).format(self.prod_desc, self.prod_desc2) def _make_proj_var(self): proj_info = self.proj_info prod_desc2 = self.prod_desc2 - attrs = {'earth_radius': 6371200.0} + attrs = {"earth_radius": 6371200.0} if self.prod_desc.projection == GiniProjection.lambert_conformal: - attrs['grid_mapping_name'] = 'lambert_conformal_conic' - attrs['standard_parallel'] = prod_desc2.lat_in - attrs['longitude_of_central_meridian'] = proj_info.lov - attrs['latitude_of_projection_origin'] = prod_desc2.lat_in + attrs["grid_mapping_name"] = "lambert_conformal_conic" + attrs["standard_parallel"] = prod_desc2.lat_in + attrs["longitude_of_central_meridian"] = proj_info.lov + attrs["latitude_of_projection_origin"] = prod_desc2.lat_in elif self.prod_desc.projection == GiniProjection.polar_stereographic: - attrs['grid_mapping_name'] = 'polar_stereographic' - attrs['straight_vertical_longitude_from_pole'] = proj_info.lov - attrs['latitude_of_projection_origin'] = -90 if proj_info.proj_center else 90 - attrs['standard_parallel'] = 60.0 # See Note 2 for Table 4.4A in ICD + attrs["grid_mapping_name"] = "polar_stereographic" + attrs["straight_vertical_longitude_from_pole"] = proj_info.lov + attrs["latitude_of_projection_origin"] = -90 if proj_info.proj_center else 90 + attrs["standard_parallel"] = 60.0 # See Note 2 for Table 4.4A in ICD elif self.prod_desc.projection == GiniProjection.mercator: - attrs['grid_mapping_name'] = 'mercator' - attrs['longitude_of_projection_origin'] = self.prod_desc.lo1 - attrs['latitude_of_projection_origin'] = self.prod_desc.la1 - attrs['standard_parallel'] = prod_desc2.lat_in + attrs["grid_mapping_name"] = "mercator" + attrs["longitude_of_projection_origin"] = self.prod_desc.lo1 + attrs["latitude_of_projection_origin"] = self.prod_desc.la1 + attrs["standard_parallel"] = prod_desc2.lat_in else: raise NotImplementedError( - f'Unhandled GINI Projection: {self.prod_desc.projection}') + f"Unhandled GINI Projection: {self.prod_desc.projection}" + ) - return 'projection', Variable((), 0, attrs) + return "projection", Variable((), 0, attrs) def _make_time_var(self): base_time = self.prod_desc.datetime.replace(hour=0, minute=0, second=0, microsecond=0) offset = self.prod_desc.datetime - base_time - time_var = Variable((), data=offset.seconds + offset.microseconds / 1e6, - attrs={'units': 'seconds since ' + base_time.isoformat()}) + time_var = Variable( + (), + data=offset.seconds + offset.microseconds / 1e6, + attrs={"units": "seconds since " + base_time.isoformat()}, + ) - return 'time', time_var + return "time", time_var def _get_proj_and_res(self): import pyproj @@ -285,29 +418,29 @@ def _get_proj_and_res(self): proj_info = self.proj_info prod_desc2 = self.prod_desc2 - kwargs = {'a': 6371200.0, 'b': 6371200.0} + kwargs = {"a": 6371200.0, "b": 6371200.0} if self.prod_desc.projection == GiniProjection.lambert_conformal: - kwargs['proj'] = 'lcc' - kwargs['lat_0'] = prod_desc2.lat_in - kwargs['lon_0'] = proj_info.lov - kwargs['lat_1'] = prod_desc2.lat_in - kwargs['lat_2'] = prod_desc2.lat_in + kwargs["proj"] = "lcc" + kwargs["lat_0"] = prod_desc2.lat_in + kwargs["lon_0"] = proj_info.lov + kwargs["lat_1"] = prod_desc2.lat_in + kwargs["lat_2"] = prod_desc2.lat_in dx, dy = proj_info.dx, proj_info.dy elif self.prod_desc.projection == GiniProjection.polar_stereographic: - kwargs['proj'] = 'stere' - kwargs['lon_0'] = proj_info.lov - kwargs['lat_0'] = -90 if proj_info.proj_center else 90 - kwargs['lat_ts'] = 60.0 # See Note 2 for Table 4.4A in ICD - kwargs['x_0'] = False # Easting - kwargs['y_0'] = False # Northing + kwargs["proj"] = "stere" + kwargs["lon_0"] = proj_info.lov + kwargs["lat_0"] = -90 if proj_info.proj_center else 90 + kwargs["lat_ts"] = 60.0 # See Note 2 for Table 4.4A in ICD + kwargs["x_0"] = False # Easting + kwargs["y_0"] = False # Northing dx, dy = proj_info.dx, proj_info.dy elif self.prod_desc.projection == GiniProjection.mercator: - kwargs['proj'] = 'merc' - kwargs['lat_0'] = self.prod_desc.la1 - kwargs['lon_0'] = self.prod_desc.lo1 - kwargs['lat_ts'] = prod_desc2.lat_in - kwargs['x_0'] = False # Easting - kwargs['y_0'] = False # Northing + kwargs["proj"] = "merc" + kwargs["lat_0"] = self.prod_desc.la1 + kwargs["lon_0"] = self.prod_desc.lo1 + kwargs["lat_ts"] = prod_desc2.lat_in + kwargs["x_0"] = False # Easting + kwargs["y_0"] = False # Northing dx, dy = prod_desc2.resolution, prod_desc2.resolution return pyproj.Proj(**kwargs), dx, dy @@ -319,28 +452,36 @@ def _make_coord_vars(self): x0, y0 = proj(self.prod_desc.lo1, self.prod_desc.la1) # Coordinate variable for x - xlocs = x0 + np.arange(self.prod_desc.nx) * (1000. * dx) - attrs = {'units': 'm', 'long_name': 'x coordinate of projection', - 'standard_name': 'projection_x_coordinate'} - x_var = Variable(('x',), xlocs, attrs) + xlocs = x0 + np.arange(self.prod_desc.nx) * (1000.0 * dx) + attrs = { + "units": "m", + "long_name": "x coordinate of projection", + "standard_name": "projection_x_coordinate", + } + x_var = Variable(("x",), xlocs, attrs) # Now y--Need to flip y because we calculated from the lower left corner, # but the raster data is stored with top row first. - ylocs = (y0 + np.arange(self.prod_desc.ny) * (1000. * dy))[::-1] - attrs = {'units': 'm', 'long_name': 'y coordinate of projection', - 'standard_name': 'projection_y_coordinate'} - y_var = Variable(('y',), ylocs, attrs) + ylocs = (y0 + np.arange(self.prod_desc.ny) * (1000.0 * dy))[::-1] + attrs = { + "units": "m", + "long_name": "y coordinate of projection", + "standard_name": "projection_y_coordinate", + } + y_var = Variable(("y",), ylocs, attrs) # Get the two-D lon,lat grid as well x, y = np.meshgrid(xlocs, ylocs) lon, lat = proj(x, y, inverse=True) - lon_var = Variable(('y', 'x'), data=lon, - attrs={'long_name': 'longitude', 'units': 'degrees_east'}) - lat_var = Variable(('y', 'x'), data=lat, - attrs={'long_name': 'latitude', 'units': 'degrees_north'}) + lon_var = Variable( + ("y", "x"), data=lon, attrs={"long_name": "longitude", "units": "degrees_east"} + ) + lat_var = Variable( + ("y", "x"), data=lat, attrs={"long_name": "latitude", "units": "degrees_north"} + ) - return [('x', x_var), ('y', y_var), ('lon', lon_var), ('lat', lat_var)] + return [("x", x_var), ("y", y_var), ("lon", lon_var), ("lat", lat_var)] def get_variables(self): """Get all variables in the file. @@ -355,16 +496,19 @@ def get_variables(self): # Now the data name = self.prod_desc.channel - if '(' in name: - name = name.split('(')[0].rstrip() + if "(" in name: + name = name.split("(")[0].rstrip() missing_val = self.missing - attrs = {'long_name': self.prod_desc.channel, 'missing_value': missing_val, - 'coordinates': 'y x time', 'grid_mapping': proj_var_name} - data_var = Variable(('y', 'x'), - data=np.ma.array(self.data, - mask=self.data == missing_val), - attrs=attrs) + attrs = { + "long_name": self.prod_desc.channel, + "missing_value": missing_val, + "coordinates": "y x time", + "grid_mapping": proj_var_name, + } + data_var = Variable( + ("y", "x"), data=np.ma.array(self.data, mask=self.data == missing_val), attrs=attrs + ) variables.append((name, data_var)) return FrozenDict(variables) @@ -375,5 +519,6 @@ def get_attrs(self): This is used by `xarray.open_dataset`. """ - return FrozenDict(satellite=self.prod_desc.creating_entity, - sector=self.prod_desc.sector_id) + return FrozenDict( + satellite=self.prod_desc.creating_entity, sector=self.prod_desc.sector_id + ) diff --git a/src/metpy/io/metar.py b/src/metpy/io/metar.py index 67a60069431..9d8ac777492 100644 --- a/src/metpy/io/metar.py +++ b/src/metpy/io/metar.py @@ -10,54 +10,79 @@ import numpy as np import pandas as pd -from ._tools import open_as_needed -from .metar_parser import parse, ParseError -from .station_data import station_info from ..calc import altimeter_to_sea_level_pressure, wind_components from ..package_tools import Exporter from ..plots.wx_symbols import wx_code_map from ..units import units +from ._tools import open_as_needed +from .metar_parser import ParseError, parse +from .station_data import station_info exporter = Exporter(globals()) # Configure the named tuple used for storing METAR data -Metar = namedtuple('metar', ['station_id', 'latitude', 'longitude', 'elevation', - 'date_time', 'wind_direction', 'wind_speed', 'current_wx1', - 'current_wx2', 'current_wx3', 'skyc1', 'skylev1', 'skyc2', - 'skylev2', 'skyc3', 'skylev3', 'skyc4', 'skylev4', - 'cloudcover', 'temperature', 'dewpoint', 'altimeter', - 'current_wx1_symbol', 'current_wx2_symbol', - 'current_wx3_symbol']) +Metar = namedtuple( + "metar", + [ + "station_id", + "latitude", + "longitude", + "elevation", + "date_time", + "wind_direction", + "wind_speed", + "current_wx1", + "current_wx2", + "current_wx3", + "skyc1", + "skylev1", + "skyc2", + "skylev2", + "skyc3", + "skylev3", + "skyc4", + "skylev4", + "cloudcover", + "temperature", + "dewpoint", + "altimeter", + "current_wx1_symbol", + "current_wx2_symbol", + "current_wx3_symbol", + ], +) # Create a dictionary for attaching units to the different variables -col_units = {'station_id': None, - 'latitude': 'degrees', - 'longitude': 'degrees', - 'elevation': 'meters', - 'date_time': None, - 'wind_direction': 'degrees', - 'wind_speed': 'kts', - 'eastward_wind': 'kts', - 'northward_wind': 'kts', - 'current_wx1': None, - 'current_wx2': None, - 'current_wx3': None, - 'low_cloud_type': None, - 'low_cloud_level': 'feet', - 'medium_cloud_type': None, - 'medium_cloud_level': 'feet', - 'high_cloud_type': None, - 'high_cloud_level': 'feet', - 'highest_cloud_type': None, - 'highest_cloud_level:': None, - 'cloud_coverage': None, - 'air_temperature': 'degC', - 'dew_point_temperature': 'degC', - 'altimeter': 'inHg', - 'air_pressure_at_sea_level': 'hPa', - 'present_weather': None, - 'past_weather': None, - 'past_weather2': None} +col_units = { + "station_id": None, + "latitude": "degrees", + "longitude": "degrees", + "elevation": "meters", + "date_time": None, + "wind_direction": "degrees", + "wind_speed": "kts", + "eastward_wind": "kts", + "northward_wind": "kts", + "current_wx1": None, + "current_wx2": None, + "current_wx3": None, + "low_cloud_type": None, + "low_cloud_level": "feet", + "medium_cloud_type": None, + "medium_cloud_level": "feet", + "high_cloud_type": None, + "high_cloud_level": "feet", + "highest_cloud_type": None, + "highest_cloud_level:": None, + "cloud_coverage": None, + "air_temperature": "degC", + "dew_point_temperature": "degC", + "altimeter": "inHg", + "air_pressure_at_sea_level": "hPa", + "present_weather": None, + "past_weather": None, + "past_weather2": None, +} @exporter.export @@ -127,56 +152,64 @@ def parse_metar_to_dataframe(metar_text, *, year=None, month=None): metar_vars = parse_metar_to_named_tuple(metar_text, station_info, year, month) # Use a pandas dataframe to store the data - df = pd.DataFrame({'station_id': metar_vars.station_id, - 'latitude': metar_vars.latitude, - 'longitude': metar_vars.longitude, - 'elevation': metar_vars.elevation, - 'date_time': metar_vars.date_time, - 'wind_direction': metar_vars.wind_direction, - 'wind_speed': metar_vars.wind_speed, - 'current_wx1': metar_vars.current_wx1, - 'current_wx2': metar_vars.current_wx2, - 'current_wx3': metar_vars.current_wx3, - 'low_cloud_type': metar_vars.skyc1, - 'low_cloud_level': metar_vars.skylev1, - 'medium_cloud_type': metar_vars.skyc2, - 'medium_cloud_level': metar_vars.skylev2, - 'high_cloud_type': metar_vars.skyc3, - 'high_cloud_level': metar_vars.skylev3, - 'highest_cloud_type': metar_vars.skyc4, - 'highest_cloud_level': metar_vars.skylev4, - 'cloud_coverage': metar_vars.cloudcover, - 'air_temperature': metar_vars.temperature, - 'dew_point_temperature': metar_vars.dewpoint, - 'altimeter': metar_vars.altimeter, - 'present_weather': metar_vars.current_wx1_symbol, - 'past_weather': metar_vars.current_wx2_symbol, - 'past_weather2': metar_vars.current_wx3_symbol}, - index=[metar_vars.station_id]) + df = pd.DataFrame( + { + "station_id": metar_vars.station_id, + "latitude": metar_vars.latitude, + "longitude": metar_vars.longitude, + "elevation": metar_vars.elevation, + "date_time": metar_vars.date_time, + "wind_direction": metar_vars.wind_direction, + "wind_speed": metar_vars.wind_speed, + "current_wx1": metar_vars.current_wx1, + "current_wx2": metar_vars.current_wx2, + "current_wx3": metar_vars.current_wx3, + "low_cloud_type": metar_vars.skyc1, + "low_cloud_level": metar_vars.skylev1, + "medium_cloud_type": metar_vars.skyc2, + "medium_cloud_level": metar_vars.skylev2, + "high_cloud_type": metar_vars.skyc3, + "high_cloud_level": metar_vars.skylev3, + "highest_cloud_type": metar_vars.skyc4, + "highest_cloud_level": metar_vars.skylev4, + "cloud_coverage": metar_vars.cloudcover, + "air_temperature": metar_vars.temperature, + "dew_point_temperature": metar_vars.dewpoint, + "altimeter": metar_vars.altimeter, + "present_weather": metar_vars.current_wx1_symbol, + "past_weather": metar_vars.current_wx2_symbol, + "past_weather2": metar_vars.current_wx3_symbol, + }, + index=[metar_vars.station_id], + ) # Convert to sea level pressure using calculation in metpy.calc try: # Create a field for sea-level pressure and make sure it is a float - df['air_pressure_at_sea_level'] = float(altimeter_to_sea_level_pressure( - df.altimeter.values * units('inHg'), - df.elevation.values * units('meters'), - df.temperature.values * units('degC')).to('hPa').magnitude) + df["air_pressure_at_sea_level"] = float( + altimeter_to_sea_level_pressure( + df.altimeter.values * units("inHg"), + df.elevation.values * units("meters"), + df.temperature.values * units("degC"), + ) + .to("hPa") + .magnitude + ) except AttributeError: - df['air_pressure_at_sea_level'] = [np.nan] + df["air_pressure_at_sea_level"] = [np.nan] # Use get wind components and assign them to u and v variables - df['eastward_wind'], df['northward_wind'] = wind_components((df.wind_speed.values - * units.kts), - df.wind_direction.values - * units.degree) + df["eastward_wind"], df["northward_wind"] = wind_components( + (df.wind_speed.values * units.kts), df.wind_direction.values * units.degree + ) # Round the altimeter and sea-level pressure values - df['altimeter'] = df.altimeter.round(2) - df['air_pressure_at_sea_level'] = df.air_pressure_at_sea_level.round(2) + df["altimeter"] = df.altimeter.round(2) + df["air_pressure_at_sea_level"] = df.air_pressure_at_sea_level.round(2) # Set the units for the dataframe--filter out warning from pandas with warnings.catch_warnings(): - warnings.simplefilter('ignore', UserWarning) + warnings.simplefilter("ignore", UserWarning) df.units = col_units return df @@ -265,12 +298,12 @@ def parse_metar_to_named_tuple(metar_text, station_metadata, year, month): # Set the wind values try: # If there are missing wind values, set wind speed and wind direction to nan - if ('/' in tree.wind.text) or (tree.wind.text == 'KT') or (tree.wind.text == ''): + if ("/" in tree.wind.text) or (tree.wind.text == "KT") or (tree.wind.text == ""): wind_dir = np.nan wind_spd = np.nan # If the wind direction is variable, set wind direction to nan but keep the wind speed else: - if (tree.wind.wind_dir.text == 'VRB') or (tree.wind.wind_dir.text == 'VAR'): + if (tree.wind.wind_dir.text == "VRB") or (tree.wind.wind_dir.text == "VAR"): wind_dir = np.nan wind_spd = float(tree.wind.wind_spd.text) else: @@ -284,7 +317,7 @@ def parse_metar_to_named_tuple(metar_text, station_metadata, year, month): # Set the weather symbols # If the weather symbol is missing, set values to nan - if tree.curwx.text == '': + if tree.curwx.text == "": current_wx1 = np.nan current_wx2 = np.nan current_wx3 = np.nan @@ -294,7 +327,7 @@ def parse_metar_to_named_tuple(metar_text, station_metadata, year, month): else: wx = [np.nan, np.nan, np.nan] # Loop through symbols and assign according WMO codes - wx[0:len((tree.curwx.text.strip()).split())] = tree.curwx.text.strip().split() + wx[0 : len((tree.curwx.text.strip()).split())] = tree.curwx.text.strip().split() current_wx1 = wx[0] current_wx2 = wx[1] current_wx3 = wx[2] @@ -312,8 +345,8 @@ def parse_metar_to_named_tuple(metar_text, station_metadata, year, month): current_wx3_symbol = 0 # Set the sky conditions - if tree.skyc.text[1:3] == 'VV': - skyc1 = 'VV' + if tree.skyc.text[1:3] == "VV": + skyc1 = "VV" skylev1 = tree.skyc.text.strip()[2:] skyc2 = np.nan skylev2 = np.nan @@ -324,16 +357,16 @@ def parse_metar_to_named_tuple(metar_text, station_metadata, year, month): else: skyc = [] - skyc[0:len((tree.skyc.text.strip()).split())] = tree.skyc.text.strip().split() + skyc[0 : len((tree.skyc.text.strip()).split())] = tree.skyc.text.strip().split() try: skyc1 = skyc[0][0:3] - if '/' in skyc1: + if "/" in skyc1: skyc1 = np.nan except (IndexError, ValueError, TypeError): skyc1 = np.nan try: skylev1 = skyc[0][3:] - if '/' in skylev1: + if "/" in skylev1: skylev1 = np.nan else: skylev1 = float(skylev1) * 100 @@ -341,13 +374,13 @@ def parse_metar_to_named_tuple(metar_text, station_metadata, year, month): skylev1 = np.nan try: skyc2 = skyc[1][0:3] - if '/' in skyc2: + if "/" in skyc2: skyc2 = np.nan except (IndexError, ValueError, TypeError): skyc2 = np.nan try: skylev2 = skyc[1][3:] - if '/' in skylev2: + if "/" in skylev2: skylev2 = np.nan else: skylev2 = float(skylev2) * 100 @@ -355,13 +388,13 @@ def parse_metar_to_named_tuple(metar_text, station_metadata, year, month): skylev2 = np.nan try: skyc3 = skyc[2][0:3] - if '/' in skyc3: + if "/" in skyc3: skyc3 = np.nan except (IndexError, ValueError): skyc3 = np.nan try: skylev3 = skyc[2][3:] - if '/' in skylev3: + if "/" in skylev3: skylev3 = np.nan else: skylev3 = float(skylev3) * 100 @@ -369,13 +402,13 @@ def parse_metar_to_named_tuple(metar_text, station_metadata, year, month): skylev3 = np.nan try: skyc4 = skyc[3][0:3] - if '/' in skyc4: + if "/" in skyc4: skyc4 = np.nan except (IndexError, ValueError, TypeError): skyc4 = np.nan try: skylev4 = skyc[3][3:] - if '/' in skylev4: + if "/" in skylev4: skylev4 = np.nan else: skylev4 = float(skylev4) * 100 @@ -383,54 +416,81 @@ def parse_metar_to_named_tuple(metar_text, station_metadata, year, month): skylev4 = np.nan # Set the cloud cover variable (measured in oktas) - if ('OVC' or 'VV') in tree.skyc.text: + if ("OVC" or "VV") in tree.skyc.text: cloudcover = 8 - elif 'BKN' in tree.skyc.text: + elif "BKN" in tree.skyc.text: cloudcover = 6 - elif 'SCT' in tree.skyc.text: + elif "SCT" in tree.skyc.text: cloudcover = 4 - elif 'FEW' in tree.skyc.text: + elif "FEW" in tree.skyc.text: cloudcover = 2 - elif ('SKC' in tree.skyc.text) or ('NCD' in tree.skyc.text) \ - or ('NSC' in tree.skyc.text) or 'CLR' in tree.skyc.text: + elif ( + ("SKC" in tree.skyc.text) + or ("NCD" in tree.skyc.text) + or ("NSC" in tree.skyc.text) + or "CLR" in tree.skyc.text + ): cloudcover = 0 else: cloudcover = 10 # Set the temperature and dewpoint - if (tree.temp_dewp.text == '') or (tree.temp_dewp.text == ' MM/MM'): + if (tree.temp_dewp.text == "") or (tree.temp_dewp.text == " MM/MM"): temp = np.nan dewp = np.nan else: try: - if 'M' in tree.temp_dewp.temp.text: - temp = (-1 * float(tree.temp_dewp.temp.text[-2:])) + if "M" in tree.temp_dewp.temp.text: + temp = -1 * float(tree.temp_dewp.temp.text[-2:]) else: temp = float(tree.temp_dewp.temp.text[-2:]) except ValueError: temp = np.nan try: - if 'M' in tree.temp_dewp.dewp.text: - dewp = (-1 * float(tree.temp_dewp.dewp.text[-2:])) + if "M" in tree.temp_dewp.dewp.text: + dewp = -1 * float(tree.temp_dewp.dewp.text[-2:]) else: dewp = float(tree.temp_dewp.dewp.text[-2:]) except ValueError: dewp = np.nan # Set the altimeter value and sea level pressure - if tree.altim.text == '': + if tree.altim.text == "": altim = np.nan else: if (float(tree.altim.text.strip()[1:5])) > 1100: altim = float(tree.altim.text.strip()[1:5]) / 100 else: - altim = (int(tree.altim.text.strip()[1:5]) * units.hPa).to('inHg').magnitude + altim = (int(tree.altim.text.strip()[1:5]) * units.hPa).to("inHg").magnitude # Returns a named tuple with all the relevant variables - return Metar(station_id, lat, lon, elev, date_time, wind_dir, wind_spd, - current_wx1, current_wx2, current_wx3, skyc1, skylev1, skyc2, - skylev2, skyc3, skylev3, skyc4, skylev4, cloudcover, temp, dewp, - altim, current_wx1_symbol, current_wx2_symbol, current_wx3_symbol) + return Metar( + station_id, + lat, + lon, + elev, + date_time, + wind_dir, + wind_spd, + current_wx1, + current_wx2, + current_wx3, + skyc1, + skylev1, + skyc2, + skylev2, + skyc3, + skylev3, + skyc4, + skylev4, + cloudcover, + temp, + dewp, + altim, + current_wx1_symbol, + current_wx2_symbol, + current_wx3_symbol, + ) @exporter.export @@ -490,24 +550,24 @@ def parse_metar_file(filename, *, year=None, month=None): month = now.month if month is None else month # Function to merge METARs - def merge(x, key=' '): + def merge(x, key=" "): tmp = [] for i in x: - if (i[0:len(key)] != key) and len(tmp): - yield ' '.join(tmp) + if (i[0 : len(key)] != key) and len(tmp): + yield " ".join(tmp) tmp = [] if i.startswith(key): i = i[5:] tmp.append(i) if len(tmp): - yield ' '.join(tmp) + yield " ".join(tmp) # Open the file - myfile = open_as_needed(filename, 'rt') + myfile = open_as_needed(filename, "rt") # Clean up the file and take out the next line (\n) value = myfile.read().rstrip() - list_values = value.split('\n') + list_values = value.split("\n") list_values = list(filter(None, list_values)) # Call the merge function and assign the result to the list of metars @@ -588,55 +648,61 @@ def merge(x, key=' '): except ParseError: continue - df = pd.DataFrame({'station_id': station_id, - 'latitude': lat, - 'longitude': lon, - 'elevation': elev, - 'date_time': date_time, - 'wind_direction': wind_dir, - 'wind_speed': wind_spd, - 'current_wx1': current_wx1, - 'current_wx2': current_wx2, - 'current_wx3': current_wx3, - 'low_cloud_type': skyc1, - 'low_cloud_level': skylev1, - 'medium_cloud_type': skyc2, - 'medium_cloud_level': skylev2, - 'high_cloud_type': skyc3, - 'high_cloud_level': skylev3, - 'highest_cloud_type': skyc4, - 'highest_cloud_level': skylev4, - 'cloud_coverage': cloudcover, - 'air_temperature': temp, - 'dew_point_temperature': dewp, - 'altimeter': altim, - 'present_weather': current_wx1_symbol, - 'past_weather': current_wx2_symbol, - 'past_weather2': current_wx3_symbol}, - index=station_id) + df = pd.DataFrame( + { + "station_id": station_id, + "latitude": lat, + "longitude": lon, + "elevation": elev, + "date_time": date_time, + "wind_direction": wind_dir, + "wind_speed": wind_spd, + "current_wx1": current_wx1, + "current_wx2": current_wx2, + "current_wx3": current_wx3, + "low_cloud_type": skyc1, + "low_cloud_level": skylev1, + "medium_cloud_type": skyc2, + "medium_cloud_level": skylev2, + "high_cloud_type": skyc3, + "high_cloud_level": skylev3, + "highest_cloud_type": skyc4, + "highest_cloud_level": skylev4, + "cloud_coverage": cloudcover, + "air_temperature": temp, + "dew_point_temperature": dewp, + "altimeter": altim, + "present_weather": current_wx1_symbol, + "past_weather": current_wx2_symbol, + "past_weather2": current_wx3_symbol, + }, + index=station_id, + ) # Calculate sea-level pressure from function in metpy.calc - df['air_pressure_at_sea_level'] = altimeter_to_sea_level_pressure( - altim * units('inHg'), - elev * units('meters'), - temp * units('degC')).to('hPa').magnitude + df["air_pressure_at_sea_level"] = ( + altimeter_to_sea_level_pressure( + altim * units("inHg"), elev * units("meters"), temp * units("degC") + ) + .to("hPa") + .magnitude + ) # Use get wind components and assign them to eastward and northward winds - df['eastward_wind'], df['northward_wind'] = wind_components((df.wind_speed.values - * units.kts), - df.wind_direction.values - * units.degree) + df["eastward_wind"], df["northward_wind"] = wind_components( + (df.wind_speed.values * units.kts), df.wind_direction.values * units.degree + ) # Drop duplicate values - df = df.drop_duplicates(subset=['date_time', 'latitude', 'longitude'], keep='last') + df = df.drop_duplicates(subset=["date_time", "latitude", "longitude"], keep="last") # Round altimeter and sea-level pressure values - df['altimeter'] = df.altimeter.round(2) - df['air_pressure_at_sea_level'] = df.air_pressure_at_sea_level.round(2) + df["altimeter"] = df.altimeter.round(2) + df["air_pressure_at_sea_level"] = df.air_pressure_at_sea_level.round(2) # Set the units for the dataframe--filter out warning from Pandas with warnings.catch_warnings(): - warnings.simplefilter('ignore', UserWarning) + warnings.simplefilter("ignore", UserWarning) df.units = col_units return df diff --git a/src/metpy/io/metar_parser.py b/src/metpy/io/metar_parser.py index 6a09f28762b..04dd001d3f3 100644 --- a/src/metpy/io/metar_parser.py +++ b/src/metpy/io/metar_parser.py @@ -95,63 +95,63 @@ class ParseError(SyntaxError): class Grammar: - REGEX_1 = re.compile('^[0-9A-Z]') - REGEX_2 = re.compile('^[0-9A-Z]') - REGEX_3 = re.compile('^[0-9A-Z]') - REGEX_4 = re.compile('^[0-9A-Z]') - REGEX_5 = re.compile('^[\\d]') - REGEX_6 = re.compile('^[\\d]') - REGEX_7 = re.compile('^[\\d]') - REGEX_8 = re.compile('^[\\d]') - REGEX_9 = re.compile('^[\\d]') - REGEX_10 = re.compile('^[\\d]') - REGEX_11 = re.compile('^[\\d]') - REGEX_12 = re.compile('^[\\d]') - REGEX_13 = re.compile('^[\\d]') - REGEX_14 = re.compile('^[\\d]') - REGEX_15 = re.compile('^[\\d]') - REGEX_16 = re.compile('^[\\d]') - REGEX_17 = re.compile('^[\\d]') - REGEX_18 = re.compile('^[\\d]') - REGEX_19 = re.compile('^[\\d]') - REGEX_20 = re.compile('^[\\d]') - REGEX_21 = re.compile('^[\\d]') - REGEX_22 = re.compile('^[\\d]') - REGEX_23 = re.compile('^[\\d]') - REGEX_24 = re.compile('^[\\d]') - REGEX_25 = re.compile('^[\\d]') - REGEX_26 = re.compile('^[\\d]') - REGEX_27 = re.compile('^[LRC]') - REGEX_28 = re.compile('^[\\d]') - REGEX_29 = re.compile('^[\\d]') - REGEX_30 = re.compile('^[LRC]') - REGEX_31 = re.compile('^[\\d]') - REGEX_32 = re.compile('^[\\d]') - REGEX_33 = re.compile('^[\\d]') - REGEX_34 = re.compile('^[\\d]') + REGEX_1 = re.compile("^[0-9A-Z]") + REGEX_2 = re.compile("^[0-9A-Z]") + REGEX_3 = re.compile("^[0-9A-Z]") + REGEX_4 = re.compile("^[0-9A-Z]") + REGEX_5 = re.compile("^[\\d]") + REGEX_6 = re.compile("^[\\d]") + REGEX_7 = re.compile("^[\\d]") + REGEX_8 = re.compile("^[\\d]") + REGEX_9 = re.compile("^[\\d]") + REGEX_10 = re.compile("^[\\d]") + REGEX_11 = re.compile("^[\\d]") + REGEX_12 = re.compile("^[\\d]") + REGEX_13 = re.compile("^[\\d]") + REGEX_14 = re.compile("^[\\d]") + REGEX_15 = re.compile("^[\\d]") + REGEX_16 = re.compile("^[\\d]") + REGEX_17 = re.compile("^[\\d]") + REGEX_18 = re.compile("^[\\d]") + REGEX_19 = re.compile("^[\\d]") + REGEX_20 = re.compile("^[\\d]") + REGEX_21 = re.compile("^[\\d]") + REGEX_22 = re.compile("^[\\d]") + REGEX_23 = re.compile("^[\\d]") + REGEX_24 = re.compile("^[\\d]") + REGEX_25 = re.compile("^[\\d]") + REGEX_26 = re.compile("^[\\d]") + REGEX_27 = re.compile("^[LRC]") + REGEX_28 = re.compile("^[\\d]") + REGEX_29 = re.compile("^[\\d]") + REGEX_30 = re.compile("^[LRC]") + REGEX_31 = re.compile("^[\\d]") + REGEX_32 = re.compile("^[\\d]") + REGEX_33 = re.compile("^[\\d]") + REGEX_34 = re.compile("^[\\d]") REGEX_35 = re.compile('^["M" \\/ "P"]') - REGEX_36 = re.compile('^[\\d]') - REGEX_37 = re.compile('^[\\d]') - REGEX_38 = re.compile('^[\\d]') - REGEX_39 = re.compile('^[\\d]') - REGEX_40 = re.compile('^[-+]') - REGEX_41 = re.compile('^[-+]') - REGEX_42 = re.compile('^[\\d]') - REGEX_43 = re.compile('^[M]') - REGEX_44 = re.compile('^[\\d]') - REGEX_45 = re.compile('^[\\d]') - REGEX_46 = re.compile('^[M]') - REGEX_47 = re.compile('^[\\d]') - REGEX_48 = re.compile('^[\\d]') + REGEX_36 = re.compile("^[\\d]") + REGEX_37 = re.compile("^[\\d]") + REGEX_38 = re.compile("^[\\d]") + REGEX_39 = re.compile("^[\\d]") + REGEX_40 = re.compile("^[-+]") + REGEX_41 = re.compile("^[-+]") + REGEX_42 = re.compile("^[\\d]") + REGEX_43 = re.compile("^[M]") + REGEX_44 = re.compile("^[\\d]") + REGEX_45 = re.compile("^[\\d]") + REGEX_46 = re.compile("^[M]") + REGEX_47 = re.compile("^[\\d]") + REGEX_48 = re.compile("^[\\d]") REGEX_49 = re.compile('^["Q" \\/ "A"]') - REGEX_50 = re.compile('^[\\d]') - REGEX_51 = re.compile('^[\\d]') - REGEX_52 = re.compile('^[\\d]') - REGEX_53 = re.compile('^[\\d]') + REGEX_50 = re.compile("^[\\d]") + REGEX_51 = re.compile("^[\\d]") + REGEX_52 = re.compile("^[\\d]") + REGEX_53 = re.compile("^[\\d]") def _read_ob(self): address0, index0 = FAILURE, self._offset - cached = self._cache['ob'].get(index0) + cached = self._cache["ob"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -250,14 +250,14 @@ def _read_ob(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode1(self._input[index1:self._offset], index1, elements0) + address0 = TreeNode1(self._input[index1 : self._offset], index1, elements0) self._offset = self._offset - self._cache['ob'][index0] = (address0, self._offset) + self._cache["ob"][index0] = (address0, self._offset) return address0 def _read_metar(self): address0, index0 = FAILURE, self._offset - cached = self._cache['metar'].get(index0) + cached = self._cache["metar"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -267,9 +267,9 @@ def _read_metar(self): index3 = self._offset chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 5] - if chunk0 == 'METAR': - address1 = TreeNode(self._input[self._offset:self._offset + 5], self._offset) + chunk0 = self._input[self._offset : self._offset + 5] + if chunk0 == "METAR": + address1 = TreeNode(self._input[self._offset : self._offset + 5], self._offset) self._offset = self._offset + 5 else: address1 = FAILURE @@ -282,9 +282,9 @@ def _read_metar(self): self._offset = index3 chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 5] - if chunk1 == 'SPECI': - address1 = TreeNode(self._input[self._offset:self._offset + 5], self._offset) + chunk1 = self._input[self._offset : self._offset + 5] + if chunk1 == "SPECI": + address1 = TreeNode(self._input[self._offset : self._offset + 5], self._offset) self._offset = self._offset + 5 else: address1 = FAILURE @@ -305,9 +305,9 @@ def _read_metar(self): index5 = self._offset chunk2 = None if self._offset < self._input_size: - chunk2 = self._input[self._offset:self._offset + 5] - if chunk2 == ' AUTO': - address2 = TreeNode(self._input[self._offset:self._offset + 5], self._offset) + chunk2 = self._input[self._offset : self._offset + 5] + if chunk2 == " AUTO": + address2 = TreeNode(self._input[self._offset : self._offset + 5], self._offset) self._offset = self._offset + 5 else: address2 = FAILURE @@ -320,9 +320,11 @@ def _read_metar(self): self._offset = index5 chunk3 = None if self._offset < self._input_size: - chunk3 = self._input[self._offset:self._offset + 4] - if chunk3 == ' COR': - address2 = TreeNode(self._input[self._offset:self._offset + 4], self._offset) + chunk3 = self._input[self._offset : self._offset + 4] + if chunk3 == " COR": + address2 = TreeNode( + self._input[self._offset : self._offset + 4], self._offset + ) self._offset = self._offset + 4 else: address2 = FAILURE @@ -347,14 +349,14 @@ def _read_metar(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode(self._input[index1:self._offset], index1, elements0) + address0 = TreeNode(self._input[index1 : self._offset], index1, elements0) self._offset = self._offset - self._cache['metar'][index0] = (address0, self._offset) + self._cache["metar"][index0] = (address0, self._offset) return address0 def _read_sep(self): address0, index0 = FAILURE, self._offset - cached = self._cache['sep'].get(index0) + cached = self._cache["sep"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -362,9 +364,9 @@ def _read_sep(self): while address1 is not FAILURE: chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 1] - if chunk0 == ' ': - address1 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk0 = self._input[self._offset : self._offset + 1] + if chunk0 == " ": + address1 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address1 = FAILURE @@ -377,16 +379,16 @@ def _read_sep(self): elements0.append(address1) remaining0 -= 1 if remaining0 <= 0: - address0 = TreeNode(self._input[index1:self._offset], index1, elements0) + address0 = TreeNode(self._input[index1 : self._offset], index1, elements0) self._offset = self._offset else: address0 = FAILURE - self._cache['sep'][index0] = (address0, self._offset) + self._cache["sep"][index0] = (address0, self._offset) return address0 def _read_siteid(self): address0, index0 = FAILURE, self._offset - cached = self._cache['siteid'].get(index0) + cached = self._cache["siteid"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -402,9 +404,9 @@ def _read_siteid(self): address2 = FAILURE chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 1] + chunk0 = self._input[self._offset : self._offset + 1] if chunk0 is not None and Grammar.REGEX_1.search(chunk0): - address2 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address2 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address2 = FAILURE @@ -412,15 +414,17 @@ def _read_siteid(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[0-9A-Z]') + self._expected.append("[0-9A-Z]") if address2 is not FAILURE: elements0.append(address2) address3 = FAILURE chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 1] + chunk1 = self._input[self._offset : self._offset + 1] if chunk1 is not None and Grammar.REGEX_2.search(chunk1): - address3 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address3 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address3 = FAILURE @@ -428,15 +432,17 @@ def _read_siteid(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[0-9A-Z]') + self._expected.append("[0-9A-Z]") if address3 is not FAILURE: elements0.append(address3) address4 = FAILURE chunk2 = None if self._offset < self._input_size: - chunk2 = self._input[self._offset:self._offset + 1] + chunk2 = self._input[self._offset : self._offset + 1] if chunk2 is not None and Grammar.REGEX_3.search(chunk2): - address4 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address4 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address4 = FAILURE @@ -444,15 +450,17 @@ def _read_siteid(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[0-9A-Z]') + self._expected.append("[0-9A-Z]") if address4 is not FAILURE: elements0.append(address4) address5 = FAILURE chunk3 = None if self._offset < self._input_size: - chunk3 = self._input[self._offset:self._offset + 1] + chunk3 = self._input[self._offset : self._offset + 1] if chunk3 is not None and Grammar.REGEX_4.search(chunk3): - address5 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address5 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address5 = FAILURE @@ -460,7 +468,7 @@ def _read_siteid(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[0-9A-Z]') + self._expected.append("[0-9A-Z]") if address5 is not FAILURE: elements0.append(address5) else: @@ -481,14 +489,14 @@ def _read_siteid(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode(self._input[index1:self._offset], index1, elements0) + address0 = TreeNode(self._input[index1 : self._offset], index1, elements0) self._offset = self._offset - self._cache['siteid'][index0] = (address0, self._offset) + self._cache["siteid"][index0] = (address0, self._offset) return address0 def _read_datetime(self): address0, index0 = FAILURE, self._offset - cached = self._cache['datetime'].get(index0) + cached = self._cache["datetime"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -502,9 +510,11 @@ def _read_datetime(self): while address3 is not FAILURE: chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 1] + chunk0 = self._input[self._offset : self._offset + 1] if chunk0 is not None and Grammar.REGEX_5.search(chunk0): - address3 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address3 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address3 = FAILURE @@ -512,12 +522,12 @@ def _read_datetime(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address3 is not FAILURE: elements1.append(address3) remaining0 -= 1 if remaining0 <= 0: - address2 = TreeNode(self._input[index2:self._offset], index2, elements1) + address2 = TreeNode(self._input[index2 : self._offset], index2, elements1) self._offset = self._offset else: address2 = FAILURE @@ -526,9 +536,11 @@ def _read_datetime(self): address4 = FAILURE chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 1] - if chunk1 == 'Z': - address4 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk1 = self._input[self._offset : self._offset + 1] + if chunk1 == "Z": + address4 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address4 = FAILURE @@ -551,14 +563,14 @@ def _read_datetime(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode2(self._input[index1:self._offset], index1, elements0) + address0 = TreeNode2(self._input[index1 : self._offset], index1, elements0) self._offset = self._offset - self._cache['datetime'][index0] = (address0, self._offset) + self._cache["datetime"][index0] = (address0, self._offset) return address0 def _read_auto(self): address0, index0 = FAILURE, self._offset - cached = self._cache['auto'].get(index0) + cached = self._cache["auto"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -572,9 +584,9 @@ def _read_auto(self): index3 = self._offset chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 4] - if chunk0 == 'AUTO': - address2 = TreeNode(self._input[self._offset:self._offset + 4], self._offset) + chunk0 = self._input[self._offset : self._offset + 4] + if chunk0 == "AUTO": + address2 = TreeNode(self._input[self._offset : self._offset + 4], self._offset) self._offset = self._offset + 4 else: address2 = FAILURE @@ -587,9 +599,11 @@ def _read_auto(self): self._offset = index3 chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 3] - if chunk1 == 'COR': - address2 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk1 = self._input[self._offset : self._offset + 3] + if chunk1 == "COR": + address2 = TreeNode( + self._input[self._offset : self._offset + 3], self._offset + ) self._offset = self._offset + 3 else: address2 = FAILURE @@ -614,14 +628,14 @@ def _read_auto(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode3(self._input[index1:self._offset], index1, elements0) + address0 = TreeNode3(self._input[index1 : self._offset], index1, elements0) self._offset = self._offset - self._cache['auto'][index0] = (address0, self._offset) + self._cache["auto"][index0] = (address0, self._offset) return address0 def _read_wind(self): address0, index0 = FAILURE, self._offset - cached = self._cache['wind'].get(index0) + cached = self._cache["wind"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -655,9 +669,11 @@ def _read_wind(self): index5 = self._offset chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 2] - if chunk0 == 'KT': - address5 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk0 = self._input[self._offset : self._offset + 2] + if chunk0 == "KT": + address5 = TreeNode( + self._input[self._offset : self._offset + 2], self._offset + ) self._offset = self._offset + 2 else: address5 = FAILURE @@ -670,9 +686,11 @@ def _read_wind(self): self._offset = index5 chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 3] - if chunk1 == 'MPS': - address5 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk1 = self._input[self._offset : self._offset + 3] + if chunk1 == "MPS": + address5 = TreeNode( + self._input[self._offset : self._offset + 3], self._offset + ) self._offset = self._offset + 3 else: address5 = FAILURE @@ -714,17 +732,17 @@ def _read_wind(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode4(self._input[index2:self._offset], index2, elements0) + address0 = TreeNode4(self._input[index2 : self._offset], index2, elements0) self._offset = self._offset if address0 is FAILURE: address0 = TreeNode(self._input[index1:index1], index1) self._offset = index1 - self._cache['wind'][index0] = (address0, self._offset) + self._cache["wind"][index0] = (address0, self._offset) return address0 def _read_wind_dir(self): address0, index0 = FAILURE, self._offset - cached = self._cache['wind_dir'].get(index0) + cached = self._cache["wind_dir"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -734,9 +752,9 @@ def _read_wind_dir(self): address1 = FAILURE chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 1] + chunk0 = self._input[self._offset : self._offset + 1] if chunk0 is not None and Grammar.REGEX_6.search(chunk0): - address1 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address1 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address1 = FAILURE @@ -744,15 +762,15 @@ def _read_wind_dir(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address1 is not FAILURE: elements0.append(address1) address2 = FAILURE chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 1] + chunk1 = self._input[self._offset : self._offset + 1] if chunk1 is not None and Grammar.REGEX_7.search(chunk1): - address2 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address2 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address2 = FAILURE @@ -760,15 +778,17 @@ def _read_wind_dir(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address2 is not FAILURE: elements0.append(address2) address3 = FAILURE chunk2 = None if self._offset < self._input_size: - chunk2 = self._input[self._offset:self._offset + 1] + chunk2 = self._input[self._offset : self._offset + 1] if chunk2 is not None and Grammar.REGEX_8.search(chunk2): - address3 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address3 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address3 = FAILURE @@ -776,7 +796,7 @@ def _read_wind_dir(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address3 is not FAILURE: elements0.append(address3) else: @@ -791,15 +811,15 @@ def _read_wind_dir(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode(self._input[index3:self._offset], index3, elements0) + address0 = TreeNode(self._input[index3 : self._offset], index3, elements0) self._offset = self._offset if address0 is FAILURE: self._offset = index2 chunk3 = None if self._offset < self._input_size: - chunk3 = self._input[self._offset:self._offset + 3] - if chunk3 == 'VAR': - address0 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk3 = self._input[self._offset : self._offset + 3] + if chunk3 == "VAR": + address0 = TreeNode(self._input[self._offset : self._offset + 3], self._offset) self._offset = self._offset + 3 else: address0 = FAILURE @@ -807,14 +827,16 @@ def _read_wind_dir(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('\'VAR\'') + self._expected.append("'VAR'") if address0 is FAILURE: self._offset = index2 chunk4 = None if self._offset < self._input_size: - chunk4 = self._input[self._offset:self._offset + 3] - if chunk4 == 'VRB': - address0 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk4 = self._input[self._offset : self._offset + 3] + if chunk4 == "VRB": + address0 = TreeNode( + self._input[self._offset : self._offset + 3], self._offset + ) self._offset = self._offset + 3 else: address0 = FAILURE @@ -822,14 +844,16 @@ def _read_wind_dir(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('\'VRB\'') + self._expected.append("'VRB'") if address0 is FAILURE: self._offset = index2 chunk5 = None if self._offset < self._input_size: - chunk5 = self._input[self._offset:self._offset + 3] - if chunk5 == '///': - address0 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk5 = self._input[self._offset : self._offset + 3] + if chunk5 == "///": + address0 = TreeNode( + self._input[self._offset : self._offset + 3], self._offset + ) self._offset = self._offset + 3 else: address0 = FAILURE @@ -843,12 +867,12 @@ def _read_wind_dir(self): if address0 is FAILURE: address0 = TreeNode(self._input[index1:index1], index1) self._offset = index1 - self._cache['wind_dir'][index0] = (address0, self._offset) + self._cache["wind_dir"][index0] = (address0, self._offset) return address0 def _read_wind_spd(self): address0, index0 = FAILURE, self._offset - cached = self._cache['wind_spd'].get(index0) + cached = self._cache["wind_spd"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -858,9 +882,9 @@ def _read_wind_spd(self): address1 = FAILURE chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 1] + chunk0 = self._input[self._offset : self._offset + 1] if chunk0 is not None and Grammar.REGEX_9.search(chunk0): - address1 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address1 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address1 = FAILURE @@ -868,15 +892,15 @@ def _read_wind_spd(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address1 is not FAILURE: elements0.append(address1) address2 = FAILURE chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 1] + chunk1 = self._input[self._offset : self._offset + 1] if chunk1 is not None and Grammar.REGEX_10.search(chunk1): - address2 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address2 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address2 = FAILURE @@ -884,16 +908,18 @@ def _read_wind_spd(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address2 is not FAILURE: elements0.append(address2) address3 = FAILURE index4 = self._offset chunk2 = None if self._offset < self._input_size: - chunk2 = self._input[self._offset:self._offset + 1] + chunk2 = self._input[self._offset : self._offset + 1] if chunk2 is not None and Grammar.REGEX_11.search(chunk2): - address3 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address3 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address3 = FAILURE @@ -901,7 +927,7 @@ def _read_wind_spd(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address3 is FAILURE: address3 = TreeNode(self._input[index4:index4], index4) self._offset = index4 @@ -919,15 +945,15 @@ def _read_wind_spd(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode(self._input[index3:self._offset], index3, elements0) + address0 = TreeNode(self._input[index3 : self._offset], index3, elements0) self._offset = self._offset if address0 is FAILURE: self._offset = index2 chunk3 = None if self._offset < self._input_size: - chunk3 = self._input[self._offset:self._offset + 2] - if chunk3 == '//': - address0 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk3 = self._input[self._offset : self._offset + 2] + if chunk3 == "//": + address0 = TreeNode(self._input[self._offset : self._offset + 2], self._offset) self._offset = self._offset + 2 else: address0 = FAILURE @@ -941,12 +967,12 @@ def _read_wind_spd(self): if address0 is FAILURE: address0 = TreeNode(self._input[index1:index1], index1) self._offset = index1 - self._cache['wind_spd'][index0] = (address0, self._offset) + self._cache["wind_spd"][index0] = (address0, self._offset) return address0 def _read_gust(self): address0, index0 = FAILURE, self._offset - cached = self._cache['gust'].get(index0) + cached = self._cache["gust"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -954,9 +980,9 @@ def _read_gust(self): address1 = FAILURE chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 1] - if chunk0 == 'G': - address1 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk0 = self._input[self._offset : self._offset + 1] + if chunk0 == "G": + address1 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address1 = FAILURE @@ -972,9 +998,11 @@ def _read_gust(self): while address3 is not FAILURE: chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 1] + chunk1 = self._input[self._offset : self._offset + 1] if chunk1 is not None and Grammar.REGEX_12.search(chunk1): - address3 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address3 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address3 = FAILURE @@ -982,12 +1010,12 @@ def _read_gust(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address3 is not FAILURE: elements1.append(address3) remaining0 -= 1 if remaining0 <= 0: - address2 = TreeNode(self._input[index2:self._offset], index2, elements1) + address2 = TreeNode(self._input[index2 : self._offset], index2, elements1) self._offset = self._offset else: address2 = FAILURE @@ -1002,14 +1030,14 @@ def _read_gust(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode(self._input[index1:self._offset], index1, elements0) + address0 = TreeNode(self._input[index1 : self._offset], index1, elements0) self._offset = self._offset - self._cache['gust'][index0] = (address0, self._offset) + self._cache["gust"][index0] = (address0, self._offset) return address0 def _read_varwind(self): address0, index0 = FAILURE, self._offset - cached = self._cache['varwind'].get(index0) + cached = self._cache["varwind"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -1021,9 +1049,9 @@ def _read_varwind(self): address2 = FAILURE chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 1] + chunk0 = self._input[self._offset : self._offset + 1] if chunk0 is not None and Grammar.REGEX_13.search(chunk0): - address2 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address2 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address2 = FAILURE @@ -1031,15 +1059,17 @@ def _read_varwind(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address2 is not FAILURE: elements0.append(address2) address3 = FAILURE chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 1] + chunk1 = self._input[self._offset : self._offset + 1] if chunk1 is not None and Grammar.REGEX_14.search(chunk1): - address3 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address3 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address3 = FAILURE @@ -1047,15 +1077,17 @@ def _read_varwind(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address3 is not FAILURE: elements0.append(address3) address4 = FAILURE chunk2 = None if self._offset < self._input_size: - chunk2 = self._input[self._offset:self._offset + 1] + chunk2 = self._input[self._offset : self._offset + 1] if chunk2 is not None and Grammar.REGEX_15.search(chunk2): - address4 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address4 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address4 = FAILURE @@ -1063,15 +1095,17 @@ def _read_varwind(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address4 is not FAILURE: elements0.append(address4) address5 = FAILURE chunk3 = None if self._offset < self._input_size: - chunk3 = self._input[self._offset:self._offset + 1] - if chunk3 == 'V': - address5 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk3 = self._input[self._offset : self._offset + 1] + if chunk3 == "V": + address5 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address5 = FAILURE @@ -1085,9 +1119,11 @@ def _read_varwind(self): address6 = FAILURE chunk4 = None if self._offset < self._input_size: - chunk4 = self._input[self._offset:self._offset + 1] + chunk4 = self._input[self._offset : self._offset + 1] if chunk4 is not None and Grammar.REGEX_16.search(chunk4): - address6 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address6 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address6 = FAILURE @@ -1095,15 +1131,18 @@ def _read_varwind(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address6 is not FAILURE: elements0.append(address6) address7 = FAILURE chunk5 = None if self._offset < self._input_size: - chunk5 = self._input[self._offset:self._offset + 1] + chunk5 = self._input[self._offset : self._offset + 1] if chunk5 is not None and Grammar.REGEX_17.search(chunk5): - address7 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address7 = TreeNode( + self._input[self._offset : self._offset + 1], + self._offset, + ) self._offset = self._offset + 1 else: address7 = FAILURE @@ -1111,15 +1150,18 @@ def _read_varwind(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address7 is not FAILURE: elements0.append(address7) address8 = FAILURE chunk6 = None if self._offset < self._input_size: - chunk6 = self._input[self._offset:self._offset + 1] + chunk6 = self._input[self._offset : self._offset + 1] if chunk6 is not None and Grammar.REGEX_18.search(chunk6): - address8 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address8 = TreeNode( + self._input[self._offset : self._offset + 1], + self._offset, + ) self._offset = self._offset + 1 else: address8 = FAILURE @@ -1127,7 +1169,7 @@ def _read_varwind(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address8 is not FAILURE: elements0.append(address8) else: @@ -1157,14 +1199,14 @@ def _read_varwind(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode5(self._input[index1:self._offset], index1, elements0) + address0 = TreeNode5(self._input[index1 : self._offset], index1, elements0) self._offset = self._offset - self._cache['varwind'][index0] = (address0, self._offset) + self._cache["varwind"][index0] = (address0, self._offset) return address0 def _read_vis(self): address0, index0 = FAILURE, self._offset - cached = self._cache['vis'].get(index0) + cached = self._cache["vis"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -1180,9 +1222,9 @@ def _read_vis(self): address3 = FAILURE chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 1] + chunk0 = self._input[self._offset : self._offset + 1] if chunk0 is not None and Grammar.REGEX_19.search(chunk0): - address3 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address3 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address3 = FAILURE @@ -1190,15 +1232,17 @@ def _read_vis(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address3 is not FAILURE: elements1.append(address3) address4 = FAILURE chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 1] + chunk1 = self._input[self._offset : self._offset + 1] if chunk1 is not None and Grammar.REGEX_20.search(chunk1): - address4 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address4 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address4 = FAILURE @@ -1206,15 +1250,17 @@ def _read_vis(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address4 is not FAILURE: elements1.append(address4) address5 = FAILURE chunk2 = None if self._offset < self._input_size: - chunk2 = self._input[self._offset:self._offset + 1] + chunk2 = self._input[self._offset : self._offset + 1] if chunk2 is not None and Grammar.REGEX_21.search(chunk2): - address5 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address5 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address5 = FAILURE @@ -1222,15 +1268,17 @@ def _read_vis(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address5 is not FAILURE: elements1.append(address5) address6 = FAILURE chunk3 = None if self._offset < self._input_size: - chunk3 = self._input[self._offset:self._offset + 1] + chunk3 = self._input[self._offset : self._offset + 1] if chunk3 is not None and Grammar.REGEX_22.search(chunk3): - address6 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address6 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address6 = FAILURE @@ -1238,16 +1286,18 @@ def _read_vis(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address6 is not FAILURE: elements1.append(address6) address7 = FAILURE index5 = self._offset chunk4 = None if self._offset < self._input_size: - chunk4 = self._input[self._offset:self._offset + 3] - if chunk4 == 'NDV': - address7 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk4 = self._input[self._offset : self._offset + 3] + if chunk4 == "NDV": + address7 = TreeNode( + self._input[self._offset : self._offset + 3], self._offset + ) self._offset = self._offset + 3 else: address7 = FAILURE @@ -1279,7 +1329,7 @@ def _read_vis(self): if elements1 is None: address2 = FAILURE else: - address2 = TreeNode(self._input[index4:self._offset], index4, elements1) + address2 = TreeNode(self._input[index4 : self._offset], index4, elements1) self._offset = self._offset if address2 is FAILURE: self._offset = index3 @@ -1287,9 +1337,11 @@ def _read_vis(self): address8 = FAILURE chunk5 = None if self._offset < self._input_size: - chunk5 = self._input[self._offset:self._offset + 1] + chunk5 = self._input[self._offset : self._offset + 1] if chunk5 is not None and Grammar.REGEX_23.search(chunk5): - address8 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address8 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address8 = FAILURE @@ -1297,7 +1349,7 @@ def _read_vis(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address8 is not FAILURE: elements2.append(address8) address9 = FAILURE @@ -1305,9 +1357,11 @@ def _read_vis(self): index8 = self._offset chunk6 = None if self._offset < self._input_size: - chunk6 = self._input[self._offset:self._offset + 1] + chunk6 = self._input[self._offset : self._offset + 1] if chunk6 is not None and Grammar.REGEX_24.search(chunk6): - address9 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address9 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address9 = FAILURE @@ -1315,7 +1369,7 @@ def _read_vis(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address9 is FAILURE: self._offset = index8 index9, elements3 = self._offset, [] @@ -1325,9 +1379,11 @@ def _read_vis(self): address11 = FAILURE chunk7 = None if self._offset < self._input_size: - chunk7 = self._input[self._offset:self._offset + 1] - if chunk7 == ' ': - address11 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk7 = self._input[self._offset : self._offset + 1] + if chunk7 == " ": + address11 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address11 = FAILURE @@ -1341,9 +1397,11 @@ def _read_vis(self): address12 = FAILURE chunk8 = None if self._offset < self._input_size: - chunk8 = self._input[self._offset:self._offset + 1] + chunk8 = self._input[self._offset : self._offset + 1] if chunk8 is not None and Grammar.REGEX_25.search(chunk8): - address12 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address12 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address12 = FAILURE @@ -1351,7 +1409,7 @@ def _read_vis(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address12 is not FAILURE: elements4.append(address12) else: @@ -1363,7 +1421,9 @@ def _read_vis(self): if elements4 is None: address10 = FAILURE else: - address10 = TreeNode(self._input[index11:self._offset], index11, elements4) + address10 = TreeNode( + self._input[index11 : self._offset], index11, elements4 + ) self._offset = self._offset if address10 is FAILURE: address10 = TreeNode(self._input[index10:index10], index10) @@ -1373,9 +1433,11 @@ def _read_vis(self): address13 = FAILURE chunk9 = None if self._offset < self._input_size: - chunk9 = self._input[self._offset:self._offset + 1] - if chunk9 == '/': - address13 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk9 = self._input[self._offset : self._offset + 1] + if chunk9 == "/": + address13 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address13 = FAILURE @@ -1389,9 +1451,12 @@ def _read_vis(self): address14 = FAILURE chunk10 = None if self._offset < self._input_size: - chunk10 = self._input[self._offset:self._offset + 1] + chunk10 = self._input[self._offset : self._offset + 1] if chunk10 is not None and Grammar.REGEX_26.search(chunk10): - address14 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address14 = TreeNode( + self._input[self._offset : self._offset + 1], + self._offset, + ) self._offset = self._offset + 1 else: address14 = FAILURE @@ -1399,7 +1464,7 @@ def _read_vis(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address14 is not FAILURE: elements3.append(address14) else: @@ -1414,7 +1479,9 @@ def _read_vis(self): if elements3 is None: address9 = FAILURE else: - address9 = TreeNode(self._input[index9:self._offset], index9, elements3) + address9 = TreeNode( + self._input[index9 : self._offset], index9, elements3 + ) self._offset = self._offset if address9 is FAILURE: self._offset = index8 @@ -1426,9 +1493,11 @@ def _read_vis(self): address15 = FAILURE chunk11 = None if self._offset < self._input_size: - chunk11 = self._input[self._offset:self._offset + 2] - if chunk11 == 'SM': - address15 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk11 = self._input[self._offset : self._offset + 2] + if chunk11 == "SM": + address15 = TreeNode( + self._input[self._offset : self._offset + 2], self._offset + ) self._offset = self._offset + 2 else: address15 = FAILURE @@ -1451,15 +1520,17 @@ def _read_vis(self): if elements2 is None: address2 = FAILURE else: - address2 = TreeNode(self._input[index6:self._offset], index6, elements2) + address2 = TreeNode(self._input[index6 : self._offset], index6, elements2) self._offset = self._offset if address2 is FAILURE: self._offset = index3 chunk12 = None if self._offset < self._input_size: - chunk12 = self._input[self._offset:self._offset + 5] - if chunk12 == 'CAVOK': - address2 = TreeNode(self._input[self._offset:self._offset + 5], self._offset) + chunk12 = self._input[self._offset : self._offset + 5] + if chunk12 == "CAVOK": + address2 = TreeNode( + self._input[self._offset : self._offset + 5], self._offset + ) self._offset = self._offset + 5 else: address2 = FAILURE @@ -1481,17 +1552,17 @@ def _read_vis(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode6(self._input[index2:self._offset], index2, elements0) + address0 = TreeNode6(self._input[index2 : self._offset], index2, elements0) self._offset = self._offset if address0 is FAILURE: address0 = TreeNode(self._input[index1:index1], index1) self._offset = index1 - self._cache['vis'][index0] = (address0, self._offset) + self._cache["vis"][index0] = (address0, self._offset) return address0 def _read_run(self): address0, index0 = FAILURE, self._offset - cached = self._cache['run'].get(index0) + cached = self._cache["run"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -1504,9 +1575,9 @@ def _read_run(self): address2 = FAILURE chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 1] - if chunk0 == 'R': - address2 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk0 = self._input[self._offset : self._offset + 1] + if chunk0 == "R": + address2 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address2 = FAILURE @@ -1521,9 +1592,11 @@ def _read_run(self): index3 = self._offset chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 1] + chunk1 = self._input[self._offset : self._offset + 1] if chunk1 is not None and Grammar.REGEX_27.search(chunk1): - address3 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address3 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address3 = FAILURE @@ -1531,7 +1604,7 @@ def _read_run(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[LRC]') + self._expected.append("[LRC]") if address3 is FAILURE: address3 = TreeNode(self._input[index3:index3], index3) self._offset = index3 @@ -1540,9 +1613,11 @@ def _read_run(self): address4 = FAILURE chunk2 = None if self._offset < self._input_size: - chunk2 = self._input[self._offset:self._offset + 1] + chunk2 = self._input[self._offset : self._offset + 1] if chunk2 is not None and Grammar.REGEX_28.search(chunk2): - address4 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address4 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address4 = FAILURE @@ -1550,15 +1625,17 @@ def _read_run(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address4 is not FAILURE: elements0.append(address4) address5 = FAILURE chunk3 = None if self._offset < self._input_size: - chunk3 = self._input[self._offset:self._offset + 1] + chunk3 = self._input[self._offset : self._offset + 1] if chunk3 is not None and Grammar.REGEX_29.search(chunk3): - address5 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address5 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address5 = FAILURE @@ -1566,16 +1643,18 @@ def _read_run(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address5 is not FAILURE: elements0.append(address5) address6 = FAILURE index4 = self._offset chunk4 = None if self._offset < self._input_size: - chunk4 = self._input[self._offset:self._offset + 1] + chunk4 = self._input[self._offset : self._offset + 1] if chunk4 is not None and Grammar.REGEX_30.search(chunk4): - address6 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address6 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address6 = FAILURE @@ -1583,7 +1662,7 @@ def _read_run(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[LRC]') + self._expected.append("[LRC]") if address6 is FAILURE: address6 = TreeNode(self._input[index4:index4], index4) self._offset = index4 @@ -1592,9 +1671,12 @@ def _read_run(self): address7 = FAILURE chunk5 = None if self._offset < self._input_size: - chunk5 = self._input[self._offset:self._offset + 1] - if chunk5 == '/': - address7 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk5 = self._input[self._offset : self._offset + 1] + if chunk5 == "/": + address7 = TreeNode( + self._input[self._offset : self._offset + 1], + self._offset, + ) self._offset = self._offset + 1 else: address7 = FAILURE @@ -1611,9 +1693,12 @@ def _read_run(self): address9 = FAILURE chunk6 = None if self._offset < self._input_size: - chunk6 = self._input[self._offset:self._offset + 1] + chunk6 = self._input[self._offset : self._offset + 1] if chunk6 is not None and Grammar.REGEX_31.search(chunk6): - address9 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address9 = TreeNode( + self._input[self._offset : self._offset + 1], + self._offset, + ) self._offset = self._offset + 1 else: address9 = FAILURE @@ -1621,15 +1706,22 @@ def _read_run(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address9 is not FAILURE: elements1.append(address9) address10 = FAILURE chunk7 = None if self._offset < self._input_size: - chunk7 = self._input[self._offset:self._offset + 1] - if chunk7 is not None and Grammar.REGEX_32.search(chunk7): - address10 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk7 = self._input[ + self._offset : self._offset + 1 + ] + if chunk7 is not None and Grammar.REGEX_32.search( + chunk7 + ): + address10 = TreeNode( + self._input[self._offset : self._offset + 1], + self._offset, + ) self._offset = self._offset + 1 else: address10 = FAILURE @@ -1637,15 +1729,24 @@ def _read_run(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address10 is not FAILURE: elements1.append(address10) address11 = FAILURE chunk8 = None if self._offset < self._input_size: - chunk8 = self._input[self._offset:self._offset + 1] - if chunk8 is not None and Grammar.REGEX_33.search(chunk8): - address11 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk8 = self._input[ + self._offset : self._offset + 1 + ] + if chunk8 is not None and Grammar.REGEX_33.search( + chunk8 + ): + address11 = TreeNode( + self._input[ + self._offset : self._offset + 1 + ], + self._offset, + ) self._offset = self._offset + 1 else: address11 = FAILURE @@ -1653,15 +1754,25 @@ def _read_run(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address11 is not FAILURE: elements1.append(address11) address12 = FAILURE chunk9 = None if self._offset < self._input_size: - chunk9 = self._input[self._offset:self._offset + 1] - if chunk9 is not None and Grammar.REGEX_34.search(chunk9): - address12 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk9 = self._input[ + self._offset : self._offset + 1 + ] + if ( + chunk9 is not None + and Grammar.REGEX_34.search(chunk9) + ): + address12 = TreeNode( + self._input[ + self._offset : self._offset + 1 + ], + self._offset, + ) self._offset = self._offset + 1 else: address12 = FAILURE @@ -1669,15 +1780,22 @@ def _read_run(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address12 is not FAILURE: elements1.append(address12) address13 = FAILURE chunk10 = None if self._offset < self._input_size: - chunk10 = self._input[self._offset:self._offset + 1] - if chunk10 == 'V': - address13 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk10 = self._input[ + self._offset : self._offset + 1 + ] + if chunk10 == "V": + address13 = TreeNode( + self._input[ + self._offset : self._offset + 1 + ], + self._offset, + ) self._offset = self._offset + 1 else: address13 = FAILURE @@ -1706,7 +1824,11 @@ def _read_run(self): if elements1 is None: address8 = FAILURE else: - address8 = TreeNode(self._input[index6:self._offset], index6, elements1) + address8 = TreeNode( + self._input[index6 : self._offset], + index6, + elements1, + ) self._offset = self._offset if address8 is FAILURE: address8 = TreeNode(self._input[index5:index5], index5) @@ -1717,9 +1839,16 @@ def _read_run(self): index7 = self._offset chunk11 = None if self._offset < self._input_size: - chunk11 = self._input[self._offset:self._offset + 1] - if chunk11 is not None and Grammar.REGEX_35.search(chunk11): - address14 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk11 = self._input[ + self._offset : self._offset + 1 + ] + if chunk11 is not None and Grammar.REGEX_35.search( + chunk11 + ): + address14 = TreeNode( + self._input[self._offset : self._offset + 1], + self._offset, + ) self._offset = self._offset + 1 else: address14 = FAILURE @@ -1729,16 +1858,28 @@ def _read_run(self): if self._offset == self._failure: self._expected.append('["M" / "P"]') if address14 is FAILURE: - address14 = TreeNode(self._input[index7:index7], index7) + address14 = TreeNode( + self._input[index7:index7], index7 + ) self._offset = index7 if address14 is not FAILURE: elements0.append(address14) address15 = FAILURE chunk12 = None if self._offset < self._input_size: - chunk12 = self._input[self._offset:self._offset + 1] - if chunk12 is not None and Grammar.REGEX_36.search(chunk12): - address15 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk12 = self._input[ + self._offset : self._offset + 1 + ] + if ( + chunk12 is not None + and Grammar.REGEX_36.search(chunk12) + ): + address15 = TreeNode( + self._input[ + self._offset : self._offset + 1 + ], + self._offset, + ) self._offset = self._offset + 1 else: address15 = FAILURE @@ -1746,15 +1887,25 @@ def _read_run(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address15 is not FAILURE: elements0.append(address15) address16 = FAILURE chunk13 = None if self._offset < self._input_size: - chunk13 = self._input[self._offset:self._offset + 1] - if chunk13 is not None and Grammar.REGEX_37.search(chunk13): - address16 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk13 = self._input[ + self._offset : self._offset + 1 + ] + if ( + chunk13 is not None + and Grammar.REGEX_37.search(chunk13) + ): + address16 = TreeNode( + self._input[ + self._offset : self._offset + 1 + ], + self._offset, + ) self._offset = self._offset + 1 else: address16 = FAILURE @@ -1762,15 +1913,25 @@ def _read_run(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address16 is not FAILURE: elements0.append(address16) address17 = FAILURE chunk14 = None if self._offset < self._input_size: - chunk14 = self._input[self._offset:self._offset + 1] - if chunk14 is not None and Grammar.REGEX_38.search(chunk14): - address17 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk14 = self._input[ + self._offset : self._offset + 1 + ] + if ( + chunk14 is not None + and Grammar.REGEX_38.search(chunk14) + ): + address17 = TreeNode( + self._input[ + self._offset : self._offset + 1 + ], + self._offset, + ) self._offset = self._offset + 1 else: address17 = FAILURE @@ -1778,15 +1939,28 @@ def _read_run(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address17 is not FAILURE: elements0.append(address17) address18 = FAILURE chunk15 = None if self._offset < self._input_size: - chunk15 = self._input[self._offset:self._offset + 1] - if chunk15 is not None and Grammar.REGEX_39.search(chunk15): - address18 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk15 = self._input[ + self._offset : self._offset + 1 + ] + if ( + chunk15 is not None + and Grammar.REGEX_39.search( + chunk15 + ) + ): + address18 = TreeNode( + self._input[ + self._offset : self._offset + + 1 + ], + self._offset, + ) self._offset = self._offset + 1 else: address18 = FAILURE @@ -1794,23 +1968,42 @@ def _read_run(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address18 is not FAILURE: elements0.append(address18) address19 = FAILURE chunk16 = None if self._offset < self._input_size: - chunk16 = self._input[self._offset:self._offset + 2] - if chunk16 == 'FT': - address19 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk16 = self._input[ + self._offset : self._offset + + 2 + ] + if chunk16 == "FT": + address19 = TreeNode( + self._input[ + self._offset : self._offset + + 2 + ], + self._offset, + ) self._offset = self._offset + 2 else: address19 = FAILURE - if self._offset > self._failure: - self._failure = self._offset + if ( + self._offset + > self._failure + ): + self._failure = ( + self._offset + ) self._expected = [] - if self._offset == self._failure: - self._expected.append('"FT"') + if ( + self._offset + == self._failure + ): + self._expected.append( + '"FT"' + ) if address19 is not FAILURE: elements0.append(address19) else: @@ -1858,17 +2051,17 @@ def _read_run(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode7(self._input[index2:self._offset], index2, elements0) + address0 = TreeNode7(self._input[index2 : self._offset], index2, elements0) self._offset = self._offset if address0 is FAILURE: address0 = TreeNode(self._input[index1:index1], index1) self._offset = index1 - self._cache['run'][index0] = (address0, self._offset) + self._cache["run"][index0] = (address0, self._offset) return address0 def _read_curwx(self): address0, index0 = FAILURE, self._offset - cached = self._cache['curwx'].get(index0) + cached = self._cache["curwx"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -1893,25 +2086,25 @@ def _read_curwx(self): if elements1 is None: address1 = FAILURE else: - address1 = TreeNode8(self._input[index3:self._offset], index3, elements1) + address1 = TreeNode8(self._input[index3 : self._offset], index3, elements1) self._offset = self._offset if address1 is not FAILURE: elements0.append(address1) remaining0 -= 1 if remaining0 <= 0: - address0 = TreeNode(self._input[index2:self._offset], index2, elements0) + address0 = TreeNode(self._input[index2 : self._offset], index2, elements0) self._offset = self._offset else: address0 = FAILURE if address0 is FAILURE: address0 = TreeNode(self._input[index1:index1], index1) self._offset = index1 - self._cache['curwx'][index0] = (address0, self._offset) + self._cache["curwx"][index0] = (address0, self._offset) return address0 def _read_wx(self): address0, index0 = FAILURE, self._offset - cached = self._cache['wx'].get(index0) + cached = self._cache["wx"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -1921,9 +2114,9 @@ def _read_wx(self): index3 = self._offset chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 1] + chunk0 = self._input[self._offset : self._offset + 1] if chunk0 is not None and Grammar.REGEX_40.search(chunk0): - address1 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address1 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address1 = FAILURE @@ -1931,14 +2124,14 @@ def _read_wx(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[-+]') + self._expected.append("[-+]") if address1 is FAILURE: self._offset = index3 chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 2] - if chunk1 == 'VC': - address1 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk1 = self._input[self._offset : self._offset + 2] + if chunk1 == "VC": + address1 = TreeNode(self._input[self._offset : self._offset + 2], self._offset) self._offset = self._offset + 2 else: address1 = FAILURE @@ -1958,9 +2151,9 @@ def _read_wx(self): index4 = self._offset chunk2 = None if self._offset < self._input_size: - chunk2 = self._input[self._offset:self._offset + 2] - if chunk2 == 'MI': - address2 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk2 = self._input[self._offset : self._offset + 2] + if chunk2 == "MI": + address2 = TreeNode(self._input[self._offset : self._offset + 2], self._offset) self._offset = self._offset + 2 else: address2 = FAILURE @@ -1973,9 +2166,11 @@ def _read_wx(self): self._offset = index4 chunk3 = None if self._offset < self._input_size: - chunk3 = self._input[self._offset:self._offset + 2] - if chunk3 == 'PR': - address2 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk3 = self._input[self._offset : self._offset + 2] + if chunk3 == "PR": + address2 = TreeNode( + self._input[self._offset : self._offset + 2], self._offset + ) self._offset = self._offset + 2 else: address2 = FAILURE @@ -1988,9 +2183,11 @@ def _read_wx(self): self._offset = index4 chunk4 = None if self._offset < self._input_size: - chunk4 = self._input[self._offset:self._offset + 2] - if chunk4 == 'DR': - address2 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk4 = self._input[self._offset : self._offset + 2] + if chunk4 == "DR": + address2 = TreeNode( + self._input[self._offset : self._offset + 2], self._offset + ) self._offset = self._offset + 2 else: address2 = FAILURE @@ -2003,9 +2200,11 @@ def _read_wx(self): self._offset = index4 chunk5 = None if self._offset < self._input_size: - chunk5 = self._input[self._offset:self._offset + 2] - if chunk5 == 'BL': - address2 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk5 = self._input[self._offset : self._offset + 2] + if chunk5 == "BL": + address2 = TreeNode( + self._input[self._offset : self._offset + 2], self._offset + ) self._offset = self._offset + 2 else: address2 = FAILURE @@ -2018,9 +2217,11 @@ def _read_wx(self): self._offset = index4 chunk6 = None if self._offset < self._input_size: - chunk6 = self._input[self._offset:self._offset + 2] - if chunk6 == 'SH': - address2 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk6 = self._input[self._offset : self._offset + 2] + if chunk6 == "SH": + address2 = TreeNode( + self._input[self._offset : self._offset + 2], self._offset + ) self._offset = self._offset + 2 else: address2 = FAILURE @@ -2033,9 +2234,12 @@ def _read_wx(self): self._offset = index4 chunk7 = None if self._offset < self._input_size: - chunk7 = self._input[self._offset:self._offset + 2] - if chunk7 == 'TS': - address2 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk7 = self._input[self._offset : self._offset + 2] + if chunk7 == "TS": + address2 = TreeNode( + self._input[self._offset : self._offset + 2], + self._offset, + ) self._offset = self._offset + 2 else: address2 = FAILURE @@ -2048,9 +2252,12 @@ def _read_wx(self): self._offset = index4 chunk8 = None if self._offset < self._input_size: - chunk8 = self._input[self._offset:self._offset + 2] - if chunk8 == 'FG': - address2 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk8 = self._input[self._offset : self._offset + 2] + if chunk8 == "FG": + address2 = TreeNode( + self._input[self._offset : self._offset + 2], + self._offset, + ) self._offset = self._offset + 2 else: address2 = FAILURE @@ -2063,9 +2270,14 @@ def _read_wx(self): self._offset = index4 chunk9 = None if self._offset < self._input_size: - chunk9 = self._input[self._offset:self._offset + 2] - if chunk9 == 'TS': - address2 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk9 = self._input[ + self._offset : self._offset + 2 + ] + if chunk9 == "TS": + address2 = TreeNode( + self._input[self._offset : self._offset + 2], + self._offset, + ) self._offset = self._offset + 2 else: address2 = FAILURE @@ -2078,9 +2290,16 @@ def _read_wx(self): self._offset = index4 chunk10 = None if self._offset < self._input_size: - chunk10 = self._input[self._offset:self._offset + 2] - if chunk10 == 'FZ': - address2 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk10 = self._input[ + self._offset : self._offset + 2 + ] + if chunk10 == "FZ": + address2 = TreeNode( + self._input[ + self._offset : self._offset + 2 + ], + self._offset, + ) self._offset = self._offset + 2 else: address2 = FAILURE @@ -2093,9 +2312,16 @@ def _read_wx(self): self._offset = index4 chunk11 = None if self._offset < self._input_size: - chunk11 = self._input[self._offset:self._offset + 2] - if chunk11 == 'RA': - address2 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk11 = self._input[ + self._offset : self._offset + 2 + ] + if chunk11 == "RA": + address2 = TreeNode( + self._input[ + self._offset : self._offset + 2 + ], + self._offset, + ) self._offset = self._offset + 2 else: address2 = FAILURE @@ -2108,9 +2334,16 @@ def _read_wx(self): self._offset = index4 chunk12 = None if self._offset < self._input_size: - chunk12 = self._input[self._offset:self._offset + 2] - if chunk12 == 'BR': - address2 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk12 = self._input[ + self._offset : self._offset + 2 + ] + if chunk12 == "BR": + address2 = TreeNode( + self._input[ + self._offset : self._offset + 2 + ], + self._offset, + ) self._offset = self._offset + 2 else: address2 = FAILURE @@ -2123,9 +2356,17 @@ def _read_wx(self): self._offset = index4 chunk13 = None if self._offset < self._input_size: - chunk13 = self._input[self._offset:self._offset + 2] - if chunk13 == 'HZ': - address2 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk13 = self._input[ + self._offset : self._offset + 2 + ] + if chunk13 == "HZ": + address2 = TreeNode( + self._input[ + self._offset : self._offset + + 2 + ], + self._offset, + ) self._offset = self._offset + 2 else: address2 = FAILURE @@ -2138,17 +2379,36 @@ def _read_wx(self): self._offset = index4 chunk14 = None if self._offset < self._input_size: - chunk14 = self._input[self._offset:self._offset + 2] - if chunk14 == 'SN': - address2 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk14 = self._input[ + self._offset : self._offset + + 2 + ] + if chunk14 == "SN": + address2 = TreeNode( + self._input[ + self._offset : self._offset + + 2 + ], + self._offset, + ) self._offset = self._offset + 2 else: address2 = FAILURE - if self._offset > self._failure: - self._failure = self._offset + if ( + self._offset + > self._failure + ): + self._failure = ( + self._offset + ) self._expected = [] - if self._offset == self._failure: - self._expected.append('"SN"') + if ( + self._offset + == self._failure + ): + self._expected.append( + '"SN"' + ) if address2 is FAILURE: self._offset = index4 if address2 is not FAILURE: @@ -2157,9 +2417,11 @@ def _read_wx(self): index5 = self._offset chunk15 = None if self._offset < self._input_size: - chunk15 = self._input[self._offset:self._offset + 1] + chunk15 = self._input[self._offset : self._offset + 1] if chunk15 is not None and Grammar.REGEX_41.search(chunk15): - address3 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address3 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address3 = FAILURE @@ -2167,7 +2429,7 @@ def _read_wx(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[-+]') + self._expected.append("[-+]") if address3 is FAILURE: address3 = TreeNode(self._input[index5:index5], index5) self._offset = index5 @@ -2178,9 +2440,11 @@ def _read_wx(self): index7 = self._offset chunk16 = None if self._offset < self._input_size: - chunk16 = self._input[self._offset:self._offset + 2] - if chunk16 == 'RA': - address4 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk16 = self._input[self._offset : self._offset + 2] + if chunk16 == "RA": + address4 = TreeNode( + self._input[self._offset : self._offset + 2], self._offset + ) self._offset = self._offset + 2 else: address4 = FAILURE @@ -2193,9 +2457,11 @@ def _read_wx(self): self._offset = index7 chunk17 = None if self._offset < self._input_size: - chunk17 = self._input[self._offset:self._offset + 2] - if chunk17 == 'BR': - address4 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk17 = self._input[self._offset : self._offset + 2] + if chunk17 == "BR": + address4 = TreeNode( + self._input[self._offset : self._offset + 2], self._offset + ) self._offset = self._offset + 2 else: address4 = FAILURE @@ -2208,9 +2474,11 @@ def _read_wx(self): self._offset = index7 chunk18 = None if self._offset < self._input_size: - chunk18 = self._input[self._offset:self._offset + 2] - if chunk18 == 'DZ': - address4 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk18 = self._input[self._offset : self._offset + 2] + if chunk18 == "DZ": + address4 = TreeNode( + self._input[self._offset : self._offset + 2], self._offset + ) self._offset = self._offset + 2 else: address4 = FAILURE @@ -2223,9 +2491,12 @@ def _read_wx(self): self._offset = index7 chunk19 = None if self._offset < self._input_size: - chunk19 = self._input[self._offset:self._offset + 2] - if chunk19 == 'FG': - address4 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk19 = self._input[self._offset : self._offset + 2] + if chunk19 == "FG": + address4 = TreeNode( + self._input[self._offset : self._offset + 2], + self._offset, + ) self._offset = self._offset + 2 else: address4 = FAILURE @@ -2238,9 +2509,12 @@ def _read_wx(self): self._offset = index7 chunk20 = None if self._offset < self._input_size: - chunk20 = self._input[self._offset:self._offset + 2] - if chunk20 == 'FU': - address4 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk20 = self._input[self._offset : self._offset + 2] + if chunk20 == "FU": + address4 = TreeNode( + self._input[self._offset : self._offset + 2], + self._offset, + ) self._offset = self._offset + 2 else: address4 = FAILURE @@ -2253,9 +2527,14 @@ def _read_wx(self): self._offset = index7 chunk21 = None if self._offset < self._input_size: - chunk21 = self._input[self._offset:self._offset + 2] - if chunk21 == 'VA': - address4 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk21 = self._input[ + self._offset : self._offset + 2 + ] + if chunk21 == "VA": + address4 = TreeNode( + self._input[self._offset : self._offset + 2], + self._offset, + ) self._offset = self._offset + 2 else: address4 = FAILURE @@ -2268,9 +2547,16 @@ def _read_wx(self): self._offset = index7 chunk22 = None if self._offset < self._input_size: - chunk22 = self._input[self._offset:self._offset + 2] - if chunk22 == 'DU': - address4 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk22 = self._input[ + self._offset : self._offset + 2 + ] + if chunk22 == "DU": + address4 = TreeNode( + self._input[ + self._offset : self._offset + 2 + ], + self._offset, + ) self._offset = self._offset + 2 else: address4 = FAILURE @@ -2283,9 +2569,16 @@ def _read_wx(self): self._offset = index7 chunk23 = None if self._offset < self._input_size: - chunk23 = self._input[self._offset:self._offset + 2] - if chunk23 == 'SA': - address4 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk23 = self._input[ + self._offset : self._offset + 2 + ] + if chunk23 == "SA": + address4 = TreeNode( + self._input[ + self._offset : self._offset + 2 + ], + self._offset, + ) self._offset = self._offset + 2 else: address4 = FAILURE @@ -2298,9 +2591,16 @@ def _read_wx(self): self._offset = index7 chunk24 = None if self._offset < self._input_size: - chunk24 = self._input[self._offset:self._offset + 2] - if chunk24 == 'SA': - address4 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk24 = self._input[ + self._offset : self._offset + 2 + ] + if chunk24 == "SA": + address4 = TreeNode( + self._input[ + self._offset : self._offset + 2 + ], + self._offset, + ) self._offset = self._offset + 2 else: address4 = FAILURE @@ -2313,9 +2613,17 @@ def _read_wx(self): self._offset = index7 chunk25 = None if self._offset < self._input_size: - chunk25 = self._input[self._offset:self._offset + 2] - if chunk25 == 'HZ': - address4 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk25 = self._input[ + self._offset : self._offset + 2 + ] + if chunk25 == "HZ": + address4 = TreeNode( + self._input[ + self._offset : self._offset + + 2 + ], + self._offset, + ) self._offset = self._offset + 2 else: address4 = FAILURE @@ -2328,17 +2636,36 @@ def _read_wx(self): self._offset = index7 chunk26 = None if self._offset < self._input_size: - chunk26 = self._input[self._offset:self._offset + 2] - if chunk26 == 'PY': - address4 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk26 = self._input[ + self._offset : self._offset + + 2 + ] + if chunk26 == "PY": + address4 = TreeNode( + self._input[ + self._offset : self._offset + + 2 + ], + self._offset, + ) self._offset = self._offset + 2 else: address4 = FAILURE - if self._offset > self._failure: - self._failure = self._offset + if ( + self._offset + > self._failure + ): + self._failure = ( + self._offset + ) self._expected = [] - if self._offset == self._failure: - self._expected.append('"PY"') + if ( + self._offset + == self._failure + ): + self._expected.append( + '"PY"' + ) if address4 is FAILURE: self._offset = index7 if address4 is FAILURE: @@ -2361,14 +2688,14 @@ def _read_wx(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode(self._input[index1:self._offset], index1, elements0) + address0 = TreeNode(self._input[index1 : self._offset], index1, elements0) self._offset = self._offset - self._cache['wx'][index0] = (address0, self._offset) + self._cache["wx"][index0] = (address0, self._offset) return address0 def _read_skyc(self): address0, index0 = FAILURE, self._offset - cached = self._cache['skyc'].get(index0) + cached = self._cache["skyc"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -2393,25 +2720,25 @@ def _read_skyc(self): if elements1 is None: address1 = FAILURE else: - address1 = TreeNode9(self._input[index3:self._offset], index3, elements1) + address1 = TreeNode9(self._input[index3 : self._offset], index3, elements1) self._offset = self._offset if address1 is not FAILURE: elements0.append(address1) remaining0 -= 1 if remaining0 <= 0: - address0 = TreeNode(self._input[index2:self._offset], index2, elements0) + address0 = TreeNode(self._input[index2 : self._offset], index2, elements0) self._offset = self._offset else: address0 = FAILURE if address0 is FAILURE: address0 = TreeNode(self._input[index1:index1], index1) self._offset = index1 - self._cache['skyc'][index0] = (address0, self._offset) + self._cache["skyc"][index0] = (address0, self._offset) return address0 def _read_cover(self): address0, index0 = FAILURE, self._offset - cached = self._cache['cover'].get(index0) + cached = self._cache["cover"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -2421,9 +2748,9 @@ def _read_cover(self): index3 = self._offset chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 3] - if chunk0 == 'FEW': - address1 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk0 = self._input[self._offset : self._offset + 3] + if chunk0 == "FEW": + address1 = TreeNode(self._input[self._offset : self._offset + 3], self._offset) self._offset = self._offset + 3 else: address1 = FAILURE @@ -2436,9 +2763,9 @@ def _read_cover(self): self._offset = index3 chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 3] - if chunk1 == 'SCT': - address1 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk1 = self._input[self._offset : self._offset + 3] + if chunk1 == "SCT": + address1 = TreeNode(self._input[self._offset : self._offset + 3], self._offset) self._offset = self._offset + 3 else: address1 = FAILURE @@ -2451,9 +2778,11 @@ def _read_cover(self): self._offset = index3 chunk2 = None if self._offset < self._input_size: - chunk2 = self._input[self._offset:self._offset + 3] - if chunk2 == 'BKN': - address1 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk2 = self._input[self._offset : self._offset + 3] + if chunk2 == "BKN": + address1 = TreeNode( + self._input[self._offset : self._offset + 3], self._offset + ) self._offset = self._offset + 3 else: address1 = FAILURE @@ -2466,9 +2795,11 @@ def _read_cover(self): self._offset = index3 chunk3 = None if self._offset < self._input_size: - chunk3 = self._input[self._offset:self._offset + 3] - if chunk3 == 'OVC': - address1 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk3 = self._input[self._offset : self._offset + 3] + if chunk3 == "OVC": + address1 = TreeNode( + self._input[self._offset : self._offset + 3], self._offset + ) self._offset = self._offset + 3 else: address1 = FAILURE @@ -2481,9 +2812,11 @@ def _read_cover(self): self._offset = index3 chunk4 = None if self._offset < self._input_size: - chunk4 = self._input[self._offset:self._offset + 2] - if chunk4 == 'VV': - address1 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk4 = self._input[self._offset : self._offset + 2] + if chunk4 == "VV": + address1 = TreeNode( + self._input[self._offset : self._offset + 2], self._offset + ) self._offset = self._offset + 2 else: address1 = FAILURE @@ -2496,9 +2829,11 @@ def _read_cover(self): self._offset = index3 chunk5 = None if self._offset < self._input_size: - chunk5 = self._input[self._offset:self._offset + 3] - if chunk5 == '///': - address1 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk5 = self._input[self._offset : self._offset + 3] + if chunk5 == "///": + address1 = TreeNode( + self._input[self._offset : self._offset + 3], self._offset + ) self._offset = self._offset + 3 else: address1 = FAILURE @@ -2517,9 +2852,11 @@ def _read_cover(self): while address3 is not FAILURE: chunk6 = None if self._offset < self._input_size: - chunk6 = self._input[self._offset:self._offset + 1] + chunk6 = self._input[self._offset : self._offset + 1] if chunk6 is not None and Grammar.REGEX_42.search(chunk6): - address3 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address3 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address3 = FAILURE @@ -2527,12 +2864,12 @@ def _read_cover(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address3 is not FAILURE: elements1.append(address3) remaining0 -= 1 if remaining0 <= 0: - address2 = TreeNode(self._input[index5:self._offset], index5, elements1) + address2 = TreeNode(self._input[index5 : self._offset], index5, elements1) self._offset = self._offset else: address2 = FAILURE @@ -2546,9 +2883,11 @@ def _read_cover(self): index7 = self._offset chunk7 = None if self._offset < self._input_size: - chunk7 = self._input[self._offset:self._offset + 3] - if chunk7 == 'TCU': - address4 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk7 = self._input[self._offset : self._offset + 3] + if chunk7 == "TCU": + address4 = TreeNode( + self._input[self._offset : self._offset + 3], self._offset + ) self._offset = self._offset + 3 else: address4 = FAILURE @@ -2561,9 +2900,11 @@ def _read_cover(self): self._offset = index7 chunk8 = None if self._offset < self._input_size: - chunk8 = self._input[self._offset:self._offset + 2] - if chunk8 == 'CB': - address4 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk8 = self._input[self._offset : self._offset + 2] + if chunk8 == "CB": + address4 = TreeNode( + self._input[self._offset : self._offset + 2], self._offset + ) self._offset = self._offset + 2 else: address4 = FAILURE @@ -2576,9 +2917,11 @@ def _read_cover(self): self._offset = index7 chunk9 = None if self._offset < self._input_size: - chunk9 = self._input[self._offset:self._offset + 3] - if chunk9 == '///': - address4 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk9 = self._input[self._offset : self._offset + 3] + if chunk9 == "///": + address4 = TreeNode( + self._input[self._offset : self._offset + 3], self._offset + ) self._offset = self._offset + 3 else: address4 = FAILURE @@ -2606,16 +2949,16 @@ def _read_cover(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode(self._input[index2:self._offset], index2, elements0) + address0 = TreeNode(self._input[index2 : self._offset], index2, elements0) self._offset = self._offset if address0 is FAILURE: self._offset = index1 index8 = self._offset chunk10 = None if self._offset < self._input_size: - chunk10 = self._input[self._offset:self._offset + 3] - if chunk10 == 'CLR': - address0 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk10 = self._input[self._offset : self._offset + 3] + if chunk10 == "CLR": + address0 = TreeNode(self._input[self._offset : self._offset + 3], self._offset) self._offset = self._offset + 3 else: address0 = FAILURE @@ -2628,9 +2971,11 @@ def _read_cover(self): self._offset = index8 chunk11 = None if self._offset < self._input_size: - chunk11 = self._input[self._offset:self._offset + 3] - if chunk11 == 'SKC': - address0 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk11 = self._input[self._offset : self._offset + 3] + if chunk11 == "SKC": + address0 = TreeNode( + self._input[self._offset : self._offset + 3], self._offset + ) self._offset = self._offset + 3 else: address0 = FAILURE @@ -2643,9 +2988,11 @@ def _read_cover(self): self._offset = index8 chunk12 = None if self._offset < self._input_size: - chunk12 = self._input[self._offset:self._offset + 3] - if chunk12 == 'NSC': - address0 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk12 = self._input[self._offset : self._offset + 3] + if chunk12 == "NSC": + address0 = TreeNode( + self._input[self._offset : self._offset + 3], self._offset + ) self._offset = self._offset + 3 else: address0 = FAILURE @@ -2658,9 +3005,11 @@ def _read_cover(self): self._offset = index8 chunk13 = None if self._offset < self._input_size: - chunk13 = self._input[self._offset:self._offset + 3] - if chunk13 == 'NCD': - address0 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk13 = self._input[self._offset : self._offset + 3] + if chunk13 == "NCD": + address0 = TreeNode( + self._input[self._offset : self._offset + 3], self._offset + ) self._offset = self._offset + 3 else: address0 = FAILURE @@ -2678,9 +3027,11 @@ def _read_cover(self): self._offset = index1 chunk14 = None if self._offset < self._input_size: - chunk14 = self._input[self._offset:self._offset + 2] - if chunk14 == '//': - address0 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk14 = self._input[self._offset : self._offset + 2] + if chunk14 == "//": + address0 = TreeNode( + self._input[self._offset : self._offset + 2], self._offset + ) self._offset = self._offset + 2 else: address0 = FAILURE @@ -2691,12 +3042,12 @@ def _read_cover(self): self._expected.append('"//"') if address0 is FAILURE: self._offset = index1 - self._cache['cover'][index0] = (address0, self._offset) + self._cache["cover"][index0] = (address0, self._offset) return address0 def _read_temp_dewp(self): address0, index0 = FAILURE, self._offset - cached = self._cache['temp_dewp'].get(index0) + cached = self._cache["temp_dewp"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -2710,9 +3061,9 @@ def _read_temp_dewp(self): index3 = self._offset chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 2] - if chunk0 == '//': - address2 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk0 = self._input[self._offset : self._offset + 2] + if chunk0 == "//": + address2 = TreeNode(self._input[self._offset : self._offset + 2], self._offset) self._offset = self._offset + 2 else: address2 = FAILURE @@ -2733,9 +3084,11 @@ def _read_temp_dewp(self): address4 = FAILURE chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 1] - if chunk1 == '/': - address4 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk1 = self._input[self._offset : self._offset + 1] + if chunk1 == "/": + address4 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address4 = FAILURE @@ -2754,9 +3107,11 @@ def _read_temp_dewp(self): index4 = self._offset chunk2 = None if self._offset < self._input_size: - chunk2 = self._input[self._offset:self._offset + 2] - if chunk2 == '//': - address6 = TreeNode(self._input[self._offset:self._offset + 2], self._offset) + chunk2 = self._input[self._offset : self._offset + 2] + if chunk2 == "//": + address6 = TreeNode( + self._input[self._offset : self._offset + 2], self._offset + ) self._offset = self._offset + 2 else: address6 = FAILURE @@ -2791,17 +3146,17 @@ def _read_temp_dewp(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode10(self._input[index2:self._offset], index2, elements0) + address0 = TreeNode10(self._input[index2 : self._offset], index2, elements0) self._offset = self._offset if address0 is FAILURE: address0 = TreeNode(self._input[index1:index1], index1) self._offset = index1 - self._cache['temp_dewp'][index0] = (address0, self._offset) + self._cache["temp_dewp"][index0] = (address0, self._offset) return address0 def _read_temp(self): address0, index0 = FAILURE, self._offset - cached = self._cache['temp'].get(index0) + cached = self._cache["temp"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -2810,9 +3165,9 @@ def _read_temp(self): index2 = self._offset chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 1] + chunk0 = self._input[self._offset : self._offset + 1] if chunk0 is not None and Grammar.REGEX_43.search(chunk0): - address1 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address1 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address1 = FAILURE @@ -2820,7 +3175,7 @@ def _read_temp(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[M]') + self._expected.append("[M]") if address1 is FAILURE: address1 = TreeNode(self._input[index2:index2], index2) self._offset = index2 @@ -2830,9 +3185,9 @@ def _read_temp(self): index3 = self._offset chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 1] + chunk1 = self._input[self._offset : self._offset + 1] if chunk1 is not None and Grammar.REGEX_44.search(chunk1): - address2 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address2 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address2 = FAILURE @@ -2840,7 +3195,7 @@ def _read_temp(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address2 is FAILURE: address2 = TreeNode(self._input[index3:index3], index3) self._offset = index3 @@ -2850,9 +3205,11 @@ def _read_temp(self): index4 = self._offset chunk2 = None if self._offset < self._input_size: - chunk2 = self._input[self._offset:self._offset + 1] + chunk2 = self._input[self._offset : self._offset + 1] if chunk2 is not None and Grammar.REGEX_45.search(chunk2): - address3 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address3 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address3 = FAILURE @@ -2860,7 +3217,7 @@ def _read_temp(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address3 is FAILURE: address3 = TreeNode(self._input[index4:index4], index4) self._offset = index4 @@ -2878,14 +3235,14 @@ def _read_temp(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode(self._input[index1:self._offset], index1, elements0) + address0 = TreeNode(self._input[index1 : self._offset], index1, elements0) self._offset = self._offset - self._cache['temp'][index0] = (address0, self._offset) + self._cache["temp"][index0] = (address0, self._offset) return address0 def _read_dewp(self): address0, index0 = FAILURE, self._offset - cached = self._cache['dewp'].get(index0) + cached = self._cache["dewp"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -2894,9 +3251,9 @@ def _read_dewp(self): index2 = self._offset chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 1] + chunk0 = self._input[self._offset : self._offset + 1] if chunk0 is not None and Grammar.REGEX_46.search(chunk0): - address1 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address1 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address1 = FAILURE @@ -2904,7 +3261,7 @@ def _read_dewp(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[M]') + self._expected.append("[M]") if address1 is FAILURE: address1 = TreeNode(self._input[index2:index2], index2) self._offset = index2 @@ -2914,9 +3271,9 @@ def _read_dewp(self): index3 = self._offset chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 1] + chunk1 = self._input[self._offset : self._offset + 1] if chunk1 is not None and Grammar.REGEX_47.search(chunk1): - address2 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address2 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address2 = FAILURE @@ -2924,7 +3281,7 @@ def _read_dewp(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address2 is FAILURE: address2 = TreeNode(self._input[index3:index3], index3) self._offset = index3 @@ -2934,9 +3291,11 @@ def _read_dewp(self): index4 = self._offset chunk2 = None if self._offset < self._input_size: - chunk2 = self._input[self._offset:self._offset + 1] + chunk2 = self._input[self._offset : self._offset + 1] if chunk2 is not None and Grammar.REGEX_48.search(chunk2): - address3 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address3 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address3 = FAILURE @@ -2944,7 +3303,7 @@ def _read_dewp(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address3 is FAILURE: address3 = TreeNode(self._input[index4:index4], index4) self._offset = index4 @@ -2962,14 +3321,14 @@ def _read_dewp(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode(self._input[index1:self._offset], index1, elements0) + address0 = TreeNode(self._input[index1 : self._offset], index1, elements0) self._offset = self._offset - self._cache['dewp'][index0] = (address0, self._offset) + self._cache["dewp"][index0] = (address0, self._offset) return address0 def _read_altim(self): address0, index0 = FAILURE, self._offset - cached = self._cache['altim'].get(index0) + cached = self._cache["altim"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -2986,9 +3345,9 @@ def _read_altim(self): address2 = FAILURE chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 1] + chunk0 = self._input[self._offset : self._offset + 1] if chunk0 is not None and Grammar.REGEX_49.search(chunk0): - address2 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address2 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address2 = FAILURE @@ -3002,9 +3361,11 @@ def _read_altim(self): address3 = FAILURE chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 1] + chunk1 = self._input[self._offset : self._offset + 1] if chunk1 is not None and Grammar.REGEX_50.search(chunk1): - address3 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address3 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address3 = FAILURE @@ -3012,15 +3373,17 @@ def _read_altim(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address3 is not FAILURE: elements0.append(address3) address4 = FAILURE chunk2 = None if self._offset < self._input_size: - chunk2 = self._input[self._offset:self._offset + 1] + chunk2 = self._input[self._offset : self._offset + 1] if chunk2 is not None and Grammar.REGEX_51.search(chunk2): - address4 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address4 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address4 = FAILURE @@ -3028,15 +3391,17 @@ def _read_altim(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address4 is not FAILURE: elements0.append(address4) address5 = FAILURE chunk3 = None if self._offset < self._input_size: - chunk3 = self._input[self._offset:self._offset + 1] + chunk3 = self._input[self._offset : self._offset + 1] if chunk3 is not None and Grammar.REGEX_52.search(chunk3): - address5 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address5 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address5 = FAILURE @@ -3044,15 +3409,17 @@ def _read_altim(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address5 is not FAILURE: elements0.append(address5) address6 = FAILURE chunk4 = None if self._offset < self._input_size: - chunk4 = self._input[self._offset:self._offset + 1] + chunk4 = self._input[self._offset : self._offset + 1] if chunk4 is not None and Grammar.REGEX_53.search(chunk4): - address6 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address6 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address6 = FAILURE @@ -3060,16 +3427,19 @@ def _read_altim(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('[\\d]') + self._expected.append("[\\d]") if address6 is not FAILURE: elements0.append(address6) address7 = FAILURE index4 = self._offset chunk5 = None if self._offset < self._input_size: - chunk5 = self._input[self._offset:self._offset + 1] - if chunk5 == '=': - address7 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk5 = self._input[self._offset : self._offset + 1] + if chunk5 == "=": + address7 = TreeNode( + self._input[self._offset : self._offset + 1], + self._offset, + ) self._offset = self._offset + 1 else: address7 = FAILURE @@ -3107,17 +3477,17 @@ def _read_altim(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode(self._input[index2:self._offset], index2, elements0) + address0 = TreeNode(self._input[index2 : self._offset], index2, elements0) self._offset = self._offset if address0 is FAILURE: address0 = TreeNode(self._input[index1:index1], index1) self._offset = index1 - self._cache['altim'][index0] = (address0, self._offset) + self._cache["altim"][index0] = (address0, self._offset) return address0 def _read_remarks(self): address0, index0 = FAILURE, self._offset - cached = self._cache['remarks'].get(index0) + cached = self._cache["remarks"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -3135,9 +3505,9 @@ def _read_remarks(self): index4 = self._offset chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 3] - if chunk0 == 'RMK': - address2 = TreeNode(self._input[self._offset:self._offset + 3], self._offset) + chunk0 = self._input[self._offset : self._offset + 3] + if chunk0 == "RMK": + address2 = TreeNode(self._input[self._offset : self._offset + 3], self._offset) self._offset = self._offset + 3 else: address2 = FAILURE @@ -3152,9 +3522,11 @@ def _read_remarks(self): while address3 is not FAILURE: chunk1 = None if self._offset < self._input_size: - chunk1 = self._input[self._offset:self._offset + 5] - if chunk1 == 'NOSIG': - address3 = TreeNode(self._input[self._offset:self._offset + 5], self._offset) + chunk1 = self._input[self._offset : self._offset + 5] + if chunk1 == "NOSIG": + address3 = TreeNode( + self._input[self._offset : self._offset + 5], self._offset + ) self._offset = self._offset + 5 else: address3 = FAILURE @@ -3167,7 +3539,7 @@ def _read_remarks(self): elements1.append(address3) remaining0 -= 1 if remaining0 <= 0: - address2 = TreeNode(self._input[index5:self._offset], index5, elements1) + address2 = TreeNode(self._input[index5 : self._offset], index5, elements1) self._offset = self._offset else: address2 = FAILURE @@ -3179,7 +3551,9 @@ def _read_remarks(self): remaining1, index6, elements2, address5 = 0, self._offset, [], True while address5 is not FAILURE: if self._offset < self._input_size: - address5 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + address5 = TreeNode( + self._input[self._offset : self._offset + 1], self._offset + ) self._offset = self._offset + 1 else: address5 = FAILURE @@ -3187,12 +3561,12 @@ def _read_remarks(self): self._failure = self._offset self._expected = [] if self._offset == self._failure: - self._expected.append('') + self._expected.append("") if address5 is not FAILURE: elements2.append(address5) remaining1 -= 1 if remaining1 <= 0: - address4 = TreeNode(self._input[index6:self._offset], index6, elements2) + address4 = TreeNode(self._input[index6 : self._offset], index6, elements2) self._offset = self._offset else: address4 = FAILURE @@ -3210,17 +3584,17 @@ def _read_remarks(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode(self._input[index2:self._offset], index2, elements0) + address0 = TreeNode(self._input[index2 : self._offset], index2, elements0) self._offset = self._offset if address0 is FAILURE: address0 = TreeNode(self._input[index1:index1], index1) self._offset = index1 - self._cache['remarks'][index0] = (address0, self._offset) + self._cache["remarks"][index0] = (address0, self._offset) return address0 def _read_end(self): address0, index0 = FAILURE, self._offset - cached = self._cache['end'].get(index0) + cached = self._cache["end"].get(index0) if cached: self._offset = cached[1] return cached[0] @@ -3237,9 +3611,9 @@ def _read_end(self): address2 = FAILURE chunk0 = None if self._offset < self._input_size: - chunk0 = self._input[self._offset:self._offset + 1] - if chunk0 == '=': - address2 = TreeNode(self._input[self._offset:self._offset + 1], self._offset) + chunk0 = self._input[self._offset : self._offset + 1] + if chunk0 == "=": + address2 = TreeNode(self._input[self._offset : self._offset + 1], self._offset) self._offset = self._offset + 1 else: address2 = FAILURE @@ -3259,12 +3633,12 @@ def _read_end(self): if elements0 is None: address0 = FAILURE else: - address0 = TreeNode(self._input[index2:self._offset], index2, elements0) + address0 = TreeNode(self._input[index2 : self._offset], index2, elements0) self._offset = self._offset if address0 is FAILURE: address0 = TreeNode(self._input[index1:index1], index1) self._offset = index1 - self._cache['end'][index0] = (address0, self._offset) + self._cache["end"][index0] = (address0, self._offset) return address0 @@ -3285,20 +3659,24 @@ def parse(self): return tree if not self._expected: self._failure = self._offset - self._expected.append('') + self._expected.append("") raise ParseError(format_error(self._input, self._failure, self._expected)) def format_error(input, offset, expected): - lines, line_no, position = input.split('\n'), 0, 0 + lines, line_no, position = input.split("\n"), 0, 0 while position <= offset: position += len(lines[line_no]) + 1 line_no += 1 - message, line = 'Line ' + str(line_no) + ': expected ' + ', '.join(expected) + '\n', lines[line_no - 1] - message += line + '\n' + message, line = ( + "Line " + str(line_no) + ": expected " + ", ".join(expected) + "\n", + lines[line_no - 1], + ) + message += line + "\n" position -= len(line) + 1 - message += ' ' * (offset - position) - return message + '^' + message += " " * (offset - position) + return message + "^" + def parse(input, actions=None, types=None): parser = Parser(input, actions, types) diff --git a/src/metpy/io/nexrad.py b/src/metpy/io/nexrad.py index f142f6cc64f..4d0b05b5a25 100644 --- a/src/metpy/io/nexrad.py +++ b/src/metpy/io/nexrad.py @@ -4,7 +4,7 @@ """Support reading information from various NEXRAD formats.""" import bz2 -from collections import defaultdict, namedtuple, OrderedDict +from collections import OrderedDict, defaultdict, namedtuple import contextlib import datetime import logging @@ -17,9 +17,18 @@ import numpy as np from scipy.constants import day, milli -from ._tools import (Array, BitField, Bits, DictStruct, Enum, IOBuffer, NamedStruct, - open_as_needed, zlib_decompress_all_frames) from ..package_tools import Exporter +from ._tools import ( + Array, + BitField, + Bits, + DictStruct, + Enum, + IOBuffer, + NamedStruct, + open_as_needed, + zlib_decompress_all_frames, +) exporter = Exporter(globals()) @@ -28,28 +37,30 @@ def version(val): """Calculate a string version from an integer value.""" - if val / 100. > 2.: - ver = val / 100. + if val / 100.0 > 2.0: + ver = val / 100.0 else: - ver = val / 10. - return f'{ver:.1f}' + ver = val / 10.0 + return f"{ver:.1f}" def scaler(scale): """Create a function that scales by a specific value.""" + def inner(val): return val * scale + return inner def angle(val): """Convert an integer value to a floating point angle.""" - return val * 360. / 2**16 + return val * 360.0 / 2 ** 16 def az_rate(val): """Convert an integer value to a floating point angular rate.""" - return val * 90. / 2**16 + return val * 90.0 / 2 ** 16 def bzip_blocks_decompress_all(data): @@ -60,21 +71,22 @@ def bzip_blocks_decompress_all(data): frames = bytearray() offset = 0 while offset < len(data): - size_bytes = data[offset:offset + 4] + size_bytes = data[offset : offset + 4] offset += 4 - block_cmp_bytes = abs(Struct('>l').unpack(size_bytes)[0]) + block_cmp_bytes = abs(Struct(">l").unpack(size_bytes)[0]) try: - frames.extend(bz2.decompress(data[offset:offset + block_cmp_bytes])) + frames.extend(bz2.decompress(data[offset : offset + block_cmp_bytes])) offset += block_cmp_bytes except OSError: # If we've decompressed any frames, this is an error mid-stream, so warn, stop # trying to decompress and let processing proceed if frames: - logging.warning('Error decompressing bz2 block stream at offset: %d', - offset - 4) + logging.warning( + "Error decompressing bz2 block stream at offset: %d", offset - 4 + ) break else: # Otherwise, this isn't a bzip2 stream, so bail - raise ValueError('Not a bz2 stream.') + raise ValueError("Not a bz2 stream.") return frames @@ -157,8 +169,8 @@ class Level2File: AR2_BLOCKSIZE = 2432 # 12 (CTM) + 2416 (Msg hdr + data) + 4 (FCS) CTM_HEADER_SIZE = 12 - MISSING = float('nan') - RANGE_FOLD = float('nan') # TODO: Need to separate from missing + MISSING = float("nan") + RANGE_FOLD = float("nan") # TODO: Need to separate from missing def __init__(self, filename, *, has_volume_header=True): r"""Create instance of `Level2File`. @@ -184,7 +196,7 @@ def __init__(self, filename, *, has_volume_header=True): if has_volume_header: self._read_volume_header() except (OSError, ValueError): - log.warning('Unable to read volume header. Attempting to read messages.') + log.warning("Unable to read volume header. Attempting to read messages.") self._buffer.reset() # See if we need to apply bz2 decompression @@ -197,21 +209,41 @@ def __init__(self, filename, *, has_volume_header=True): # Now we're all initialized, we can proceed with reading in data self._read_data() - vol_hdr_fmt = NamedStruct([('version', '9s'), ('vol_num', '3s'), - ('date', 'L'), ('time_ms', 'L'), ('stid', '4s')], '>', 'VolHdr') + vol_hdr_fmt = NamedStruct( + [ + ("version", "9s"), + ("vol_num", "3s"), + ("date", "L"), + ("time_ms", "L"), + ("stid", "4s"), + ], + ">", + "VolHdr", + ) def _read_volume_header(self): self.vol_hdr = self._buffer.read_struct(self.vol_hdr_fmt) self.dt = nexrad_to_datetime(self.vol_hdr.date, self.vol_hdr.time_ms) self.stid = self.vol_hdr.stid - msg_hdr_fmt = NamedStruct([('size_hw', 'H'), - ('rda_channel', 'B', BitField('Redundant Channel 1', - 'Redundant Channel 2', - None, 'ORDA')), - ('msg_type', 'B'), ('seq_num', 'H'), ('date', 'H'), - ('time_ms', 'I'), ('num_segments', 'H'), ('segment_num', 'H')], - '>', 'MsgHdr') + msg_hdr_fmt = NamedStruct( + [ + ("size_hw", "H"), + ( + "rda_channel", + "B", + BitField("Redundant Channel 1", "Redundant Channel 2", None, "ORDA"), + ), + ("msg_type", "B"), + ("seq_num", "H"), + ("date", "H"), + ("time_ms", "I"), + ("num_segments", "H"), + ("segment_num", "H"), + ], + ">", + "MsgHdr", + ) def _read_data(self): self._msg_buf = {} @@ -228,7 +260,7 @@ def _read_data(self): # Read the message header msg_hdr = self._buffer.read_struct(self.msg_hdr_fmt) - log.debug('Got message: %s (at offset %d)', str(msg_hdr), self._buffer._offset) + log.debug("Got message: %s (at offset %d)", str(msg_hdr), self._buffer._offset) # The AR2_BLOCKSIZE accounts for the CTM header before the # data, as well as the Frame Check Sequence (4 bytes) after @@ -246,20 +278,21 @@ def _read_data(self): # As of 2620002P, this is a special value used to indicate that the segment # number/count bytes are used to indicate total size in bytes. if msg_hdr.size_hw == 65535: - msg_bytes = (msg_hdr.num_segments << 16 | msg_hdr.segment_num - + self.CTM_HEADER_SIZE) + msg_bytes = ( + msg_hdr.num_segments << 16 | msg_hdr.segment_num + self.CTM_HEADER_SIZE + ) elif msg_hdr.msg_type in (29, 31): msg_bytes = self.CTM_HEADER_SIZE + 2 * msg_hdr.size_hw - log.debug('Total message size: %d', msg_bytes) + log.debug("Total message size: %d", msg_bytes) # Try to handle the message. If we don't handle it, skipping # past it is handled at the end anyway. - decoder = f'_decode_msg{msg_hdr.msg_type:d}' + decoder = f"_decode_msg{msg_hdr.msg_type:d}" if hasattr(self, decoder): getattr(self, decoder)(msg_hdr) else: - log.warning('Unknown message: %d', msg_hdr.msg_type) + log.warning("Unknown message: %d", msg_hdr.msg_type) # Jump to the start of the next message. This depends on whether # the message was legacy with fixed block size or not. @@ -267,30 +300,50 @@ def _read_data(self): # Check if we have any message segments still in the buffer if self._msg_buf: - log.warning('Remaining buffered messages segments for message type(s): %s', - ' '.join(map(str, self._msg_buf))) + log.warning( + "Remaining buffered messages segments for message type(s): %s", + " ".join(map(str, self._msg_buf)), + ) del self._msg_buf - msg1_fmt = NamedStruct([('time_ms', 'L'), ('date', 'H'), - ('unamb_range', 'H', scaler(0.1)), ('az_angle', 'H', angle), - ('az_num', 'H'), ('rad_status', 'H', remap_status), - ('el_angle', 'H', angle), ('el_num', 'H'), - ('surv_first_gate', 'h', scaler(0.001)), - ('doppler_first_gate', 'h', scaler(0.001)), - ('surv_gate_width', 'H', scaler(0.001)), - ('doppler_gate_width', 'H', scaler(0.001)), - ('surv_num_gates', 'H'), ('doppler_num_gates', 'H'), - ('cut_sector_num', 'H'), ('calib_dbz0', 'f'), - ('ref_offset', 'H'), ('vel_offset', 'H'), ('sw_offset', 'H'), - ('dop_res', 'H', BitField(None, 0.5, 1.0)), ('vcp', 'H'), - (None, '14x'), ('nyq_vel', 'H', scaler(0.01)), - ('atmos_atten', 'H', scaler(0.001)), ('tover', 'H', scaler(0.1)), - ('spot_blanking', 'B', BitField('Radial', 'Elevation', 'Volume')), - (None, '32x')], '>', 'Msg1Fmt') - - msg1_data_hdr = namedtuple('Msg1DataHdr', - 'name first_gate gate_width num_gates scale offset') + msg1_fmt = NamedStruct( + [ + ("time_ms", "L"), + ("date", "H"), + ("unamb_range", "H", scaler(0.1)), + ("az_angle", "H", angle), + ("az_num", "H"), + ("rad_status", "H", remap_status), + ("el_angle", "H", angle), + ("el_num", "H"), + ("surv_first_gate", "h", scaler(0.001)), + ("doppler_first_gate", "h", scaler(0.001)), + ("surv_gate_width", "H", scaler(0.001)), + ("doppler_gate_width", "H", scaler(0.001)), + ("surv_num_gates", "H"), + ("doppler_num_gates", "H"), + ("cut_sector_num", "H"), + ("calib_dbz0", "f"), + ("ref_offset", "H"), + ("vel_offset", "H"), + ("sw_offset", "H"), + ("dop_res", "H", BitField(None, 0.5, 1.0)), + ("vcp", "H"), + (None, "14x"), + ("nyq_vel", "H", scaler(0.01)), + ("atmos_atten", "H", scaler(0.001)), + ("tover", "H", scaler(0.1)), + ("spot_blanking", "B", BitField("Radial", "Elevation", "Volume")), + (None, "32x"), + ], + ">", + "Msg1Fmt", + ) + + msg1_data_hdr = namedtuple( + "Msg1DataHdr", "name first_gate gate_width num_gates scale offset" + ) def _decode_msg1(self, msg_hdr): msg_start = self._buffer.set_mark() @@ -300,28 +353,54 @@ def _decode_msg1(self, msg_hdr): # Process all data pointers: read_info = [] if hdr.surv_num_gates and hdr.ref_offset: - read_info.append((hdr.ref_offset, - self.msg1_data_hdr('REF', hdr.surv_first_gate, - hdr.surv_gate_width, - hdr.surv_num_gates, 2.0, 66.0))) + read_info.append( + ( + hdr.ref_offset, + self.msg1_data_hdr( + "REF", + hdr.surv_first_gate, + hdr.surv_gate_width, + hdr.surv_num_gates, + 2.0, + 66.0, + ), + ) + ) if hdr.vel_offset: - read_info.append((hdr.vel_offset, - self.msg1_data_hdr('VEL', hdr.doppler_first_gate, - hdr.doppler_gate_width, - hdr.doppler_num_gates, - 1. / hdr.dop_res, 129.0))) + read_info.append( + ( + hdr.vel_offset, + self.msg1_data_hdr( + "VEL", + hdr.doppler_first_gate, + hdr.doppler_gate_width, + hdr.doppler_num_gates, + 1.0 / hdr.dop_res, + 129.0, + ), + ) + ) if hdr.sw_offset: - read_info.append((hdr.sw_offset, - self.msg1_data_hdr('SW', hdr.doppler_first_gate, - hdr.doppler_gate_width, - hdr.doppler_num_gates, 2.0, 129.0))) + read_info.append( + ( + hdr.sw_offset, + self.msg1_data_hdr( + "SW", + hdr.doppler_first_gate, + hdr.doppler_gate_width, + hdr.doppler_num_gates, + 2.0, + 129.0, + ), + ) + ) for ptr, data_hdr in read_info: # Jump and read self._buffer.jump_to(msg_start, ptr) - vals = self._buffer.read_array(data_hdr.num_gates, 'B') + vals = self._buffer.read_array(data_hdr.num_gates, "B") # Scale and flag data scaled_vals = (vals - data_hdr.offset) / data_hdr.scale @@ -334,59 +413,125 @@ def _decode_msg1(self, msg_hdr): self._add_sweep(hdr) self.sweeps[-1].append((hdr, data_dict)) - msg2_fmt = NamedStruct([ - ('rda_status', 'H', BitField('None', 'Start-Up', 'Standby', 'Restart', - 'Operate', 'Spare', 'Off-line Operate')), - ('op_status', 'H', BitField('Disabled', 'On-Line', - 'Maintenance Action Required', - 'Maintenance Action Mandatory', - 'Commanded Shut Down', 'Inoperable', - 'Automatic Calibration')), - ('control_status', 'H', BitField('None', 'Local Only', - 'RPG (Remote) Only', 'Either')), - ('aux_power_gen_state', 'H', BitField('Switch to Aux Power', - 'Utility PWR Available', - 'Generator On', - 'Transfer Switch Manual', - 'Commanded Switchover')), - ('avg_tx_pwr', 'H'), ('ref_calib_cor', 'h', scaler(0.01)), - ('data_transmission_enabled', 'H', BitField('None', 'None', - 'Reflectivity', 'Velocity', 'Width')), - ('vcp_num', 'h'), ('rda_control_auth', 'H', BitField('No Action', - 'Local Control Requested', - 'Remote Control Enabled')), - ('rda_build', 'H', version), ('op_mode', 'H', BitField('None', 'Test', - 'Operational', 'Maintenance')), - ('super_res_status', 'H', BitField('None', 'Enabled', 'Disabled')), - ('cmd_status', 'H', Bits(6)), - ('avset_status', 'H', BitField('None', 'Enabled', 'Disabled')), - ('rda_alarm_status', 'H', BitField('No Alarms', 'Tower/Utilities', - 'Pedestal', 'Transmitter', 'Receiver', - 'RDA Control', 'Communication', - 'Signal Processor')), - ('command_acknowledge', 'H', BitField('Remote VCP Received', - 'Clutter Bypass map received', - 'Redundant Chan Ctrl Cmd received')), - ('channel_control_status', 'H'), - ('spot_blanking', 'H', BitField('Enabled', 'Disabled')), - ('bypass_map_gen_date', 'H'), ('bypass_map_gen_time', 'H'), - ('clutter_filter_map_gen_date', 'H'), ('clutter_filter_map_gen_time', 'H'), - ('refv_calib_cor', 'h', scaler(0.01)), - ('transition_pwr_src_state', 'H', BitField('Off', 'OK')), - ('RMS_control_status', 'H', BitField('RMS in control', 'RDA in control')), - # See Table IV-A for definition of alarms - (None, '2x'), ('alarms', '28s', Array('>14H'))], '>', 'Msg2Fmt') - - msg2_additional_fmt = NamedStruct([ - ('sig_proc_options', 'H', BitField('CMD RhoHV Test')), - (None, '36x'), ('status_version', 'H')], '>', 'Msg2AdditionalFmt') + msg2_fmt = NamedStruct( + [ + ( + "rda_status", + "H", + BitField( + "None", + "Start-Up", + "Standby", + "Restart", + "Operate", + "Spare", + "Off-line Operate", + ), + ), + ( + "op_status", + "H", + BitField( + "Disabled", + "On-Line", + "Maintenance Action Required", + "Maintenance Action Mandatory", + "Commanded Shut Down", + "Inoperable", + "Automatic Calibration", + ), + ), + ( + "control_status", + "H", + BitField("None", "Local Only", "RPG (Remote) Only", "Either"), + ), + ( + "aux_power_gen_state", + "H", + BitField( + "Switch to Aux Power", + "Utility PWR Available", + "Generator On", + "Transfer Switch Manual", + "Commanded Switchover", + ), + ), + ("avg_tx_pwr", "H"), + ("ref_calib_cor", "h", scaler(0.01)), + ( + "data_transmission_enabled", + "H", + BitField("None", "None", "Reflectivity", "Velocity", "Width"), + ), + ("vcp_num", "h"), + ( + "rda_control_auth", + "H", + BitField("No Action", "Local Control Requested", "Remote Control Enabled"), + ), + ("rda_build", "H", version), + ("op_mode", "H", BitField("None", "Test", "Operational", "Maintenance")), + ("super_res_status", "H", BitField("None", "Enabled", "Disabled")), + ("cmd_status", "H", Bits(6)), + ("avset_status", "H", BitField("None", "Enabled", "Disabled")), + ( + "rda_alarm_status", + "H", + BitField( + "No Alarms", + "Tower/Utilities", + "Pedestal", + "Transmitter", + "Receiver", + "RDA Control", + "Communication", + "Signal Processor", + ), + ), + ( + "command_acknowledge", + "H", + BitField( + "Remote VCP Received", + "Clutter Bypass map received", + "Redundant Chan Ctrl Cmd received", + ), + ), + ("channel_control_status", "H"), + ("spot_blanking", "H", BitField("Enabled", "Disabled")), + ("bypass_map_gen_date", "H"), + ("bypass_map_gen_time", "H"), + ("clutter_filter_map_gen_date", "H"), + ("clutter_filter_map_gen_time", "H"), + ("refv_calib_cor", "h", scaler(0.01)), + ("transition_pwr_src_state", "H", BitField("Off", "OK")), + ("RMS_control_status", "H", BitField("RMS in control", "RDA in control")), + # See Table IV-A for definition of alarms + (None, "2x"), + ("alarms", "28s", Array(">14H")), + ], + ">", + "Msg2Fmt", + ) + + msg2_additional_fmt = NamedStruct( + [ + ("sig_proc_options", "H", BitField("CMD RhoHV Test")), + (None, "36x"), + ("status_version", "H"), + ], + ">", + "Msg2AdditionalFmt", + ) def _decode_msg2(self, msg_hdr): msg_start = self._buffer.set_mark() self.rda_status.append(self._buffer.read_struct(self.msg2_fmt)) - remaining = (msg_hdr.size_hw * 2 - self.msg_hdr_fmt.size - - self._buffer.offset_from(msg_start)) + remaining = ( + msg_hdr.size_hw * 2 - self.msg_hdr_fmt.size - self._buffer.offset_from(msg_start) + ) # RDA Build 18.0 expanded the size if remaining >= self.msg2_additional_fmt.size: @@ -394,78 +539,121 @@ def _decode_msg2(self, msg_hdr): remaining -= self.msg2_additional_fmt.size if remaining: - log.info('Padding detected in message 2. Length encoded as %d but offset when ' - 'done is %d', 2 * msg_hdr.size_hw, self._buffer.offset_from(msg_start)) + log.info( + "Padding detected in message 2. Length encoded as %d but offset when " + "done is %d", + 2 * msg_hdr.size_hw, + self._buffer.offset_from(msg_start), + ) def _decode_msg3(self, msg_hdr): from ._nexrad_msgs.msg3 import descriptions, fields + self.maintenance_data_desc = descriptions - msg_fmt = DictStruct(fields, '>') + msg_fmt = DictStruct(fields, ">") self.maintenance_data = self._buffer.read_struct(msg_fmt) self._check_size(msg_hdr, msg_fmt.size) - vcp_fmt = NamedStruct([('size_hw', 'H'), ('pattern_type', 'H'), - ('num', 'H'), ('num_el_cuts', 'H'), - ('vcp_version', 'B'), ('clutter_map_group', 'B'), - ('dop_res', 'B', BitField(None, 0.5, 1.0)), - ('pulse_width', 'B', BitField('None', 'Short', 'Long')), - (None, '4x'), ('vcp_sequencing', 'H'), - ('vcp_supplemental_info', 'H'), (None, '2x'), - ('els', None)], '>', 'VCPFmt') - - vcp_el_fmt = NamedStruct([('el_angle', 'H', angle), - ('channel_config', 'B', Enum('Constant Phase', 'Random Phase', - 'SZ2 Phase')), - ('waveform', 'B', Enum('None', 'Contiguous Surveillance', - 'Contig. Doppler with Ambiguity Res.', - 'Contig. Doppler without Ambiguity Res.', - 'Batch', 'Staggered Pulse Pair')), - ('super_res', 'B', BitField('0.5 azimuth and 0.25km range res.', - 'Doppler to 300km', - 'Dual Polarization Control', - 'Dual Polarization to 300km')), - ('surv_prf_num', 'B'), ('surv_pulse_count', 'H'), - ('az_rate', 'h', az_rate), - ('ref_thresh', 'h', scaler(0.125)), - ('vel_thresh', 'h', scaler(0.125)), - ('sw_thresh', 'h', scaler(0.125)), - ('zdr_thresh', 'h', scaler(0.125)), - ('phidp_thresh', 'h', scaler(0.125)), - ('rhohv_thresh', 'h', scaler(0.125)), - ('sector1_edge', 'H', angle), - ('sector1_doppler_prf_num', 'H'), - ('sector1_pulse_count', 'H'), ('supplemental_data', 'H'), - ('sector2_edge', 'H', angle), - ('sector2_doppler_prf_num', 'H'), - ('sector2_pulse_count', 'H'), ('ebc_angle', 'H', angle), - ('sector3_edge', 'H', angle), - ('sector3_doppler_prf_num', 'H'), - ('sector3_pulse_count', 'H'), (None, '2x')], '>', 'VCPEl') + vcp_fmt = NamedStruct( + [ + ("size_hw", "H"), + ("pattern_type", "H"), + ("num", "H"), + ("num_el_cuts", "H"), + ("vcp_version", "B"), + ("clutter_map_group", "B"), + ("dop_res", "B", BitField(None, 0.5, 1.0)), + ("pulse_width", "B", BitField("None", "Short", "Long")), + (None, "4x"), + ("vcp_sequencing", "H"), + ("vcp_supplemental_info", "H"), + (None, "2x"), + ("els", None), + ], + ">", + "VCPFmt", + ) + + vcp_el_fmt = NamedStruct( + [ + ("el_angle", "H", angle), + ("channel_config", "B", Enum("Constant Phase", "Random Phase", "SZ2 Phase")), + ( + "waveform", + "B", + Enum( + "None", + "Contiguous Surveillance", + "Contig. Doppler with Ambiguity Res.", + "Contig. Doppler without Ambiguity Res.", + "Batch", + "Staggered Pulse Pair", + ), + ), + ( + "super_res", + "B", + BitField( + "0.5 azimuth and 0.25km range res.", + "Doppler to 300km", + "Dual Polarization Control", + "Dual Polarization to 300km", + ), + ), + ("surv_prf_num", "B"), + ("surv_pulse_count", "H"), + ("az_rate", "h", az_rate), + ("ref_thresh", "h", scaler(0.125)), + ("vel_thresh", "h", scaler(0.125)), + ("sw_thresh", "h", scaler(0.125)), + ("zdr_thresh", "h", scaler(0.125)), + ("phidp_thresh", "h", scaler(0.125)), + ("rhohv_thresh", "h", scaler(0.125)), + ("sector1_edge", "H", angle), + ("sector1_doppler_prf_num", "H"), + ("sector1_pulse_count", "H"), + ("supplemental_data", "H"), + ("sector2_edge", "H", angle), + ("sector2_doppler_prf_num", "H"), + ("sector2_pulse_count", "H"), + ("ebc_angle", "H", angle), + ("sector3_edge", "H", angle), + ("sector3_doppler_prf_num", "H"), + ("sector3_pulse_count", "H"), + (None, "2x"), + ], + ">", + "VCPEl", + ) def _decode_msg5(self, msg_hdr): vcp_info = self._buffer.read_struct(self.vcp_fmt) els = [self._buffer.read_struct(self.vcp_el_fmt) for _ in range(vcp_info.num_el_cuts)] self.vcp_info = vcp_info._replace(els=els) - self._check_size(msg_hdr, - self.vcp_fmt.size + vcp_info.num_el_cuts * self.vcp_el_fmt.size) + self._check_size( + msg_hdr, self.vcp_fmt.size + vcp_info.num_el_cuts * self.vcp_el_fmt.size + ) def _decode_msg13(self, msg_hdr): data = self._buffer_segment(msg_hdr) if data: - data = list(Struct('>{:d}h'.format(len(data) // 2)).unpack(data)) + data = list(Struct(">{:d}h".format(len(data) // 2)).unpack(data)) bmap = {} date, time, num_el = data[:3] - bmap['datetime'] = nexrad_to_datetime(date, time) + bmap["datetime"] = nexrad_to_datetime(date, time) offset = 3 - bmap['data'] = [] + bmap["data"] = [] bit_conv = Bits(16) for e in range(num_el): seg_num = data[offset] offset += 1 if seg_num != (e + 1): - log.warning('Message 13 segments out of sync -- read {} but on {}'.format( - seg_num, e + 1)) + log.warning( + "Message 13 segments out of sync -- read {} but on {}".format( + seg_num, e + 1 + ) + ) az_data = [] for _ in range(360): @@ -474,45 +662,44 @@ def _decode_msg13(self, msg_hdr): gates.extend(bit_conv(data[offset])) offset += 1 az_data.append(gates) - bmap['data'].append(az_data) + bmap["data"].append(az_data) self.clutter_filter_bypass_map = bmap if offset != len(data): - log.warning('Message 13 left data -- Used: %d Avail: %d', offset, len(data)) + log.warning("Message 13 left data -- Used: %d Avail: %d", offset, len(data)) - msg15_code_map = {0: 'Bypass Filter', 1: 'Bypass map in Control', - 2: 'Force Filter'} + msg15_code_map = {0: "Bypass Filter", 1: "Bypass map in Control", 2: "Force Filter"} def _decode_msg15(self, msg_hdr): # buffer the segments until we have the whole thing. The data # will be returned concatenated when this is the case data = self._buffer_segment(msg_hdr) if data: - data = list(Struct('>{:d}h'.format(len(data) // 2)).unpack(data)) + data = list(Struct(">{:d}h".format(len(data) // 2)).unpack(data)) cmap = {} date, time, num_el = data[:3] - cmap['datetime'] = nexrad_to_datetime(date, time) + cmap["datetime"] = nexrad_to_datetime(date, time) offset = 3 - cmap['data'] = [] + cmap["data"] = [] for _ in range(num_el): az_data = [] for _ in range(360): num_rng = data[offset] offset += 1 - codes = data[offset:2 * num_rng + offset:2] + codes = data[offset : 2 * num_rng + offset : 2] offset += 1 - ends = data[offset:2 * num_rng + offset:2] + ends = data[offset : 2 * num_rng + offset : 2] offset += 2 * num_rng - 1 az_data.append(list(zip(ends, codes))) - cmap['data'].append(az_data) + cmap["data"].append(az_data) self.clutter_filter_map = cmap if offset != len(data): - log.warning('Message 15 left data -- Used: %d Avail: %d', offset, len(data)) + log.warning("Message 15 left data -- Used: %d Avail: %d", offset, len(data)) def _decode_msg18(self, msg_hdr): # buffer the segments until we have the whole thing. The data @@ -520,14 +707,15 @@ def _decode_msg18(self, msg_hdr): data = self._buffer_segment(msg_hdr) if data: from ._nexrad_msgs.msg18 import descriptions, fields + self.rda_adaptation_desc = descriptions # Can't use NamedStruct because we have more than 255 items--this # is a CPython limit for arguments. - msg_fmt = DictStruct(fields, '>') + msg_fmt = DictStruct(fields, ">") self.rda = msg_fmt.unpack(data) for num in (11, 21, 31, 32, 300, 301): - attr = f'VCPAT{num}' + attr = f"VCPAT{num}" dat = self.rda[attr] vcp_hdr = self.vcp_fmt.unpack_from(dat, 0) off = self.vcp_fmt.size @@ -537,81 +725,141 @@ def _decode_msg18(self, msg_hdr): off += self.vcp_el_fmt.size self.rda[attr] = vcp_hdr._replace(els=els) - msg31_data_hdr_fmt = NamedStruct([('stid', '4s'), ('time_ms', 'L'), - ('date', 'H'), ('az_num', 'H'), - ('az_angle', 'f'), ('compression', 'B'), - (None, 'x'), ('rad_length', 'H'), - ('az_spacing', 'B', Enum(0, 0.5, 1.0)), - ('rad_status', 'B', remap_status), - ('el_num', 'B'), ('sector_num', 'B'), - ('el_angle', 'f'), - ('spot_blanking', 'B', BitField('Radial', 'Elevation', - 'Volume')), - ('az_index_mode', 'B', scaler(0.01)), - ('num_data_blks', 'H')], '>', 'Msg31DataHdr') - - msg31_vol_const_fmt = NamedStruct([('type', 's'), ('name', '3s'), - ('size', 'H'), ('major', 'B'), - ('minor', 'B'), ('lat', 'f'), ('lon', 'f'), - ('site_amsl', 'h'), ('feedhorn_agl', 'H'), - ('calib_dbz', 'f'), ('txpower_h', 'f'), - ('txpower_v', 'f'), ('sys_zdr', 'f'), - ('phidp0', 'f'), ('vcp', 'H'), - ('processing_status', 'H', BitField('RxR Noise', - 'CBT'))], - '>', 'VolConsts') - - msg31_el_const_fmt = NamedStruct([('type', 's'), ('name', '3s'), - ('size', 'H'), ('atmos_atten', 'h', scaler(0.001)), - ('calib_dbz0', 'f')], '>', 'ElConsts') - - rad_const_fmt_v1 = NamedStruct([('type', 's'), ('name', '3s'), ('size', 'H'), - ('unamb_range', 'H', scaler(0.1)), - ('noise_h', 'f'), ('noise_v', 'f'), - ('nyq_vel', 'H', scaler(0.01)), - (None, '2x')], '>', 'RadConstsV1') - rad_const_fmt_v2 = NamedStruct([('type', 's'), ('name', '3s'), ('size', 'H'), - ('unamb_range', 'H', scaler(0.1)), - ('noise_h', 'f'), ('noise_v', 'f'), - ('nyq_vel', 'H', scaler(0.01)), - (None, '2x'), ('calib_dbz0_h', 'f'), - ('calib_dbz0_v', 'f')], '>', 'RadConstsV2') - - data_block_fmt = NamedStruct([('type', 's'), ('name', '3s'), - ('reserved', 'L'), ('num_gates', 'H'), - ('first_gate', 'H', scaler(0.001)), - ('gate_width', 'H', scaler(0.001)), - ('tover', 'H', scaler(0.1)), - ('snr_thresh', 'h', scaler(0.1)), - ('recombined', 'B', BitField('Azimuths', 'Gates')), - ('data_size', 'B'), - ('scale', 'f'), ('offset', 'f')], '>', 'DataBlockHdr') - - Radial = namedtuple('Radial', 'header vol_consts elev_consts radial_consts moments') + msg31_data_hdr_fmt = NamedStruct( + [ + ("stid", "4s"), + ("time_ms", "L"), + ("date", "H"), + ("az_num", "H"), + ("az_angle", "f"), + ("compression", "B"), + (None, "x"), + ("rad_length", "H"), + ("az_spacing", "B", Enum(0, 0.5, 1.0)), + ("rad_status", "B", remap_status), + ("el_num", "B"), + ("sector_num", "B"), + ("el_angle", "f"), + ("spot_blanking", "B", BitField("Radial", "Elevation", "Volume")), + ("az_index_mode", "B", scaler(0.01)), + ("num_data_blks", "H"), + ], + ">", + "Msg31DataHdr", + ) + + msg31_vol_const_fmt = NamedStruct( + [ + ("type", "s"), + ("name", "3s"), + ("size", "H"), + ("major", "B"), + ("minor", "B"), + ("lat", "f"), + ("lon", "f"), + ("site_amsl", "h"), + ("feedhorn_agl", "H"), + ("calib_dbz", "f"), + ("txpower_h", "f"), + ("txpower_v", "f"), + ("sys_zdr", "f"), + ("phidp0", "f"), + ("vcp", "H"), + ("processing_status", "H", BitField("RxR Noise", "CBT")), + ], + ">", + "VolConsts", + ) + + msg31_el_const_fmt = NamedStruct( + [ + ("type", "s"), + ("name", "3s"), + ("size", "H"), + ("atmos_atten", "h", scaler(0.001)), + ("calib_dbz0", "f"), + ], + ">", + "ElConsts", + ) + + rad_const_fmt_v1 = NamedStruct( + [ + ("type", "s"), + ("name", "3s"), + ("size", "H"), + ("unamb_range", "H", scaler(0.1)), + ("noise_h", "f"), + ("noise_v", "f"), + ("nyq_vel", "H", scaler(0.01)), + (None, "2x"), + ], + ">", + "RadConstsV1", + ) + rad_const_fmt_v2 = NamedStruct( + [ + ("type", "s"), + ("name", "3s"), + ("size", "H"), + ("unamb_range", "H", scaler(0.1)), + ("noise_h", "f"), + ("noise_v", "f"), + ("nyq_vel", "H", scaler(0.01)), + (None, "2x"), + ("calib_dbz0_h", "f"), + ("calib_dbz0_v", "f"), + ], + ">", + "RadConstsV2", + ) + + data_block_fmt = NamedStruct( + [ + ("type", "s"), + ("name", "3s"), + ("reserved", "L"), + ("num_gates", "H"), + ("first_gate", "H", scaler(0.001)), + ("gate_width", "H", scaler(0.001)), + ("tover", "H", scaler(0.1)), + ("snr_thresh", "h", scaler(0.1)), + ("recombined", "B", BitField("Azimuths", "Gates")), + ("data_size", "B"), + ("scale", "f"), + ("offset", "f"), + ], + ">", + "DataBlockHdr", + ) + + Radial = namedtuple("Radial", "header vol_consts elev_consts radial_consts moments") def _decode_msg31(self, msg_hdr): msg_start = self._buffer.set_mark() data_hdr = self._buffer.read_struct(self.msg31_data_hdr_fmt) if data_hdr.compression: - log.warning('Compressed message 31 not supported!') + log.warning("Compressed message 31 not supported!") # Read all the block pointers. While the ICD specifies that at least the vol, el, rad # constant blocks as well as REF moment block are present, it says "the pointers are # not order or location dependent." radial = self.Radial(data_hdr, None, None, None, {}) block_count = 0 - for ptr in self._buffer.read_binary(data_hdr.num_data_blks, '>L'): + for ptr in self._buffer.read_binary(data_hdr.num_data_blks, ">L"): if ptr: block_count += 1 self._buffer.jump_to(msg_start, ptr) info = self._buffer.get_next(6) - if info.startswith(b'RVOL'): + if info.startswith(b"RVOL"): radial = radial._replace( - vol_consts=self._buffer.read_struct(self.msg31_vol_const_fmt)) - elif info.startswith(b'RELV'): + vol_consts=self._buffer.read_struct(self.msg31_vol_const_fmt) + ) + elif info.startswith(b"RELV"): radial = radial._replace( - elev_consts=self._buffer.read_struct(self.msg31_el_const_fmt)) - elif info.startswith(b'RRAD'): + elev_consts=self._buffer.read_struct(self.msg31_el_const_fmt) + ) + elif info.startswith(b"RRAD"): # Relies on the fact that the struct is small enough for its size # to fit in a single byte if int(info[-1]) == self.rad_const_fmt_v2.size: @@ -619,63 +867,83 @@ def _decode_msg31(self, msg_hdr): else: rad_consts = self._buffer.read_struct(self.rad_const_fmt_v1) radial = radial._replace(radial_consts=rad_consts) - elif info.startswith(b'D'): + elif info.startswith(b"D"): hdr = self._buffer.read_struct(self.data_block_fmt) # TODO: The correctness of this code is not tested - vals = self._buffer.read_array(count=hdr.num_gates, - dtype=f'>u{hdr.data_size // 8}') + vals = self._buffer.read_array( + count=hdr.num_gates, dtype=f">u{hdr.data_size // 8}" + ) scaled_vals = (vals - hdr.offset) / hdr.scale scaled_vals[vals == 0] = self.MISSING scaled_vals[vals == 1] = self.RANGE_FOLD radial.moments[hdr.name.strip()] = (hdr, scaled_vals) else: - log.warning('Unknown Message 31 block type: %s', str(info[:4])) + log.warning("Unknown Message 31 block type: %s", str(info[:4])) self._add_sweep(data_hdr) self.sweeps[-1].append(radial) if data_hdr.num_data_blks != block_count: - log.warning('Incorrect number of blocks detected -- Got %d' - ' instead of %d', block_count, data_hdr.num_data_blks) + log.warning( + "Incorrect number of blocks detected -- Got %d" " instead of %d", + block_count, + data_hdr.num_data_blks, + ) if data_hdr.rad_length != self._buffer.offset_from(msg_start): - log.info('Padding detected in message. Length encoded as %d but offset when ' - 'done is %d', data_hdr.rad_length, self._buffer.offset_from(msg_start)) + log.info( + "Padding detected in message. Length encoded as %d but offset when " + "done is %d", + data_hdr.rad_length, + self._buffer.offset_from(msg_start), + ) def _buffer_segment(self, msg_hdr): # Add to the buffer bufs = self._msg_buf.setdefault(msg_hdr.msg_type, {}) - bufs[msg_hdr.segment_num] = self._buffer.read(2 * msg_hdr.size_hw - - self.msg_hdr_fmt.size) + bufs[msg_hdr.segment_num] = self._buffer.read( + 2 * msg_hdr.size_hw - self.msg_hdr_fmt.size + ) # Warn for badly formatted data if len(bufs) != msg_hdr.segment_num: - log.warning('Segment out of order (Got: %d Count: %d) for message type %d.', - msg_hdr.segment_num, len(bufs), msg_hdr.msg_type) + log.warning( + "Segment out of order (Got: %d Count: %d) for message type %d.", + msg_hdr.segment_num, + len(bufs), + msg_hdr.msg_type, + ) # If we're complete, return the full collection of data if msg_hdr.num_segments == len(bufs): self._msg_buf.pop(msg_hdr.msg_type) - return b''.join(bytes(item[1]) for item in sorted(bufs.items())) + return b"".join(bytes(item[1]) for item in sorted(bufs.items())) def _add_sweep(self, hdr): if not self.sweeps and not hdr.rad_status & START_VOLUME: - log.warning('Missed start of volume!') + log.warning("Missed start of volume!") if hdr.rad_status & START_ELEVATION: self.sweeps.append([]) if len(self.sweeps) != hdr.el_num: - log.warning('Missed elevation -- Have %d but data on %d.' - ' Compensating...', len(self.sweeps), hdr.el_num) + log.warning( + "Missed elevation -- Have %d but data on %d." " Compensating...", + len(self.sweeps), + hdr.el_num, + ) while len(self.sweeps) < hdr.el_num: self.sweeps.append([]) def _check_size(self, msg_hdr, size): hdr_size = msg_hdr.size_hw * 2 - self.msg_hdr_fmt.size if size != hdr_size: - log.warning('Message type %d should be %d bytes but got %d', - msg_hdr.msg_type, size, hdr_size) + log.warning( + "Message type %d should be %d bytes but got %d", + msg_hdr.msg_type, + size, + hdr_size, + ) def reduce_lists(d): @@ -689,21 +957,21 @@ def reduce_lists(d): def two_comp16(val): """Return the two's-complement signed representation of a 16-bit unsigned integer.""" if val >> 15: - val = -(~val & 0x7fff) - 1 + val = -(~val & 0x7FFF) - 1 return val def float16(val): """Convert a 16-bit floating point value to a standard Python float.""" # Fraction is 10 LSB, Exponent middle 5, and Sign the MSB - frac = val & 0x03ff + frac = val & 0x03FF exp = (val >> 10) & 0x1F sign = val >> 15 if exp: - value = 2 ** (exp - 16) * (1 + float(frac) / 2**10) + value = 2 ** (exp - 16) * (1 + float(frac) / 2 ** 10) else: - value = float(frac) / 2**9 + value = float(frac) / 2 ** 9 if sign: value *= -1 @@ -714,32 +982,38 @@ def float16(val): def float32(short1, short2): """Unpack a pair of 16-bit integers as a Python float.""" # Masking below in python will properly convert signed values to unsigned - return struct.unpack('>f', struct.pack('>HH', short1 & 0xFFFF, short2 & 0xFFFF))[0] + return struct.unpack(">f", struct.pack(">HH", short1 & 0xFFFF, short2 & 0xFFFF))[0] def date_elem(ind_days, ind_minutes): """Create a function to parse a datetime from the product-specific blocks.""" + def inner(seq): return nexrad_to_datetime(seq[ind_days], seq[ind_minutes] * 60 * 1000) + return inner def scaled_elem(index, scale): """Create a function to scale a certain product-specific block.""" + def inner(seq): return seq[index] * scale + return inner def combine_elem(ind1, ind2): """Create a function to combine two specified product-specific blocks into a single int.""" + def inner(seq): - shift = 2**16 + shift = 2 ** 16 if seq[ind1] < 0: seq[ind1] += shift if seq[ind2] < 0: seq[ind2] += shift return (seq[ind1] << 16) | seq[ind2] + return inner @@ -750,32 +1024,41 @@ def float_elem(ind1, ind2): def high_byte(ind): """Create a function to return the high-byte of a product-specific block.""" + def inner(seq): return seq[ind] >> 8 + return inner def low_byte(ind): """Create a function to return the low-byte of a product-specific block.""" + def inner(seq): return seq[ind] & 0x00FF + return inner def delta_time(ind): """Create a function to return the delta time from a product-specific block.""" + def inner(seq): return seq[ind] >> 5 + return inner def supplemental_scan(ind): """Create a function to return the supplement scan type from a product-specific block.""" + def inner(seq): # ICD says 1->SAILS, 2->MRLE, but testing on 2020-08-17 makes this seem inverted # given what's being reported by sites in the GSM. - return {0: 'Non-supplemental scan', - 2: 'SAILS scan', 1: 'MRLE scan'}.get(seq[ind] & 0x001F, 'Unknown') + return {0: "Non-supplemental scan", 2: "SAILS scan", 1: "MRLE scan"}.get( + seq[ind] & 0x001F, "Unknown" + ) + return inner @@ -788,8 +1071,8 @@ class DataMapper: # Need to find way to handle range folded # RANGE_FOLD = -9999 - RANGE_FOLD = float('nan') - MISSING = float('nan') + RANGE_FOLD = float("nan") + MISSING = float("nan") def __init__(self, num=256): self.lut = np.full(num, self.MISSING, dtype=np.float) @@ -826,13 +1109,13 @@ def __init__(self, prod): class DigitalRefMapper(DigitalMapper): """Mapper for digital reflectivity products.""" - units = 'dBZ' + units = "dBZ" class DigitalVelMapper(DigitalMapper): """Mapper for digital velocity products.""" - units = 'm/s' + units = "m/s" range_fold = True @@ -850,13 +1133,13 @@ class PrecipArrayMapper(DigitalMapper): _inc_scale = 0.001 _min_data = 1 _max_data = 254 - units = 'dBA' + units = "dBA" class DigitalStormPrecipMapper(DigitalMapper): """Mapper for digital storm precipitation products.""" - units = 'inches' + units = "inches" _inc_scale = 0.01 @@ -932,8 +1215,23 @@ class DigitalHMCMapper(DataMapper): Handles assigning string labels based on values. """ - labels = ['ND', 'BI', 'GC', 'IC', 'DS', 'WS', 'RA', 'HR', - 'BD', 'GR', 'HA', 'LH', 'GH', 'UK', 'RF'] + labels = [ + "ND", + "BI", + "GC", + "IC", + "DS", + "WS", + "RA", + "HR", + "BD", + "GR", + "HA", + "LH", + "GH", + "UK", + "RF", + ] def __init__(self, prod): """Initialize the mapper.""" @@ -951,8 +1249,8 @@ def __init__(self, prod): """Initialize the mapper based on the product.""" data_levels = prod.thresholds[2] super().__init__(data_levels) - scale = prod.thresholds[0] / 1000. - offset = prod.thresholds[1] / 1000. + scale = prod.thresholds[0] / 1000.0 + offset = prod.thresholds[1] / 1000.0 leading_flags = prod.thresholds[3] for i in range(leading_flags, data_levels): self.lut = scale * i + offset @@ -961,8 +1259,23 @@ def __init__(self, prod): class LegacyMapper(DataMapper): """Mapper for legacy products.""" - lut_names = ['Blank', 'TH', 'ND', 'RF', 'BI', 'GC', 'IC', 'GR', 'WS', - 'DS', 'RA', 'HR', 'BD', 'HA', 'UK'] + lut_names = [ + "Blank", + "TH", + "ND", + "RF", + "BI", + "GC", + "IC", + "GR", + "WS", + "DS", + "RA", + "HR", + "BD", + "HA", + "UK", + ] def __init__(self, prod): """Initialize the values and labels from the product.""" @@ -971,34 +1284,34 @@ def __init__(self, prod): self.lut = [] for t in prod.thresholds: codes, val = t >> 8, t & 0xFF - label = '' + label = "" if codes >> 7: label = self.lut_names[val] - if label in ('Blank', 'TH', 'ND'): + if label in ("Blank", "TH", "ND"): val = self.MISSING - elif label == 'RF': + elif label == "RF": val = self.RANGE_FOLD elif codes >> 6: val *= 0.01 - label = f'{val:.2f}' + label = f"{val:.2f}" elif codes >> 5: val *= 0.05 - label = f'{val:.2f}' + label = f"{val:.2f}" elif codes >> 4: val *= 0.1 - label = f'{val:.1f}' + label = f"{val:.1f}" if codes & 0x1: val *= -1 - label = '-' + label + label = "-" + label elif (codes >> 1) & 0x1: - label = '+' + label + label = "+" + label if (codes >> 2) & 0x1: - label = '<' + label + label = "<" + label elif (codes >> 3) & 0x1: - label = '>' + label + label = ">" + label if not label: label = str(val) @@ -1053,570 +1366,1153 @@ class Level3File: """ ij_to_km = 0.25 - wmo_finder = re.compile('((?:NX|SD|NO)US)\\d{2}[\\s\\w\\d]+\\w*(\\w{3})\r\r\n') - header_fmt = NamedStruct([('code', 'H'), ('date', 'H'), ('time', 'l'), - ('msg_len', 'L'), ('src_id', 'h'), ('dest_id', 'h'), - ('num_blks', 'H')], '>', 'MsgHdr') + wmo_finder = re.compile("((?:NX|SD|NO)US)\\d{2}[\\s\\w\\d]+\\w*(\\w{3})\r\r\n") + header_fmt = NamedStruct( + [ + ("code", "H"), + ("date", "H"), + ("time", "l"), + ("msg_len", "L"), + ("src_id", "h"), + ("dest_id", "h"), + ("num_blks", "H"), + ], + ">", + "MsgHdr", + ) # See figure 3-17 in 2620001 document for definition of status bit fields - gsm_fmt = NamedStruct([('divider', 'h'), ('block_len', 'H'), - ('op_mode', 'h', BitField('Clear Air', 'Precip')), - ('rda_op_status', 'h', BitField('Spare', 'Online', - 'Maintenance Required', - 'Maintenance Mandatory', - 'Commanded Shutdown', 'Inoperable', - 'Spare', 'Wideband Disconnect')), - ('vcp', 'h'), ('num_el', 'h'), - ('el1', 'h', scaler(0.1)), ('el2', 'h', scaler(0.1)), - ('el3', 'h', scaler(0.1)), ('el4', 'h', scaler(0.1)), - ('el5', 'h', scaler(0.1)), ('el6', 'h', scaler(0.1)), - ('el7', 'h', scaler(0.1)), ('el8', 'h', scaler(0.1)), - ('el9', 'h', scaler(0.1)), ('el10', 'h', scaler(0.1)), - ('el11', 'h', scaler(0.1)), ('el12', 'h', scaler(0.1)), - ('el13', 'h', scaler(0.1)), ('el14', 'h', scaler(0.1)), - ('el15', 'h', scaler(0.1)), ('el16', 'h', scaler(0.1)), - ('el17', 'h', scaler(0.1)), ('el18', 'h', scaler(0.1)), - ('el19', 'h', scaler(0.1)), ('el20', 'h', scaler(0.1)), - ('rda_status', 'h', BitField('Spare', 'Startup', 'Standby', - 'Restart', 'Operate', - 'Off-line Operate')), - ('rda_alarms', 'h', BitField('Indeterminate', 'Tower/Utilities', - 'Pedestal', 'Transmitter', 'Receiver', - 'RDA Control', 'RDA Communications', - 'Signal Processor')), - ('tranmission_enable', 'h', BitField('Spare', 'None', - 'Reflectivity', - 'Velocity', 'Spectrum Width', - 'Dual Pol')), - ('rpg_op_status', 'h', BitField('Loadshed', 'Online', - 'Maintenance Required', - 'Maintenance Mandatory', - 'Commanded shutdown')), - ('rpg_alarms', 'h', BitField('None', 'Node Connectivity', - 'Wideband Failure', - 'RPG Control Task Failure', - 'Data Base Failure', 'Spare', - 'RPG Input Buffer Loadshed', - 'Spare', 'Product Storage Loadshed' - 'Spare', 'Spare', 'Spare', - 'RPG/RPG Intercomputer Link Failure', - 'Redundant Channel Error', - 'Task Failure', 'Media Failure')), - ('rpg_status', 'h', BitField('Restart', 'Operate', 'Standby')), - ('rpg_narrowband_status', 'h', BitField('Commanded Disconnect', - 'Narrowband Loadshed')), - ('h_ref_calib', 'h', scaler(0.25)), - ('prod_avail', 'h', BitField('Product Availability', - 'Degraded Availability', - 'Not Available')), - ('super_res_cuts', 'h', Bits(16)), - ('cmd_status', 'h', Bits(6)), - ('v_ref_calib', 'h', scaler(0.25)), - ('rda_build', 'h', version), ('rda_channel', 'h'), - ('reserved', 'h'), ('reserved2', 'h'), - ('build_version', 'h', version)], '>', 'GSM') + gsm_fmt = NamedStruct( + [ + ("divider", "h"), + ("block_len", "H"), + ("op_mode", "h", BitField("Clear Air", "Precip")), + ( + "rda_op_status", + "h", + BitField( + "Spare", + "Online", + "Maintenance Required", + "Maintenance Mandatory", + "Commanded Shutdown", + "Inoperable", + "Spare", + "Wideband Disconnect", + ), + ), + ("vcp", "h"), + ("num_el", "h"), + ("el1", "h", scaler(0.1)), + ("el2", "h", scaler(0.1)), + ("el3", "h", scaler(0.1)), + ("el4", "h", scaler(0.1)), + ("el5", "h", scaler(0.1)), + ("el6", "h", scaler(0.1)), + ("el7", "h", scaler(0.1)), + ("el8", "h", scaler(0.1)), + ("el9", "h", scaler(0.1)), + ("el10", "h", scaler(0.1)), + ("el11", "h", scaler(0.1)), + ("el12", "h", scaler(0.1)), + ("el13", "h", scaler(0.1)), + ("el14", "h", scaler(0.1)), + ("el15", "h", scaler(0.1)), + ("el16", "h", scaler(0.1)), + ("el17", "h", scaler(0.1)), + ("el18", "h", scaler(0.1)), + ("el19", "h", scaler(0.1)), + ("el20", "h", scaler(0.1)), + ( + "rda_status", + "h", + BitField( + "Spare", "Startup", "Standby", "Restart", "Operate", "Off-line Operate" + ), + ), + ( + "rda_alarms", + "h", + BitField( + "Indeterminate", + "Tower/Utilities", + "Pedestal", + "Transmitter", + "Receiver", + "RDA Control", + "RDA Communications", + "Signal Processor", + ), + ), + ( + "tranmission_enable", + "h", + BitField( + "Spare", "None", "Reflectivity", "Velocity", "Spectrum Width", "Dual Pol" + ), + ), + ( + "rpg_op_status", + "h", + BitField( + "Loadshed", + "Online", + "Maintenance Required", + "Maintenance Mandatory", + "Commanded shutdown", + ), + ), + ( + "rpg_alarms", + "h", + BitField( + "None", + "Node Connectivity", + "Wideband Failure", + "RPG Control Task Failure", + "Data Base Failure", + "Spare", + "RPG Input Buffer Loadshed", + "Spare", + "Product Storage Loadshed" "Spare", + "Spare", + "Spare", + "RPG/RPG Intercomputer Link Failure", + "Redundant Channel Error", + "Task Failure", + "Media Failure", + ), + ), + ("rpg_status", "h", BitField("Restart", "Operate", "Standby")), + ( + "rpg_narrowband_status", + "h", + BitField("Commanded Disconnect", "Narrowband Loadshed"), + ), + ("h_ref_calib", "h", scaler(0.25)), + ( + "prod_avail", + "h", + BitField("Product Availability", "Degraded Availability", "Not Available"), + ), + ("super_res_cuts", "h", Bits(16)), + ("cmd_status", "h", Bits(6)), + ("v_ref_calib", "h", scaler(0.25)), + ("rda_build", "h", version), + ("rda_channel", "h"), + ("reserved", "h"), + ("reserved2", "h"), + ("build_version", "h", version), + ], + ">", + "GSM", + ) # Build 14.0 added more bytes to the GSM - additional_gsm_fmt = NamedStruct([('el21', 'h', scaler(0.1)), - ('el22', 'h', scaler(0.1)), - ('el23', 'h', scaler(0.1)), - ('el24', 'h', scaler(0.1)), - ('el25', 'h', scaler(0.1)), - ('vcp_supplemental', 'H', - BitField('AVSET', 'SAILS', 'Site VCP', 'RxR Noise', - 'CBT', 'VCP Sequence', 'SPRT', 'MRLE', - 'Base Tilt', 'MPDA')), - ('supplemental_cut_map', 'H', Bits(16)), - ('supplemental_cut_count', 'B'), - ('supplemental_cut_map2', 'B', Bits(8)), - ('spare', '80s')], '>', 'GSM') - prod_desc_fmt = NamedStruct([('divider', 'h'), ('lat', 'l'), ('lon', 'l'), - ('height', 'h'), ('prod_code', 'h'), - ('op_mode', 'h'), ('vcp', 'h'), ('seq_num', 'h'), - ('vol_num', 'h'), ('vol_date', 'h'), - ('vol_start_time', 'l'), ('prod_gen_date', 'h'), - ('prod_gen_time', 'l'), ('dep1', 'h'), - ('dep2', 'h'), ('el_num', 'h'), ('dep3', 'h'), - ('thr1', 'h'), ('thr2', 'h'), ('thr3', 'h'), - ('thr4', 'h'), ('thr5', 'h'), ('thr6', 'h'), - ('thr7', 'h'), ('thr8', 'h'), ('thr9', 'h'), - ('thr10', 'h'), ('thr11', 'h'), ('thr12', 'h'), - ('thr13', 'h'), ('thr14', 'h'), ('thr15', 'h'), - ('thr16', 'h'), ('dep4', 'h'), ('dep5', 'h'), - ('dep6', 'h'), ('dep7', 'h'), ('dep8', 'h'), - ('dep9', 'h'), ('dep10', 'h'), ('version', 'b'), - ('spot_blank', 'b'), ('sym_off', 'L'), ('graph_off', 'L'), - ('tab_off', 'L')], '>', 'ProdDesc') - sym_block_fmt = NamedStruct([('divider', 'h'), ('block_id', 'h'), - ('block_len', 'L'), ('nlayer', 'H')], '>', 'SymBlock') - tab_header_fmt = NamedStruct([('divider', 'h'), ('block_id', 'h'), - ('block_len', 'L')], '>', 'TabHeader') - tab_block_fmt = NamedStruct([('divider', 'h'), ('num_pages', 'h')], '>', 'TabBlock') - sym_layer_fmt = NamedStruct([('divider', 'h'), ('length', 'L')], '>', - 'SymLayer') - graph_block_fmt = NamedStruct([('divider', 'h'), ('block_id', 'h'), - ('block_len', 'L'), ('num_pages', 'H')], '>', 'GraphBlock') + additional_gsm_fmt = NamedStruct( + [ + ("el21", "h", scaler(0.1)), + ("el22", "h", scaler(0.1)), + ("el23", "h", scaler(0.1)), + ("el24", "h", scaler(0.1)), + ("el25", "h", scaler(0.1)), + ( + "vcp_supplemental", + "H", + BitField( + "AVSET", + "SAILS", + "Site VCP", + "RxR Noise", + "CBT", + "VCP Sequence", + "SPRT", + "MRLE", + "Base Tilt", + "MPDA", + ), + ), + ("supplemental_cut_map", "H", Bits(16)), + ("supplemental_cut_count", "B"), + ("supplemental_cut_map2", "B", Bits(8)), + ("spare", "80s"), + ], + ">", + "GSM", + ) + prod_desc_fmt = NamedStruct( + [ + ("divider", "h"), + ("lat", "l"), + ("lon", "l"), + ("height", "h"), + ("prod_code", "h"), + ("op_mode", "h"), + ("vcp", "h"), + ("seq_num", "h"), + ("vol_num", "h"), + ("vol_date", "h"), + ("vol_start_time", "l"), + ("prod_gen_date", "h"), + ("prod_gen_time", "l"), + ("dep1", "h"), + ("dep2", "h"), + ("el_num", "h"), + ("dep3", "h"), + ("thr1", "h"), + ("thr2", "h"), + ("thr3", "h"), + ("thr4", "h"), + ("thr5", "h"), + ("thr6", "h"), + ("thr7", "h"), + ("thr8", "h"), + ("thr9", "h"), + ("thr10", "h"), + ("thr11", "h"), + ("thr12", "h"), + ("thr13", "h"), + ("thr14", "h"), + ("thr15", "h"), + ("thr16", "h"), + ("dep4", "h"), + ("dep5", "h"), + ("dep6", "h"), + ("dep7", "h"), + ("dep8", "h"), + ("dep9", "h"), + ("dep10", "h"), + ("version", "b"), + ("spot_blank", "b"), + ("sym_off", "L"), + ("graph_off", "L"), + ("tab_off", "L"), + ], + ">", + "ProdDesc", + ) + sym_block_fmt = NamedStruct( + [("divider", "h"), ("block_id", "h"), ("block_len", "L"), ("nlayer", "H")], + ">", + "SymBlock", + ) + tab_header_fmt = NamedStruct( + [("divider", "h"), ("block_id", "h"), ("block_len", "L")], ">", "TabHeader" + ) + tab_block_fmt = NamedStruct([("divider", "h"), ("num_pages", "h")], ">", "TabBlock") + sym_layer_fmt = NamedStruct([("divider", "h"), ("length", "L")], ">", "SymLayer") + graph_block_fmt = NamedStruct( + [("divider", "h"), ("block_id", "h"), ("block_len", "L"), ("num_pages", "H")], + ">", + "GraphBlock", + ) standalone_tabular = [62, 73, 75, 82] - prod_spec_map = {16: ('Base Reflectivity', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('calib_const', float_elem(7, 8)))), - 17: ('Base Reflectivity', 460., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('calib_const', float_elem(7, 8)))), - 18: ('Base Reflectivity', 460., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('calib_const', float_elem(7, 8)))), - 19: ('Base Reflectivity', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)), - ('calib_const', float_elem(7, 8)))), - 20: ('Base Reflectivity', 460., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)), - ('calib_const', float_elem(7, 8)))), - 21: ('Base Reflectivity', 460., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('calib_const', float_elem(7, 8)))), - 22: ('Base Velocity', 60., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', 3), ('max', 4))), - 23: ('Base Velocity', 115., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', 3), ('max', 4))), - 24: ('Base Velocity', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', 3), ('max', 4))), - 25: ('Base Velocity', 60., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', 3), ('max', 4))), - 26: ('Base Velocity', 115., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', 3), ('max', 4))), - 27: ('Base Velocity', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', 3), ('max', 4), - ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)))), - 28: ('Base Spectrum Width', 60., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3))), - 29: ('Base Spectrum Width', 115., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3))), - 30: ('Base Spectrum Width', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)))), - 31: ('User Selectable Storm Total Precipitation', 230., LegacyMapper, - (('end_hour', 0), - ('hour_span', 1), - ('null_product', 2), - ('max_rainfall', scaled_elem(3, 0.1)), - ('rainfall_begin', date_elem(4, 5)), - ('rainfall_end', date_elem(6, 7)), - ('bias', scaled_elem(8, 0.01)), - ('gr_pairs', scaled_elem(5, 0.01)))), - 32: ('Digital Hybrid Scan Reflectivity', 230., DigitalRefMapper, - (('max', 3), - ('avg_time', date_elem(4, 5)), - ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 33: ('Hybrid Scan Reflectivity', 230., LegacyMapper, - (('max', 3), ('avg_time', date_elem(4, 5)))), - 34: ('Clutter Filter Control', 230., LegacyMapper, - (('clutter_bitmap', 0), - ('cmd_map', 1), - ('bypass_map_date', date_elem(4, 5)), - ('notchwidth_map_date', date_elem(6, 7)))), - 35: ('Composite Reflectivity', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('calib_const', float_elem(7, 8)))), - 36: ('Composite Reflectivity', 460., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('calib_const', float_elem(7, 8)))), - 37: ('Composite Reflectivity', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('calib_const', float_elem(7, 8)))), - 38: ('Composite Reflectivity', 460., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('calib_const', float_elem(7, 8)))), - 41: ('Echo Tops', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', scaled_elem(3, 1000)))), # Max in ft - 48: ('VAD Wind Profile', None, LegacyMapper, - (('max', 3), - ('dir_max', 4), - ('alt_max', scaled_elem(5, 10)))), # Max in ft - 50: ('Cross Section Reflectivity', 230., LegacyMapper, - (('azimuth1', scaled_elem(0, 0.1)), - ('range1', scaled_elem(1, 0.1)), - ('azimuth2', scaled_elem(2, 0.1)), - ('range2', scaled_elem(3, 0.1)))), - 51: ('Cross Section Velocity', 230., LegacyMapper, - (('azimuth1', scaled_elem(0, 0.1)), - ('range1', scaled_elem(1, 0.1)), - ('azimuth2', scaled_elem(2, 0.1)), - ('range2', scaled_elem(3, 0.1)))), - 55: ('Storm Relative Mean Radial Velocity', 50., LegacyMapper, - (('window_az', scaled_elem(0, 0.1)), - ('window_range', scaled_elem(1, 0.1)), - ('el_angle', scaled_elem(2, 0.1)), - ('min', 3), - ('max', 4), - ('source', 5), - ('height', 6), - ('avg_speed', scaled_elem(7, 0.1)), - ('avg_dir', scaled_elem(8, 0.1)), - ('alert_category', 9))), - 56: ('Storm Relative Mean Radial Velocity', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', 3), - ('max', 4), - ('source', 5), - ('avg_speed', scaled_elem(7, 0.1)), - ('avg_dir', scaled_elem(8, 0.1)))), - 57: ('Vertically Integrated Liquid', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3))), # Max in kg / m^2 - 58: ('Storm Tracking Information', 460., LegacyMapper, - (('num_storms', 3),)), - 59: ('Hail Index', 230., LegacyMapper, ()), - 61: ('Tornado Vortex Signature', 230., LegacyMapper, - (('num_tvs', 3), ('num_etvs', 4))), - 62: ('Storm Structure', 460., LegacyMapper, ()), - 63: ('Layer Composite Reflectivity (Layer 1 Average)', 230., LegacyMapper, - (('max', 3), - ('layer_bottom', scaled_elem(4, 1000.)), - ('layer_top', scaled_elem(5, 1000.)), - ('calib_const', float_elem(7, 8)))), - 64: ('Layer Composite Reflectivity (Layer 2 Average)', 230., LegacyMapper, - (('max', 3), - ('layer_bottom', scaled_elem(4, 1000.)), - ('layer_top', scaled_elem(5, 1000.)), - ('calib_const', float_elem(7, 8)))), - 65: ('Layer Composite Reflectivity (Layer 1 Max)', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('layer_bottom', scaled_elem(4, 1000.)), - ('layer_top', scaled_elem(5, 1000.)), - ('calib_const', float_elem(7, 8)))), - 66: ('Layer Composite Reflectivity (Layer 2 Max)', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('layer_bottom', scaled_elem(4, 1000.)), - ('layer_top', scaled_elem(5, 1000.)), - ('calib_const', float_elem(7, 8)))), - 67: ('Layer Composite Reflectivity - AP Removed', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('layer_bottom', scaled_elem(4, 1000.)), - ('layer_top', scaled_elem(5, 1000.)), - ('calib_const', float_elem(7, 8)))), - 74: ('Radar Coded Message', 460., LegacyMapper, ()), - 78: ('Surface Rainfall Accumulation (1 hour)', 230., LegacyMapper, - (('max_rainfall', scaled_elem(3, 0.1)), - ('bias', scaled_elem(4, 0.01)), - ('gr_pairs', scaled_elem(5, 0.01)), - ('rainfall_end', date_elem(6, 7)))), - 79: ('Surface Rainfall Accumulation (3 hour)', 230., LegacyMapper, - (('max_rainfall', scaled_elem(3, 0.1)), - ('bias', scaled_elem(4, 0.01)), - ('gr_pairs', scaled_elem(5, 0.01)), - ('rainfall_end', date_elem(6, 7)))), - 80: ('Storm Total Rainfall Accumulation', 230., LegacyMapper, - (('max_rainfall', scaled_elem(3, 0.1)), - ('rainfall_begin', date_elem(4, 5)), - ('rainfall_end', date_elem(6, 7)), - ('bias', scaled_elem(8, 0.01)), - ('gr_pairs', scaled_elem(9, 0.01)))), - 81: ('Hourly Digital Precipitation Array', 230., PrecipArrayMapper, - (('max_rainfall', scaled_elem(3, 0.001)), - ('bias', scaled_elem(4, 0.01)), - ('gr_pairs', scaled_elem(5, 0.01)), - ('rainfall_end', date_elem(6, 7)))), - 82: ('Supplemental Precipitation Data', None, LegacyMapper, ()), - 89: ('Layer Composite Reflectivity (Layer 3 Average)', 230., LegacyMapper, - (('max', 3), - ('layer_bottom', scaled_elem(4, 1000.)), - ('layer_top', scaled_elem(5, 1000.)), - ('calib_const', float_elem(7, 8)))), - 90: ('Layer Composite Reflectivity (Layer 3 Max)', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('layer_bottom', scaled_elem(4, 1000.)), - ('layer_top', scaled_elem(5, 1000.)), - ('calib_const', float_elem(7, 8)))), - 93: ('ITWS Digital Base Velocity', 115., DigitalVelMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', 3), - ('max', 4), ('precision', 6))), - 94: ('Base Reflectivity Data Array', 460., DigitalRefMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)), - ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 95: ('Composite Reflectivity Edited for AP', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('calib_const', float_elem(7, 8)))), - 96: ('Composite Reflectivity Edited for AP', 460., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('calib_const', float_elem(7, 8)))), - 97: ('Composite Reflectivity Edited for AP', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('calib_const', float_elem(7, 8)))), - 98: ('Composite Reflectivity Edited for AP', 460., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('calib_const', float_elem(7, 8)))), - 99: ('Base Velocity Data Array', 300., DigitalVelMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', 3), - ('max', 4), - ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)), - ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 113: ('Power Removed Control', 300., LegacyMapper, - (('rpg_cut_num', 0), ('cmd_generated', 1), - ('el_angle', scaled_elem(2, 0.1)), - ('clutter_filter_map_dt', date_elem(4, 3)), - # While the 2620001Y ICD doesn't talk about using these - # product-specific blocks for this product, they have data in them - # and the compression info is necessary for proper decoding. - ('compression', 7), ('uncompressed_size', combine_elem(8, 9)))), - 132: ('Clutter Likelihood Reflectivity', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)),)), - 133: ('Clutter Likelihood Doppler', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)),)), - 134: ('High Resolution VIL', 460., DigitalVILMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('num_edited', 4), - ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 135: ('Enhanced Echo Tops', 345., DigitalEETMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', scaled_elem(3, 1000.)), # Max in ft - ('num_edited', 4), - ('ref_thresh', 5), - ('points_removed', 6), - ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 138: ('Digital Storm Total Precipitation', 230., DigitalStormPrecipMapper, - (('rainfall_begin', date_elem(0, 1)), - ('bias', scaled_elem(2, 0.01)), - ('max', scaled_elem(3, 0.01)), - ('rainfall_end', date_elem(4, 5)), - ('gr_pairs', scaled_elem(6, 0.01)), - ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 141: ('Mesocyclone Detection', 230., LegacyMapper, - (('min_ref_thresh', 0), - ('overlap_display_filter', 1), - ('min_strength_rank', 2))), - 152: ('Archive III Status Product', None, LegacyMapper, - (('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 153: ('Super Resolution Reflectivity Data Array', 460., DigitalRefMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)), ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 154: ('Super Resolution Velocity Data Array', 300., DigitalVelMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', 3), ('max', 4), ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)), ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 155: ('Super Resolution Spectrum Width Data Array', 300., - DigitalSPWMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)), ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 156: ('Turbulence Detection (Eddy Dissipation Rate)', 230., EDRMapper, - (('el_start_time', 0), - ('el_end_time', 1), - ('el_angle', scaled_elem(2, 0.1)), - ('min_el', scaled_elem(3, 0.01)), - ('mean_el', scaled_elem(4, 0.01)), - ('max_el', scaled_elem(5, 0.01)))), - 157: ('Turbulence Detection (Eddy Dissipation Rate Confidence)', 230., - EDRMapper, - (('el_start_time', 0), - ('el_end_time', 1), - ('el_angle', scaled_elem(2, 0.1)), - ('min_el', scaled_elem(3, 0.01)), - ('mean_el', scaled_elem(4, 0.01)), - ('max_el', scaled_elem(5, 0.01)))), - 158: ('Differential Reflectivity', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', scaled_elem(3, 0.1)), - ('max', scaled_elem(4, 0.1)))), - 159: ('Digital Differential Reflectivity', 300., GenericDigitalMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', scaled_elem(3, 0.1)), - ('max', scaled_elem(4, 0.1)), ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)), ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 160: ('Correlation Coefficient', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', scaled_elem(3, 0.00333)), - ('max', scaled_elem(4, 0.00333)))), - 161: ('Digital Correlation Coefficient', 300., GenericDigitalMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', scaled_elem(3, 0.00333)), - ('max', scaled_elem(4, 0.00333)), ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)), ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 162: ('Specific Differential Phase', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', scaled_elem(3, 0.05)), - ('max', scaled_elem(4, 0.05)))), - 163: ('Digital Specific Differential Phase', 300., GenericDigitalMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', scaled_elem(3, 0.05)), - ('max', scaled_elem(4, 0.05)), ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)), ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 164: ('Hydrometeor Classification', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)),)), - 165: ('Digital Hydrometeor Classification', 300., DigitalHMCMapper, - (('el_angle', scaled_elem(2, 0.1)), ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)), ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 166: ('Melting Layer', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)),)), - 167: ('Super Res Digital Correlation Coefficient', 300., - GenericDigitalMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', scaled_elem(3, 0.00333)), - ('max', scaled_elem(4, 0.00333)), ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)), ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 168: ('Super Res Digital Phi', 300., GenericDigitalMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', 3), ('max', 4), ('delta_time', delta_time(6)), - ('supplemental_scan', supplemental_scan(6)), ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 169: ('One Hour Accumulation', 230., LegacyMapper, - (('null_product', low_byte(2)), - ('max', scaled_elem(3, 0.1)), - ('rainfall_end', date_elem(4, 5)), - ('bias', scaled_elem(6, 0.01)), - ('gr_pairs', scaled_elem(7, 0.01)))), - 170: ('Digital Accumulation Array', 230., GenericDigitalMapper, - (('null_product', low_byte(2)), - ('max', scaled_elem(3, 0.1)), - ('rainfall_end', date_elem(4, 5)), - ('bias', scaled_elem(6, 0.01)), - ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 171: ('Storm Total Accumulation', 230., LegacyMapper, - (('rainfall_begin', date_elem(0, 1)), - ('null_product', low_byte(2)), - ('max', scaled_elem(3, 0.1)), - ('rainfall_end', date_elem(4, 5)), - ('bias', scaled_elem(6, 0.01)), - ('gr_pairs', scaled_elem(7, 0.01)))), - 172: ('Digital Storm Total Accumulation', 230., GenericDigitalMapper, - (('rainfall_begin', date_elem(0, 1)), - ('null_product', low_byte(2)), - ('max', scaled_elem(3, 0.1)), - ('rainfall_end', date_elem(4, 5)), - ('bias', scaled_elem(6, 0.01)), - ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 173: ('Digital User-Selectable Accumulation', 230., GenericDigitalMapper, - (('period', 1), - ('missing_period', high_byte(2)), - ('null_product', low_byte(2)), - ('max', scaled_elem(3, 0.1)), - ('rainfall_end', date_elem(4, 0)), - ('start_time', 5), - ('bias', scaled_elem(6, 0.01)), - ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 174: ('Digital One-Hour Difference Accumulation', 230., - GenericDigitalMapper, - (('max', scaled_elem(3, 0.1)), - ('rainfall_end', date_elem(4, 5)), - ('min', scaled_elem(6, 0.1)), - ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 175: ('Digital Storm Total Difference Accumulation', 230., - GenericDigitalMapper, - (('rainfall_begin', date_elem(0, 1)), - ('null_product', low_byte(2)), - ('max', scaled_elem(3, 0.1)), - ('rainfall_end', date_elem(4, 5)), - ('min', scaled_elem(6, 0.1)), - ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 176: ('Digital Instantaneous Precipitation Rate', 230., - GenericDigitalMapper, - (('rainfall_begin', date_elem(0, 1)), - ('precip_detected', high_byte(2)), - ('need_bias', low_byte(2)), - ('max', 3), - ('percent_filled', scaled_elem(4, 0.01)), - ('max_elev', scaled_elem(5, 0.1)), - ('bias', scaled_elem(6, 0.01)), - ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 177: ('Hybrid Hydrometeor Classification', 230., DigitalHMCMapper, - (('mode_filter_size', 3), - ('hybrid_percent_filled', 4), - ('max_elev', scaled_elem(5, 0.1)), - ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 180: ('TDWR Base Reflectivity', 90., DigitalRefMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 181: ('TDWR Base Reflectivity', 90., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3))), - 182: ('TDWR Base Velocity', 90., DigitalVelMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', 3), - ('max', 4), - ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 183: ('TDWR Base Velocity', 90., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('min', 3), - ('max', 4))), - 185: ('TDWR Base Spectrum Width', 90., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3))), - 186: ('TDWR Long Range Base Reflectivity', 416., DigitalRefMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3), - ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)))), - 187: ('TDWR Long Range Base Reflectivity', 416., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), - ('max', 3)))} + prod_spec_map = { + 16: ( + "Base Reflectivity", + 230.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3), ("calib_const", float_elem(7, 8))), + ), + 17: ( + "Base Reflectivity", + 460.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3), ("calib_const", float_elem(7, 8))), + ), + 18: ( + "Base Reflectivity", + 460.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3), ("calib_const", float_elem(7, 8))), + ), + 19: ( + "Base Reflectivity", + 230.0, + LegacyMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("max", 3), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ("calib_const", float_elem(7, 8)), + ), + ), + 20: ( + "Base Reflectivity", + 460.0, + LegacyMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("max", 3), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ("calib_const", float_elem(7, 8)), + ), + ), + 21: ( + "Base Reflectivity", + 460.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3), ("calib_const", float_elem(7, 8))), + ), + 22: ( + "Base Velocity", + 60.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("min", 3), ("max", 4)), + ), + 23: ( + "Base Velocity", + 115.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("min", 3), ("max", 4)), + ), + 24: ( + "Base Velocity", + 230.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("min", 3), ("max", 4)), + ), + 25: ( + "Base Velocity", + 60.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("min", 3), ("max", 4)), + ), + 26: ( + "Base Velocity", + 115.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("min", 3), ("max", 4)), + ), + 27: ( + "Base Velocity", + 230.0, + LegacyMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("min", 3), + ("max", 4), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ), + ), + 28: ( + "Base Spectrum Width", + 60.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3)), + ), + 29: ( + "Base Spectrum Width", + 115.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3)), + ), + 30: ( + "Base Spectrum Width", + 230.0, + LegacyMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("max", 3), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ), + ), + 31: ( + "User Selectable Storm Total Precipitation", + 230.0, + LegacyMapper, + ( + ("end_hour", 0), + ("hour_span", 1), + ("null_product", 2), + ("max_rainfall", scaled_elem(3, 0.1)), + ("rainfall_begin", date_elem(4, 5)), + ("rainfall_end", date_elem(6, 7)), + ("bias", scaled_elem(8, 0.01)), + ("gr_pairs", scaled_elem(5, 0.01)), + ), + ), + 32: ( + "Digital Hybrid Scan Reflectivity", + 230.0, + DigitalRefMapper, + ( + ("max", 3), + ("avg_time", date_elem(4, 5)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 33: ( + "Hybrid Scan Reflectivity", + 230.0, + LegacyMapper, + (("max", 3), ("avg_time", date_elem(4, 5))), + ), + 34: ( + "Clutter Filter Control", + 230.0, + LegacyMapper, + ( + ("clutter_bitmap", 0), + ("cmd_map", 1), + ("bypass_map_date", date_elem(4, 5)), + ("notchwidth_map_date", date_elem(6, 7)), + ), + ), + 35: ( + "Composite Reflectivity", + 230.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3), ("calib_const", float_elem(7, 8))), + ), + 36: ( + "Composite Reflectivity", + 460.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3), ("calib_const", float_elem(7, 8))), + ), + 37: ( + "Composite Reflectivity", + 230.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3), ("calib_const", float_elem(7, 8))), + ), + 38: ( + "Composite Reflectivity", + 460.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3), ("calib_const", float_elem(7, 8))), + ), + 41: ( + "Echo Tops", + 230.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", scaled_elem(3, 1000))), + ), # Max in ft + 48: ( + "VAD Wind Profile", + None, + LegacyMapper, + (("max", 3), ("dir_max", 4), ("alt_max", scaled_elem(5, 10))), + ), # Max in ft + 50: ( + "Cross Section Reflectivity", + 230.0, + LegacyMapper, + ( + ("azimuth1", scaled_elem(0, 0.1)), + ("range1", scaled_elem(1, 0.1)), + ("azimuth2", scaled_elem(2, 0.1)), + ("range2", scaled_elem(3, 0.1)), + ), + ), + 51: ( + "Cross Section Velocity", + 230.0, + LegacyMapper, + ( + ("azimuth1", scaled_elem(0, 0.1)), + ("range1", scaled_elem(1, 0.1)), + ("azimuth2", scaled_elem(2, 0.1)), + ("range2", scaled_elem(3, 0.1)), + ), + ), + 55: ( + "Storm Relative Mean Radial Velocity", + 50.0, + LegacyMapper, + ( + ("window_az", scaled_elem(0, 0.1)), + ("window_range", scaled_elem(1, 0.1)), + ("el_angle", scaled_elem(2, 0.1)), + ("min", 3), + ("max", 4), + ("source", 5), + ("height", 6), + ("avg_speed", scaled_elem(7, 0.1)), + ("avg_dir", scaled_elem(8, 0.1)), + ("alert_category", 9), + ), + ), + 56: ( + "Storm Relative Mean Radial Velocity", + 230.0, + LegacyMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("min", 3), + ("max", 4), + ("source", 5), + ("avg_speed", scaled_elem(7, 0.1)), + ("avg_dir", scaled_elem(8, 0.1)), + ), + ), + 57: ( + "Vertically Integrated Liquid", + 230.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3)), + ), # Max in kg / m^2 + 58: ("Storm Tracking Information", 460.0, LegacyMapper, (("num_storms", 3),)), + 59: ("Hail Index", 230.0, LegacyMapper, ()), + 61: ( + "Tornado Vortex Signature", + 230.0, + LegacyMapper, + (("num_tvs", 3), ("num_etvs", 4)), + ), + 62: ("Storm Structure", 460.0, LegacyMapper, ()), + 63: ( + "Layer Composite Reflectivity (Layer 1 Average)", + 230.0, + LegacyMapper, + ( + ("max", 3), + ("layer_bottom", scaled_elem(4, 1000.0)), + ("layer_top", scaled_elem(5, 1000.0)), + ("calib_const", float_elem(7, 8)), + ), + ), + 64: ( + "Layer Composite Reflectivity (Layer 2 Average)", + 230.0, + LegacyMapper, + ( + ("max", 3), + ("layer_bottom", scaled_elem(4, 1000.0)), + ("layer_top", scaled_elem(5, 1000.0)), + ("calib_const", float_elem(7, 8)), + ), + ), + 65: ( + "Layer Composite Reflectivity (Layer 1 Max)", + 230.0, + LegacyMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("max", 3), + ("layer_bottom", scaled_elem(4, 1000.0)), + ("layer_top", scaled_elem(5, 1000.0)), + ("calib_const", float_elem(7, 8)), + ), + ), + 66: ( + "Layer Composite Reflectivity (Layer 2 Max)", + 230.0, + LegacyMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("max", 3), + ("layer_bottom", scaled_elem(4, 1000.0)), + ("layer_top", scaled_elem(5, 1000.0)), + ("calib_const", float_elem(7, 8)), + ), + ), + 67: ( + "Layer Composite Reflectivity - AP Removed", + 230.0, + LegacyMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("max", 3), + ("layer_bottom", scaled_elem(4, 1000.0)), + ("layer_top", scaled_elem(5, 1000.0)), + ("calib_const", float_elem(7, 8)), + ), + ), + 74: ("Radar Coded Message", 460.0, LegacyMapper, ()), + 78: ( + "Surface Rainfall Accumulation (1 hour)", + 230.0, + LegacyMapper, + ( + ("max_rainfall", scaled_elem(3, 0.1)), + ("bias", scaled_elem(4, 0.01)), + ("gr_pairs", scaled_elem(5, 0.01)), + ("rainfall_end", date_elem(6, 7)), + ), + ), + 79: ( + "Surface Rainfall Accumulation (3 hour)", + 230.0, + LegacyMapper, + ( + ("max_rainfall", scaled_elem(3, 0.1)), + ("bias", scaled_elem(4, 0.01)), + ("gr_pairs", scaled_elem(5, 0.01)), + ("rainfall_end", date_elem(6, 7)), + ), + ), + 80: ( + "Storm Total Rainfall Accumulation", + 230.0, + LegacyMapper, + ( + ("max_rainfall", scaled_elem(3, 0.1)), + ("rainfall_begin", date_elem(4, 5)), + ("rainfall_end", date_elem(6, 7)), + ("bias", scaled_elem(8, 0.01)), + ("gr_pairs", scaled_elem(9, 0.01)), + ), + ), + 81: ( + "Hourly Digital Precipitation Array", + 230.0, + PrecipArrayMapper, + ( + ("max_rainfall", scaled_elem(3, 0.001)), + ("bias", scaled_elem(4, 0.01)), + ("gr_pairs", scaled_elem(5, 0.01)), + ("rainfall_end", date_elem(6, 7)), + ), + ), + 82: ("Supplemental Precipitation Data", None, LegacyMapper, ()), + 89: ( + "Layer Composite Reflectivity (Layer 3 Average)", + 230.0, + LegacyMapper, + ( + ("max", 3), + ("layer_bottom", scaled_elem(4, 1000.0)), + ("layer_top", scaled_elem(5, 1000.0)), + ("calib_const", float_elem(7, 8)), + ), + ), + 90: ( + "Layer Composite Reflectivity (Layer 3 Max)", + 230.0, + LegacyMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("max", 3), + ("layer_bottom", scaled_elem(4, 1000.0)), + ("layer_top", scaled_elem(5, 1000.0)), + ("calib_const", float_elem(7, 8)), + ), + ), + 93: ( + "ITWS Digital Base Velocity", + 115.0, + DigitalVelMapper, + (("el_angle", scaled_elem(2, 0.1)), ("min", 3), ("max", 4), ("precision", 6)), + ), + 94: ( + "Base Reflectivity Data Array", + 460.0, + DigitalRefMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("max", 3), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 95: ( + "Composite Reflectivity Edited for AP", + 230.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3), ("calib_const", float_elem(7, 8))), + ), + 96: ( + "Composite Reflectivity Edited for AP", + 460.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3), ("calib_const", float_elem(7, 8))), + ), + 97: ( + "Composite Reflectivity Edited for AP", + 230.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3), ("calib_const", float_elem(7, 8))), + ), + 98: ( + "Composite Reflectivity Edited for AP", + 460.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3), ("calib_const", float_elem(7, 8))), + ), + 99: ( + "Base Velocity Data Array", + 300.0, + DigitalVelMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("min", 3), + ("max", 4), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 113: ( + "Power Removed Control", + 300.0, + LegacyMapper, + ( + ("rpg_cut_num", 0), + ("cmd_generated", 1), + ("el_angle", scaled_elem(2, 0.1)), + ("clutter_filter_map_dt", date_elem(4, 3)), + # While the 2620001Y ICD doesn't talk about using these + # product-specific blocks for this product, they have data in them + # and the compression info is necessary for proper decoding. + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 132: ( + "Clutter Likelihood Reflectivity", + 230.0, + LegacyMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ), + ), + 133: ( + "Clutter Likelihood Doppler", + 230.0, + LegacyMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ), + ), + 134: ( + "High Resolution VIL", + 460.0, + DigitalVILMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("max", 3), + ("num_edited", 4), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 135: ( + "Enhanced Echo Tops", + 345.0, + DigitalEETMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("max", scaled_elem(3, 1000.0)), # Max in ft + ("num_edited", 4), + ("ref_thresh", 5), + ("points_removed", 6), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 138: ( + "Digital Storm Total Precipitation", + 230.0, + DigitalStormPrecipMapper, + ( + ("rainfall_begin", date_elem(0, 1)), + ("bias", scaled_elem(2, 0.01)), + ("max", scaled_elem(3, 0.01)), + ("rainfall_end", date_elem(4, 5)), + ("gr_pairs", scaled_elem(6, 0.01)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 141: ( + "Mesocyclone Detection", + 230.0, + LegacyMapper, + (("min_ref_thresh", 0), ("overlap_display_filter", 1), ("min_strength_rank", 2)), + ), + 152: ( + "Archive III Status Product", + None, + LegacyMapper, + (("compression", 7), ("uncompressed_size", combine_elem(8, 9))), + ), + 153: ( + "Super Resolution Reflectivity Data Array", + 460.0, + DigitalRefMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("max", 3), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 154: ( + "Super Resolution Velocity Data Array", + 300.0, + DigitalVelMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("min", 3), + ("max", 4), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 155: ( + "Super Resolution Spectrum Width Data Array", + 300.0, + DigitalSPWMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("max", 3), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 156: ( + "Turbulence Detection (Eddy Dissipation Rate)", + 230.0, + EDRMapper, + ( + ("el_start_time", 0), + ("el_end_time", 1), + ("el_angle", scaled_elem(2, 0.1)), + ("min_el", scaled_elem(3, 0.01)), + ("mean_el", scaled_elem(4, 0.01)), + ("max_el", scaled_elem(5, 0.01)), + ), + ), + 157: ( + "Turbulence Detection (Eddy Dissipation Rate Confidence)", + 230.0, + EDRMapper, + ( + ("el_start_time", 0), + ("el_end_time", 1), + ("el_angle", scaled_elem(2, 0.1)), + ("min_el", scaled_elem(3, 0.01)), + ("mean_el", scaled_elem(4, 0.01)), + ("max_el", scaled_elem(5, 0.01)), + ), + ), + 158: ( + "Differential Reflectivity", + 230.0, + LegacyMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("min", scaled_elem(3, 0.1)), + ("max", scaled_elem(4, 0.1)), + ), + ), + 159: ( + "Digital Differential Reflectivity", + 300.0, + GenericDigitalMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("min", scaled_elem(3, 0.1)), + ("max", scaled_elem(4, 0.1)), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 160: ( + "Correlation Coefficient", + 230.0, + LegacyMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("min", scaled_elem(3, 0.00333)), + ("max", scaled_elem(4, 0.00333)), + ), + ), + 161: ( + "Digital Correlation Coefficient", + 300.0, + GenericDigitalMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("min", scaled_elem(3, 0.00333)), + ("max", scaled_elem(4, 0.00333)), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 162: ( + "Specific Differential Phase", + 230.0, + LegacyMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("min", scaled_elem(3, 0.05)), + ("max", scaled_elem(4, 0.05)), + ), + ), + 163: ( + "Digital Specific Differential Phase", + 300.0, + GenericDigitalMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("min", scaled_elem(3, 0.05)), + ("max", scaled_elem(4, 0.05)), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 164: ( + "Hydrometeor Classification", + 230.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)),), + ), + 165: ( + "Digital Hydrometeor Classification", + 300.0, + DigitalHMCMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 166: ( + "Melting Layer", + 230.0, + LegacyMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ), + ), + 167: ( + "Super Res Digital Correlation Coefficient", + 300.0, + GenericDigitalMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("min", scaled_elem(3, 0.00333)), + ("max", scaled_elem(4, 0.00333)), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 168: ( + "Super Res Digital Phi", + 300.0, + GenericDigitalMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("min", 3), + ("max", 4), + ("delta_time", delta_time(6)), + ("supplemental_scan", supplemental_scan(6)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 169: ( + "One Hour Accumulation", + 230.0, + LegacyMapper, + ( + ("null_product", low_byte(2)), + ("max", scaled_elem(3, 0.1)), + ("rainfall_end", date_elem(4, 5)), + ("bias", scaled_elem(6, 0.01)), + ("gr_pairs", scaled_elem(7, 0.01)), + ), + ), + 170: ( + "Digital Accumulation Array", + 230.0, + GenericDigitalMapper, + ( + ("null_product", low_byte(2)), + ("max", scaled_elem(3, 0.1)), + ("rainfall_end", date_elem(4, 5)), + ("bias", scaled_elem(6, 0.01)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 171: ( + "Storm Total Accumulation", + 230.0, + LegacyMapper, + ( + ("rainfall_begin", date_elem(0, 1)), + ("null_product", low_byte(2)), + ("max", scaled_elem(3, 0.1)), + ("rainfall_end", date_elem(4, 5)), + ("bias", scaled_elem(6, 0.01)), + ("gr_pairs", scaled_elem(7, 0.01)), + ), + ), + 172: ( + "Digital Storm Total Accumulation", + 230.0, + GenericDigitalMapper, + ( + ("rainfall_begin", date_elem(0, 1)), + ("null_product", low_byte(2)), + ("max", scaled_elem(3, 0.1)), + ("rainfall_end", date_elem(4, 5)), + ("bias", scaled_elem(6, 0.01)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 173: ( + "Digital User-Selectable Accumulation", + 230.0, + GenericDigitalMapper, + ( + ("period", 1), + ("missing_period", high_byte(2)), + ("null_product", low_byte(2)), + ("max", scaled_elem(3, 0.1)), + ("rainfall_end", date_elem(4, 0)), + ("start_time", 5), + ("bias", scaled_elem(6, 0.01)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 174: ( + "Digital One-Hour Difference Accumulation", + 230.0, + GenericDigitalMapper, + ( + ("max", scaled_elem(3, 0.1)), + ("rainfall_end", date_elem(4, 5)), + ("min", scaled_elem(6, 0.1)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 175: ( + "Digital Storm Total Difference Accumulation", + 230.0, + GenericDigitalMapper, + ( + ("rainfall_begin", date_elem(0, 1)), + ("null_product", low_byte(2)), + ("max", scaled_elem(3, 0.1)), + ("rainfall_end", date_elem(4, 5)), + ("min", scaled_elem(6, 0.1)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 176: ( + "Digital Instantaneous Precipitation Rate", + 230.0, + GenericDigitalMapper, + ( + ("rainfall_begin", date_elem(0, 1)), + ("precip_detected", high_byte(2)), + ("need_bias", low_byte(2)), + ("max", 3), + ("percent_filled", scaled_elem(4, 0.01)), + ("max_elev", scaled_elem(5, 0.1)), + ("bias", scaled_elem(6, 0.01)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 177: ( + "Hybrid Hydrometeor Classification", + 230.0, + DigitalHMCMapper, + ( + ("mode_filter_size", 3), + ("hybrid_percent_filled", 4), + ("max_elev", scaled_elem(5, 0.1)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 180: ( + "TDWR Base Reflectivity", + 90.0, + DigitalRefMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("max", 3), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 181: ( + "TDWR Base Reflectivity", + 90.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3)), + ), + 182: ( + "TDWR Base Velocity", + 90.0, + DigitalVelMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("min", 3), + ("max", 4), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 183: ( + "TDWR Base Velocity", + 90.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("min", 3), ("max", 4)), + ), + 185: ( + "TDWR Base Spectrum Width", + 90.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3)), + ), + 186: ( + "TDWR Long Range Base Reflectivity", + 416.0, + DigitalRefMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("max", 3), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ), + ), + 187: ( + "TDWR Long Range Base Reflectivity", + 416.0, + LegacyMapper, + (("el_angle", scaled_elem(2, 0.1)), ("max", 3)), + ), + } def __init__(self, filename): r"""Create instance of `Level3File`. @@ -1634,7 +2530,7 @@ def __init__(self, filename): elif isinstance(filename, pathlib.Path): self.filename = str(filename) else: - self.filename = 'No File' + self.filename = "No File" # Just read in the entire set of data at once with contextlib.closing(fobj): @@ -1650,13 +2546,13 @@ def __init__(self, filename): self.metadata = {} # Handle free text message products that are pure text - if self.wmo_code == 'NOUS': + if self.wmo_code == "NOUS": self.header = None self.prod_desc = None self.thresholds = None self.depVals = None - self.product_name = 'Free Text Message' - self.text = ''.join(self._buffer.read_ascii()) + self.product_name = "Free Text Message" + self.text = "".join(self._buffer.read_ascii()) return # Decompress the data if necessary, and if so, pop off new header @@ -1665,25 +2561,31 @@ def __init__(self, filename): # Check for empty product if len(self._buffer) == 0: - log.warning('%s: Empty product!', self.filename) + log.warning("%s: Empty product!", self.filename) return # Unpack the message header and the product description block msg_start = self._buffer.set_mark() self.header = self._buffer.read_struct(self.header_fmt) - log.debug('Buffer size: %d (%d expected) Header: %s', len(self._buffer), - self.header.msg_len, self.header) + log.debug( + "Buffer size: %d (%d expected) Header: %s", + len(self._buffer), + self.header.msg_len, + self.header, + ) if not self._buffer.check_remains(self.header.msg_len - self.header_fmt.size): - log.warning('Product contains an unexpected amount of data remaining--have: %d ' - 'expected: %d. This product may not parse correctly.', - len(self._buffer) - self._buffer._offset, - self.header.msg_len - self.header_fmt.size) + log.warning( + "Product contains an unexpected amount of data remaining--have: %d " + "expected: %d. This product may not parse correctly.", + len(self._buffer) - self._buffer._offset, + self.header.msg_len - self.header_fmt.size, + ) # Handle GSM and jump out if self.header.code == 2: self.gsm = self._buffer.read_struct(self.gsm_fmt) - self.product_name = 'General Status Message' + self.product_name = "General Status Message" assert self.gsm.divider == -1 if self.gsm.block_len > 82: # Due to the way the structures read it in, one bit from the count needs @@ -1699,19 +2601,22 @@ def __init__(self, filename): return self.prod_desc = self._buffer.read_struct(self.prod_desc_fmt) - log.debug('Product description block: %s', self.prod_desc) + log.debug("Product description block: %s", self.prod_desc) # Convert thresholds and dependent values to lists of values - self.thresholds = [getattr(self.prod_desc, f'thr{i}') for i in range(1, 17)] - self.depVals = [getattr(self.prod_desc, f'dep{i}') for i in range(1, 11)] + self.thresholds = [getattr(self.prod_desc, f"thr{i}") for i in range(1, 17)] + self.depVals = [getattr(self.prod_desc, f"dep{i}") for i in range(1, 11)] # Set up some time/location metadata - self.metadata['msg_time'] = nexrad_to_datetime(self.header.date, - self.header.time * 1000) - self.metadata['vol_time'] = nexrad_to_datetime(self.prod_desc.vol_date, - self.prod_desc.vol_start_time * 1000) - self.metadata['prod_time'] = nexrad_to_datetime(self.prod_desc.prod_gen_date, - self.prod_desc.prod_gen_time * 1000) + self.metadata["msg_time"] = nexrad_to_datetime( + self.header.date, self.header.time * 1000 + ) + self.metadata["vol_time"] = nexrad_to_datetime( + self.prod_desc.vol_date, self.prod_desc.vol_start_time * 1000 + ) + self.metadata["prod_time"] = nexrad_to_datetime( + self.prod_desc.prod_gen_date, self.prod_desc.prod_gen_time * 1000 + ) self.lat = self.prod_desc.lat * 0.001 self.lon = self.prod_desc.lon * 0.001 self.height = self.prod_desc.height @@ -1719,13 +2624,27 @@ def __init__(self, filename): # Handle product-specific blocks. Default to compression and elevation angle # Also get other product specific information, like name, # maximum range, and how to map data bytes to values - default = ('Unknown Product', 230., LegacyMapper, - (('el_angle', scaled_elem(2, 0.1)), ('compression', 7), - ('uncompressed_size', combine_elem(8, 9)), ('defaultVals', 0))) + default = ( + "Unknown Product", + 230.0, + LegacyMapper, + ( + ("el_angle", scaled_elem(2, 0.1)), + ("compression", 7), + ("uncompressed_size", combine_elem(8, 9)), + ("defaultVals", 0), + ), + ) self.product_name, self.max_range, mapper, meta = self.prod_spec_map.get( - self.header.code, default) - log.debug('Product info--name: %s max_range: %f mapper: %s metadata: %s', - self.product_name, self.max_range, mapper, meta) + self.header.code, default + ) + log.debug( + "Product info--name: %s max_range: %f mapper: %s metadata: %s", + self.product_name, + self.max_range, + mapper, + meta, + ) for name, block in meta: if callable(block): @@ -1739,12 +2658,12 @@ def __init__(self, filename): # Process compression if indicated. We need to fail # gracefully here since we default to it being on - if self.metadata.get('compression', False): + if self.metadata.get("compression", False): try: comp_start = self._buffer.set_mark() decomp_data = self._buffer.read_func(bz2.decompress) self._buffer.splice(comp_start, decomp_data) - assert self._buffer.check_remains(self.metadata['uncompressed_size']) + assert self._buffer.check_remains(self.metadata["uncompressed_size"]) except OSError: pass @@ -1760,8 +2679,9 @@ def __init__(self, filename): if self.prod_desc.graph_off: # Offset seems to be off by 1 from where we're counting, but # it's not clear why. - self._unpack_standalone_graphblock(msg_start, - 2 * (self.prod_desc.graph_off - 1)) + self._unpack_standalone_graphblock( + msg_start, 2 * (self.prod_desc.graph_off - 1) + ) # Need special handling for (old) radar coded message format elif self.header.code == 74: self._unpack_rcm(msg_start, 2 * self.prod_desc.sym_off) @@ -1773,26 +2693,27 @@ def __init__(self, filename): if self.prod_desc.tab_off: self._unpack_tabblock(msg_start, 2 * self.prod_desc.tab_off) - if 'defaultVals' in self.metadata: - log.warning('%s: Using default metadata for product %d', - self.filename, self.header.code) + if "defaultVals" in self.metadata: + log.warning( + "%s: Using default metadata for product %d", self.filename, self.header.code + ) def _process_wmo_header(self): # Read off the WMO header if necessary - data = self._buffer.get_next(64).decode('ascii', 'ignore') + data = self._buffer.get_next(64).decode("ascii", "ignore") match = self.wmo_finder.search(data) - log.debug('WMO Header: %s', match) + log.debug("WMO Header: %s", match) if match: self.wmo_code = match.groups()[0] self.siteID = match.groups()[-1] self._buffer.skip(match.end()) else: - self.wmo_code = '' + self.wmo_code = "" def _process_end_bytes(self): check_bytes = self._buffer[-4:-1] - log.debug('End Bytes: %s', check_bytes) - if check_bytes in (b'\r\r\n', b'\xff\xff\n'): + log.debug("End Bytes: %s", check_bytes) + if check_bytes in (b"\r\r\n", b"\xff\xff\n"): self._buffer.truncate(4) @staticmethod @@ -1812,27 +2733,29 @@ def pos_scale(is_sym_block): def _unpack_rcm(self, start, offset): self._buffer.jump_to(start, offset) header = self._buffer.read_ascii(10) - assert header == '1234 ROBUU' + assert header == "1234 ROBUU" text_data = self._buffer.read_ascii() end = 0 # Appendix B of ICD tells how to interpret this stuff, but that just # doesn't seem worth it. - for marker, name in [('AA', 'ref'), ('BB', 'vad'), ('CC', 'remarks')]: - start = text_data.find('/NEXR' + marker, end) + for marker, name in [("AA", "ref"), ("BB", "vad"), ("CC", "remarks")]: + start = text_data.find("/NEXR" + marker, end) # For part C the search for end fails, but returns -1, which works - end = text_data.find('/END' + marker, start) - setattr(self, 'rcm_' + name, text_data[start:end]) + end = text_data.find("/END" + marker, start) + setattr(self, "rcm_" + name, text_data[start:end]) def _unpack_symblock(self, start, offset): self._buffer.jump_to(start, offset) blk = self._buffer.read_struct(self.sym_block_fmt) - log.debug('Symbology block info: %s', blk) + log.debug("Symbology block info: %s", blk) self.sym_block = [] - assert blk.divider == -1, ('Bad divider for symbology block: {:d} should be -1' - .format(blk.divider)) - assert blk.block_id == 1, ('Bad block ID for symbology block: {:d} should be 1' - .format(blk.block_id)) + assert blk.divider == -1, "Bad divider for symbology block: {:d} should be -1".format( + blk.divider + ) + assert blk.block_id == 1, "Bad block ID for symbology block: {:d} should be 1".format( + blk.block_id + ) for _ in range(blk.nlayer): layer_hdr = self._buffer.read_struct(self.sym_layer_fmt) assert layer_hdr.divider == -1 @@ -1840,36 +2763,46 @@ def _unpack_symblock(self, start, offset): self.sym_block.append(layer) layer_start = self._buffer.set_mark() while self._buffer.offset_from(layer_start) < layer_hdr.length: - packet_code = self._buffer.read_int(2, 'big', signed=False) + packet_code = self._buffer.read_int(2, "big", signed=False) if packet_code in self.packet_map: layer.append(self.packet_map[packet_code](self, packet_code, True)) else: - log.warning('%s: Unknown symbology packet type %d/%x.', - self.filename, packet_code, packet_code) + log.warning( + "%s: Unknown symbology packet type %d/%x.", + self.filename, + packet_code, + packet_code, + ) self._buffer.jump_to(layer_start, layer_hdr.length) assert self._buffer.offset_from(layer_start) == layer_hdr.length def _unpack_graphblock(self, start, offset): self._buffer.jump_to(start, offset) hdr = self._buffer.read_struct(self.graph_block_fmt) - assert hdr.divider == -1, ('Bad divider for graphical block: {:d} should be -1' - .format(hdr.divider)) - assert hdr.block_id == 2, ('Bad block ID for graphical block: {:d} should be 1' - .format(hdr.block_id)) + assert hdr.divider == -1, "Bad divider for graphical block: {:d} should be -1".format( + hdr.divider + ) + assert hdr.block_id == 2, "Bad block ID for graphical block: {:d} should be 1".format( + hdr.block_id + ) self.graph_pages = [] for page in range(hdr.num_pages): - page_num = self._buffer.read_int(2, 'big', signed=False) + page_num = self._buffer.read_int(2, "big", signed=False) assert page + 1 == page_num - page_size = self._buffer.read_int(2, 'big', signed=False) + page_size = self._buffer.read_int(2, "big", signed=False) page_start = self._buffer.set_mark() packets = [] while self._buffer.offset_from(page_start) < page_size: - packet_code = self._buffer.read_int(2, 'big', signed=False) + packet_code = self._buffer.read_int(2, "big", signed=False) if packet_code in self.packet_map: packets.append(self.packet_map[packet_code](self, packet_code, False)) else: - log.warning('%s: Unknown graphical packet type %d/%x.', - self.filename, packet_code, packet_code) + log.warning( + "%s: Unknown graphical packet type %d/%x.", + self.filename, + packet_code, + packet_code, + ) self._buffer.skip(page_size) self.graph_pages.append(packets) @@ -1877,14 +2810,18 @@ def _unpack_standalone_graphblock(self, start, offset): self._buffer.jump_to(start, offset) packets = [] while not self._buffer.at_end(): - packet_code = self._buffer.read_int(2, 'big', signed=False) + packet_code = self._buffer.read_int(2, "big", signed=False) if packet_code in self.packet_map: packets.append(self.packet_map[packet_code](self, packet_code, False)) else: - log.warning('%s: Unknown standalone graphical packet type %d/%x.', - self.filename, packet_code, packet_code) + log.warning( + "%s: Unknown standalone graphical packet type %d/%x.", + self.filename, + packet_code, + packet_code, + ) # Assume next 2 bytes is packet length and try skipping - num_bytes = self._buffer.read_int(2, 'big', signed=False) + num_bytes = self._buffer.read_int(2, "big", signed=False) self._buffer.skip(num_bytes) self.graph_pages = [packets] @@ -1911,50 +2848,92 @@ def _unpack_tabblock(self, start, offset, have_header=True): self.tab_pages = [] for _ in range(blk.num_pages): lines = [] - num_chars = self._buffer.read_int(2, 'big', signed=True) + num_chars = self._buffer.read_int(2, "big", signed=True) while num_chars != -1: - lines.append(''.join(self._buffer.read_ascii(num_chars))) - num_chars = self._buffer.read_int(2, 'big', signed=True) - self.tab_pages.append('\n'.join(lines)) + lines.append("".join(self._buffer.read_ascii(num_chars))) + num_chars = self._buffer.read_int(2, "big", signed=True) + self.tab_pages.append("\n".join(lines)) if have_header: assert self._buffer.offset_from(block_start) == header.block_len def __repr__(self): """Return the string representation of the product.""" - attrs = ('product_name', 'header', 'prod_desc', 'thresholds', 'depVals', 'metadata', - 'gsm', 'gsm_additional', 'siteID') + attrs = ( + "product_name", + "header", + "prod_desc", + "thresholds", + "depVals", + "metadata", + "gsm", + "gsm_additional", + "siteID", + ) blocks = [str(getattr(self, name)) for name in attrs if hasattr(self, name)] - return self.filename + ': ' + '\n'.join(blocks) + return self.filename + ": " + "\n".join(blocks) def _unpack_packet_radial_data(self, code, in_sym_block): - hdr_fmt = NamedStruct([('ind_first_bin', 'H'), ('nbins', 'H'), - ('i_center', 'h'), ('j_center', 'h'), - ('scale_factor', 'h'), ('num_rad', 'H')], - '>', 'RadialHeader') - rad_fmt = NamedStruct([('num_hwords', 'H'), ('start_angle', 'h'), - ('angle_delta', 'h')], '>', 'RadialData') + hdr_fmt = NamedStruct( + [ + ("ind_first_bin", "H"), + ("nbins", "H"), + ("i_center", "h"), + ("j_center", "h"), + ("scale_factor", "h"), + ("num_rad", "H"), + ], + ">", + "RadialHeader", + ) + rad_fmt = NamedStruct( + [("num_hwords", "H"), ("start_angle", "h"), ("angle_delta", "h")], + ">", + "RadialData", + ) hdr = self._buffer.read_struct(hdr_fmt) rads = [] for _ in range(hdr.num_rad): rad = self._buffer.read_struct(rad_fmt) start_az = rad.start_angle * 0.1 end_az = start_az + rad.angle_delta * 0.1 - rads.append((start_az, end_az, - self._unpack_rle_data( - self._buffer.read_binary(2 * rad.num_hwords)))) + rads.append( + ( + start_az, + end_az, + self._unpack_rle_data(self._buffer.read_binary(2 * rad.num_hwords)), + ) + ) start, end, vals = zip(*rads) - return {'start_az': list(start), 'end_az': list(end), 'data': list(vals), - 'center': (hdr.i_center * self.pos_scale(in_sym_block), - hdr.j_center * self.pos_scale(in_sym_block)), - 'gate_scale': hdr.scale_factor * 0.001, 'first': hdr.ind_first_bin} - - digital_radial_hdr_fmt = NamedStruct([('ind_first_bin', 'H'), ('nbins', 'H'), - ('i_center', 'h'), ('j_center', 'h'), - ('scale_factor', 'h'), ('num_rad', 'H')], - '>', 'DigitalRadialHeader') - digital_radial_fmt = NamedStruct([('num_bytes', 'H'), ('start_angle', 'h'), - ('angle_delta', 'h')], '>', 'DigitalRadialData') + return { + "start_az": list(start), + "end_az": list(end), + "data": list(vals), + "center": ( + hdr.i_center * self.pos_scale(in_sym_block), + hdr.j_center * self.pos_scale(in_sym_block), + ), + "gate_scale": hdr.scale_factor * 0.001, + "first": hdr.ind_first_bin, + } + + digital_radial_hdr_fmt = NamedStruct( + [ + ("ind_first_bin", "H"), + ("nbins", "H"), + ("i_center", "h"), + ("j_center", "h"), + ("scale_factor", "h"), + ("num_rad", "H"), + ], + ">", + "DigitalRadialHeader", + ) + digital_radial_fmt = NamedStruct( + [("num_bytes", "H"), ("start_angle", "h"), ("angle_delta", "h")], + ">", + "DigitalRadialData", + ) def _unpack_packet_digital_radial(self, code, in_sym_block): hdr = self._buffer.read_struct(self.digital_radial_hdr_fmt) @@ -1965,75 +2944,120 @@ def _unpack_packet_digital_radial(self, code, in_sym_block): end_az = start_az + rad.angle_delta * 0.1 rads.append((start_az, end_az, self._buffer.read_binary(rad.num_bytes))) start, end, vals = zip(*rads) - return {'start_az': list(start), 'end_az': list(end), 'data': list(vals), - 'center': (hdr.i_center * self.pos_scale(in_sym_block), - hdr.j_center * self.pos_scale(in_sym_block)), - 'gate_scale': hdr.scale_factor * 0.001, 'first': hdr.ind_first_bin} + return { + "start_az": list(start), + "end_az": list(end), + "data": list(vals), + "center": ( + hdr.i_center * self.pos_scale(in_sym_block), + hdr.j_center * self.pos_scale(in_sym_block), + ), + "gate_scale": hdr.scale_factor * 0.001, + "first": hdr.ind_first_bin, + } def _unpack_packet_raster_data(self, code, in_sym_block): - hdr_fmt = NamedStruct([('code', 'L'), - ('i_start', 'h'), ('j_start', 'h'), # start in km/4 - ('xscale_int', 'h'), ('xscale_frac', 'h'), - ('yscale_int', 'h'), ('yscale_frac', 'h'), - ('num_rows', 'h'), ('packing', 'h')], '>', 'RasterData') + hdr_fmt = NamedStruct( + [ + ("code", "L"), + ("i_start", "h"), + ("j_start", "h"), # start in km/4 + ("xscale_int", "h"), + ("xscale_frac", "h"), + ("yscale_int", "h"), + ("yscale_frac", "h"), + ("num_rows", "h"), + ("packing", "h"), + ], + ">", + "RasterData", + ) hdr = self._buffer.read_struct(hdr_fmt) assert hdr.code == 0x800000C0 assert hdr.packing == 2 rows = [] for _ in range(hdr.num_rows): - num_bytes = self._buffer.read_int(2, 'big', signed=False) + num_bytes = self._buffer.read_int(2, "big", signed=False) rows.append(self._unpack_rle_data(self._buffer.read_binary(num_bytes))) - return {'start_x': hdr.i_start * hdr.xscale_int, - 'start_y': hdr.j_start * hdr.yscale_int, 'data': rows} + return { + "start_x": hdr.i_start * hdr.xscale_int, + "start_y": hdr.j_start * hdr.yscale_int, + "data": rows, + } def _unpack_packet_uniform_text(self, code, in_sym_block): # By not using a struct, we can handle multiple codes - num_bytes = self._buffer.read_int(2, 'big', signed=False) + num_bytes = self._buffer.read_int(2, "big", signed=False) if code == 8: - value = self._buffer.read_int(2, 'big', signed=False) + value = self._buffer.read_int(2, "big", signed=False) read_bytes = 6 else: value = None read_bytes = 4 - i_start = self._buffer.read_int(2, 'big', signed=True) - j_start = self._buffer.read_int(2, 'big', signed=True) + i_start = self._buffer.read_int(2, "big", signed=True) + j_start = self._buffer.read_int(2, "big", signed=True) # Text is what remains beyond what's been read, not including byte count - text = ''.join(self._buffer.read_ascii(num_bytes - read_bytes)) - return {'x': i_start * self.pos_scale(in_sym_block), - 'y': j_start * self.pos_scale(in_sym_block), 'color': value, 'text': text} + text = "".join(self._buffer.read_ascii(num_bytes - read_bytes)) + return { + "x": i_start * self.pos_scale(in_sym_block), + "y": j_start * self.pos_scale(in_sym_block), + "color": value, + "text": text, + } def _unpack_packet_special_text_symbol(self, code, in_sym_block): d = self._unpack_packet_uniform_text(code, in_sym_block) # Translate special characters to their meaning ret = {} - symbol_map = {'!': 'past storm position', '"': 'current storm position', - '#': 'forecast storm position', '$': 'past MDA position', - '%': 'forecast MDA position', ' ': None} + symbol_map = { + "!": "past storm position", + '"': "current storm position", + "#": "forecast storm position", + "$": "past MDA position", + "%": "forecast MDA position", + " ": None, + } # Use this meaning as the key in the returned packet - for c in d['text']: + for c in d["text"]: if c not in symbol_map: - log.warning('%s: Unknown special symbol %d/%x.', self.filename, c, ord(c)) + log.warning("%s: Unknown special symbol %d/%x.", self.filename, c, ord(c)) else: key = symbol_map[c] if key: - ret[key] = d['x'], d['y'] - del d['text'] + ret[key] = d["x"], d["y"] + del d["text"] return ret def _unpack_packet_special_graphic_symbol(self, code, in_sym_block): - type_map = {3: 'Mesocyclone', 11: '3D Correlated Shear', 12: 'TVS', - 26: 'ETVS', 13: 'Positive Hail', 14: 'Probable Hail', - 15: 'Storm ID', 19: 'HDA', 25: 'STI Circle'} - point_feature_map = {1: 'Mesocyclone (ext.)', 3: 'Mesocyclone', - 5: 'TVS (Ext.)', 6: 'ETVS (Ext.)', 7: 'TVS', - 8: 'ETVS', 9: 'MDA', 10: 'MDA (Elev.)', 11: 'MDA (Weak)'} + type_map = { + 3: "Mesocyclone", + 11: "3D Correlated Shear", + 12: "TVS", + 26: "ETVS", + 13: "Positive Hail", + 14: "Probable Hail", + 15: "Storm ID", + 19: "HDA", + 25: "STI Circle", + } + point_feature_map = { + 1: "Mesocyclone (ext.)", + 3: "Mesocyclone", + 5: "TVS (Ext.)", + 6: "ETVS (Ext.)", + 7: "TVS", + 8: "ETVS", + 9: "MDA", + 10: "MDA (Elev.)", + 11: "MDA (Weak)", + } # Read the number of bytes and set a mark for sanity checking - num_bytes = self._buffer.read_int(2, 'big', signed=False) + num_bytes = self._buffer.read_int(2, "big", signed=False) packet_data_start = self._buffer.set_mark() scale = self.pos_scale(in_sym_block) @@ -2042,39 +3066,44 @@ def _unpack_packet_special_graphic_symbol(self, code, in_sym_block): ret = defaultdict(list) while self._buffer.offset_from(packet_data_start) < num_bytes: # Read position - ret['x'].append(self._buffer.read_int(2, 'big', signed=True) * scale) - ret['y'].append(self._buffer.read_int(2, 'big', signed=True) * scale) + ret["x"].append(self._buffer.read_int(2, "big", signed=True) * scale) + ret["y"].append(self._buffer.read_int(2, "big", signed=True) * scale) # Handle any types that have additional info if code in (3, 11, 25): - ret['radius'].append(self._buffer.read_int(2, 'big', signed=True) * scale) + ret["radius"].append(self._buffer.read_int(2, "big", signed=True) * scale) elif code == 15: - ret['id'].append(''.join(self._buffer.read_ascii(2))) + ret["id"].append("".join(self._buffer.read_ascii(2))) elif code == 19: - ret['POH'].append(self._buffer.read_int(2, 'big', signed=True)) - ret['POSH'].append(self._buffer.read_int(2, 'big', signed=True)) - ret['Max Size'].append(self._buffer.read_int(2, 'big', signed=False)) + ret["POH"].append(self._buffer.read_int(2, "big", signed=True)) + ret["POSH"].append(self._buffer.read_int(2, "big", signed=True)) + ret["Max Size"].append(self._buffer.read_int(2, "big", signed=False)) elif code == 20: - kind = self._buffer.read_int(2, 'big', signed=False) - attr = self._buffer.read_int(2, 'big', signed=False) + kind = self._buffer.read_int(2, "big", signed=False) + attr = self._buffer.read_int(2, "big", signed=False) if kind < 5 or kind > 8: - ret['radius'].append(attr * scale) + ret["radius"].append(attr * scale) if kind not in point_feature_map: - log.warning('%s: Unknown graphic symbol point kind %d/%x.', - self.filename, kind, kind) - ret['type'].append(f'Unknown ({kind:d})') + log.warning( + "%s: Unknown graphic symbol point kind %d/%x.", + self.filename, + kind, + kind, + ) + ret["type"].append(f"Unknown ({kind:d})") else: - ret['type'].append(point_feature_map[kind]) + ret["type"].append(point_feature_map[kind]) # Map the code to a name for this type of symbol if code != 20: if code not in type_map: - log.warning('%s: Unknown graphic symbol type %d/%x.', - self.filename, code, code) - ret['type'] = 'Unknown' + log.warning( + "%s: Unknown graphic symbol type %d/%x.", self.filename, code, code + ) + ret["type"] = "Unknown" else: - ret['type'] = type_map[code] + ret["type"] = type_map[code] # Check and return assert self._buffer.offset_from(packet_data_start) == num_bytes @@ -2085,44 +3114,49 @@ def _unpack_packet_special_graphic_symbol(self, code, in_sym_block): return ret def _unpack_packet_scit(self, code, in_sym_block): - num_bytes = self._buffer.read_int(2, 'big', signed=False) + num_bytes = self._buffer.read_int(2, "big", signed=False) packet_data_start = self._buffer.set_mark() ret = defaultdict(list) while self._buffer.offset_from(packet_data_start) < num_bytes: - next_code = self._buffer.read_int(2, 'big', signed=False) + next_code = self._buffer.read_int(2, "big", signed=False) if next_code not in self.packet_map: - log.warning('%s: Unknown packet in SCIT %d/%x.', - self.filename, next_code, next_code) + log.warning( + "%s: Unknown packet in SCIT %d/%x.", self.filename, next_code, next_code + ) self._buffer.jump_to(packet_data_start, num_bytes) return ret else: next_packet = self.packet_map[next_code](self, next_code, in_sym_block) if next_code == 6: - ret['track'].append(next_packet['vectors']) + ret["track"].append(next_packet["vectors"]) elif next_code == 25: - ret['STI Circle'].append(next_packet) + ret["STI Circle"].append(next_packet) elif next_code == 2: - ret['markers'].append(next_packet) + ret["markers"].append(next_packet) else: - log.warning('%s: Unsupported packet in SCIT %d/%x.', - self.filename, next_code, next_code) - ret['data'].append(next_packet) + log.warning( + "%s: Unsupported packet in SCIT %d/%x.", + self.filename, + next_code, + next_code, + ) + ret["data"].append(next_packet) reduce_lists(ret) return ret def _unpack_packet_digital_precipitation(self, code, in_sym_block): # Read off a couple of unused spares - self._buffer.read_int(2, 'big', signed=False) - self._buffer.read_int(2, 'big', signed=False) + self._buffer.read_int(2, "big", signed=False) + self._buffer.read_int(2, "big", signed=False) # Get the size of the grid - lfm_boxes = self._buffer.read_int(2, 'big', signed=False) - num_rows = self._buffer.read_int(2, 'big', signed=False) + lfm_boxes = self._buffer.read_int(2, "big", signed=False) + num_rows = self._buffer.read_int(2, "big", signed=False) rows = [] # Read off each row and decode the RLE data for _ in range(num_rows): - row_num_bytes = self._buffer.read_int(2, 'big', signed=False) + row_num_bytes = self._buffer.read_int(2, "big", signed=False) row_bytes = self._buffer.read_binary(row_num_bytes) if code == 18: row = self._unpack_rle_data(row_bytes) @@ -2133,108 +3167,116 @@ def _unpack_packet_digital_precipitation(self, code, in_sym_block): assert len(row) == lfm_boxes rows.append(row) - return {'data': rows} + return {"data": rows} def _unpack_packet_linked_vector(self, code, in_sym_block): - num_bytes = self._buffer.read_int(2, 'big', signed=True) + num_bytes = self._buffer.read_int(2, "big", signed=True) if code == 9: - value = self._buffer.read_int(2, 'big', signed=True) + value = self._buffer.read_int(2, "big", signed=True) num_bytes -= 2 else: value = None scale = self.pos_scale(in_sym_block) - pos = [b * scale for b in self._buffer.read_binary(num_bytes / 2, '>h')] + pos = [b * scale for b in self._buffer.read_binary(num_bytes / 2, ">h")] vectors = list(zip(pos[::2], pos[1::2])) - return {'vectors': vectors, 'color': value} + return {"vectors": vectors, "color": value} def _unpack_packet_vector(self, code, in_sym_block): - num_bytes = self._buffer.read_int(2, 'big', signed=True) + num_bytes = self._buffer.read_int(2, "big", signed=True) if code == 10: - value = self._buffer.read_int(2, 'big', signed=True) + value = self._buffer.read_int(2, "big", signed=True) num_bytes -= 2 else: value = None scale = self.pos_scale(in_sym_block) - pos = [p * scale for p in self._buffer.read_binary(num_bytes / 2, '>h')] + pos = [p * scale for p in self._buffer.read_binary(num_bytes / 2, ">h")] vectors = list(zip(pos[::4], pos[1::4], pos[2::4], pos[3::4])) - return {'vectors': vectors, 'color': value} + return {"vectors": vectors, "color": value} def _unpack_packet_contour_color(self, code, in_sym_block): # Check for color value indicator - assert self._buffer.read_int(2, 'big', signed=False) == 0x0002 + assert self._buffer.read_int(2, "big", signed=False) == 0x0002 # Read and return value (level) of contour - return {'color': self._buffer.read_int(2, 'big', signed=False)} + return {"color": self._buffer.read_int(2, "big", signed=False)} def _unpack_packet_linked_contour(self, code, in_sym_block): # Check for initial point indicator - assert self._buffer.read_int(2, 'big', signed=False) == 0x8000 + assert self._buffer.read_int(2, "big", signed=False) == 0x8000 scale = self.pos_scale(in_sym_block) - startx = self._buffer.read_int(2, 'big', signed=True) * scale - starty = self._buffer.read_int(2, 'big', signed=True) * scale + startx = self._buffer.read_int(2, "big", signed=True) * scale + starty = self._buffer.read_int(2, "big", signed=True) * scale vectors = [(startx, starty)] - num_bytes = self._buffer.read_int(2, 'big', signed=False) - pos = [b * scale for b in self._buffer.read_binary(num_bytes / 2, '>h')] + num_bytes = self._buffer.read_int(2, "big", signed=False) + pos = [b * scale for b in self._buffer.read_binary(num_bytes / 2, ">h")] vectors.extend(zip(pos[::2], pos[1::2])) - return {'vectors': vectors} + return {"vectors": vectors} def _unpack_packet_wind_barbs(self, code, in_sym_block): # Figure out how much to read - num_bytes = self._buffer.read_int(2, 'big', signed=True) + num_bytes = self._buffer.read_int(2, "big", signed=True) packet_data_start = self._buffer.set_mark() ret = defaultdict(list) # Read while we have data, then return while self._buffer.offset_from(packet_data_start) < num_bytes: - ret['color'].append(self._buffer.read_int(2, 'big', signed=True)) - ret['x'].append(self._buffer.read_int(2, 'big', signed=True) - * self.pos_scale(in_sym_block)) - ret['y'].append(self._buffer.read_int(2, 'big', signed=True) - * self.pos_scale(in_sym_block)) - ret['direc'].append(self._buffer.read_int(2, 'big', signed=True)) - ret['speed'].append(self._buffer.read_int(2, 'big', signed=True)) + ret["color"].append(self._buffer.read_int(2, "big", signed=True)) + ret["x"].append( + self._buffer.read_int(2, "big", signed=True) * self.pos_scale(in_sym_block) + ) + ret["y"].append( + self._buffer.read_int(2, "big", signed=True) * self.pos_scale(in_sym_block) + ) + ret["direc"].append(self._buffer.read_int(2, "big", signed=True)) + ret["speed"].append(self._buffer.read_int(2, "big", signed=True)) return ret def _unpack_packet_generic(self, code, in_sym_block): # Reserved HW - assert self._buffer.read_int(2, 'big', signed=True) == 0 + assert self._buffer.read_int(2, "big", signed=True) == 0 # Read number of bytes (2 HW) and return - num_bytes = self._buffer.read_int(4, 'big', signed=True) + num_bytes = self._buffer.read_int(4, "big", signed=True) hunk = self._buffer.read(num_bytes) xdrparser = Level3XDRParser(hunk) return xdrparser(code) def _unpack_packet_trend_times(self, code, in_sym_block): - self._buffer.read_int(2, 'big', signed=True) # number of bytes, not needed to process - return {'times': self._read_trends()} + self._buffer.read_int(2, "big", signed=True) # number of bytes, not needed to process + return {"times": self._read_trends()} def _unpack_packet_cell_trend(self, code, in_sym_block): - code_map = ['Cell Top', 'Cell Base', 'Max Reflectivity Height', - 'Probability of Hail', 'Probability of Severe Hail', - 'Cell-based VIL', 'Maximum Reflectivity', - 'Centroid Height'] + code_map = [ + "Cell Top", + "Cell Base", + "Max Reflectivity Height", + "Probability of Hail", + "Probability of Severe Hail", + "Cell-based VIL", + "Maximum Reflectivity", + "Centroid Height", + ] code_scales = [100, 100, 100, 1, 1, 1, 1, 100] - num_bytes = self._buffer.read_int(2, 'big', signed=True) + num_bytes = self._buffer.read_int(2, "big", signed=True) packet_data_start = self._buffer.set_mark() - cell_id = ''.join(self._buffer.read_ascii(2)) - x = self._buffer.read_int(2, 'big', signed=True) * self.pos_scale(in_sym_block) - y = self._buffer.read_int(2, 'big', signed=True) * self.pos_scale(in_sym_block) - ret = {'id': cell_id, 'x': x, 'y': y} + cell_id = "".join(self._buffer.read_ascii(2)) + x = self._buffer.read_int(2, "big", signed=True) * self.pos_scale(in_sym_block) + y = self._buffer.read_int(2, "big", signed=True) * self.pos_scale(in_sym_block) + ret = {"id": cell_id, "x": x, "y": y} while self._buffer.offset_from(packet_data_start) < num_bytes: - code = self._buffer.read_int(2, 'big', signed=True) + code = self._buffer.read_int(2, "big", signed=True) try: ind = code - 1 key = code_map[ind] scale = code_scales[ind] except IndexError: - log.warning('%s: Unsupported trend code %d/%x.', self.filename, code, code) - key = 'Unknown' + log.warning("%s: Unsupported trend code %d/%x.", self.filename, code, code) + key = "Unknown" scale = 1 vals = self._read_trends() if code in (1, 2): - ret[f'{key} Limited'] = [True if v > 700 else False for v in vals] + ret[f"{key} Limited"] = [True if v > 700 else False for v in vals] vals = [v - 1000 if v > 700 else v for v in vals] ret[key] = [v * scale for v in vals] @@ -2242,42 +3284,44 @@ def _unpack_packet_cell_trend(self, code, in_sym_block): def _read_trends(self): num_vols, latest = self._buffer.read(2) - vals = [self._buffer.read_int(2, 'big', signed=True) for _ in range(num_vols)] + vals = [self._buffer.read_int(2, "big", signed=True) for _ in range(num_vols)] # Wrap the circular buffer so that latest is last vals = vals[latest:] + vals[:latest] return vals - packet_map = {1: _unpack_packet_uniform_text, - 2: _unpack_packet_special_text_symbol, - 3: _unpack_packet_special_graphic_symbol, - 4: _unpack_packet_wind_barbs, - 6: _unpack_packet_linked_vector, - 8: _unpack_packet_uniform_text, - # 9: _unpack_packet_linked_vector, - 10: _unpack_packet_vector, - 11: _unpack_packet_special_graphic_symbol, - 12: _unpack_packet_special_graphic_symbol, - 13: _unpack_packet_special_graphic_symbol, - 14: _unpack_packet_special_graphic_symbol, - 15: _unpack_packet_special_graphic_symbol, - 16: _unpack_packet_digital_radial, - 17: _unpack_packet_digital_precipitation, - 18: _unpack_packet_digital_precipitation, - 19: _unpack_packet_special_graphic_symbol, - 20: _unpack_packet_special_graphic_symbol, - 21: _unpack_packet_cell_trend, - 22: _unpack_packet_trend_times, - 23: _unpack_packet_scit, - 24: _unpack_packet_scit, - 25: _unpack_packet_special_graphic_symbol, - 26: _unpack_packet_special_graphic_symbol, - 28: _unpack_packet_generic, - 29: _unpack_packet_generic, - 0x0802: _unpack_packet_contour_color, - 0x0E03: _unpack_packet_linked_contour, - 0xaf1f: _unpack_packet_radial_data, - 0xba07: _unpack_packet_raster_data} + packet_map = { + 1: _unpack_packet_uniform_text, + 2: _unpack_packet_special_text_symbol, + 3: _unpack_packet_special_graphic_symbol, + 4: _unpack_packet_wind_barbs, + 6: _unpack_packet_linked_vector, + 8: _unpack_packet_uniform_text, + # 9: _unpack_packet_linked_vector, + 10: _unpack_packet_vector, + 11: _unpack_packet_special_graphic_symbol, + 12: _unpack_packet_special_graphic_symbol, + 13: _unpack_packet_special_graphic_symbol, + 14: _unpack_packet_special_graphic_symbol, + 15: _unpack_packet_special_graphic_symbol, + 16: _unpack_packet_digital_radial, + 17: _unpack_packet_digital_precipitation, + 18: _unpack_packet_digital_precipitation, + 19: _unpack_packet_special_graphic_symbol, + 20: _unpack_packet_special_graphic_symbol, + 21: _unpack_packet_cell_trend, + 22: _unpack_packet_trend_times, + 23: _unpack_packet_scit, + 24: _unpack_packet_scit, + 25: _unpack_packet_special_graphic_symbol, + 26: _unpack_packet_special_graphic_symbol, + 28: _unpack_packet_generic, + 29: _unpack_packet_generic, + 0x0802: _unpack_packet_contour_color, + 0x0E03: _unpack_packet_linked_contour, + 0xAF1F: _unpack_packet_radial_data, + 0xBA07: _unpack_packet_raster_data, + } class Level3XDRParser(Unpacker): @@ -2290,7 +3334,7 @@ def __call__(self, code): if code == 28: xdr.update(self._unpack_prod_desc()) else: - log.warning('XDR: code %d not implemented', code) + log.warning("XDR: code %d not implemented", code) # Check that we got it all self.done() @@ -2298,33 +3342,33 @@ def __call__(self, code): def unpack_string(self): """Unpack the internal data as a string.""" - return Unpacker.unpack_string(self).decode('ascii') + return Unpacker.unpack_string(self).decode("ascii") def _unpack_prod_desc(self): xdr = OrderedDict() # NOTE: The ICD (262001U) incorrectly lists op-mode, vcp, el_num, and # spare as int*2. Changing to int*4 makes things parse correctly. - xdr['name'] = self.unpack_string() - xdr['description'] = self.unpack_string() - xdr['code'] = self.unpack_int() - xdr['type'] = self.unpack_int() - xdr['prod_time'] = self.unpack_uint() - xdr['radar_name'] = self.unpack_string() - xdr['latitude'] = self.unpack_float() - xdr['longitude'] = self.unpack_float() - xdr['height'] = self.unpack_float() - xdr['vol_time'] = self.unpack_uint() - xdr['el_time'] = self.unpack_uint() - xdr['el_angle'] = self.unpack_float() - xdr['vol_num'] = self.unpack_int() - xdr['op_mode'] = self.unpack_int() - xdr['vcp_num'] = self.unpack_int() - xdr['el_num'] = self.unpack_int() - xdr['compression'] = self.unpack_int() - xdr['uncompressed_size'] = self.unpack_int() - xdr['parameters'] = self._unpack_parameters() - xdr['components'] = self._unpack_components() + xdr["name"] = self.unpack_string() + xdr["description"] = self.unpack_string() + xdr["code"] = self.unpack_int() + xdr["type"] = self.unpack_int() + xdr["prod_time"] = self.unpack_uint() + xdr["radar_name"] = self.unpack_string() + xdr["latitude"] = self.unpack_float() + xdr["longitude"] = self.unpack_float() + xdr["height"] = self.unpack_float() + xdr["vol_time"] = self.unpack_uint() + xdr["el_time"] = self.unpack_uint() + xdr["el_angle"] = self.unpack_float() + xdr["vol_num"] = self.unpack_int() + xdr["op_mode"] = self.unpack_int() + xdr["vcp_num"] = self.unpack_int() + xdr["el_num"] = self.unpack_int() + xdr["compression"] = self.unpack_int() + xdr["uncompressed_size"] = self.unpack_int() + xdr["parameters"] = self._unpack_parameters() + xdr["components"] = self._unpack_components() return xdr @@ -2364,7 +3408,7 @@ def _unpack_components(self): if i < num - 1: self.unpack_int() # Another pointer for the 'list' ? except KeyError: - log.warning('Unknown XDR Component: %d', code) + log.warning("Unknown XDR Component: %d", code) break if num == 1: @@ -2372,36 +3416,41 @@ def _unpack_components(self): return ret - radial_fmt = namedtuple('RadialComponent', ['description', 'gate_width', - 'first_gate', 'parameters', - 'radials']) - radial_data_fmt = namedtuple('RadialData', ['azimuth', 'elevation', 'width', - 'num_bins', 'attributes', - 'data']) + radial_fmt = namedtuple( + "RadialComponent", ["description", "gate_width", "first_gate", "parameters", "radials"] + ) + radial_data_fmt = namedtuple( + "RadialData", ["azimuth", "elevation", "width", "num_bins", "attributes", "data"] + ) def _unpack_radial(self): - ret = self.radial_fmt(description=self.unpack_string(), - gate_width=self.unpack_float(), - first_gate=self.unpack_float(), - parameters=self._unpack_parameters(), - radials=None) + ret = self.radial_fmt( + description=self.unpack_string(), + gate_width=self.unpack_float(), + first_gate=self.unpack_float(), + parameters=self._unpack_parameters(), + radials=None, + ) num_rads = self.unpack_int() rads = [] for _ in range(num_rads): # ICD is wrong, says num_bins is float, should be int - rads.append(self.radial_data_fmt(azimuth=self.unpack_float(), - elevation=self.unpack_float(), - width=self.unpack_float(), - num_bins=self.unpack_int(), - attributes=self.unpack_string(), - data=self.unpack_array(self.unpack_int))) + rads.append( + self.radial_data_fmt( + azimuth=self.unpack_float(), + elevation=self.unpack_float(), + width=self.unpack_float(), + num_bins=self.unpack_int(), + attributes=self.unpack_string(), + data=self.unpack_array(self.unpack_int), + ) + ) return ret._replace(radials=rads) - text_fmt = namedtuple('TextComponent', ['parameters', 'text']) + text_fmt = namedtuple("TextComponent", ["parameters", "text"]) def _unpack_text(self): - return self.text_fmt(parameters=self._unpack_parameters(), - text=self.unpack_string()) + return self.text_fmt(parameters=self._unpack_parameters(), text=self.unpack_string()) _component_lookup = {1: _unpack_radial, 4: _unpack_text} diff --git a/src/metpy/io/station_data.py b/src/metpy/io/station_data.py index 5d31a1c14ab..8e34d2e1dde 100644 --- a/src/metpy/io/station_data.py +++ b/src/metpy/io/station_data.py @@ -12,19 +12,31 @@ from ..units import units exporter = Exporter(globals()) -Station = namedtuple('Station', ['id', 'synop_id', 'name', 'state', 'country', - 'longitude', 'latitude', 'altitude', 'source']) +Station = namedtuple( + "Station", + [ + "id", + "synop_id", + "name", + "state", + "country", + "longitude", + "latitude", + "altitude", + "source", + ], +) def to_dec_deg(dms): """Convert to decimal degrees.""" if not dms: - return 0. + return 0.0 deg, minutes = dms.split() side = minutes[-1] minutes = minutes[:2] - float_deg = int(deg) + int(minutes) / 60. - return float_deg if side in ('N', 'E') else -float_deg + float_deg = int(deg) + int(minutes) / 60.0 + return float_deg if side in ("N", "E") else -float_deg def _read_station_table(input_file=None): @@ -33,7 +45,7 @@ def _read_station_table(input_file=None): Yields tuple of station ID and `Station` for each entry. """ if input_file is None: - input_file = get_test_data('sfstns.tbl', as_file_obj=False) + input_file = get_test_data("sfstns.tbl", as_file_obj=False) with open(input_file) as station_file: for line in station_file: stid = line[:9].strip() @@ -41,12 +53,20 @@ def _read_station_table(input_file=None): name = line[16:49].strip() state = line[49:52].strip() country = line[52:55].strip() - lat = int(line[55:61].strip()) / 100. - lon = int(line[61:68].strip()) / 100. + lat = int(line[55:61].strip()) / 100.0 + lon = int(line[61:68].strip()) / 100.0 alt = int(line[68:74].strip()) - yield stid, Station(stid, synop_id=synop_id, name=name.title(), latitude=lat, - longitude=lon, altitude=alt, country=country, state=state, - source=input_file) + yield stid, Station( + stid, + synop_id=synop_id, + name=name.title(), + latitude=lat, + longitude=lon, + altitude=alt, + country=country, + state=state, + source=input_file, + ) def _read_master_text_file(input_file=None): @@ -55,27 +75,35 @@ def _read_master_text_file(input_file=None): Yields tuple of station ID and `Station` for each entry. """ if input_file is None: - input_file = get_test_data('master.txt', as_file_obj=False) + input_file = get_test_data("master.txt", as_file_obj=False) with open(input_file) as station_file: station_file.readline() for line in station_file: state = line[:3].strip() - name = line[3:20].strip().replace('_', ' ') + name = line[3:20].strip().replace("_", " ") stid = line[20:25].strip() synop_id = line[32:38].strip() lat = to_dec_deg(line[39:46].strip()) lon = to_dec_deg(line[47:55].strip()) alt_part = line[55:60].strip() - alt = int(alt_part if alt_part else 0.) + alt = int(alt_part if alt_part else 0.0) if stid: - if stid[0] in ('P', 'K'): - country = 'US' + if stid[0] in ("P", "K"): + country = "US" else: country = state - state = '--' - yield stid, Station(stid, synop_id=synop_id, name=name.title(), latitude=lat, - longitude=lon, altitude=alt, country=country, state=state, - source=input_file) + state = "--" + yield stid, Station( + stid, + synop_id=synop_id, + name=name.title(), + latitude=lat, + longitude=lon, + altitude=alt, + country=country, + state=state, + source=input_file, + ) def _read_station_text_file(input_file=None): @@ -84,40 +112,51 @@ def _read_station_text_file(input_file=None): Yields tuple of station ID and `Station` for each entry. """ if input_file is None: - input_file = get_test_data('stations.txt', as_file_obj=False) + input_file = get_test_data("stations.txt", as_file_obj=False) with open(input_file) as station_file: for line in station_file: - if line[0] == '!': + if line[0] == "!": continue lat = line[39:45].strip() - if not lat or lat == 'LAT': + if not lat or lat == "LAT": continue lat = to_dec_deg(lat) state = line[:3].strip() - name = line[3:20].strip().replace('_', ' ') + name = line[3:20].strip().replace("_", " ") stid = line[20:25].strip() synop_id = line[32:38].strip() lon = to_dec_deg(line[47:55].strip()) alt = int(line[55:60].strip()) country = line[81:83].strip() - yield stid, Station(stid, synop_id=synop_id, name=name.title(), latitude=lat, - longitude=lon, altitude=alt, country=country, state=state, - source=input_file) + yield stid, Station( + stid, + synop_id=synop_id, + name=name.title(), + latitude=lat, + longitude=lon, + altitude=alt, + country=country, + state=state, + source=input_file, + ) def _read_airports_file(input_file=None): """Read the airports file.""" if input_file is None: - input_file = get_test_data('airport-codes.csv', as_file_obj=False) + input_file = get_test_data("airport-codes.csv", as_file_obj=False) df = pd.read_csv(input_file) - station_map = pd.DataFrame({'id': df.ident.values, 'synop_id': 99999, - 'latitude': df.latitude_deg.values, - 'longitude': df.longitude_deg.values, - 'altitude': ((df.elevation_ft.values * units.ft).to('m')).m, - 'country': df.iso_region.str.split('-', n=1, - expand=True)[1].values, - 'source': input_file - }).to_dict() + station_map = pd.DataFrame( + { + "id": df.ident.values, + "synop_id": 99999, + "latitude": df.latitude_deg.values, + "longitude": df.longitude_deg.values, + "altitude": ((df.elevation_ft.values * units.ft).to("m")).m, + "country": df.iso_region.str.split("-", n=1, expand=True)[1].values, + "source": input_file, + } + ).to_dict() return station_map @@ -126,15 +165,19 @@ class StationLookup: def __init__(self): """Initialize different files.""" - self._sources = [dict(_read_station_table()), dict(_read_master_text_file()), - dict(_read_station_text_file()), dict(_read_airports_file())] + self._sources = [ + dict(_read_station_table()), + dict(_read_master_text_file()), + dict(_read_station_text_file()), + dict(_read_airports_file()), + ] def __getitem__(self, stid): """Lookup station information from the ID.""" for table in self._sources: if stid in table: return table[stid] - raise KeyError(f'No station information for {stid}') + raise KeyError(f"No station information for {stid}") with exporter: @@ -161,14 +204,14 @@ def add_station_lat_lon(df, stn_var): `pandas.DataFrame` that contains original Dataframe now with the latitude and longitude values for each location found in `station_info`. """ - df['latitude'] = None - df['longitude'] = None + df["latitude"] = None + df["longitude"] = None for stn in df[stn_var].unique(): try: info = station_info[stn] - df.loc[df[stn_var] == stn, 'latitude'] = info.latitude - df.loc[df[stn_var] == stn, 'longitude'] = info.longitude + df.loc[df[stn_var] == stn, "latitude"] = info.latitude + df.loc[df[stn_var] == stn, "longitude"] = info.longitude except KeyError: - df.loc[df[stn_var] == stn, 'latitude'] = np.nan - df.loc[df[stn_var] == stn, 'longitude'] = np.nan + df.loc[df[stn_var] == stn, "latitude"] = np.nan + df.loc[df[stn_var] == stn, "longitude"] = np.nan return df diff --git a/src/metpy/package_tools.py b/src/metpy/package_tools.py index 3b8faf9d0ab..0c1b7d49a7d 100644 --- a/src/metpy/package_tools.py +++ b/src/metpy/package_tools.py @@ -7,7 +7,7 @@ # Inspired by David Beazley and taken from python-ideas: # https://mail.python.org/pipermail/python-ideas/2014-May/027824.html -__all__ = ('Exporter',) +__all__ = ("Exporter",) class Exporter: @@ -22,7 +22,7 @@ class Exporter: def __init__(self, globls): """Initialize the Exporter.""" self.globls = globls - self.exports = globls.setdefault('__all__', []) + self.exports = globls.setdefault("__all__", []) def export(self, defn): """Declare a function or class as exported.""" diff --git a/src/metpy/pandas.py b/src/metpy/pandas.py index b32bd31ec11..6eb5eefa734 100644 --- a/src/metpy/pandas.py +++ b/src/metpy/pandas.py @@ -13,12 +13,15 @@ def preprocess_pandas(func): """Decorate a function to convert all data series arguments to `np.ndarray`.""" + @functools.wraps(func) def wrapper(*args, **kwargs): # not using hasattr(a, values) because it picks up dict.values() # and this is more explictly handling pandas args = tuple(a.values if isinstance(a, pd.Series) else a for a in args) - kwargs = {name: (v.values if isinstance(v, pd.Series) else v) - for name, v in kwargs.items()} + kwargs = { + name: (v.values if isinstance(v, pd.Series) else v) for name, v in kwargs.items() + } return func(*args, **kwargs) + return wrapper diff --git a/src/metpy/plots/__init__.py b/src/metpy/plots/__init__.py index 7058aa6aab0..0d8a2c3fd35 100644 --- a/src/metpy/plots/__init__.py +++ b/src/metpy/plots/__init__.py @@ -7,8 +7,12 @@ # Trigger matplotlib wrappers from . import _mpl # noqa: F401 -from ._util import (add_metpy_logo, add_timestamp, add_unidata_logo, # noqa: F401 - convert_gempak_color) +from ._util import ( # noqa: F401 + add_metpy_logo, + add_timestamp, + add_unidata_logo, + convert_gempak_color, +) from .ctables import * # noqa: F403 from .declarative import * # noqa: F403 from .skewt import * # noqa: F403 @@ -22,10 +26,10 @@ __all__.extend(skewt.__all__) # pylint: disable=undefined-variable __all__.extend(station_plot.__all__) # pylint: disable=undefined-variable __all__.extend(wx_symbols.__all__) # pylint: disable=undefined-variable -__all__.extend(['add_metpy_logo', 'add_timestamp', 'add_unidata_logo', - 'convert_gempak_color']) +__all__.extend(["add_metpy_logo", "add_timestamp", "add_unidata_logo", "convert_gempak_color"]) try: from .cartopy_utils import USCOUNTIES, USSTATES # noqa: F401 - __all__.extend(['USCOUNTIES', 'USSTATES']) + + __all__.extend(["USCOUNTIES", "USSTATES"]) except ImportError: - logger.warning('Cannot import USCOUNTIES and USSTATES without Cartopy installed.') + logger.warning("Cannot import USCOUNTIES and USSTATES without Cartopy installed.") diff --git a/src/metpy/plots/_mpl.py b/src/metpy/plots/_mpl.py index 084055111aa..0eb6ba81a59 100644 --- a/src/metpy/plots/_mpl.py +++ b/src/metpy/plots/_mpl.py @@ -3,18 +3,19 @@ # SPDX-License-Identifier: BSD-3-Clause """Functionality that we have upstreamed or will upstream into matplotlib.""" +# See if we need to patch in our own scattertext implementation +from matplotlib.axes import Axes # noqa: E402, I100, I202 + # See if we should monkey-patch Barbs for better pivot import matplotlib.transforms as transforms import numpy as np -# See if we need to patch in our own scattertext implementation -from matplotlib.axes import Axes # noqa: E402, I100, I202 -if not hasattr(Axes, 'scattertext'): - import matplotlib.cbook as cbook - import matplotlib.transforms as mtransforms +if not hasattr(Axes, "scattertext"): from matplotlib import rcParams from matplotlib.artist import allow_rasterization + import matplotlib.cbook as cbook from matplotlib.text import Text + import matplotlib.transforms as mtransforms def scattertext(self, x, y, texts, loc=(0, 0), **kw): """Add text to the axes. @@ -61,10 +62,11 @@ def scattertext(self, x, y, texts, loc=(0, 0), **kw): """ # Start with default args and update from kw new_kw = { - 'verticalalignment': 'center', - 'horizontalalignment': 'center', - 'transform': self.transData, - 'clip_on': False} + "verticalalignment": "center", + "horizontalalignment": "center", + "transform": self.transData, + "clip_on": False, + } new_kw.update(kw) # Default to centered on point--special case it to keep transform @@ -149,7 +151,7 @@ def __init__(self, x, y, text, offset=(0, 0), **kwargs): def __str__(self): """Make a string representation of `TextCollection`.""" - return 'TextCollection' + return "TextCollection" __repr__ = __str__ @@ -184,14 +186,13 @@ def draw(self, renderer): if not any(self.text): return - renderer.open_group('text', self.get_gid()) + renderer.open_group("text", self.get_gid()) trans = self.get_transform() if self.offset != (0, 0): scale = self.axes.figure.dpi / 72 xoff, yoff = self.offset - trans += mtransforms.Affine2D().translate(scale * xoff, - scale * yoff) + trans += mtransforms.Affine2D().translate(scale * xoff, scale * yoff) posx = self.convert_xunits(self.x) posy = self.convert_yunits(self.y) @@ -215,7 +216,7 @@ def draw(self, renderer): self._text = t # hack to allow self._get_layout to work bbox, info, descent = self._get_layout(renderer) - self._text = '' + self._text = "" for line, _, x, y in info: @@ -226,28 +227,37 @@ def draw(self, renderer): y = canvash - y # Can simplify next three lines once support for matplotlib<3.1 is dropped - is_math_text = getattr(self, 'is_math_text', False) - check_line = getattr(self, '_preprocess_math', is_math_text) + is_math_text = getattr(self, "is_math_text", False) + check_line = getattr(self, "_preprocess_math", is_math_text) clean_line, ismath = check_line(line) if self.get_path_effects(): from matplotlib.patheffects import PathEffectRenderer + textrenderer = PathEffectRenderer( - self.get_path_effects(), renderer) # noqa: E126 + self.get_path_effects(), renderer + ) # noqa: E126 else: textrenderer = renderer if self.get_usetex(): - textrenderer.draw_tex(gc, x, y, clean_line, - self._fontproperties, angle, - mtext=mtext) + textrenderer.draw_tex( + gc, x, y, clean_line, self._fontproperties, angle, mtext=mtext + ) else: - textrenderer.draw_text(gc, x, y, clean_line, - self._fontproperties, angle, - ismath=ismath, mtext=mtext) + textrenderer.draw_text( + gc, + x, + y, + clean_line, + self._fontproperties, + angle, + ismath=ismath, + mtext=mtext, + ) gc.restore() - renderer.close_group('text') + renderer.close_group("text") def set_usetex(self, usetex): """ @@ -270,7 +280,7 @@ def get_usetex(self): the value of `rcParams['text.usetex']` """ if self._usetex is None: - return rcParams['text.usetex'] + return rcParams["text.usetex"] else: return self._usetex diff --git a/src/metpy/plots/_util.py b/src/metpy/plots/_util.py index 4ac05e97137..5ab856ca476 100644 --- a/src/metpy/plots/_util.py +++ b/src/metpy/plots/_util.py @@ -13,8 +13,17 @@ from ..units import concatenate -def add_timestamp(ax, time=None, x=0.99, y=-0.04, ha='right', high_contrast=False, - pretext='Created: ', time_format='%Y-%m-%dT%H:%M:%SZ', **kwargs): +def add_timestamp( + ax, + time=None, + x=0.99, + y=-0.04, + ha="right", + high_contrast=False, + pretext="Created: ", + time_format="%Y-%m-%dT%H:%M:%SZ", + **kwargs +): """Add a timestamp to a plot. Adds a timestamp to a plot, defaulting to the time of plot creation in ISO format. @@ -45,9 +54,10 @@ def add_timestamp(ax, time=None, x=0.99, y=-0.04, ha='right', high_contrast=Fals """ if high_contrast: - text_args = {'color': 'white', - 'path_effects': - [mpatheffects.withStroke(linewidth=2, foreground='black')]} + text_args = { + "color": "white", + "path_effects": [mpatheffects.withStroke(linewidth=2, foreground="black")], + } else: text_args = {} text_args.update(**kwargs) @@ -57,7 +67,7 @@ def add_timestamp(ax, time=None, x=0.99, y=-0.04, ha='right', high_contrast=Fals return ax.text(x, y, timestr, ha=ha, transform=ax.transAxes, **text_args) -def _add_logo(fig, x=10, y=25, zorder=100, which='metpy', size='small', **kwargs): +def _add_logo(fig, x=10, y=25, zorder=100, which="metpy", size="small", **kwargs): """Add the MetPy or Unidata logo to a figure. Adds an image to the figure. @@ -89,21 +99,19 @@ def _add_logo(fig, x=10, y=25, zorder=100, which='metpy', size='small', **kwargs except ImportError: # Can remove when we require Python > 3.8 from importlib_resources import files as importlib_resources_files - fname_suffix = {'small': '_75x75.png', - 'large': '_150x150.png'} - fname_prefix = {'unidata': 'unidata', - 'metpy': 'metpy'} + fname_suffix = {"small": "_75x75.png", "large": "_150x150.png"} + fname_prefix = {"unidata": "unidata", "metpy": "metpy"} try: fname = fname_prefix[which] + fname_suffix[size] except KeyError: - raise ValueError('Unknown logo size or selection') from None + raise ValueError("Unknown logo size or selection") from None - with (importlib_resources_files('metpy.plots') / '_static' / fname).open('rb') as fobj: + with (importlib_resources_files("metpy.plots") / "_static" / fname).open("rb") as fobj: logo = imread(fobj) return fig.figimage(logo, x, y, zorder=zorder, **kwargs) -def add_metpy_logo(fig, x=10, y=25, zorder=100, size='small', **kwargs): +def add_metpy_logo(fig, x=10, y=25, zorder=100, size="small", **kwargs): """Add the MetPy logo to a figure. Adds an image of the MetPy logo to the figure. @@ -128,10 +136,10 @@ def add_metpy_logo(fig, x=10, y=25, zorder=100, size='small', **kwargs): The `matplotlib.image.FigureImage` instance created """ - return _add_logo(fig, x=x, y=y, zorder=zorder, which='metpy', size=size, **kwargs) + return _add_logo(fig, x=x, y=y, zorder=zorder, which="metpy", size=size, **kwargs) -def add_unidata_logo(fig, x=10, y=25, zorder=100, size='small', **kwargs): +def add_unidata_logo(fig, x=10, y=25, zorder=100, size="small", **kwargs): """Add the Unidata logo to a figure. Adds an image of the MetPy logo to the figure. @@ -156,7 +164,7 @@ def add_unidata_logo(fig, x=10, y=25, zorder=100, size='small', **kwargs): The `matplotlib.image.FigureImage` instance created """ - return _add_logo(fig, x=x, y=y, zorder=zorder, which='unidata', size=size, **kwargs) + return _add_logo(fig, x=x, y=y, zorder=zorder, which="unidata", size=size, **kwargs) # Not part of public API @@ -197,8 +205,9 @@ def colored_line(x, y, c, **kwargs): num_pts = points.size // 2 final_shape = (num_pts - 1, 2, 2) final_strides = (points.itemsize, points.itemsize, num_pts * points.itemsize) - segments = np.lib.stride_tricks.as_strided(points, shape=final_shape, - strides=final_strides) + segments = np.lib.stride_tricks.as_strided( + points, shape=final_shape, strides=final_strides + ) # Create a LineCollection from the segments and set it to colormap based on c lc = LineCollection(segments, **kwargs) @@ -206,7 +215,7 @@ def colored_line(x, y, c, **kwargs): return lc -def convert_gempak_color(c, style='psc'): +def convert_gempak_color(c, style="psc"): """Convert GEMPAK color numbers into corresponding Matplotlib colors. Takes a sequence of GEMPAK color numbers and turns them into @@ -226,6 +235,7 @@ def convert_gempak_color(c, style='psc'): List of strings of Matplotlib colors, or a single string if only one color requested. """ + def normalize(x): """Transform input x to an int in range 0 to 31 consistent with GEMPAK color quirks.""" x = int(x) @@ -236,46 +246,48 @@ def normalize(x): return x # Define GEMPAK colors (Matplotlib doesn't appear to like numbered variants) - cols = ['white', # 0/32 - 'black', # 1 - 'red', # 2 - 'green', # 3 - 'blue', # 4 - 'yellow', # 5 - 'cyan', # 6 - 'magenta', # 7 - '#CD6839', # 8 (sienna3) - '#FF8247', # 9 (sienna1) - '#FFA54F', # 10 (tan1) - '#FFAEB9', # 11 (LightPink1) - '#FF6A6A', # 12 (IndianRed1) - '#EE2C2C', # 13 (firebrick2) - '#8B0000', # 14 (red4) - '#CD0000', # 15 (red3) - '#EE4000', # 16 (OrangeRed2) - '#FF7F00', # 17 (DarkOrange1) - '#CD8500', # 18 (orange3) - 'gold', # 19 - '#EEEE00', # 20 (yellow2) - 'chartreuse', # 21 - '#00CD00', # 22 (green3) - '#008B00', # 23 (green4) - '#104E8B', # 24 (DodgerBlue4) - 'DodgerBlue', # 25 - '#00B2EE', # 26 (DeepSkyBlue2) - '#00EEEE', # 27 (cyan2) - '#8968CD', # 28 (MediumPurple3) - '#912CEE', # 29 (purple2) - '#8B008B', # 30 (magenta4) - 'bisque'] # 31 - - if style != 'psc': - if style == 'xw': - cols[0] = 'black' - cols[1] = 'bisque' - cols[31] = 'white' + cols = [ + "white", # 0/32 + "black", # 1 + "red", # 2 + "green", # 3 + "blue", # 4 + "yellow", # 5 + "cyan", # 6 + "magenta", # 7 + "#CD6839", # 8 (sienna3) + "#FF8247", # 9 (sienna1) + "#FFA54F", # 10 (tan1) + "#FFAEB9", # 11 (LightPink1) + "#FF6A6A", # 12 (IndianRed1) + "#EE2C2C", # 13 (firebrick2) + "#8B0000", # 14 (red4) + "#CD0000", # 15 (red3) + "#EE4000", # 16 (OrangeRed2) + "#FF7F00", # 17 (DarkOrange1) + "#CD8500", # 18 (orange3) + "gold", # 19 + "#EEEE00", # 20 (yellow2) + "chartreuse", # 21 + "#00CD00", # 22 (green3) + "#008B00", # 23 (green4) + "#104E8B", # 24 (DodgerBlue4) + "DodgerBlue", # 25 + "#00B2EE", # 26 (DeepSkyBlue2) + "#00EEEE", # 27 (cyan2) + "#8968CD", # 28 (MediumPurple3) + "#912CEE", # 29 (purple2) + "#8B008B", # 30 (magenta4) + "bisque", + ] # 31 + + if style != "psc": + if style == "xw": + cols[0] = "black" + cols[1] = "bisque" + cols[31] = "white" else: - raise ValueError('Unknown style parameter') + raise ValueError("Unknown style parameter") try: c_list = list(c) diff --git a/src/metpy/plots/cartopy_utils.py b/src/metpy/plots/cartopy_utils.py index a1ac1dc7a82..0fe14039317 100644 --- a/src/metpy/plots/cartopy_utils.py +++ b/src/metpy/plots/cartopy_utils.py @@ -14,6 +14,7 @@ class MetPyMapFeature(cfeature.Feature): def __init__(self, name, scale, **kwargs): """Create MetPyMapFeature instance.""" import cartopy.crs as ccrs + super().__init__(ccrs.PlateCarree(), **kwargs) self.name = name @@ -24,11 +25,12 @@ def __init__(self, name, scale, **kwargs): def geometries(self): """Return an iterator of (shapely) geometries for this feature.""" import cartopy.io.shapereader as shapereader + # Ensure that the associated files are in the cache - fname = f'{self.name}_{self.scaler.scale}' - for extension in ['.dbf', '.shx']: + fname = f"{self.name}_{self.scaler.scale}" + for extension in [".dbf", ".shx"]: get_test_data(fname + extension) - path = get_test_data(fname + '.shp', as_file_obj=False) + path = get_test_data(fname + ".shp", as_file_obj=False) return iter(tuple(shapereader.Reader(path).geometries())) def intersecting_geometries(self, extent): @@ -50,9 +52,9 @@ def with_scale(self, new_scale): """ return MetPyMapFeature(self.name, new_scale, **self.kwargs) - USCOUNTIES = MetPyMapFeature('us_counties', '20m', facecolor='None', edgecolor='black') + USCOUNTIES = MetPyMapFeature("us_counties", "20m", facecolor="None", edgecolor="black") - USSTATES = MetPyMapFeature('us_states', '20m', facecolor='None', edgecolor='black') + USSTATES = MetPyMapFeature("us_states", "20m", facecolor="None", edgecolor="black") except ImportError: pass @@ -64,6 +66,7 @@ def import_cartopy(): """ try: import cartopy.crs as ccrs + return ccrs except ImportError: return CartopyStub() @@ -74,4 +77,4 @@ class CartopyStub: def __getattr__(self, item): """Raise an error on any attribute access.""" - raise RuntimeError(f'CartoPy is required to use this feature ({item}).') + raise RuntimeError(f"CartoPy is required to use this feature ({item}).") diff --git a/src/metpy/plots/ctables.py b/src/metpy/plots/ctables.py index 8e8d16b2001..0d7f296a808 100644 --- a/src/metpy/plots/ctables.py +++ b/src/metpy/plots/ctables.py @@ -49,16 +49,16 @@ def plot_color_gradients(cmap_category, cmap_list, nrows): exporter = Exporter(globals()) -TABLE_EXT = '.tbl' +TABLE_EXT = ".tbl" log = logging.getLogger(__name__) def _parse(s): - if hasattr(s, 'decode'): - s = s.decode('ascii') + if hasattr(s, "decode"): + s = s.decode("ascii") - if not s.startswith('#'): + if not s.startswith("#"): return ast.literal_eval(s) return None @@ -92,7 +92,7 @@ def read_colortable(fobj): ret.append(mcolors.colorConverter.to_rgb(literal)) return ret except (SyntaxError, ValueError) as e: - raise RuntimeError(f'Malformed colortable (bad line: {line})') from e + raise RuntimeError(f"Malformed colortable (bad line: {line})") from e def convert_gempak_table(infile, outfile): @@ -110,9 +110,9 @@ def convert_gempak_table(infile, outfile): """ for line in infile: - if not line.startswith('!') and line.strip(): + if not line.startswith("!") and line.strip(): r, g, b = map(int, line.split()) - outfile.write('({:f}, {:f}, {:f})\n'.format(r / 255, g / 255, b / 255)) + outfile.write("({:f}, {:f}, {:f})\n".format(r / 255, g / 255, b / 255)) class ColortableRegistry(dict): @@ -141,7 +141,7 @@ def scan_resource(self, pkg, path): for entry in (importlib_resources_files(pkg) / path).iterdir(): if entry.suffix == TABLE_EXT: with entry.open() as stream: - self.add_colortable(stream, entry.with_suffix('').name) + self.add_colortable(stream, entry.with_suffix("").name) def scan_dir(self, path): r"""Scan a directory on disk for color table files and add them to the registry. @@ -152,15 +152,15 @@ def scan_dir(self, path): The path to the directory with the color tables """ - for fname in glob.glob(os.path.join(path, '*' + TABLE_EXT)): + for fname in glob.glob(os.path.join(path, "*" + TABLE_EXT)): if os.path.isfile(fname): with open(fname) as fobj: try: self.add_colortable(fobj, os.path.splitext(os.path.basename(fname))[0]) - log.debug('Added colortable from file: %s', fname) + log.debug("Added colortable from file: %s", fname) except RuntimeError: # If we get a file we can't handle, assume we weren't meant to. - log.info('Skipping unparsable file: %s', fname) + log.info("Skipping unparsable file: %s", fname) def add_colortable(self, fobj, name): r"""Add a color table from a file to the registry. @@ -174,7 +174,7 @@ def add_colortable(self, fobj, name): """ self[name] = read_colortable(fobj) - self[name + '_r'] = self[name][::-1] + self[name + "_r"] = self[name][::-1] def get_with_steps(self, name, start, step): r"""Get a color table from the registry with a corresponding norm. @@ -273,7 +273,7 @@ def get_colortable(self, name): registry = ColortableRegistry() -registry.scan_resource('metpy.plots', 'colortable_files') +registry.scan_resource("metpy.plots", "colortable_files") registry.scan_dir(os.path.curdir) with exporter: diff --git a/src/metpy/plots/declarative.py b/src/metpy/plots/declarative.py index 5e2ddca56fa..93e2cd565ac 100644 --- a/src/metpy/plots/declarative.py +++ b/src/metpy/plots/declarative.py @@ -8,461 +8,471 @@ import matplotlib.pyplot as plt import numpy as np import pandas as pd -from traitlets import (Any, Bool, Float, HasTraits, Instance, Int, List, observe, Tuple, - Unicode, Union) - -from . import ctables -from . import wx_symbols -from .cartopy_utils import import_cartopy -from .station_plot import StationPlot +from traitlets import ( + Any, + Bool, + Float, + HasTraits, + Instance, + Int, + List, + Tuple, + Unicode, + Union, + observe, +) + +from . import ctables, wx_symbols from ..calc import reduce_point_density from ..package_tools import Exporter from ..units import units +from .cartopy_utils import import_cartopy +from .station_plot import StationPlot ccrs = import_cartopy() exporter = Exporter(globals()) _areas = { - '105': (-129.3, -22.37, 17.52, 53.78), - 'local': (-92., -64., 28.5, 48.5), - 'wvaac': (120.86, -15.07, -53.6, 89.74), - 'tropsfc': (-100., -55., 8., 33.), - 'epacsfc': (-155., -75., -20., 33.), - 'ofagx': (-100., -80., 20., 35.), - 'ahsf': (-105., -30., -5., 35.), - 'ehsf': (-145., -75., -5., 35.), - 'shsf': (-125., -75., -20., 5.), - 'tropful': (-160., 0., -20., 50.), - 'tropatl': (-115., 10., 0., 40.), - 'subtrop': (-90., -20., 20., 60.), - 'troppac': (-165., -80., -25., 45.), - 'gulf': (-105., -70., 10., 40.), - 'carib': (-100., -50., 0., 40.), - 'sthepac': (-170., -70., -60., 0.), - 'opcahsf': (-102., -20., 0., 45.), - 'opcphsf': (175., -70., -28., 45.), - 'wwe': (-106., -50., 18., 54.), - 'world': (-24., -24., -90., 90.), - 'nwwrd1': (-180., 180., -90., 90.), - 'nwwrd2': (0., 0., -90., 90.), - 'afna': (-135.02, -23.04, 10.43, 40.31), - 'awna': (-141.03, -18.58, 7.84, 35.62), - 'medr': (-178., -25., -15., 5.), - 'pacsfc': (129., -95., -5., 18.), - 'saudi': (4.6, 92.5, -13.2, 60.3), - 'natlmed': (-30., 70., 0., 65.), - 'ncna': (-135.5, -19.25, 8., 37.7), - 'ncna2': (-133.5, -20.5, 10., 42.), - 'hpcsfc': (-124., -26., 15., 53.), - 'atlhur': (-96., -6., 4., 3.), - 'nam': (-134., 3., -4., 39.), - 'sam': (-120., -20., -60., 20.), - 'samps': (-148., -36., -28., 12.), - 'eur': (-16., 80., 24., 52.), - 'afnh': (-155.19, 18.76, -6.8, -3.58), - 'awnh': (-158.94, 15.35, -11.55, -8.98), - 'wwwus': (-127.7, -59., 19.8, 56.6), - 'ccfp': (-130., -65., 22., 52.), - 'llvl': (-119.6, -59.5, 19.9, 44.5), - 'llvl2': (-125., -32.5, 5., 46.), - 'llvl_e': (-89., -59.5, 23.5, 44.5), - 'llvl_c': (-102.4, -81.25, 23.8, 51.6), - 'llvl_w': (-119.8, -106.5, 19.75, 52.8), - 'ak_artc': (163.7, -65.3, 17.5, 52.6), - 'fxpswna': (-80.5, 135., -1., 79.), - 'fxpsnna': (-80.5, 54., -1., 25.5), - 'fxpsna': (-72.6, 31.4, -3.6, 31.), - 'natl_ps': (-80.5, 54., -1., 25.5), - 'fxpsena': (-45., 54., 11., 25.5), - 'fxpsnp': (155.5, -106.5, 22.5, 47.), - 'npac_ps': (155.5, -106.5, 22.5, 47.), - 'fxpsus': (-120., -59., 20., 44.5), - 'fxmrwrd': (58., 58., -70., 70.), - 'fxmrwr2': (-131., -131., -70., 70.), - 'nwmrwrd': (70., 70., -70., 70.), - 'wrld_mr': (58., 58., -70., 70.), - 'fxmr110': (-180., -110., -20., 50.5), - 'fxmr180': (110., -180., -20., 50.5), - 'fxmrswp': (97.5, -147.5, -36., 45.5), - 'fxmrus': (-162.5, -37.5, -28., 51.2), - 'fxmrea': (-40., 20., -20., 54.2), - 'fxmrjp': (100., -160., 0., 45.), - 'icao_a': (-137.4, -12.6, -54., 67.), - 'icao_b': (-52.5, -16., -62.5, 77.5), - 'icao_b1': (-125., 40., -45.5, 62.7), - 'icao_c': (-35., 70., -45., 75.), - 'icao_d': (-15., 132., -27., 63.), - 'icao_e': (25., 180., -54., 40.), - 'icao_f': (100., -110., -52.7, 50.), - 'icao_g': (34.8, 157.2, -0.8, 13.7), - 'icao_h': (-79.1, 56.7, 1.6, 25.2), - 'icao_i': (166.24, -60.62, -6.74, 33.32), - 'icao_j': (106.8, -101.1, -27.6, 0.8), - 'icao_k': (3.3, 129.1, -11.1, 6.7), - 'icao_m': (100., -110., -10., 70.), - 'icao_eu': (-21.6, 68.4, 21.4, 58.7), - 'icao_me': (17., 70., 10., 44.), - 'icao_as': (53., 108., 00., 36.), - 'icao_na': (-54.1, 60.3, 17.2, 50.7), - 'nhem': (-135., 45., -15., -15.), - 'nhem_ps': (-135., 45., -15., -15.), - 'nhem180': (135., -45., -15., -15.), - 'nhem155': (160., -20., -15., -15.), - 'nhem165': (150., -30., -15., -15.), - 'nh45_ps': (-90., 90., -15., -15.), - 'nhem0': (-45., 135., -15., -15.), - 'shem_ps': (88., -92., 30., 30.), - 'hfo_gu': (160., -130., -30., 40.), - 'natl': (-110., 20.1, 15., 70.), - 'watl': (-84., -38., 25., 46.), - 'tatl': (-90., -15., -10., 35.), - 'npac': (102., -110., -12., 60.), - 'spac': (102., -70., -60., 20.), - 'tpac': (-165., -75., -10., 40.), - 'epac': (-134., -110., 12., 75.), - 'wpac': (130., -120., 0., 63.), - 'mpac': (128., -108., 15., 71.95), - 'opcsfp': (128.89, -105.3, 3.37, 16.77), - 'opcsfa': (-55.5, 75., -8.5, 52.6), - 'opchur': (-99., -15., 1., 50.05), - 'us': (-119., -56., 19., 47.), - 'spcus': (-116.4, -63.9, 22.1, 47.2), - 'afus': (-119.04, -63.44, 23.1, 44.63), - 'ncus': (-124.2, -40.98, 17.89, 47.39), - 'nwus': (-118., -55.5, 17., 46.5), - 'awips': (-127., -59., 20., 50.), - 'bwus': (-124.6, -46.7, 13.1, 43.1), - 'usa': (-118., -62., 22.8, 45.), - 'usnps': (-118., -62., 18., 51.), - 'uslcc': (-118., -62., 20., 51.), - 'uswn': (-129., -45., 17., 53.), - 'ussf': (-123.5, -44.5, 13., 32.1), - 'ussp': (-126., -49., 13., 54.), - 'whlf': (-123.8, -85.9, 22.9, 50.2), - 'chlf': (-111., -79., 27.5, 50.5), - 'centus': (-105.4, -77., 24.7, 47.6), - 'ehlf': (-96.2, -62.7, 22., 49.), - 'mehlf': (-89.9, -66.6, 23.8, 49.1), - 'bosfa': (-87.5, -63.5, 34.5, 50.5), - 'miafa': (-88., -72., 23., 39.), - 'chifa': (-108., -75., 34., 50.), - 'dfwfa': (-106.5, -80.5, 22., 40.), - 'slcfa': (-126., -98., 29.5, 50.5), - 'sfofa': (-129., -111., 30., 50.), - 'g8us': (-116., -58., 19., 56.), - 'wsig': (155., -115., 18., 58.), - 'esig': (-80., -30., 25., 51.), - 'eg8': (-79., -13., 24., 52.), - 'west': (-125., -90., 25., 55.), - 'cent': (-107.4, -75.3, 24.3, 49.7), - 'east': (-100.55, -65.42, 24.57, 47.2), - 'nwse': (-126., -102., 38.25, 50.25), - 'swse': (-126., -100., 28.25, 40.25), - 'ncse': (-108., -84., 38.25, 50.25), - 'scse': (-108.9, -84., 24., 40.25), - 'nese': (-89., -64., 37.25, 47.25), - 'sese': (-90., -66., 28.25, 40.25), - 'afwh': (170.7, 15.4, -48.6, 69.4), - 'afeh': (-9.3, -164.6, -48.6, 69.4), - 'afpc': (80.7, -74.6, -48.6, 69.4), - 'ak': (-179., -116.4, 49., 69.), - 'ak2': (-180., -106., 42., 73.), - 'nwak': (-180., -110., 50., 60.), - 'al': (-95., -79., 27., 38.), - 'ar': (-100.75, -84.75, 29.5, 40.5), - 'ca': (-127.75, -111.75, 31.5, 42.5), - 'co': (-114., -98., 33.5, 44.5), - 'ct': (-81.25, -65.25, 36., 47.), - 'dc': (-85., -69., 33.35, 44.35), - 'de': (-83.75, -67.75, 33.25, 44.25), - 'fl': (-90., -74., 23., 34.), - 'ga': (-92., -76., 27.5, 38.5), - 'hi': (-161.5, -152.5, 17., 23.), - 'nwxhi': (-166., -148., 14., 26.), - 'ia': (-102., -86., 36.5, 47.5), - 'id': (-123., -107., 39.25, 50.25), - 'il': (-97.75, -81.75, 34.5, 45.5), - 'in': (-94.5, -78.5, 34.5, 45.5), - 'ks': (-106.5, -90.5, 33.25, 44.25), - 'ky': (-93., -77., 31.75, 42.75), - 'la': (-100.75, -84.75, 25.75, 36.75), - 'ma': (-80.25, -64.25, 36.75, 47.75), - 'md': (-85.25, -69.25, 33.75, 44.75), - 'me': (-77.75, -61.75, 39.5, 50.5), - 'mi': (-93., -77., 37.75, 48.75), - 'mn': (-102., -86., 40.5, 51.5), - 'mo': (-101., -85., 33., 44.), - 'ms': (-98., -82., 27., 38.), - 'mt': (-117., -101., 41.5, 52.5), - 'nc': (-87.25, -71.25, 30., 41.), - 'nd': (-107.5, -91.5, 42.25, 53.25), - 'ne': (-107.5, -91.5, 36.25, 47.25), - 'nh': (-79.5, -63.5, 38.25, 49.25), - 'nj': (-82.5, -66.5, 34.75, 45.75), - 'nm': (-114.25, -98.25, 29., 40.), - 'nv': (-125., -109., 34., 45.), - 'ny': (-84., -68., 37.25, 48.25), - 'oh': (-91., -75., 34.5, 45.5), - 'ok': (-105.25, -89.25, 30.25, 41.25), - 'or': (-128., -112., 38.75, 49.75), - 'pa': (-86., -70., 35.5, 46.5), - 'ri': (-79.75, -63.75, 36., 47.), - 'sc': (-89., -73., 28.5, 39.5), - 'sd': (-107.5, -91.5, 39., 50.), - 'tn': (-95., -79., 30., 41.), - 'tx': (-107., -91., 25.4, 36.5), - 'ut': (-119., -103., 34., 45.), - 'va': (-86.5, -70.5, 32.25, 43.25), - 'vt': (-80.75, -64.75, 38.25, 49.25), - 'wi': (-98., -82., 38.5, 49.5), - 'wv': (-89., -73., 33., 44.), - 'wy': (-116., -100., 37.75, 48.75), - 'az': (-119., -103., 29., 40.), - 'wa': (-128., -112., 41.75, 52.75), - 'abrfc': (-108., -88., 30., 42.), - 'ab10': (-106.53, -90.28, 31.69, 40.01), - 'cbrfc': (-117., -103., 28., 46.), - 'cb10': (-115.69, -104.41, 29.47, 44.71), - 'lmrfc': (-100., -77., 26., 40.), - 'lm10': (-97.17, -80.07, 28.09, 38.02), - 'marfc': (-83.5, -70., 35.5, 44.), - 'ma10': (-81.27, -72.73, 36.68, 43.1), - 'mbrfc': (-116., -86., 33., 53.), - 'mb10': (-112.8, -89.33, 35.49, 50.72), - 'ncrfc': (-108., -76., 34., 53.), - 'nc10': (-104.75, -80.05, 35.88, 50.6), - 'nerfc': (-84., -61., 39., 49.), - 'ne10': (-80.11, -64.02, 40.95, 47.62), - 'nwrfc': (-128., -105., 35., 55.), - 'nw10': (-125.85, -109.99, 38.41, 54.46), - 'ohrfc': (-92., -75., 34., 44.), - 'oh10': (-90.05, -77.32, 35.2, 42.9), - 'serfc': (-94., -70., 22., 40.), - 'se10': (-90.6, -73.94, 24.12, 37.91), - 'wgrfc': (-112., -88., 21., 42.), - 'wg10': (-108.82, -92.38, 23.99, 39.18), - 'nwcn': (-133.5, -10.5, 32., 56.), - 'cn': (-120.4, -14., 37.9, 58.6), - 'ab': (-119.6, -108.2, 48.6, 60.4), - 'bc': (-134.5, -109., 47.2, 60.7), - 'mb': (-102.4, -86.1, 48.3, 60.2), - 'nb': (-75.7, -57.6, 42.7, 49.6), - 'nf': (-68., -47., 45., 62.), - 'ns': (-67., -59., 43., 47.5), - 'nt': (-131.8, -33.3, 57.3, 67.8), - 'on': (-94.5, -68.2, 41.9, 55.), - 'pe': (-64.6, -61.7, 45.8, 47.1), - 'qb': (-80., -49.2, 44.1, 60.9), - 'sa': (-111.2, -97.8, 48.5, 60.3), - 'yt': (-142., -117., 59., 70.5), - 'ag': (-80., -53., -56., -20.), - 'ah': (60., 77., 27., 40.), - 'afrca': (-25., 59.4, -36., 41.), - 'ai': (-14.3, -14.1, -8., -7.8), - 'alba': (18., 23., 39., 43.), - 'alge': (-9., 12., 15., 38.), - 'an': (10., 25., -20., -5.), - 'antl': (-70., -58., 11., 19.), - 'antg': (-86., -65., 17., 25.), - 'atg': (-62., -61.6, 16.9, 17.75), - 'au': (101., 148., -45., -6.5), - 'azor': (-27.6, -23., 36., 41.), - 'ba': (-80.5, -72.5, 22.5, 28.5), - 'be': (-64.9, -64.5, 32.2, 32.6), - 'bel': (2.5, 6.5, 49.4, 51.6), - 'bf': (113., 116., 4., 5.5), - 'bfa': (-6., 3., 9., 15.1), - 'bh': (-89.3, -88.1, 15.7, 18.5), - 'bi': (29., 30.9, -4.6, -2.2), - 'bj': (0., 5., 6., 12.6), - 'bn': (50., 51., 25.5, 27.1), - 'bo': (-72., -50., -24., -8.), - 'bots': (19., 29.6, -27., -17.), - 'br': (-62.5, -56.5, 12.45, 13.85), - 'bt': (71.25, 72.6, -7.5, -5.), - 'bu': (22., 30., 40., 45.), - 'bv': (3., 4., -55., -54.), - 'bw': (87., 93., 20.8, 27.), - 'by': (19., 33., 51., 60.), - 'bz': (-75., -30., -35., 5.), - 'cais': (-172., -171., -3., -2.), - 'nwcar': (-120., -50., -15., 35.), - 'cari': (-103., -53., 3., 36.), - 'cb': (13., 25., 7., 24.), - 'ce': (14., 29., 2., 11.5), - 'cg': (10., 20., -6., 5.), - 'ch': (-80., -66., -56., -15.), - 'ci': (85., 145., 14., 48.5), - 'cm': (7.5, 17.1, 1., 14.), - 'colm': (-81., -65., -5., 14.), - 'cr': (-19., -13., 27., 30.), - 'cs': (-86.5, -81.5, 8.2, 11.6), - 'cu': (-85., -74., 19., 24.), - 'cv': (-26., -22., 14., 18.), - 'cy': (32., 35., 34., 36.), - 'cz': (8.9, 22.9, 47.4, 52.4), - 'dj': (41.5, 44.1, 10.5, 13.1), - 'dl': (4.8, 16.8, 47., 55.), - 'dn': (8., 11., 54., 58.6), - 'do': (-61.6, -61.2, 15.2, 15.8), - 'dr': (-72.2, -68., 17.5, 20.2), - 'eg': (24., 37., 21., 33.), - 'eq': (-85., -74., -7., 3.), - 'er': (50., 57., 22., 26.6), - 'es': (-90.3, -87.5, 13., 14.6), - 'et': (33., 49., 2., 19.), - 'fa': (-8., -6., 61., 63.), - 'fg': (-55., -49., 1., 7.), - 'fi': (20.9, 35.1, 59., 70.6), - 'fj': (176., -179., 16., 19.), - 'fk': (-61.3, -57.5, -53., -51.), - 'fn': (0., 17., 11., 24.), - 'fr': (-5., 11., 41., 51.5), - 'gb': (-17.1, -13.5, 13., 14.6), - 'gc': (-82.8, -77.6, 17.9, 21.1), - 'gh': (-4.5, 1.5, 4., 12.), - 'gi': (-8., -4., 35., 38.), - 'gl': (-56.7, 14., 58.3, 79.7), - 'glp': (-64.2, -59.8, 14.8, 19.2), - 'gm': (144.5, 145.1, 13., 14.), - 'gn': (2., 16., 3.5, 15.5), - 'go': (8., 14.5, -4.6, 3.), - 'gr': (20., 27.6, 34., 42.), - 'gu': (-95.6, -85., 10.5, 21.1), - 'gw': (-17.5, -13.5, 10.8, 12.8), - 'gy': (-62., -55., 0., 10.), - 'ha': (-75., -71., 18., 20.), - 'he': (-6.1, -5.5, -16.3, -15.5), - 'hk': (113.5, 114.7, 22., 23.), - 'ho': (-90., -83., 13., 16.6), - 'hu': (16., 23., 45.5, 49.1), - 'ic': (43., 45., -13.2, -11.), - 'icel': (-24.1, -11.5, 63., 67.5), - 'ie': (-11.1, -4.5, 50., 55.6), - 'inda': (67., 92., 4.2, 36.), - 'indo': (95., 141., -8., 6.), - 'iq': (38., 50., 29., 38.), - 'ir': (44., 65., 25., 40.), - 'is': (34., 37., 29., 34.), - 'iv': (-9., -2., 4., 11.), - 'iw': (34.8, 35.6, 31.2, 32.6), - 'iy': (6.6, 20.6, 35.6, 47.2), - 'jd': (34., 39.6, 29., 33.6), - 'jm': (-80., -76., 16., 19.), - 'jp': (123., 155., 24., 47.), - 'ka': (131., 155., 1., 9.6), - 'kash': (74., 78., 32., 35.), - 'kb': (172., 177., -3., 3.2), - 'khm': (102., 108., 10., 15.), - 'ki': (105.2, 106.2, -11., -10.), - 'kn': (32.5, 42.1, -6., 6.), - 'kna': (-62.9, -62.4, 17., 17.5), - 'ko': (124., 131.5, 33., 43.5), - 'ku': (-168., -155., -24.1, -6.1), - 'kw': (46.5, 48.5, 28.5, 30.5), - 'laos': (100., 108., 13.5, 23.1), - 'lb': (34.5, 37.1, 33., 35.), - 'lc': (60.9, 61.3, 13.25, 14.45), - 'li': (-12., -7., 4., 9.), - 'ln': (-162.1, -154.9, -4.2, 6.), - 'ls': (27., 29.6, -30.6, -28.), - 'lt': (9.3, 9.9, 47., 47.6), - 'lux': (5.6, 6.6, 49.35, 50.25), - 'ly': (8., 26., 19., 35.), - 'maar': (-63.9, -62.3, 17., 18.6), - 'made': (-17.3, -16.5, 32.6, 33.), - 'mala': (100., 119.6, 1., 8.), - 'mali': (-12.5, 6., 8.5, 25.5), - 'maur': (57.2, 57.8, -20.7, -19.9), - 'maut': (-17.1, -4.5, 14.5, 28.1), - 'mc': (-13., -1., 25., 36.), - 'mg': (43., 50.6, -25.6, -12.), - 'mh': (160., 172., 4.5, 12.1), - 'ml': (14.3, 14.7, 35.8, 36.), - 'mmr': (92., 102., 7.5, 28.5), - 'mong': (87.5, 123.1, 38.5, 52.6), - 'mr': (-61.2, -60.8, 14.3, 15.1), - 'mu': (113., 114., 22., 23.), - 'mv': (70.1, 76.1, -6., 10.), - 'mw': (32.5, 36.1, -17., -9.), - 'mx': (-119., -83., 13., 34.), - 'my': (142.5, 148.5, 9., 25.), - 'mz': (29., 41., -26.5, -9.5), - 'nama': (11., 25., -29.5, -16.5), - 'ncal': (158., 172., -23., -18.), - 'ng': (130., 152., -11., 0.), - 'ni': (2., 14.6, 3., 14.), - 'nk': (-88., -83., 10.5, 15.1), - 'nl': (3.5, 7.5, 50.5, 54.1), - 'no': (3., 35., 57., 71.5), - 'np': (80., 89., 25., 31.), - 'nw': (166.4, 167.4, -1., 0.), - 'nz': (165., 179., -48., -33.), - 'om': (52., 60., 16., 25.6), - 'os': (9., 18., 46., 50.), - 'pf': (-154., -134., -28., -8.), - 'ph': (116., 127., 4., 21.), - 'pi': (-177.5, -167.5, -9., 1.), - 'pk': (60., 78., 23., 37.), - 'pl': (14., 25., 48.5, 55.), - 'pm': (-83., -77., 7., 10.), - 'po': (-10., -4., 36.5, 42.5), - 'pr': (-82., -68., -20., 5.), - 'pt': (-130.6, -129.6, -25.56, -24.56), - 'pu': (-67.5, -65.5, 17.5, 18.5), - 'py': (-65., -54., -32., -17.), - 'qg': (7., 12., -2., 3.), - 'qt': (50., 52., 24., 27.), - 'ra': (60., -165., 25., 55.), - 're': (55., 56., -21.5, -20.5), - 'riro': (-18., -12., 17.5, 27.5), - 'ro': (19., 31., 42.5, 48.5), - 'rw': (29., 31., -3., -1.), - 'saud': (34.5, 56.1, 15., 32.6), - 'sb': (79., 83., 5., 10.), - 'seyc': (55., 56., -5., -4.), - 'sg': (-18., -10., 12., 17.), - 'si': (39.5, 52.1, -4.5, 13.5), - 'sk': (109.5, 119.3, 1., 7.), - 'sl': (-13.6, -10.2, 6.9, 10.1), - 'sm': (-59., -53., 1., 6.), - 'sn': (10., 25., 55., 69.6), - 'so': (156., 167., -12., -6.), - 'sp': (-10., 6., 35., 44.), - 'sr': (103., 105., 1., 2.), - 'su': (21.5, 38.5, 3.5, 23.5), - 'sv': (30.5, 33.1, -27.5, -25.3), - 'sw': (5.9, 10.5, 45.8, 48.), - 'sy': (35., 42.6, 32., 37.6), - 'tanz': (29., 40.6, -13., 0.), - 'td': (-62.1, -60.5, 10., 11.6), - 'tg': (-0.5, 2.5, 5., 12.), - 'th': (97., 106., 5., 21.), - 'ti': (-71.6, -70.6, 21., 22.), - 'tk': (-173., -171., -11.5, -7.5), - 'to': (-178.5, -170.5, -22., -15.), - 'tp': (6., 7.6, 0., 2.), - 'ts': (7., 13., 30., 38.), - 'tu': (25., 48., 34.1, 42.1), - 'tv': (176., 180., -11., -5.), - 'tw': (120., 122., 21.9, 25.3), - 'ug': (29., 35., -3.5, 5.5), - 'uk': (-11., 5., 49., 60.), - 'ur': (24., 41., 44., 55.), - 'uy': (-60., -52., -35.5, -29.5), - 'vanu': (167., 170., -21., -13.), - 'vi': (-65.5, -64., 16.6, 19.6), - 'vk': (13.8, 25.8, 46.75, 50.75), - 'vn': (-75., -60., -2., 14.), - 'vs': (102., 110., 8., 24.), - 'wk': (166.1, 167.1, 18.8, 19.8), - 'ye': (42.5, 54.1, 12.5, 19.1), - 'yg': (13.5, 24.6, 40., 47.), - 'za': (16., 34., -36., -22.), - 'zb': (21., 35., -20., -7.), - 'zm': (170.5, 173.5, -15., -13.), - 'zr': (12., 31.6, -14., 6.), - 'zw': (25., 34., -22.9, -15.5) + "105": (-129.3, -22.37, 17.52, 53.78), + "local": (-92.0, -64.0, 28.5, 48.5), + "wvaac": (120.86, -15.07, -53.6, 89.74), + "tropsfc": (-100.0, -55.0, 8.0, 33.0), + "epacsfc": (-155.0, -75.0, -20.0, 33.0), + "ofagx": (-100.0, -80.0, 20.0, 35.0), + "ahsf": (-105.0, -30.0, -5.0, 35.0), + "ehsf": (-145.0, -75.0, -5.0, 35.0), + "shsf": (-125.0, -75.0, -20.0, 5.0), + "tropful": (-160.0, 0.0, -20.0, 50.0), + "tropatl": (-115.0, 10.0, 0.0, 40.0), + "subtrop": (-90.0, -20.0, 20.0, 60.0), + "troppac": (-165.0, -80.0, -25.0, 45.0), + "gulf": (-105.0, -70.0, 10.0, 40.0), + "carib": (-100.0, -50.0, 0.0, 40.0), + "sthepac": (-170.0, -70.0, -60.0, 0.0), + "opcahsf": (-102.0, -20.0, 0.0, 45.0), + "opcphsf": (175.0, -70.0, -28.0, 45.0), + "wwe": (-106.0, -50.0, 18.0, 54.0), + "world": (-24.0, -24.0, -90.0, 90.0), + "nwwrd1": (-180.0, 180.0, -90.0, 90.0), + "nwwrd2": (0.0, 0.0, -90.0, 90.0), + "afna": (-135.02, -23.04, 10.43, 40.31), + "awna": (-141.03, -18.58, 7.84, 35.62), + "medr": (-178.0, -25.0, -15.0, 5.0), + "pacsfc": (129.0, -95.0, -5.0, 18.0), + "saudi": (4.6, 92.5, -13.2, 60.3), + "natlmed": (-30.0, 70.0, 0.0, 65.0), + "ncna": (-135.5, -19.25, 8.0, 37.7), + "ncna2": (-133.5, -20.5, 10.0, 42.0), + "hpcsfc": (-124.0, -26.0, 15.0, 53.0), + "atlhur": (-96.0, -6.0, 4.0, 3.0), + "nam": (-134.0, 3.0, -4.0, 39.0), + "sam": (-120.0, -20.0, -60.0, 20.0), + "samps": (-148.0, -36.0, -28.0, 12.0), + "eur": (-16.0, 80.0, 24.0, 52.0), + "afnh": (-155.19, 18.76, -6.8, -3.58), + "awnh": (-158.94, 15.35, -11.55, -8.98), + "wwwus": (-127.7, -59.0, 19.8, 56.6), + "ccfp": (-130.0, -65.0, 22.0, 52.0), + "llvl": (-119.6, -59.5, 19.9, 44.5), + "llvl2": (-125.0, -32.5, 5.0, 46.0), + "llvl_e": (-89.0, -59.5, 23.5, 44.5), + "llvl_c": (-102.4, -81.25, 23.8, 51.6), + "llvl_w": (-119.8, -106.5, 19.75, 52.8), + "ak_artc": (163.7, -65.3, 17.5, 52.6), + "fxpswna": (-80.5, 135.0, -1.0, 79.0), + "fxpsnna": (-80.5, 54.0, -1.0, 25.5), + "fxpsna": (-72.6, 31.4, -3.6, 31.0), + "natl_ps": (-80.5, 54.0, -1.0, 25.5), + "fxpsena": (-45.0, 54.0, 11.0, 25.5), + "fxpsnp": (155.5, -106.5, 22.5, 47.0), + "npac_ps": (155.5, -106.5, 22.5, 47.0), + "fxpsus": (-120.0, -59.0, 20.0, 44.5), + "fxmrwrd": (58.0, 58.0, -70.0, 70.0), + "fxmrwr2": (-131.0, -131.0, -70.0, 70.0), + "nwmrwrd": (70.0, 70.0, -70.0, 70.0), + "wrld_mr": (58.0, 58.0, -70.0, 70.0), + "fxmr110": (-180.0, -110.0, -20.0, 50.5), + "fxmr180": (110.0, -180.0, -20.0, 50.5), + "fxmrswp": (97.5, -147.5, -36.0, 45.5), + "fxmrus": (-162.5, -37.5, -28.0, 51.2), + "fxmrea": (-40.0, 20.0, -20.0, 54.2), + "fxmrjp": (100.0, -160.0, 0.0, 45.0), + "icao_a": (-137.4, -12.6, -54.0, 67.0), + "icao_b": (-52.5, -16.0, -62.5, 77.5), + "icao_b1": (-125.0, 40.0, -45.5, 62.7), + "icao_c": (-35.0, 70.0, -45.0, 75.0), + "icao_d": (-15.0, 132.0, -27.0, 63.0), + "icao_e": (25.0, 180.0, -54.0, 40.0), + "icao_f": (100.0, -110.0, -52.7, 50.0), + "icao_g": (34.8, 157.2, -0.8, 13.7), + "icao_h": (-79.1, 56.7, 1.6, 25.2), + "icao_i": (166.24, -60.62, -6.74, 33.32), + "icao_j": (106.8, -101.1, -27.6, 0.8), + "icao_k": (3.3, 129.1, -11.1, 6.7), + "icao_m": (100.0, -110.0, -10.0, 70.0), + "icao_eu": (-21.6, 68.4, 21.4, 58.7), + "icao_me": (17.0, 70.0, 10.0, 44.0), + "icao_as": (53.0, 108.0, 00.0, 36.0), + "icao_na": (-54.1, 60.3, 17.2, 50.7), + "nhem": (-135.0, 45.0, -15.0, -15.0), + "nhem_ps": (-135.0, 45.0, -15.0, -15.0), + "nhem180": (135.0, -45.0, -15.0, -15.0), + "nhem155": (160.0, -20.0, -15.0, -15.0), + "nhem165": (150.0, -30.0, -15.0, -15.0), + "nh45_ps": (-90.0, 90.0, -15.0, -15.0), + "nhem0": (-45.0, 135.0, -15.0, -15.0), + "shem_ps": (88.0, -92.0, 30.0, 30.0), + "hfo_gu": (160.0, -130.0, -30.0, 40.0), + "natl": (-110.0, 20.1, 15.0, 70.0), + "watl": (-84.0, -38.0, 25.0, 46.0), + "tatl": (-90.0, -15.0, -10.0, 35.0), + "npac": (102.0, -110.0, -12.0, 60.0), + "spac": (102.0, -70.0, -60.0, 20.0), + "tpac": (-165.0, -75.0, -10.0, 40.0), + "epac": (-134.0, -110.0, 12.0, 75.0), + "wpac": (130.0, -120.0, 0.0, 63.0), + "mpac": (128.0, -108.0, 15.0, 71.95), + "opcsfp": (128.89, -105.3, 3.37, 16.77), + "opcsfa": (-55.5, 75.0, -8.5, 52.6), + "opchur": (-99.0, -15.0, 1.0, 50.05), + "us": (-119.0, -56.0, 19.0, 47.0), + "spcus": (-116.4, -63.9, 22.1, 47.2), + "afus": (-119.04, -63.44, 23.1, 44.63), + "ncus": (-124.2, -40.98, 17.89, 47.39), + "nwus": (-118.0, -55.5, 17.0, 46.5), + "awips": (-127.0, -59.0, 20.0, 50.0), + "bwus": (-124.6, -46.7, 13.1, 43.1), + "usa": (-118.0, -62.0, 22.8, 45.0), + "usnps": (-118.0, -62.0, 18.0, 51.0), + "uslcc": (-118.0, -62.0, 20.0, 51.0), + "uswn": (-129.0, -45.0, 17.0, 53.0), + "ussf": (-123.5, -44.5, 13.0, 32.1), + "ussp": (-126.0, -49.0, 13.0, 54.0), + "whlf": (-123.8, -85.9, 22.9, 50.2), + "chlf": (-111.0, -79.0, 27.5, 50.5), + "centus": (-105.4, -77.0, 24.7, 47.6), + "ehlf": (-96.2, -62.7, 22.0, 49.0), + "mehlf": (-89.9, -66.6, 23.8, 49.1), + "bosfa": (-87.5, -63.5, 34.5, 50.5), + "miafa": (-88.0, -72.0, 23.0, 39.0), + "chifa": (-108.0, -75.0, 34.0, 50.0), + "dfwfa": (-106.5, -80.5, 22.0, 40.0), + "slcfa": (-126.0, -98.0, 29.5, 50.5), + "sfofa": (-129.0, -111.0, 30.0, 50.0), + "g8us": (-116.0, -58.0, 19.0, 56.0), + "wsig": (155.0, -115.0, 18.0, 58.0), + "esig": (-80.0, -30.0, 25.0, 51.0), + "eg8": (-79.0, -13.0, 24.0, 52.0), + "west": (-125.0, -90.0, 25.0, 55.0), + "cent": (-107.4, -75.3, 24.3, 49.7), + "east": (-100.55, -65.42, 24.57, 47.2), + "nwse": (-126.0, -102.0, 38.25, 50.25), + "swse": (-126.0, -100.0, 28.25, 40.25), + "ncse": (-108.0, -84.0, 38.25, 50.25), + "scse": (-108.9, -84.0, 24.0, 40.25), + "nese": (-89.0, -64.0, 37.25, 47.25), + "sese": (-90.0, -66.0, 28.25, 40.25), + "afwh": (170.7, 15.4, -48.6, 69.4), + "afeh": (-9.3, -164.6, -48.6, 69.4), + "afpc": (80.7, -74.6, -48.6, 69.4), + "ak": (-179.0, -116.4, 49.0, 69.0), + "ak2": (-180.0, -106.0, 42.0, 73.0), + "nwak": (-180.0, -110.0, 50.0, 60.0), + "al": (-95.0, -79.0, 27.0, 38.0), + "ar": (-100.75, -84.75, 29.5, 40.5), + "ca": (-127.75, -111.75, 31.5, 42.5), + "co": (-114.0, -98.0, 33.5, 44.5), + "ct": (-81.25, -65.25, 36.0, 47.0), + "dc": (-85.0, -69.0, 33.35, 44.35), + "de": (-83.75, -67.75, 33.25, 44.25), + "fl": (-90.0, -74.0, 23.0, 34.0), + "ga": (-92.0, -76.0, 27.5, 38.5), + "hi": (-161.5, -152.5, 17.0, 23.0), + "nwxhi": (-166.0, -148.0, 14.0, 26.0), + "ia": (-102.0, -86.0, 36.5, 47.5), + "id": (-123.0, -107.0, 39.25, 50.25), + "il": (-97.75, -81.75, 34.5, 45.5), + "in": (-94.5, -78.5, 34.5, 45.5), + "ks": (-106.5, -90.5, 33.25, 44.25), + "ky": (-93.0, -77.0, 31.75, 42.75), + "la": (-100.75, -84.75, 25.75, 36.75), + "ma": (-80.25, -64.25, 36.75, 47.75), + "md": (-85.25, -69.25, 33.75, 44.75), + "me": (-77.75, -61.75, 39.5, 50.5), + "mi": (-93.0, -77.0, 37.75, 48.75), + "mn": (-102.0, -86.0, 40.5, 51.5), + "mo": (-101.0, -85.0, 33.0, 44.0), + "ms": (-98.0, -82.0, 27.0, 38.0), + "mt": (-117.0, -101.0, 41.5, 52.5), + "nc": (-87.25, -71.25, 30.0, 41.0), + "nd": (-107.5, -91.5, 42.25, 53.25), + "ne": (-107.5, -91.5, 36.25, 47.25), + "nh": (-79.5, -63.5, 38.25, 49.25), + "nj": (-82.5, -66.5, 34.75, 45.75), + "nm": (-114.25, -98.25, 29.0, 40.0), + "nv": (-125.0, -109.0, 34.0, 45.0), + "ny": (-84.0, -68.0, 37.25, 48.25), + "oh": (-91.0, -75.0, 34.5, 45.5), + "ok": (-105.25, -89.25, 30.25, 41.25), + "or": (-128.0, -112.0, 38.75, 49.75), + "pa": (-86.0, -70.0, 35.5, 46.5), + "ri": (-79.75, -63.75, 36.0, 47.0), + "sc": (-89.0, -73.0, 28.5, 39.5), + "sd": (-107.5, -91.5, 39.0, 50.0), + "tn": (-95.0, -79.0, 30.0, 41.0), + "tx": (-107.0, -91.0, 25.4, 36.5), + "ut": (-119.0, -103.0, 34.0, 45.0), + "va": (-86.5, -70.5, 32.25, 43.25), + "vt": (-80.75, -64.75, 38.25, 49.25), + "wi": (-98.0, -82.0, 38.5, 49.5), + "wv": (-89.0, -73.0, 33.0, 44.0), + "wy": (-116.0, -100.0, 37.75, 48.75), + "az": (-119.0, -103.0, 29.0, 40.0), + "wa": (-128.0, -112.0, 41.75, 52.75), + "abrfc": (-108.0, -88.0, 30.0, 42.0), + "ab10": (-106.53, -90.28, 31.69, 40.01), + "cbrfc": (-117.0, -103.0, 28.0, 46.0), + "cb10": (-115.69, -104.41, 29.47, 44.71), + "lmrfc": (-100.0, -77.0, 26.0, 40.0), + "lm10": (-97.17, -80.07, 28.09, 38.02), + "marfc": (-83.5, -70.0, 35.5, 44.0), + "ma10": (-81.27, -72.73, 36.68, 43.1), + "mbrfc": (-116.0, -86.0, 33.0, 53.0), + "mb10": (-112.8, -89.33, 35.49, 50.72), + "ncrfc": (-108.0, -76.0, 34.0, 53.0), + "nc10": (-104.75, -80.05, 35.88, 50.6), + "nerfc": (-84.0, -61.0, 39.0, 49.0), + "ne10": (-80.11, -64.02, 40.95, 47.62), + "nwrfc": (-128.0, -105.0, 35.0, 55.0), + "nw10": (-125.85, -109.99, 38.41, 54.46), + "ohrfc": (-92.0, -75.0, 34.0, 44.0), + "oh10": (-90.05, -77.32, 35.2, 42.9), + "serfc": (-94.0, -70.0, 22.0, 40.0), + "se10": (-90.6, -73.94, 24.12, 37.91), + "wgrfc": (-112.0, -88.0, 21.0, 42.0), + "wg10": (-108.82, -92.38, 23.99, 39.18), + "nwcn": (-133.5, -10.5, 32.0, 56.0), + "cn": (-120.4, -14.0, 37.9, 58.6), + "ab": (-119.6, -108.2, 48.6, 60.4), + "bc": (-134.5, -109.0, 47.2, 60.7), + "mb": (-102.4, -86.1, 48.3, 60.2), + "nb": (-75.7, -57.6, 42.7, 49.6), + "nf": (-68.0, -47.0, 45.0, 62.0), + "ns": (-67.0, -59.0, 43.0, 47.5), + "nt": (-131.8, -33.3, 57.3, 67.8), + "on": (-94.5, -68.2, 41.9, 55.0), + "pe": (-64.6, -61.7, 45.8, 47.1), + "qb": (-80.0, -49.2, 44.1, 60.9), + "sa": (-111.2, -97.8, 48.5, 60.3), + "yt": (-142.0, -117.0, 59.0, 70.5), + "ag": (-80.0, -53.0, -56.0, -20.0), + "ah": (60.0, 77.0, 27.0, 40.0), + "afrca": (-25.0, 59.4, -36.0, 41.0), + "ai": (-14.3, -14.1, -8.0, -7.8), + "alba": (18.0, 23.0, 39.0, 43.0), + "alge": (-9.0, 12.0, 15.0, 38.0), + "an": (10.0, 25.0, -20.0, -5.0), + "antl": (-70.0, -58.0, 11.0, 19.0), + "antg": (-86.0, -65.0, 17.0, 25.0), + "atg": (-62.0, -61.6, 16.9, 17.75), + "au": (101.0, 148.0, -45.0, -6.5), + "azor": (-27.6, -23.0, 36.0, 41.0), + "ba": (-80.5, -72.5, 22.5, 28.5), + "be": (-64.9, -64.5, 32.2, 32.6), + "bel": (2.5, 6.5, 49.4, 51.6), + "bf": (113.0, 116.0, 4.0, 5.5), + "bfa": (-6.0, 3.0, 9.0, 15.1), + "bh": (-89.3, -88.1, 15.7, 18.5), + "bi": (29.0, 30.9, -4.6, -2.2), + "bj": (0.0, 5.0, 6.0, 12.6), + "bn": (50.0, 51.0, 25.5, 27.1), + "bo": (-72.0, -50.0, -24.0, -8.0), + "bots": (19.0, 29.6, -27.0, -17.0), + "br": (-62.5, -56.5, 12.45, 13.85), + "bt": (71.25, 72.6, -7.5, -5.0), + "bu": (22.0, 30.0, 40.0, 45.0), + "bv": (3.0, 4.0, -55.0, -54.0), + "bw": (87.0, 93.0, 20.8, 27.0), + "by": (19.0, 33.0, 51.0, 60.0), + "bz": (-75.0, -30.0, -35.0, 5.0), + "cais": (-172.0, -171.0, -3.0, -2.0), + "nwcar": (-120.0, -50.0, -15.0, 35.0), + "cari": (-103.0, -53.0, 3.0, 36.0), + "cb": (13.0, 25.0, 7.0, 24.0), + "ce": (14.0, 29.0, 2.0, 11.5), + "cg": (10.0, 20.0, -6.0, 5.0), + "ch": (-80.0, -66.0, -56.0, -15.0), + "ci": (85.0, 145.0, 14.0, 48.5), + "cm": (7.5, 17.1, 1.0, 14.0), + "colm": (-81.0, -65.0, -5.0, 14.0), + "cr": (-19.0, -13.0, 27.0, 30.0), + "cs": (-86.5, -81.5, 8.2, 11.6), + "cu": (-85.0, -74.0, 19.0, 24.0), + "cv": (-26.0, -22.0, 14.0, 18.0), + "cy": (32.0, 35.0, 34.0, 36.0), + "cz": (8.9, 22.9, 47.4, 52.4), + "dj": (41.5, 44.1, 10.5, 13.1), + "dl": (4.8, 16.8, 47.0, 55.0), + "dn": (8.0, 11.0, 54.0, 58.6), + "do": (-61.6, -61.2, 15.2, 15.8), + "dr": (-72.2, -68.0, 17.5, 20.2), + "eg": (24.0, 37.0, 21.0, 33.0), + "eq": (-85.0, -74.0, -7.0, 3.0), + "er": (50.0, 57.0, 22.0, 26.6), + "es": (-90.3, -87.5, 13.0, 14.6), + "et": (33.0, 49.0, 2.0, 19.0), + "fa": (-8.0, -6.0, 61.0, 63.0), + "fg": (-55.0, -49.0, 1.0, 7.0), + "fi": (20.9, 35.1, 59.0, 70.6), + "fj": (176.0, -179.0, 16.0, 19.0), + "fk": (-61.3, -57.5, -53.0, -51.0), + "fn": (0.0, 17.0, 11.0, 24.0), + "fr": (-5.0, 11.0, 41.0, 51.5), + "gb": (-17.1, -13.5, 13.0, 14.6), + "gc": (-82.8, -77.6, 17.9, 21.1), + "gh": (-4.5, 1.5, 4.0, 12.0), + "gi": (-8.0, -4.0, 35.0, 38.0), + "gl": (-56.7, 14.0, 58.3, 79.7), + "glp": (-64.2, -59.8, 14.8, 19.2), + "gm": (144.5, 145.1, 13.0, 14.0), + "gn": (2.0, 16.0, 3.5, 15.5), + "go": (8.0, 14.5, -4.6, 3.0), + "gr": (20.0, 27.6, 34.0, 42.0), + "gu": (-95.6, -85.0, 10.5, 21.1), + "gw": (-17.5, -13.5, 10.8, 12.8), + "gy": (-62.0, -55.0, 0.0, 10.0), + "ha": (-75.0, -71.0, 18.0, 20.0), + "he": (-6.1, -5.5, -16.3, -15.5), + "hk": (113.5, 114.7, 22.0, 23.0), + "ho": (-90.0, -83.0, 13.0, 16.6), + "hu": (16.0, 23.0, 45.5, 49.1), + "ic": (43.0, 45.0, -13.2, -11.0), + "icel": (-24.1, -11.5, 63.0, 67.5), + "ie": (-11.1, -4.5, 50.0, 55.6), + "inda": (67.0, 92.0, 4.2, 36.0), + "indo": (95.0, 141.0, -8.0, 6.0), + "iq": (38.0, 50.0, 29.0, 38.0), + "ir": (44.0, 65.0, 25.0, 40.0), + "is": (34.0, 37.0, 29.0, 34.0), + "iv": (-9.0, -2.0, 4.0, 11.0), + "iw": (34.8, 35.6, 31.2, 32.6), + "iy": (6.6, 20.6, 35.6, 47.2), + "jd": (34.0, 39.6, 29.0, 33.6), + "jm": (-80.0, -76.0, 16.0, 19.0), + "jp": (123.0, 155.0, 24.0, 47.0), + "ka": (131.0, 155.0, 1.0, 9.6), + "kash": (74.0, 78.0, 32.0, 35.0), + "kb": (172.0, 177.0, -3.0, 3.2), + "khm": (102.0, 108.0, 10.0, 15.0), + "ki": (105.2, 106.2, -11.0, -10.0), + "kn": (32.5, 42.1, -6.0, 6.0), + "kna": (-62.9, -62.4, 17.0, 17.5), + "ko": (124.0, 131.5, 33.0, 43.5), + "ku": (-168.0, -155.0, -24.1, -6.1), + "kw": (46.5, 48.5, 28.5, 30.5), + "laos": (100.0, 108.0, 13.5, 23.1), + "lb": (34.5, 37.1, 33.0, 35.0), + "lc": (60.9, 61.3, 13.25, 14.45), + "li": (-12.0, -7.0, 4.0, 9.0), + "ln": (-162.1, -154.9, -4.2, 6.0), + "ls": (27.0, 29.6, -30.6, -28.0), + "lt": (9.3, 9.9, 47.0, 47.6), + "lux": (5.6, 6.6, 49.35, 50.25), + "ly": (8.0, 26.0, 19.0, 35.0), + "maar": (-63.9, -62.3, 17.0, 18.6), + "made": (-17.3, -16.5, 32.6, 33.0), + "mala": (100.0, 119.6, 1.0, 8.0), + "mali": (-12.5, 6.0, 8.5, 25.5), + "maur": (57.2, 57.8, -20.7, -19.9), + "maut": (-17.1, -4.5, 14.5, 28.1), + "mc": (-13.0, -1.0, 25.0, 36.0), + "mg": (43.0, 50.6, -25.6, -12.0), + "mh": (160.0, 172.0, 4.5, 12.1), + "ml": (14.3, 14.7, 35.8, 36.0), + "mmr": (92.0, 102.0, 7.5, 28.5), + "mong": (87.5, 123.1, 38.5, 52.6), + "mr": (-61.2, -60.8, 14.3, 15.1), + "mu": (113.0, 114.0, 22.0, 23.0), + "mv": (70.1, 76.1, -6.0, 10.0), + "mw": (32.5, 36.1, -17.0, -9.0), + "mx": (-119.0, -83.0, 13.0, 34.0), + "my": (142.5, 148.5, 9.0, 25.0), + "mz": (29.0, 41.0, -26.5, -9.5), + "nama": (11.0, 25.0, -29.5, -16.5), + "ncal": (158.0, 172.0, -23.0, -18.0), + "ng": (130.0, 152.0, -11.0, 0.0), + "ni": (2.0, 14.6, 3.0, 14.0), + "nk": (-88.0, -83.0, 10.5, 15.1), + "nl": (3.5, 7.5, 50.5, 54.1), + "no": (3.0, 35.0, 57.0, 71.5), + "np": (80.0, 89.0, 25.0, 31.0), + "nw": (166.4, 167.4, -1.0, 0.0), + "nz": (165.0, 179.0, -48.0, -33.0), + "om": (52.0, 60.0, 16.0, 25.6), + "os": (9.0, 18.0, 46.0, 50.0), + "pf": (-154.0, -134.0, -28.0, -8.0), + "ph": (116.0, 127.0, 4.0, 21.0), + "pi": (-177.5, -167.5, -9.0, 1.0), + "pk": (60.0, 78.0, 23.0, 37.0), + "pl": (14.0, 25.0, 48.5, 55.0), + "pm": (-83.0, -77.0, 7.0, 10.0), + "po": (-10.0, -4.0, 36.5, 42.5), + "pr": (-82.0, -68.0, -20.0, 5.0), + "pt": (-130.6, -129.6, -25.56, -24.56), + "pu": (-67.5, -65.5, 17.5, 18.5), + "py": (-65.0, -54.0, -32.0, -17.0), + "qg": (7.0, 12.0, -2.0, 3.0), + "qt": (50.0, 52.0, 24.0, 27.0), + "ra": (60.0, -165.0, 25.0, 55.0), + "re": (55.0, 56.0, -21.5, -20.5), + "riro": (-18.0, -12.0, 17.5, 27.5), + "ro": (19.0, 31.0, 42.5, 48.5), + "rw": (29.0, 31.0, -3.0, -1.0), + "saud": (34.5, 56.1, 15.0, 32.6), + "sb": (79.0, 83.0, 5.0, 10.0), + "seyc": (55.0, 56.0, -5.0, -4.0), + "sg": (-18.0, -10.0, 12.0, 17.0), + "si": (39.5, 52.1, -4.5, 13.5), + "sk": (109.5, 119.3, 1.0, 7.0), + "sl": (-13.6, -10.2, 6.9, 10.1), + "sm": (-59.0, -53.0, 1.0, 6.0), + "sn": (10.0, 25.0, 55.0, 69.6), + "so": (156.0, 167.0, -12.0, -6.0), + "sp": (-10.0, 6.0, 35.0, 44.0), + "sr": (103.0, 105.0, 1.0, 2.0), + "su": (21.5, 38.5, 3.5, 23.5), + "sv": (30.5, 33.1, -27.5, -25.3), + "sw": (5.9, 10.5, 45.8, 48.0), + "sy": (35.0, 42.6, 32.0, 37.6), + "tanz": (29.0, 40.6, -13.0, 0.0), + "td": (-62.1, -60.5, 10.0, 11.6), + "tg": (-0.5, 2.5, 5.0, 12.0), + "th": (97.0, 106.0, 5.0, 21.0), + "ti": (-71.6, -70.6, 21.0, 22.0), + "tk": (-173.0, -171.0, -11.5, -7.5), + "to": (-178.5, -170.5, -22.0, -15.0), + "tp": (6.0, 7.6, 0.0, 2.0), + "ts": (7.0, 13.0, 30.0, 38.0), + "tu": (25.0, 48.0, 34.1, 42.1), + "tv": (176.0, 180.0, -11.0, -5.0), + "tw": (120.0, 122.0, 21.9, 25.3), + "ug": (29.0, 35.0, -3.5, 5.5), + "uk": (-11.0, 5.0, 49.0, 60.0), + "ur": (24.0, 41.0, 44.0, 55.0), + "uy": (-60.0, -52.0, -35.5, -29.5), + "vanu": (167.0, 170.0, -21.0, -13.0), + "vi": (-65.5, -64.0, 16.6, 19.6), + "vk": (13.8, 25.8, 46.75, 50.75), + "vn": (-75.0, -60.0, -2.0, 14.0), + "vs": (102.0, 110.0, 8.0, 24.0), + "wk": (166.1, 167.1, 18.8, 19.8), + "ye": (42.5, 54.1, 12.5, 19.1), + "yg": (13.5, 24.6, 40.0, 47.0), + "za": (16.0, 34.0, -36.0, -22.0), + "zb": (21.0, 35.0, -20.0, -7.0), + "zm": (170.5, 173.5, -15.0, -13.0), + "zr": (12.0, 31.6, -14.0, 6.0), + "zw": (25.0, 34.0, -22.9, -15.5), } @@ -470,25 +480,29 @@ def lookup_projection(projection_code): """Get a Cartopy projection based on a short abbreviation.""" import cartopy.crs as ccrs - projections = {'lcc': ccrs.LambertConformal(central_latitude=40, central_longitude=-100, - standard_parallels=[30, 60]), - 'ps': ccrs.NorthPolarStereo(central_longitude=-100), - 'mer': ccrs.Mercator()} + projections = { + "lcc": ccrs.LambertConformal( + central_latitude=40, central_longitude=-100, standard_parallels=[30, 60] + ), + "ps": ccrs.NorthPolarStereo(central_longitude=-100), + "mer": ccrs.Mercator(), + } return projections[projection_code] def lookup_map_feature(feature_name): """Get a Cartopy map feature based on a name.""" import cartopy.feature as cfeature + from . import cartopy_utils name = feature_name.upper() try: feat = getattr(cfeature, name) - scaler = cfeature.AdaptiveScaler('110m', (('50m', 50), ('10m', 15))) + scaler = cfeature.AdaptiveScaler("110m", (("50m", 50), ("10m", 15))) except AttributeError: feat = getattr(cartopy_utils, name) - scaler = cfeature.AdaptiveScaler('20m', (('5m', 5), ('500k', 1))) + scaler = cfeature.AdaptiveScaler("20m", (("5m", 5), ("500k", 1))) return feat.with_scale(scaler) @@ -521,16 +535,16 @@ def panel(self): def panel(self, val): self.panels = [val] - @observe('panels') + @observe("panels") def _panels_changed(self, change): for panel in change.new: panel.parent = self - panel.observe(self.refresh, names=('_need_redraw')) + panel.observe(self.refresh, names=("_need_redraw")) @property def figure(self): """Provide access to the underlying figure object.""" - if not hasattr(self, '_fig'): + if not hasattr(self, "_fig"): self._fig = plt.figure(figsize=self.size) return self._fig @@ -602,8 +616,11 @@ class MapPanel(Panel): _need_redraw = Bool(default_value=True) - area = Union([Unicode(), Tuple(Float(), Float(), Float(), Float())], allow_none=True, - default_value=None) + area = Union( + [Unicode(), Tuple(Float(), Float(), Float(), Float())], + allow_none=True, + default_value=None, + ) area.__doc__ = """A tuple or string value that indicates the graphical area of the plot. The tuple value coresponds to longitude/latitude box based on the projection of the map @@ -617,7 +634,7 @@ class MapPanel(Panel): For regional plots, US postal state abbreviations can be used. """ - projection = Union([Unicode(), Instance('cartopy.crs.Projection')], default_value='data') + projection = Union([Unicode(), Instance("cartopy.crs.Projection")], default_value="data") projection.__doc__ = """A string for a pre-defined projection or a Cartopy projection object. @@ -626,8 +643,9 @@ class MapPanel(Panel): Additionally, this trait can be set to a Cartopy projection object. """ - layers = List(Union([Unicode(), Instance('cartopy.feature.Feature')]), - default_value=['coastline']) + layers = List( + Union([Unicode(), Instance("cartopy.feature.Feature")]), default_value=["coastline"] + ) layers.__doc__ = """A list of strings for a pre-defined feature layer or a Cartopy Feature object. Like the projection, there are a couple of pre-defined feature layers that can be called @@ -642,15 +660,15 @@ class MapPanel(Panel): This trait sets a user-defined title that will plot at the top center of the figure. """ - @observe('plots') + @observe("plots") def _plots_changed(self, change): """Handle when our collection of plots changes.""" for plot in change.new: plot.parent = self - plot.observe(self.refresh, names=('_need_redraw')) + plot.observe(self.refresh, names=("_need_redraw")) self._need_redraw = True - @observe('parent') + @observe("parent") def _parent_changed(self, _): """Handle when the parent is changed.""" self.ax = None @@ -664,7 +682,7 @@ def _proj_obj(self): """ if isinstance(self.projection, str): - if self.projection == 'data': + if self.projection == "data": if isinstance(self.plots[0].griddata, tuple): return self.plots[0].griddata[0].metpy.cartopy_crs else: @@ -689,7 +707,7 @@ def _layer_features(self): yield feat - @observe('area') + @observe("area") def _set_need_redraw(self, _): """Watch traits and set the need redraw flag as necessary.""" self._need_redraw = True @@ -703,7 +721,7 @@ def ax(self): """ # If we haven't actually made an instance yet, make one with the right size and # map projection. - if getattr(self, '_ax', None) is None: + if getattr(self, "_ax", None) is None: self._ax = self.parent.figure.add_subplot(*self.layout, projection=self._proj_obj) return self._ax @@ -715,7 +733,7 @@ def ax(self, val): Clears existing state as necessary. """ - if getattr(self, '_ax', None) is not None: + if getattr(self, "_ax", None) is not None: self._ax.cla() self._ax = val @@ -729,7 +747,7 @@ def draw(self): if self._need_redraw: # Set the extent as appropriate based on the area. One special case for 'global' - if self.area == 'global': + if self.area == "global": self.ax.set_global() elif self.area is not None: # Try to look up if we have a string @@ -750,7 +768,7 @@ def draw(self): self.ax.add_feature(feat) # Use the set title or generate one. - title = self.title or ',\n'.join(plot.name for plot in self.plots) + title = self.title or ",\n".join(plot.name for plot in self.plots) self.ax.set_title(title) self._need_redraw = False @@ -823,8 +841,8 @@ def clear(self): Resets all internal state and sets need for redraw. """ - if getattr(self, 'handle', None) is not None: - if getattr(self.handle, 'collections', None) is not None: + if getattr(self, "handle", None) is not None: + if getattr(self.handle, "collections", None) is not None: self.clear_collections() else: self.clear_handle() @@ -841,12 +859,12 @@ def clear_collections(self): col.remove() self.handle = None - @observe('parent') + @observe("parent") def _parent_changed(self, _): """Handle setting the parent object for the plot.""" self.clear() - @observe('level', 'time') + @observe("level", "time") def _update_data(self, _=None): """Handle updating the internal cache of data. @@ -872,12 +890,12 @@ def data(self, val): def name(self): """Generate a name for the plot.""" if isinstance(self.field, tuple): - ret = '' - ret += ' and '.join(f for f in self.field) + ret = "" + ret += " and ".join(f for f in self.field) else: ret = self.field if self.level is not None: - ret += f'@{self.level:d}' + ret += f"@{self.level:d}" return ret @@ -901,7 +919,7 @@ class PlotScalar(Plots2D): `list(ds)` """ - @observe('field') + @observe("field") def _update_data(self, _=None): """Handle updating the internal cache of data. @@ -914,18 +932,18 @@ def _update_data(self, _=None): @property def griddata(self): """Return the internal cached data.""" - if getattr(self, '_griddata', None) is None: + if getattr(self, "_griddata", None) is None: if self.field: data = self.data.metpy.parse_cf(self.field) - elif not hasattr(self.data.metpy, 'x'): + elif not hasattr(self.data.metpy, "x"): # Handles the case where we have a dataset but no specified field - raise ValueError('field attribute has not been set.') + raise ValueError("field attribute has not been set.") else: data = self.data - subset = {'method': 'nearest'} + subset = {"method": "nearest"} if self.level is not None: subset[data.metpy.vertical.name] = self.level @@ -949,9 +967,10 @@ def plotdata(self): x = self.griddata.metpy.x y = self.griddata.metpy.y - if 'degree' in x.units: - x, y, _ = self.griddata.metpy.cartopy_crs.transform_points(ccrs.PlateCarree(), - *np.meshgrid(x, y)).T + if "degree" in x.units: + x, y, _ = self.griddata.metpy.cartopy_crs.transform_points( + ccrs.PlateCarree(), *np.meshgrid(x, y) + ).T x = x[:, 0] % 360 y = y[0, :] @@ -960,11 +979,12 @@ def plotdata(self): def draw(self): """Draw the plot.""" if self._need_redraw: - if getattr(self, 'handle', None) is None: + if getattr(self, "handle", None) is None: self._build() - if getattr(self, 'colorbar', None) is not None: + if getattr(self, "colorbar", None) is not None: self.parent.ax.figure.colorbar( - self.handle, orientation=self.colorbar, pad=0, aspect=50) + self.handle, orientation=self.colorbar, pad=0, aspect=50 + ) self._need_redraw = False @@ -996,8 +1016,10 @@ class ColorfillTraits(HasTraits): For example, the Blue-Purple colormap from Matplotlib can be accessed using 'BuPu'. """ - image_range = Union([Tuple(Int(allow_none=True), Int(allow_none=True)), - Instance(plt.Normalize)], default_value=(None, None)) + image_range = Union( + [Tuple(Int(allow_none=True), Int(allow_none=True)), Instance(plt.Normalize)], + default_value=(None, None), + ) image_range.__doc__ = """A tuple of min and max values that represent the range of values to color the rasterized image. @@ -1018,15 +1040,15 @@ class ColorfillTraits(HasTraits): class ImagePlot(PlotScalar, ColorfillTraits): """Make raster image using `~matplotlib.pyplot.imshow` for satellite or colored image.""" - @observe('colormap', 'image_range') + @observe("colormap", "image_range") def _set_need_redraw(self, _): """Handle changes to attributes that just need a simple redraw.""" - if hasattr(self, 'handle'): + if hasattr(self, "handle"): self.handle.set_cmap(self._cmap_obj) self.handle.set_norm(self._norm_obj) self._need_redraw = True - @observe('colorbar') + @observe("colorbar") def _set_need_rebuild(self, _): """Handle changes to attributes that need to regenerate everything.""" # Because matplotlib doesn't let you just change these properties, we need @@ -1044,7 +1066,7 @@ def plotdata(self): y = self.griddata.metpy.y # At least currently imshow with cartopy does not like this - if 'degree' in x.units: + if "degree" in x.units: x = x.data x[x > 180] -= 360 @@ -1057,17 +1079,22 @@ def _build(self): # We use min/max for y and manually figure out origin to try to avoid upside down # images created by images where y[0] > y[-1] extents = (x[0], x[-1], y.min(), y.max()) - origin = 'upper' if y[0] > y[-1] else 'lower' - self.handle = self.parent.ax.imshow(imdata, extent=extents, origin=origin, - cmap=self._cmap_obj, norm=self._norm_obj, - transform=imdata.metpy.cartopy_crs) + origin = "upper" if y[0] > y[-1] else "lower" + self.handle = self.parent.ax.imshow( + imdata, + extent=extents, + origin=origin, + cmap=self._cmap_obj, + norm=self._norm_obj, + transform=imdata.metpy.cartopy_crs, + ) @exporter.export class ContourPlot(PlotScalar, ContourTraits): """Make contour plots by defining specific traits.""" - linecolor = Unicode('black') + linecolor = Unicode("black") linecolor.__doc__ = """A string value to set the color of plotted contours; default is black. @@ -1083,7 +1110,7 @@ class ContourPlot(PlotScalar, ContourTraits): line. """ - linestyle = Unicode('solid', allow_none=True) + linestyle = Unicode("solid", allow_none=True) linestyle.__doc__ = """A string value to set the linestyle (e.g., dashed); default is solid. @@ -1091,7 +1118,7 @@ class ContourPlot(PlotScalar, ContourTraits): dashdot. """ - @observe('contours', 'linecolor', 'linewidth', 'linestyle', 'clabels') + @observe("contours", "linecolor", "linewidth", "linestyle", "clabels") def _set_need_rebuild(self, _): """Handle changes to attributes that need to regenerate everything.""" # Because matplotlib doesn't let you just change these properties, we need @@ -1101,20 +1128,25 @@ def _set_need_rebuild(self, _): def _build(self): """Build the plot by calling any plotting methods as necessary.""" x, y, imdata = self.plotdata - self.handle = self.parent.ax.contour(x, y, imdata, self.contours, - colors=self.linecolor, linewidths=self.linewidth, - linestyles=self.linestyle, - transform=imdata.metpy.cartopy_crs) + self.handle = self.parent.ax.contour( + x, + y, + imdata, + self.contours, + colors=self.linecolor, + linewidths=self.linewidth, + linestyles=self.linestyle, + transform=imdata.metpy.cartopy_crs, + ) if self.clabels: - self.handle.clabel(inline=1, fmt='%.0f', inline_spacing=8, - use_clabeltext=True) + self.handle.clabel(inline=1, fmt="%.0f", inline_spacing=8, use_clabeltext=True) @exporter.export class FilledContourPlot(PlotScalar, ColorfillTraits, ContourTraits): """Make color-filled contours plots by defining appropriate traits.""" - @observe('contours', 'colorbar', 'colormap') + @observe("contours", "colorbar", "colormap") def _set_need_rebuild(self, _): """Handle changes to attributes that need to regenerate everything.""" # Because matplotlib doesn't let you just change these properties, we need @@ -1124,9 +1156,15 @@ def _set_need_rebuild(self, _): def _build(self): """Build the plot by calling any plotting methods as necessary.""" x, y, imdata = self.plotdata - self.handle = self.parent.ax.contourf(x, y, imdata, self.contours, - cmap=self._cmap_obj, norm=self._norm_obj, - transform=imdata.metpy.cartopy_crs) + self.handle = self.parent.ax.contourf( + x, + y, + imdata, + self.contours, + cmap=self._cmap_obj, + norm=self._norm_obj, + transform=imdata.metpy.cartopy_crs, + ) @exporter.export @@ -1145,7 +1183,7 @@ class PlotVector(Plots2D): (u-wind, v-wind). """ - pivot = Unicode('middle') + pivot = Unicode("middle") pivot.__doc__ = """A string setting the pivot point of the vector. Default value is 'middle'. @@ -1172,7 +1210,7 @@ class PlotVector(Plots2D): components that are grid-relative. For any grid-relative vectors set this trait to `False`. """ - color = Unicode(default_value='black') + color = Unicode(default_value="black") color.__doc__ = """A string value that controls the color of the vectors. Default value is black. @@ -1180,7 +1218,7 @@ class PlotVector(Plots2D): `Matplotlibs Colors ` """ - @observe('field') + @observe("field") def _update_data(self, _=None): """Handle updating the internal cache of data. @@ -1194,15 +1232,15 @@ def _update_data(self, _=None): @property def griddata(self): """Return the internal cached data.""" - if getattr(self, '_griddata_u', None) is None: + if getattr(self, "_griddata_u", None) is None: if self.field[0]: u = self.data.metpy.parse_cf(self.field[0]) v = self.data.metpy.parse_cf(self.field[1]) else: - raise ValueError('field attribute not set correctly') + raise ValueError("field attribute not set correctly") - subset = {'method': 'nearest'} + subset = {"method": "nearest"} if self.level is not None: subset[u.metpy.vertical.name] = self.level @@ -1230,14 +1268,20 @@ def plotdata(self): y = self.griddata[0].metpy.y if self.earth_relative: - x, y, _ = ccrs.PlateCarree().transform_points(self.griddata[0].metpy.cartopy_crs, - *np.meshgrid(x, y)).T + x, y, _ = ( + ccrs.PlateCarree() + .transform_points(self.griddata[0].metpy.cartopy_crs, *np.meshgrid(x, y)) + .T + ) x = x.T y = y.T else: - if 'degree' in x.units: - x, y, _ = self.griddata[0].metpy.cartopy_crs.transform_points( - ccrs.PlateCarree(), *np.meshgrid(x, y)).T + if "degree" in x.units: + x, y, _ = ( + self.griddata[0] + .metpy.cartopy_crs.transform_points(ccrs.PlateCarree(), *np.meshgrid(x, y)) + .T + ) x = x.T % 360 y = y.T @@ -1251,7 +1295,7 @@ def plotdata(self): def draw(self): """Draw the plot.""" if self._need_redraw: - if getattr(self, 'handle', None) is None: + if getattr(self, "handle", None) is None: self._build() self._need_redraw = False @@ -1267,7 +1311,7 @@ class BarbPlot(PlotVector): This trait corresponds to the keyword length in `matplotlib.pyplot.barbs`. """ - @observe('barblength', 'pivot', 'skip', 'earth_relative', 'color') + @observe("barblength", "pivot", "skip", "earth_relative", "color") def _set_need_rebuild(self, _): """Handle changes to attributes that need to regenerate everything.""" # Because matplotlib doesn't let you just change these properties, we need @@ -1285,10 +1329,15 @@ def _build(self): wind_slice = (slice(None, None, self.skip[0]), slice(None, None, self.skip[1])) self.handle = self.parent.ax.barbs( - x[wind_slice], y[wind_slice], - u.values[wind_slice], v.values[wind_slice], - color=self.color, pivot=self.pivot, length=self.barblength, - transform=transform) + x[wind_slice], + y[wind_slice], + u.values[wind_slice], + v.values[wind_slice], + color=self.color, + pivot=self.pivot, + length=self.barblength, + transform=transform, + ) @exporter.export @@ -1348,7 +1397,7 @@ class PlotObs(HasTraits): List of parameters to be plotted around station plot (e.g., temperature, dewpoint, skyc). """ - locations = List(default_value=['C']) + locations = List(default_value=["C"]) locations.__doc__ = """List of strings for scalar or symbol field plotting locations. List of parameters locations for plotting parameters around the station plot (e.g., @@ -1368,7 +1417,7 @@ class PlotObs(HasTraits): For plotting text, use the format setting of 'text'. """ - colors = List(Unicode(), default_value=['black']) + colors = List(Unicode(), default_value=["black"]) colors.__doc__ = """List of the scalar and symbol field colors. List of strings that represent the colors to be used for the variable being plotted. @@ -1382,7 +1431,7 @@ class PlotObs(HasTraits): (e.g., wind components). (optional) """ - vector_field_color = Unicode('black', allow_none=True) + vector_field_color = Unicode("black", allow_none=True) vector_field_color.__doc__ = """String color name to plot the vector. (optional)""" vector_field_length = Int(default_value=None, allow_none=True) @@ -1413,17 +1462,17 @@ def clear(self): Resets all internal state and sets need for redraw. """ - if getattr(self, 'handle', None) is not None: + if getattr(self, "handle", None) is not None: self.handle.ax.cla() self.handle = None self._need_redraw = True - @observe('parent') + @observe("parent") def _parent_changed(self, _): """Handle setting the parent object for the plot.""" self.clear() - @observe('fields', 'level', 'time', 'vector_field', 'time_window') + @observe("fields", "level", "time", "vector_field", "time_window") def _update_data(self, _=None): """Handle updating the internal cache of data. @@ -1448,22 +1497,22 @@ def data(self, val): @property def name(self): """Generate a name for the plot.""" - ret = '' - ret += ' and '.join(f for f in self.fields) + ret = "" + ret += " and ".join(f for f in self.fields) if self.level is not None: - ret += f'@{self.level:d}' + ret += f"@{self.level:d}" return ret @property def obsdata(self): """Return the internal cached data.""" - if getattr(self, '_obsdata', None) is None: + if getattr(self, "_obsdata", None) is None: # Use a copy of data so we retain all of the original data passed in unmodified data = self.data # Subset for a particular level if given if self.level is not None: - mag = getattr(self.level, 'magnitude', self.level) + mag = getattr(self.level, "magnitude", self.level) data = data[data.pressure == mag] # Subset for our particular time @@ -1471,13 +1520,14 @@ def obsdata(self): # If data are not currently indexed by time, we need to do so choosing one of # the columns we're looking for if not isinstance(data.index, pd.DatetimeIndex): - time_vars = ['valid', 'time', 'valid_time', 'date_time', 'date'] - dim_times = [time_var for time_var in time_vars if - time_var in list(self.data)] + time_vars = ["valid", "time", "valid_time", "date_time", "date"] + dim_times = [ + time_var for time_var in time_vars if time_var in list(self.data) + ] if not dim_times: raise AttributeError( - 'Time variable not found. Valid variable names are:' - f'{time_vars}') + "Time variable not found. Valid variable names are:" f"{time_vars}" + ) data = data.set_index(dim_times[0]) if not isinstance(data.index, pd.DatetimeIndex): @@ -1494,14 +1544,15 @@ def obsdata(self): # error you get if that's not the case really convoluted, which is why # we don't rely on users doing it. data = data.sort_index() - data = data[self.time - window:self.time + window] + data = data[self.time - window : self.time + window] # Look for the station column - stn_vars = ['station', 'stn', 'station_id', 'stid'] + stn_vars = ["station", "stn", "station_id", "stid"] dim_stns = [stn_var for stn_var in stn_vars if stn_var in list(self.data)] if not dim_stns: - raise AttributeError('Station variable not found. Valid variable names are: ' - f'{stn_vars}') + raise AttributeError( + "Station variable not found. Valid variable names are: " f"{stn_vars}" + ) else: dim_stn = dim_stns[0] @@ -1519,9 +1570,9 @@ def plotdata(self): """ plot_data = {} for dim_name in list(self.obsdata): - if dim_name.find('lat') != -1: + if dim_name.find("lat") != -1: lat = self.obsdata[dim_name] - elif dim_name.find('lon') != -1: + elif dim_name.find("lon") != -1: lon = self.obsdata[dim_name] else: plot_data[dim_name] = self.obsdata[dim_name] @@ -1530,11 +1581,11 @@ def plotdata(self): def draw(self): """Draw the plot.""" if self._need_redraw: - if getattr(self, 'handle', None) is None: + if getattr(self, "handle", None) is None: self._build() self._need_redraw = False - @observe('colors', 'formats', 'locations', 'reduce_points', 'vector_field_color') + @observe("colors", "formats", "locations", "reduce_points", "vector_field_color") def _set_need_rebuild(self, _): """Handle changes to attributes that need to regenerate everything.""" # Because matplotlib doesn't let you just change these properties, we need @@ -1548,14 +1599,20 @@ def _build(self): # Use the cartopy map projection to transform station locations to the map and # then refine the number of stations plotted by setting a radius if self.parent._proj_obj == ccrs.PlateCarree(): - scale = 1. + scale = 1.0 else: - scale = 100000. + scale = 100000.0 point_locs = self.parent._proj_obj.transform_points(ccrs.PlateCarree(), lon, lat) subset = reduce_point_density(point_locs, self.reduce_points * scale) - self.handle = StationPlot(self.parent.ax, lon[subset], lat[subset], clip_on=True, - transform=ccrs.PlateCarree(), fontsize=10) + self.handle = StationPlot( + self.parent.ax, + lon[subset], + lat[subset], + clip_on=True, + transform=ccrs.PlateCarree(), + fontsize=10, + ) for i, ob_type in enumerate(self.fields): field_kwargs = {} @@ -1564,51 +1621,54 @@ def _build(self): else: location = self.locations[0] if len(self.colors) > 1: - field_kwargs['color'] = self.colors[i] + field_kwargs["color"] = self.colors[i] else: - field_kwargs['color'] = self.colors[0] + field_kwargs["color"] = self.colors[0] if len(self.formats) > 1: - field_kwargs['formatter'] = self.formats[i] + field_kwargs["formatter"] = self.formats[i] else: - field_kwargs['formatter'] = self.formats[0] + field_kwargs["formatter"] = self.formats[0] if len(self.plot_units) > 1: - field_kwargs['plot_units'] = self.plot_units[i] + field_kwargs["plot_units"] = self.plot_units[i] else: - field_kwargs['plot_units'] = self.plot_units[0] - if hasattr(self.data, 'units') and (field_kwargs['plot_units'] is not None): + field_kwargs["plot_units"] = self.plot_units[0] + if hasattr(self.data, "units") and (field_kwargs["plot_units"] is not None): parameter = data[ob_type][subset].values * units(self.data.units[ob_type]) else: parameter = data[ob_type][subset] - if field_kwargs['formatter'] is not None: - mapper = getattr(wx_symbols, str(field_kwargs['formatter']), None) + if field_kwargs["formatter"] is not None: + mapper = getattr(wx_symbols, str(field_kwargs["formatter"]), None) if mapper is not None: - field_kwargs.pop('formatter') - self.handle.plot_symbol(location, parameter, - mapper, **field_kwargs) + field_kwargs.pop("formatter") + self.handle.plot_symbol(location, parameter, mapper, **field_kwargs) else: - if self.formats[i] == 'text': - self.handle.plot_text(location, data[ob_type][subset], - color=field_kwargs['color']) + if self.formats[i] == "text": + self.handle.plot_text( + location, data[ob_type][subset], color=field_kwargs["color"] + ) else: - self.handle.plot_parameter(location, data[ob_type][subset], - **field_kwargs) + self.handle.plot_parameter( + location, data[ob_type][subset], **field_kwargs + ) else: - field_kwargs.pop('formatter') + field_kwargs.pop("formatter") self.handle.plot_parameter(location, parameter, **field_kwargs) if self.vector_field[0] is not None: vector_kwargs = {} - vector_kwargs['color'] = self.vector_field_color - vector_kwargs['plot_units'] = self.vector_plot_units - if hasattr(self.data, 'units') and (vector_kwargs['plot_units'] is not None): - u = (data[self.vector_field[0]][subset].values - * units(self.data.units[self.vector_field[0]])) - v = (data[self.vector_field[1]][subset].values - * units(self.data.units[self.vector_field[1]])) + vector_kwargs["color"] = self.vector_field_color + vector_kwargs["plot_units"] = self.vector_plot_units + if hasattr(self.data, "units") and (vector_kwargs["plot_units"] is not None): + u = data[self.vector_field[0]][subset].values * units( + self.data.units[self.vector_field[0]] + ) + v = data[self.vector_field[1]][subset].values * units( + self.data.units[self.vector_field[1]] + ) else: - vector_kwargs.pop('plot_units') + vector_kwargs.pop("plot_units") u = data[self.vector_field[0]][subset] v = data[self.vector_field[1]][subset] if self.vector_field_length is not None: - vector_kwargs['length'] = self.vector_field_length + vector_kwargs["length"] = self.vector_field_length self.handle.plot_barb(u, v, **vector_kwargs) diff --git a/src/metpy/plots/mapping.py b/src/metpy/plots/mapping.py index d8cb9e4162e..96dbae57e32 100644 --- a/src/metpy/plots/mapping.py +++ b/src/metpy/plots/mapping.py @@ -16,10 +16,12 @@ class CFProjection: """Handle parsing CF projection metadata.""" # mapping from Cartopy to CF vocabulary - _default_attr_mapping = [('false_easting', 'false_easting'), - ('false_northing', 'false_northing'), - ('central_latitude', 'latitude_of_projection_origin'), - ('central_longitude', 'longitude_of_projection_origin')] + _default_attr_mapping = [ + ("false_easting", "false_easting"), + ("false_northing", "false_northing"), + ("central_latitude", "latitude_of_projection_origin"), + ("central_longitude", "longitude_of_projection_origin"), + ] projection_registry = Registry() @@ -40,24 +42,32 @@ def build_projection_kwargs(cls, source, mapping): @staticmethod def _map_arg_names(source, mapping): """Map one set of keys to another.""" - return {cartopy_name: source[cf_name] for cartopy_name, cf_name in mapping - if cf_name in source} + return { + cartopy_name: source[cf_name] + for cartopy_name, cf_name in mapping + if cf_name in source + } @property def cartopy_globe(self): """Initialize a `cartopy.crs.Globe` from the metadata.""" - if 'earth_radius' in self._attrs: - kwargs = {'ellipse': 'sphere', 'semimajor_axis': self._attrs['earth_radius'], - 'semiminor_axis': self._attrs['earth_radius']} + if "earth_radius" in self._attrs: + kwargs = { + "ellipse": "sphere", + "semimajor_axis": self._attrs["earth_radius"], + "semiminor_axis": self._attrs["earth_radius"], + } else: - attr_mapping = [('semimajor_axis', 'semi_major_axis'), - ('semiminor_axis', 'semi_minor_axis'), - ('inverse_flattening', 'inverse_flattening')] + attr_mapping = [ + ("semimajor_axis", "semi_major_axis"), + ("semiminor_axis", "semi_minor_axis"), + ("inverse_flattening", "inverse_flattening"), + ] kwargs = self._map_arg_names(self._attrs, attr_mapping) # WGS84 with semi_major==semi_minor is NOT the same as spherical Earth # Also need to handle the case where we're not given any spheroid - kwargs['ellipse'] = None if kwargs else 'sphere' + kwargs["ellipse"] = None if kwargs else "sphere" return ccrs.Globe(**kwargs) @@ -69,11 +79,11 @@ def cartopy_geodetic(self): def to_cartopy(self): """Convert to a CartoPy projection.""" globe = self.cartopy_globe - proj_name = self._attrs['grid_mapping_name'] + proj_name = self._attrs["grid_mapping_name"] try: proj_handler = self.projection_registry[proj_name] except KeyError: - raise ValueError(f'Unhandled projection: {proj_name}') from None + raise ValueError(f"Unhandled projection: {proj_name}") from None return proj_handler(self._attrs, globe) @@ -83,7 +93,7 @@ def to_dict(self): def __str__(self): """Get a string representation of the projection.""" - return 'Projection: ' + self._attrs['grid_mapping_name'] + return "Projection: " + self._attrs["grid_mapping_name"] def __getitem__(self, item): """Return a given attribute.""" @@ -98,94 +108,104 @@ def __ne__(self, other): return not self.__eq__(other) -@CFProjection.register('geostationary') +@CFProjection.register("geostationary") def make_geo(attrs_dict, globe): """Handle geostationary projection.""" - attr_mapping = [('satellite_height', 'perspective_point_height'), - ('sweep_axis', 'sweep_angle_axis')] + attr_mapping = [ + ("satellite_height", "perspective_point_height"), + ("sweep_axis", "sweep_angle_axis"), + ] kwargs = CFProjection.build_projection_kwargs(attrs_dict, attr_mapping) # CartoPy can't handle central latitude for Geostationary (nor should it) # Just remove it if it's 0. - if not kwargs.get('central_latitude'): - kwargs.pop('central_latitude', None) + if not kwargs.get("central_latitude"): + kwargs.pop("central_latitude", None) # If sweep_angle_axis is not present, we should look for fixed_angle_axis and adjust - if 'sweep_axis' not in kwargs: - kwargs['sweep_axis'] = 'x' if attrs_dict['fixed_angle_axis'] == 'y' else 'y' + if "sweep_axis" not in kwargs: + kwargs["sweep_axis"] = "x" if attrs_dict["fixed_angle_axis"] == "y" else "y" return ccrs.Geostationary(globe=globe, **kwargs) -@CFProjection.register('lambert_conformal_conic') +@CFProjection.register("lambert_conformal_conic") def make_lcc(attrs_dict, globe): """Handle Lambert conformal conic projection.""" - attr_mapping = [('central_longitude', 'longitude_of_central_meridian'), - ('standard_parallels', 'standard_parallel')] + attr_mapping = [ + ("central_longitude", "longitude_of_central_meridian"), + ("standard_parallels", "standard_parallel"), + ] kwargs = CFProjection.build_projection_kwargs(attrs_dict, attr_mapping) - if 'standard_parallels' in kwargs: + if "standard_parallels" in kwargs: try: - len(kwargs['standard_parallels']) + len(kwargs["standard_parallels"]) except TypeError: - kwargs['standard_parallels'] = [kwargs['standard_parallels']] + kwargs["standard_parallels"] = [kwargs["standard_parallels"]] return ccrs.LambertConformal(globe=globe, **kwargs) -@CFProjection.register('albers_conical_equal_area') +@CFProjection.register("albers_conical_equal_area") def make_aea(attrs_dict, globe): """Handle Albers Equal Area.""" - attr_mapping = [('central_longitude', 'longitude_of_central_meridian'), - ('standard_parallels', 'standard_parallel')] + attr_mapping = [ + ("central_longitude", "longitude_of_central_meridian"), + ("standard_parallels", "standard_parallel"), + ] kwargs = CFProjection.build_projection_kwargs(attrs_dict, attr_mapping) - if 'standard_parallels' in kwargs: + if "standard_parallels" in kwargs: try: - len(kwargs['standard_parallels']) + len(kwargs["standard_parallels"]) except TypeError: - kwargs['standard_parallels'] = [kwargs['standard_parallels']] + kwargs["standard_parallels"] = [kwargs["standard_parallels"]] return ccrs.AlbersEqualArea(globe=globe, **kwargs) -@CFProjection.register('latitude_longitude') +@CFProjection.register("latitude_longitude") def make_latlon(attrs_dict, globe): """Handle plain latitude/longitude mapping.""" # TODO: Really need to use Geodetic to pass the proper globe return ccrs.PlateCarree() -@CFProjection.register('mercator') +@CFProjection.register("mercator") def make_mercator(attrs_dict, globe): """Handle Mercator projection.""" - attr_mapping = [('latitude_true_scale', 'standard_parallel'), - ('scale_factor', 'scale_factor_at_projection_origin')] + attr_mapping = [ + ("latitude_true_scale", "standard_parallel"), + ("scale_factor", "scale_factor_at_projection_origin"), + ] kwargs = CFProjection.build_projection_kwargs(attrs_dict, attr_mapping) # Work around the fact that in CartoPy <= 0.16 can't handle the easting/northing # or central_latitude in Mercator - if not kwargs.get('false_easting'): - kwargs.pop('false_easting', None) - if not kwargs.get('false_northing'): - kwargs.pop('false_northing', None) - if not kwargs.get('central_latitude'): - kwargs.pop('central_latitude', None) + if not kwargs.get("false_easting"): + kwargs.pop("false_easting", None) + if not kwargs.get("false_northing"): + kwargs.pop("false_northing", None) + if not kwargs.get("central_latitude"): + kwargs.pop("central_latitude", None) return ccrs.Mercator(globe=globe, **kwargs) -@CFProjection.register('stereographic') +@CFProjection.register("stereographic") def make_stereo(attrs_dict, globe): """Handle generic stereographic projection.""" - attr_mapping = [('scale_factor', 'scale_factor_at_projection_origin')] + attr_mapping = [("scale_factor", "scale_factor_at_projection_origin")] kwargs = CFProjection.build_projection_kwargs(attrs_dict, attr_mapping) return ccrs.Stereographic(globe=globe, **kwargs) -@CFProjection.register('polar_stereographic') +@CFProjection.register("polar_stereographic") def make_polar_stereo(attrs_dict, globe): """Handle polar stereographic projection.""" - attr_mapping = [('central_longitude', 'straight_vertical_longitude_from_pole'), - ('true_scale_latitude', 'standard_parallel'), - ('scale_factor', 'scale_factor_at_projection_origin')] + attr_mapping = [ + ("central_longitude", "straight_vertical_longitude_from_pole"), + ("true_scale_latitude", "standard_parallel"), + ("scale_factor", "scale_factor_at_projection_origin"), + ] kwargs = CFProjection.build_projection_kwargs(attrs_dict, attr_mapping) return ccrs.Stereographic(globe=globe, **kwargs) diff --git a/src/metpy/plots/skewt.py b/src/metpy/plots/skewt.py index 91e01f62c30..e6bc35f7147 100644 --- a/src/metpy/plots/skewt.py +++ b/src/metpy/plots/skewt.py @@ -22,12 +22,12 @@ import matplotlib.transforms as transforms import numpy as np -from ._util import colored_line from ..calc import dewpoint, dry_lapse, el, lcl, moist_lapse, vapor_pressure from ..calc.tools import _delete_masked_points from ..interpolate import interpolate_1d from ..package_tools import Exporter from ..units import concatenate, units +from ._util import colored_line exporter = Exporter(globals()) @@ -62,9 +62,13 @@ def get_matrix(self): # x0, y0 = self._bbox.xmin, self._bbox.ymin # self.translate(-x0, -y0).skew_deg(self._rot, 0).translate(x0, y0) # Setting it this way is just more efficient. - self._mtx = np.array([[1.0, self._rot_factor, -self._rot_factor * self._bbox.ymin], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0]]) + self._mtx = np.array( + [ + [1.0, self._rot_factor, -self._rot_factor * self._bbox.ymin], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + ) # Need to clear both the invalid flag *and* reset the inverse, which is cached # by the parent class. @@ -90,8 +94,13 @@ def draw(self, renderer): # restore these states (`set_visible`) at the end of the block (after # the draw). with ExitStack() as stack: - for artist in [self.gridline, self.tick1line, self.tick2line, - self.label1, self.label2]: + for artist in [ + self.gridline, + self.tick1line, + self.tick2line, + self.label1, + self.label2, + ]: stack.callback(artist.set_visible, artist.get_visible()) self.tick1line.set_visible(self.tick1line.get_visible() and self.lower_in_bounds) @@ -114,8 +123,9 @@ def upper_in_bounds(self): @property def grid_in_bounds(self): """Whether any of the tick grid line is in bounds.""" - return transforms.interval_contains(self.axes.xaxis.get_view_interval(), - self.get_loc()) + return transforms.interval_contains( + self.axes.xaxis.get_view_interval(), self.get_loc() + ) class SkewXAxis(maxis.XAxis): @@ -129,17 +139,27 @@ class SkewXAxis(maxis.XAxis): def _get_tick(self, major): # Warning stuff can go away when we only support Matplotlib >=3.3 with warnings.catch_warnings(): - warnings.simplefilter('ignore', getattr( - matplotlib, 'MatplotlibDeprecationWarning', DeprecationWarning)) + warnings.simplefilter( + "ignore", + getattr(matplotlib, "MatplotlibDeprecationWarning", DeprecationWarning), + ) return SkewXTick(self.axes, None, label=None, major=major) # Needed to properly handle tight bbox def _get_tick_bboxes(self, ticks, renderer): """Return lists of bboxes for ticks' label1's and label2's.""" - return ([tick.label1.get_window_extent(renderer) - for tick in ticks if tick.label1.get_visible() and tick.lower_in_bounds], - [tick.label2.get_window_extent(renderer) - for tick in ticks if tick.label2.get_visible() and tick.upper_in_bounds]) + return ( + [ + tick.label1.get_window_extent(renderer) + for tick in ticks + if tick.label1.get_visible() and tick.lower_in_bounds + ], + [ + tick.label2.get_window_extent(renderer) + for tick in ticks + if tick.label2.get_visible() and tick.upper_in_bounds + ], + ) def get_view_interval(self): """Get the view interval.""" @@ -155,7 +175,7 @@ class SkewSpine(mspines.Spine): def _adjust_location(self): pts = self._path.vertices - if self.spine_type == 'top': + if self.spine_type == "top": pts[:, 0] = self.axes.upper_xlim else: pts[:, 0] = self.axes.lower_xlim @@ -173,7 +193,7 @@ class SkewXAxes(Axes): # The projection must specify a name. This will be used be the # user to select the projection, i.e. ``subplot(111, # projection='skewx')``. - name = 'skewx' + name = "skewx" def __init__(self, *args, **kwargs): r"""Initialize `SkewXAxes`. @@ -191,24 +211,26 @@ def __init__(self, *args, **kwargs): """ # This needs to be popped and set before moving on - self.rot = kwargs.pop('rotation', 30) + self.rot = kwargs.pop("rotation", 30) super().__init__(*args, **kwargs) def _init_axis(self): # Taken from Axes and modified to use our modified X-axis self.xaxis = SkewXAxis(self) - self.spines['top'].register_axis(self.xaxis) - self.spines['bottom'].register_axis(self.xaxis) + self.spines["top"].register_axis(self.xaxis) + self.spines["bottom"].register_axis(self.xaxis) self.yaxis = maxis.YAxis(self) - self.spines['left'].register_axis(self.yaxis) - self.spines['right'].register_axis(self.yaxis) + self.spines["left"].register_axis(self.yaxis) + self.spines["right"].register_axis(self.yaxis) - def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'): + def _gen_axes_spines(self, locations=None, offset=0.0, units="inches"): # pylint: disable=unused-argument - spines = {'top': SkewSpine.linear_spine(self, 'top'), - 'bottom': mspines.Spine.linear_spine(self, 'bottom'), - 'left': mspines.Spine.linear_spine(self, 'left'), - 'right': mspines.Spine.linear_spine(self, 'right')} + spines = { + "top": SkewSpine.linear_spine(self, "top"), + "bottom": mspines.Spine.linear_spine(self, "bottom"), + "left": mspines.Spine.linear_spine(self, "left"), + "right": mspines.Spine.linear_spine(self, "right"), + } return spines def _set_lim_and_transforms(self): @@ -239,8 +261,9 @@ def lower_xlim(self): @property def upper_xlim(self): """Get the data limits for the x-axis along the top of the axes.""" - return self.transData.inverted().transform([[self.bbox.xmin, self.bbox.ymax], - self.bbox.max])[:, 0] + return self.transData.inverted().transform( + [[self.bbox.xmin, self.bbox.ymax], self.bbox.max] + )[:, 0] # Now register the projection with matplotlib so the user can select it. @@ -293,7 +316,8 @@ def __init__(self, fig=None, rotation=30, subplot=None, rect=None, aspect=80.5): """ if fig is None: import matplotlib.pyplot as plt - figsize = plt.rcParams.get('figure.figsize', (7, 7)) + + figsize = plt.rcParams.get("figure.figsize", (7, 7)) fig = plt.figure(figsize=figsize) self._fig = fig @@ -301,7 +325,7 @@ def __init__(self, fig=None, rotation=30, subplot=None, rect=None, aspect=80.5): raise ValueError("Specify only one of `rect' and `subplot', but not both") elif rect: - self.ax = fig.add_axes(rect, projection='skewx', rotation=rotation) + self.ax = fig.add_axes(rect, projection="skewx", rotation=rotation) else: if subplot is not None: @@ -313,10 +337,10 @@ def __init__(self, fig=None, rotation=30, subplot=None, rect=None, aspect=80.5): else: subplot = (1, 1, 1) - self.ax = fig.add_subplot(*subplot, projection='skewx', rotation=rotation) + self.ax = fig.add_subplot(*subplot, projection="skewx", rotation=rotation) # Set the yaxis as inverted with log scaling - self.ax.set_yscale('log') + self.ax.set_yscale("log") # Override default ticking for log scaling self.ax.yaxis.set_major_formatter(ScalarFormatter()) @@ -339,8 +363,8 @@ def __init__(self, fig=None, rotation=30, subplot=None, rect=None, aspect=80.5): self.moist_adiabats = None # Maintain a reasonable ratio of data limits. Only works on Matplotlib >= 3.2 - if matplotlib.__version__[:3] > '3.1': - self.ax.set_aspect(aspect, adjustable='box') + if matplotlib.__version__[:3] > "3.1": + self.ax.set_aspect(aspect, adjustable="box") def plot(self, pressure, t, *args, **kwargs): r"""Plot data. @@ -373,8 +397,9 @@ def plot(self, pressure, t, *args, **kwargs): t, pressure = _delete_masked_points(t, pressure) return self.ax.plot(t, pressure, *args, **kwargs) - def plot_barbs(self, pressure, u, v, c=None, xloc=1.0, x_clip_radius=0.1, - y_clip_radius=0.08, **kwargs): + def plot_barbs( + self, pressure, u, v, c=None, xloc=1.0, x_clip_radius=0.1, y_clip_radius=0.08, **kwargs + ): r"""Plot wind barbs. Adds wind barbs to the skew-T plot. This is a wrapper around the @@ -416,14 +441,16 @@ def plot_barbs(self, pressure, u, v, c=None, xloc=1.0, x_clip_radius=0.1, """ # If plot_units specified, convert the data to those units - plotting_units = kwargs.pop('plot_units', None) + plotting_units = kwargs.pop("plot_units", None) if plotting_units: - if hasattr(u, 'units') and hasattr(v, 'units'): + if hasattr(u, "units") and hasattr(v, "units"): u = u.to(plotting_units) v = v.to(plotting_units) else: - raise ValueError('To convert to plotting units, units must be attached to ' - 'u and v wind components.') + raise ValueError( + "To convert to plotting units, units must be attached to " + "u and v wind components." + ) # Assemble array of x-locations in axes space x = np.empty_like(pressure) @@ -431,18 +458,37 @@ def plot_barbs(self, pressure, u, v, c=None, xloc=1.0, x_clip_radius=0.1, # Do barbs plot at this location if c is not None: - b = self.ax.barbs(x, pressure, u, v, c, - transform=self.ax.get_yaxis_transform(which='tick2'), - clip_on=True, zorder=2, **kwargs) + b = self.ax.barbs( + x, + pressure, + u, + v, + c, + transform=self.ax.get_yaxis_transform(which="tick2"), + clip_on=True, + zorder=2, + **kwargs, + ) else: - b = self.ax.barbs(x, pressure, u, v, - transform=self.ax.get_yaxis_transform(which='tick2'), - clip_on=True, zorder=2, **kwargs) + b = self.ax.barbs( + x, + pressure, + u, + v, + transform=self.ax.get_yaxis_transform(which="tick2"), + clip_on=True, + zorder=2, + **kwargs, + ) # Override the default clip box, which is the axes rectangle, so we can have # barbs that extend outside. - ax_bbox = transforms.Bbox([[xloc - x_clip_radius, -y_clip_radius], - [xloc + x_clip_radius, 1.0 + y_clip_radius]]) + ax_bbox = transforms.Bbox( + [ + [xloc - x_clip_radius, -y_clip_radius], + [xloc + x_clip_radius, 1.0 + y_clip_radius], + ] + ) b.set_clip_box(transforms.TransformedBbox(ax_bbox, self.ax.transAxes)) return b @@ -492,13 +538,13 @@ def plot_dry_adiabats(self, t0=None, pressure=None, **kwargs): pressure = np.linspace(*self.ax.get_ylim()) * units.mbar # Assemble into data for plotting - t = dry_lapse(pressure, t0[:, np.newaxis], 1000. * units.mbar).to(units.degC) + t = dry_lapse(pressure, t0[:, np.newaxis], 1000.0 * units.mbar).to(units.degC) linedata = [np.vstack((ti.m, pressure.m)).T for ti in t] # Add to plot - kwargs.setdefault('colors', 'r') - kwargs.setdefault('linestyles', 'dashed') - kwargs.setdefault('alpha', 0.5) + kwargs.setdefault("colors", "r") + kwargs.setdefault("linestyles", "dashed") + kwargs.setdefault("alpha", 0.5) self.dry_adiabats = self.ax.add_collection(LineCollection(linedata, **kwargs)) return self.dry_adiabats @@ -542,21 +588,23 @@ def plot_moist_adiabats(self, t0=None, pressure=None, **kwargs): # Determine set of starting temps if necessary if t0 is None: xmin, xmax = self.ax.get_xlim() - t0 = np.concatenate((np.arange(xmin, 0, 10), - np.arange(0, xmax + 1, 5))) * units.degC + t0 = ( + np.concatenate((np.arange(xmin, 0, 10), np.arange(0, xmax + 1, 5))) + * units.degC + ) # Get pressure levels based on ylims if necessary if pressure is None: pressure = np.linspace(*self.ax.get_ylim()) * units.mbar # Assemble into data for plotting - t = moist_lapse(pressure, t0[:, np.newaxis], 1000. * units.mbar).to(units.degC) + t = moist_lapse(pressure, t0[:, np.newaxis], 1000.0 * units.mbar).to(units.degC) linedata = [np.vstack((ti.m, pressure.m)).T for ti in t] # Add to plot - kwargs.setdefault('colors', 'b') - kwargs.setdefault('linestyles', 'dashed') - kwargs.setdefault('alpha', 0.5) + kwargs.setdefault("colors", "b") + kwargs.setdefault("linestyles", "dashed") + kwargs.setdefault("alpha", 0.5) self.moist_adiabats = self.ax.add_collection(LineCollection(linedata, **kwargs)) return self.moist_adiabats @@ -595,8 +643,9 @@ def plot_mixing_lines(self, mixing_ratio=None, pressure=None, **kwargs): # Default mixing level values if necessary if mixing_ratio is None: - mixing_ratio = np.array([0.0004, 0.001, 0.002, 0.004, 0.007, 0.01, - 0.016, 0.024, 0.032]).reshape(-1, 1) + mixing_ratio = np.array( + [0.0004, 0.001, 0.002, 0.004, 0.007, 0.01, 0.016, 0.024, 0.032] + ).reshape(-1, 1) # Set pressure range if necessary if pressure is None: @@ -607,13 +656,13 @@ def plot_mixing_lines(self, mixing_ratio=None, pressure=None, **kwargs): linedata = [np.vstack((t.m, pressure.m)).T for t in td] # Add to plot - kwargs.setdefault('colors', 'g') - kwargs.setdefault('linestyles', 'dashed') - kwargs.setdefault('alpha', 0.8) + kwargs.setdefault("colors", "g") + kwargs.setdefault("linestyles", "dashed") + kwargs.setdefault("alpha", 0.8) self.mixing_lines = self.ax.add_collection(LineCollection(linedata, **kwargs)) return self.mixing_lines - def shade_area(self, y, x1, x2=0, which='both', **kwargs): + def shade_area(self, y, x1, x2=0, which="both", **kwargs): r"""Shade area between two curves. Shades areas between curves. Area can be where one is greater or less than the other @@ -643,26 +692,25 @@ def shade_area(self, y, x1, x2=0, which='both', **kwargs): :func:`matplotlib.axes.Axes.fill_betweenx` """ - fill_properties = {'positive': - {'facecolor': 'tab:red', 'alpha': 0.4, 'where': x1 > x2}, - 'negative': - {'facecolor': 'tab:blue', 'alpha': 0.4, 'where': x1 < x2}, - 'both': - {'facecolor': 'tab:green', 'alpha': 0.4, 'where': None}} + fill_properties = { + "positive": {"facecolor": "tab:red", "alpha": 0.4, "where": x1 > x2}, + "negative": {"facecolor": "tab:blue", "alpha": 0.4, "where": x1 < x2}, + "both": {"facecolor": "tab:green", "alpha": 0.4, "where": None}, + } try: fill_args = fill_properties[which] fill_args.update(kwargs) except KeyError: - raise ValueError(f'Unknown option for which: {which}') from None + raise ValueError(f"Unknown option for which: {which}") from None arrs = y, x1, x2 - if fill_args['where'] is not None: - arrs = arrs + (fill_args['where'],) - fill_args.pop('where', None) + if fill_args["where"] is not None: + arrs = arrs + (fill_args["where"],) + fill_args.pop("where", None) - fill_args['interpolate'] = True + fill_args["interpolate"] = True arrs = _delete_masked_points(*arrs) @@ -699,7 +747,7 @@ def shade_cape(self, pressure, t, t_parcel, **kwargs): :func:`matplotlib.axes.Axes.fill_betweenx` """ - return self.shade_area(pressure, t_parcel, t, which='positive', **kwargs) + return self.shade_area(pressure, t_parcel, t, which="positive", **kwargs) def shade_cin(self, pressure, t, t_parcel, dewpoint=None, **kwargs): r"""Shade areas of Convective INhibition (CIN). @@ -737,8 +785,9 @@ def shade_cin(self, pressure, t, t_parcel, dewpoint=None, **kwargs): idx = np.logical_and(pressure > el_p, pressure < lcl_p) else: idx = np.arange(0, len(pressure)) - return self.shade_area(pressure[idx], t_parcel[idx], t[idx], which='negative', - **kwargs) + return self.shade_area( + pressure[idx], t_parcel[idx], t[idx], which="negative", **kwargs + ) @exporter.export @@ -772,17 +821,18 @@ def __init__(self, ax=None, component_range=80): """ if ax is None: import matplotlib.pyplot as plt + self.ax = plt.figure().add_subplot(1, 1, 1) else: self.ax = ax - self.ax.set_aspect('equal', 'box') + self.ax.set_aspect("equal", "box") self.ax.set_xlim(-component_range, component_range) self.ax.set_ylim(-component_range, component_range) # == sqrt(2) * max_range, which is the distance at the corner self.max_range = 1.4142135 * component_range - def add_grid(self, increment=10., **kwargs): + def add_grid(self, increment=10.0, **kwargs): r"""Add grid lines to hodograph. Creates lines for the x- and y-axes, as well as circles denoting wind speed values. @@ -803,15 +853,15 @@ def add_grid(self, increment=10., **kwargs): """ # Some default arguments. Take those, and update with any # arguments passed in - grid_args = {'color': 'grey', 'linestyle': 'dashed'} + grid_args = {"color": "grey", "linestyle": "dashed"} if kwargs: grid_args.update(kwargs) # Take those args and make appropriate for a Circle circle_args = grid_args.copy() - color = circle_args.pop('color', None) - circle_args['edgecolor'] = color - circle_args['fill'] = False + color = circle_args.pop("color", None) + circle_args["edgecolor"] = color + circle_args["fill"] = False self.rings = [] for r in np.arange(increment, self.max_range, increment): @@ -826,7 +876,7 @@ def add_grid(self, increment=10., **kwargs): @staticmethod def _form_line_args(kwargs): """Simplify taking the default line style and extending with kwargs.""" - def_args = {'linewidth': 3} + def_args = {"linewidth": 3} def_args.update(kwargs) return def_args @@ -878,11 +928,10 @@ def wind_vectors(self, u, v, **kwargs): arrows plotted """ - quiver_args = {'units': 'xy', 'scale': 1} + quiver_args = {"units": "xy", "scale": 1} quiver_args.update(**kwargs) center_position = np.zeros_like(u) - return self.ax.quiver(center_position, center_position, - u, v, **quiver_args) + return self.ax.quiver(center_position, center_position, u, v, **quiver_args) def plot_colormapped(self, u, v, c, intervals=None, colors=None, **kwargs): r"""Plot u, v data, with line colored based on a third set of data. @@ -927,15 +976,17 @@ def plot_colormapped(self, u, v, c, intervals=None, colors=None, **kwargs): if colors: cmap = mcolors.ListedColormap(colors) # If we are segmenting by height (a length), interpolate the contour intervals - if intervals.dimensionality == {'[length]': 1.0}: + if intervals.dimensionality == {"[length]": 1.0}: # Find any intervals not in the data and interpolate them interpolation_heights = [bound.m for bound in intervals if bound not in c] interpolation_heights = np.array(interpolation_heights) * intervals.units - interpolation_heights = (np.sort(interpolation_heights.magnitude) - * interpolation_heights.units) - (interpolated_heights, interpolated_u, - interpolated_v) = interpolate_1d(interpolation_heights, c, c, u, v) + interpolation_heights = ( + np.sort(interpolation_heights.magnitude) * interpolation_heights.units + ) + (interpolated_heights, interpolated_u, interpolated_v) = interpolate_1d( + interpolation_heights, c, c, u, v + ) # Combine the interpolated data with the actual data c = concatenate([c, interpolated_heights]) @@ -955,10 +1006,10 @@ def plot_colormapped(self, u, v, c, intervals=None, colors=None, **kwargs): intervals = np.asarray(intervals) * intervals.units norm = mcolors.BoundaryNorm(intervals.magnitude, cmap.N) - cmap.set_over('none') - cmap.set_under('none') - kwargs['cmap'] = cmap - kwargs['norm'] = norm + cmap.set_over("none") + cmap.set_under("none") + kwargs["cmap"] = cmap + kwargs["norm"] = norm line_args = self._form_line_args(kwargs) # Plotting a continuously colored line diff --git a/src/metpy/plots/station_plot.py b/src/metpy/plots/station_plot.py index 70e83fa4c77..d2f24104c45 100644 --- a/src/metpy/plots/station_plot.py +++ b/src/metpy/plots/station_plot.py @@ -7,9 +7,16 @@ import numpy as np -from .wx_symbols import (current_weather, high_clouds, low_clouds, mid_clouds, - pressure_tendency, sky_cover, wx_symbol_font) from ..package_tools import Exporter +from .wx_symbols import ( + current_weather, + high_clouds, + low_clouds, + mid_clouds, + pressure_tendency, + sky_cover, + wx_symbol_font, +) exporter = Exporter(globals()) @@ -22,11 +29,29 @@ class StationPlot: barbs as the center of the location. """ - location_names = {'C': (0, 0), 'N': (0, 1), 'NE': (1, 1), 'E': (1, 0), 'SE': (1, -1), - 'S': (0, -1), 'SW': (-1, -1), 'W': (-1, 0), 'NW': (-1, 1), - 'N2': (0, 2), 'NNE': (1, 2), 'ENE': (2, 1), 'E2': (2, 0), - 'ESE': (2, -1), 'SSE': (1, -2), 'S2': (0, -2), 'SSW': (-1, -2), - 'WSW': (-2, -1), 'W2': (-2, 0), 'WNW': (-2, 1), 'NNW': (-1, 2)} + location_names = { + "C": (0, 0), + "N": (0, 1), + "NE": (1, 1), + "E": (1, 0), + "SE": (1, -1), + "S": (0, -1), + "SW": (-1, -1), + "W": (-1, 0), + "NW": (-1, 1), + "N2": (0, 2), + "NNE": (1, 2), + "ENE": (2, 1), + "E2": (2, 0), + "ESE": (2, -1), + "SSE": (1, -2), + "S2": (0, -2), + "SSW": (-1, -2), + "WSW": (-2, -1), + "W2": (-2, 0), + "WNW": (-2, 1), + "NNW": (-1, 2), + } def __init__(self, ax, x, y, fontsize=10, spacing=None, transform=None, **kwargs): """Initialize the StationPlot with items that do not change. @@ -158,10 +183,10 @@ def plot_symbols(mapper, name, nwrap=12, figsize=(10, 1.4)): """ # Make sure we use our font for symbols - kwargs['fontproperties'] = wx_symbol_font.copy() + kwargs["fontproperties"] = wx_symbol_font.copy() return self.plot_parameter(location, codes, symbol_mapper, **kwargs) - def plot_parameter(self, location, parameter, formatter='.0f', **kwargs): + def plot_parameter(self, location, parameter, formatter=".0f", **kwargs): """At the specified location in the station model plot a set of values. This specifies that at the offset `location`, the data in `parameter` should be @@ -197,9 +222,9 @@ def plot_parameter(self, location, parameter, formatter='.0f', **kwargs): """ # If plot_units specified, convert the data to those units - plotting_units = kwargs.pop('plot_units', None) + plotting_units = kwargs.pop("plot_units", None) parameter = self._scalar_plotting_units(parameter, plotting_units) - if hasattr(parameter, 'units'): + if hasattr(parameter, "units"): parameter = parameter.magnitude text = self._to_string_list(parameter, formatter) return self.plot_text(location, text, **kwargs) @@ -235,9 +260,14 @@ def plot_text(self, location, text, **kwargs): location = self._handle_location(location) kwargs = self._make_kwargs(kwargs) - text_collection = self.ax.scattertext(self.x, self.y, text, loc=location, - size=kwargs.pop('fontsize', self.fontsize), - **kwargs) + text_collection = self.ax.scattertext( + self.x, + self.y, + text, + loc=location, + size=kwargs.pop("fontsize", self.fontsize), + **kwargs + ) if location in self.items: self.items[location].remove() self.items[location] = text_collection @@ -270,14 +300,17 @@ def plot_barb(self, u, v, **kwargs): kwargs = self._make_kwargs(kwargs) # If plot_units specified, convert the data to those units - plotting_units = kwargs.pop('plot_units', None) + plotting_units = kwargs.pop("plot_units", None) u, v = self._vector_plotting_units(u, v, plotting_units) # Empirically determined pivot = 0.51 * np.sqrt(self.fontsize) length = 1.95 * np.sqrt(self.fontsize) - defaults = {'sizes': {'spacing': .15, 'height': 0.5, 'emptybarb': 0.35}, - 'length': length, 'pivot': pivot} + defaults = { + "sizes": {"spacing": 0.15, "height": 0.5, "emptybarb": 0.35}, + "length": length, + "pivot": pivot, + } defaults.update(kwargs) # Remove old barbs @@ -313,10 +346,10 @@ def plot_arrow(self, u, v, **kwargs): kwargs = self._make_kwargs(kwargs) # If plot_units specified, convert the data to those units - plotting_units = kwargs.pop('plot_units', None) + plotting_units = kwargs.pop("plot_units", None) u, v = self._vector_plotting_units(u, v, plotting_units) - defaults = {'pivot': 'tail', 'scale': 20, 'scale_units': 'inches', 'width': 0.002} + defaults = {"pivot": "tail", "scale": 20, "scale_units": "inches", "width": 0.002} defaults.update(kwargs) # Remove old arrows @@ -329,12 +362,14 @@ def plot_arrow(self, u, v, **kwargs): def _vector_plotting_units(u, v, plotting_units): """Handle conversion to plotting units for barbs and arrows.""" if plotting_units: - if hasattr(u, 'units') and hasattr(v, 'units'): + if hasattr(u, "units") and hasattr(v, "units"): u = u.to(plotting_units) v = v.to(plotting_units) else: - raise ValueError('To convert to plotting units, units must be attached to ' - 'u and v wind components.') + raise ValueError( + "To convert to plotting units, units must be attached to " + "u and v wind components." + ) # Strip units, CartoPy transform doesn't like u = np.array(u) @@ -345,11 +380,13 @@ def _vector_plotting_units(u, v, plotting_units): def _scalar_plotting_units(scalar_value, plotting_units): """Handle conversion to plotting units for barbs and arrows.""" if plotting_units: - if hasattr(scalar_value, 'units'): + if hasattr(scalar_value, "units"): scalar_value = scalar_value.to(plotting_units) else: - raise ValueError('To convert to plotting units, units must be attached to ' - 'scalar value being converted.') + raise ValueError( + "To convert to plotting units, units must be attached to " + "scalar value being converted." + ) return scalar_value def _make_kwargs(self, kwargs): @@ -362,8 +399,8 @@ def _make_kwargs(self, kwargs): all_kw.update(kwargs) # Pass transform if necessary - if 'transform' not in all_kw and self.transform: - all_kw['transform'] = self.transform + if "transform" not in all_kw and self.transform: + all_kw["transform"] = self.transform return all_kw @@ -371,13 +408,15 @@ def _make_kwargs(self, kwargs): def _to_string_list(vals, fmt): """Convert a sequence of values to a list of strings.""" if not callable(fmt): + def formatter(s): """Turn a format string into a callable.""" return format(s, fmt) + else: formatter = fmt - return [formatter(v) if np.isfinite(v) else '' for v in vals] + return [formatter(v) if np.isfinite(v) else "" for v in vals] def _handle_location(self, location): """Process locations to get a consistent set of tuples for location.""" @@ -412,7 +451,7 @@ class PlotTypes(Enum): text = 3 barb = 4 - def add_value(self, location, name, fmt='.0f', units=None, **kwargs): + def add_value(self, location, name, fmt=".0f", units=None, **kwargs): r"""Add a numeric value to the station layout. This specifies that at the offset `location`, data should be pulled from the data @@ -545,7 +584,7 @@ def add_barb(self, u_name, v_name, units=None, **kwargs): """ # Not sure if putting the v_name as a plot-specific option is appropriate, # but it seems simpler than making name code in plot handle tuples - self['barb'] = (self.PlotTypes.barb, (u_name, v_name), (units, kwargs)) + self["barb"] = (self.PlotTypes.barb, (u_name, v_name), (units, kwargs)) def names(self): """Get the list of names used by the layout. @@ -581,6 +620,7 @@ def plot(self, plotter, data_dict): will be used to fill out the station plot. """ + def coerce_data(dat, u): try: return dat.to(u).magnitude @@ -598,8 +638,9 @@ def coerce_data(dat, u): # Plot if we have the data if not (v_data is None or u_data is None): units, kwargs = args - plotter.plot_barb(coerce_data(u_data, units), coerce_data(v_data, units), - **kwargs) + plotter.plot_barb( + coerce_data(u_data, units), coerce_data(v_data, units), **kwargs + ) else: # Check that we have the data for this location data = data_dict.get(name) @@ -616,41 +657,57 @@ def coerce_data(dat, u): def __repr__(self): """Return string representation of layout.""" - return ('{' - + ', '.join('{0}: ({1[0].name}, {1[1]}, ...)'.format(loc, info) - for loc, info in sorted(self.items())) - + '}') + return ( + "{" + + ", ".join( + "{0}: ({1[0].name}, {1[1]}, ...)".format(loc, info) + for loc, info in sorted(self.items()) + ) + + "}" + ) with exporter: #: :desc: Simple station plot layout simple_layout = StationPlotLayout() - simple_layout.add_barb('eastward_wind', 'northward_wind', 'knots') - simple_layout.add_value('NW', 'air_temperature', units='degC') - simple_layout.add_value('SW', 'dew_point_temperature', units='degC') - simple_layout.add_value('NE', 'air_pressure_at_sea_level', units='mbar', - fmt=lambda v: format(10 * v, '03.0f')[-3:]) - simple_layout.add_symbol('C', 'cloud_coverage', sky_cover) - simple_layout.add_symbol('W', 'present_weather', current_weather) + simple_layout.add_barb("eastward_wind", "northward_wind", "knots") + simple_layout.add_value("NW", "air_temperature", units="degC") + simple_layout.add_value("SW", "dew_point_temperature", units="degC") + simple_layout.add_value( + "NE", + "air_pressure_at_sea_level", + units="mbar", + fmt=lambda v: format(10 * v, "03.0f")[-3:], + ) + simple_layout.add_symbol("C", "cloud_coverage", sky_cover) + simple_layout.add_symbol("W", "present_weather", current_weather) #: Full NWS station plot `layout`__ #: #: __ http://oceanservice.noaa.gov/education/yos/resource/JetStream/synoptic/wxmaps.htm nws_layout = StationPlotLayout() - nws_layout.add_value((-1, 1), 'air_temperature', units='degF') - nws_layout.add_symbol((0, 2), 'high_cloud_type', high_clouds) - nws_layout.add_symbol((0, 1), 'medium_cloud_type', mid_clouds) - nws_layout.add_symbol((0, -1), 'low_cloud_type', low_clouds) - nws_layout.add_value((1, 1), 'air_pressure_at_sea_level', units='mbar', - fmt=lambda v: format(10 * v, '03.0f')[-3:]) - nws_layout.add_value((-2, 0), 'visibility_in_air', fmt='.0f', units='miles') - nws_layout.add_symbol((-1, 0), 'present_weather', current_weather) - nws_layout.add_symbol((0, 0), 'cloud_coverage', sky_cover) - nws_layout.add_value((1, 0), 'tendency_of_air_pressure', units='mbar', - fmt=lambda v: ('-' if v < 0 else '') + format(10 * abs(v), '02.0f')) - nws_layout.add_symbol((2, 0), 'tendency_of_air_pressure_symbol', pressure_tendency) - nws_layout.add_barb('eastward_wind', 'northward_wind', units='knots') - nws_layout.add_value((-1, -1), 'dew_point_temperature', units='degF') + nws_layout.add_value((-1, 1), "air_temperature", units="degF") + nws_layout.add_symbol((0, 2), "high_cloud_type", high_clouds) + nws_layout.add_symbol((0, 1), "medium_cloud_type", mid_clouds) + nws_layout.add_symbol((0, -1), "low_cloud_type", low_clouds) + nws_layout.add_value( + (1, 1), + "air_pressure_at_sea_level", + units="mbar", + fmt=lambda v: format(10 * v, "03.0f")[-3:], + ) + nws_layout.add_value((-2, 0), "visibility_in_air", fmt=".0f", units="miles") + nws_layout.add_symbol((-1, 0), "present_weather", current_weather) + nws_layout.add_symbol((0, 0), "cloud_coverage", sky_cover) + nws_layout.add_value( + (1, 0), + "tendency_of_air_pressure", + units="mbar", + fmt=lambda v: ("-" if v < 0 else "") + format(10 * abs(v), "02.0f"), + ) + nws_layout.add_symbol((2, 0), "tendency_of_air_pressure_symbol", pressure_tendency) + nws_layout.add_barb("eastward_wind", "northward_wind", units="knots") + nws_layout.add_value((-1, -1), "dew_point_temperature", units="degF") # TODO: Fix once we have the past weather symbols converted - nws_layout.add_symbol((1, -1), 'past_weather', current_weather) + nws_layout.add_symbol((1, -1), "past_weather", current_weather) diff --git a/src/metpy/plots/wx_symbols.py b/src/metpy/plots/wx_symbols.py index 99f9e981049..9c391536284 100644 --- a/src/metpy/plots/wx_symbols.py +++ b/src/metpy/plots/wx_symbols.py @@ -7,11 +7,13 @@ """ try: - from importlib.resources import (files as importlib_resources_files, - as_file as importlib_resources_as_file) + from importlib.resources import as_file as importlib_resources_as_file + from importlib.resources import files as importlib_resources_files except ImportError: # Can remove when we require Python > 3.8 - from importlib_resources import (files as importlib_resources_files, - as_file as importlib_resources_as_file) + from importlib_resources import ( + files as importlib_resources_files, + as_file as importlib_resources_as_file, + ) import matplotlib.font_manager as fm import numpy as np @@ -21,7 +23,7 @@ exporter = Exporter(globals()) # Create a matplotlib font object pointing to our weather symbol font -fontfile = importlib_resources_files('metpy.plots') / 'fonts/wx_symbols.ttf' +fontfile = importlib_resources_files("metpy.plots") / "fonts/wx_symbols.ttf" with importlib_resources_as_file(fontfile) as fname: # Need to pass str, not Path, for older matplotlib wx_symbol_font = fm.FontProperties(fname=str(fname)) @@ -48,13 +50,18 @@ def wx_code_to_numeric(codes): """ wx_sym_list = [] for s in codes: - wxcode = s.split()[0] if ' ' in s else s + wxcode = s.split()[0] if " " in s else s try: wx_sym_list.append(wx_code_map[wxcode]) except KeyError: - if wxcode[0].startswith(('-', '+')): - options = [slice(None, 7), slice(None, 5), slice(1, 5), slice(None, 3), - slice(1, 3)] + if wxcode[0].startswith(("-", "+")): + options = [ + slice(None, 7), + slice(None, 5), + slice(1, 5), + slice(None, 3), + slice(1, 3), + ] else: options = [slice(None, 6), slice(None, 4), slice(None, 2)] @@ -103,7 +110,7 @@ def __init__(self, num, font_start, font_jumps=None, char_jumps=None): if next_char_jump and code >= next_char_jump[0]: jump_len = next_char_jump[1] code += jump_len - self.chrs.extend([''] * jump_len) + self.chrs.extend([""] * jump_len) next_char_jump = self._safe_pop(char_jumps) else: self.chrs.append(chr(font_point)) @@ -159,13 +166,28 @@ def alt_char(self, code, alt): with exporter: #: Current weather - current_weather = CodePointMapping(100, 0xE9A2, [(7, 2), (93, 2), (94, 2), (95, 2), - (97, 2)], [(0, 4)]) + current_weather = CodePointMapping( + 100, 0xE9A2, [(7, 2), (93, 2), (94, 2), (95, 2), (97, 2)], [(0, 4)] + ) #: Current weather from an automated station - current_weather_auto = CodePointMapping(100, 0xE94B, [(92, 2), (95, 2)], - [(6, 4), (13, 5), (19, 1), (36, 4), (49, 1), - (59, 1), (69, 1), (79, 1), (88, 1), (97, 2)]) + current_weather_auto = CodePointMapping( + 100, + 0xE94B, + [(92, 2), (95, 2)], + [ + (6, 4), + (13, 5), + (19, 1), + (36, 4), + (49, 1), + (59, 1), + (69, 1), + (79, 1), + (88, 1), + (97, 2), + ], + ) #: Low clouds low_clouds = CodePointMapping(10, 0xE933, [(7, 1)], [(0, 1)]) @@ -191,51 +213,197 @@ def alt_char(self, code, alt): # It may become necessary to add automated station wx_codes in the future, # but that will also require knowing the status of all stations. - wx_code_map = {'': 0, 'M': 0, 'TSNO': 0, 'VA': 4, 'FU': 4, - 'HZ': 5, 'DU': 6, 'BLDU': 7, 'SA': 7, - 'BLSA': 7, 'VCBLSA': 7, 'VCBLDU': 7, 'BLPY': 7, - 'PO': 8, 'VCPO': 8, 'VCDS': 9, 'VCSS': 9, - 'BR': 10, 'BCBR': 10, 'BC': 11, 'MIFG': 12, - 'VCTS': 13, 'VIRGA': 14, 'VCSH': 16, 'TS': 17, - 'THDR': 17, 'VCTSHZ': 17, 'TSFZFG': 17, 'TSBR': 17, - 'TSDZ': 17, 'SQ': 18, 'FC': 19, '+FC': 19, - 'DS': 31, 'SS': 31, 'DRSA': 31, 'DRDU': 31, - 'TSUP': 32, '+DS': 34, '+SS': 34, '-BLSN': 36, - 'BLSN': 36, '+BLSN': 36, 'VCBLSN': 36, 'DRSN': 38, - '+DRSN': 38, 'VCFG': 40, 'BCFG': 41, 'PRFG': 44, - 'FG': 45, 'FZFG': 49, '-VCTSDZ': 51, '-DZ': 51, - '-DZBR': 51, 'VCTSDZ': 53, 'DZ': 53, '+VCTSDZ': 55, - '+DZ': 55, '-FZDZ': 56, '-FZDZSN': 56, 'FZDZ': 57, - '+FZDZ': 57, 'FZDZSN': 57, '-DZRA': 58, 'DZRA': 59, - '+DZRA': 59, '-VCTSRA': 61, '-RA': 61, '-RABR': 61, - 'VCTSRA': 63, 'RA': 63, 'RABR': 63, 'RAFG': 63, - '+VCTSRA': 65, '+RA': 65, '-FZRA': 66, '-FZRASN': 66, - '-FZRABR': 66, '-FZRAPL': 66, '-FZRASNPL': 66, 'TSFZRAPL': 67, - '-TSFZRA': 67, 'FZRA': 67, '+FZRA': 67, 'FZRASN': 67, - 'TSFZRA': 67, '-DZSN': 68, '-RASN': 68, '-SNRA': 68, - '-SNDZ': 68, 'RASN': 69, '+RASN': 69, 'SNRA': 69, - 'DZSN': 69, 'SNDZ': 69, '+DZSN': 69, '+SNDZ': 69, - '-VCTSSN': 71, '-SN': 71, '-SNBR': 71, 'VCTSSN': 73, - 'SN': 73, '+VCTSSN': 75, '+SN': 75, 'VCTSUP': 76, - 'IN': 76, '-UP': 76, 'UP': 76, '+UP': 76, - '-SNSG': 77, 'SG': 77, '-SG': 77, 'IC': 78, - '-FZDZPL': 79, '-FZDZPLSN': 79, 'FZDZPL': 79, '-FZRAPLSN': 79, - 'FZRAPL': 79, '+FZRAPL': 79, '-RAPL': 79, '-RASNPL': 79, - '-RAPLSN': 79, '+RAPL': 79, 'RAPL': 79, '-SNPL': 79, - 'SNPL': 79, '-PL': 79, 'PL': 79, '-PLSN': 79, - '-PLRA': 79, 'PLRA': 79, '-PLDZ': 79, '+PL': 79, - 'PLSN': 79, 'PLUP': 79, '+PLSN': 79, '-SH': 80, - '-SHRA': 80, 'SH': 81, 'SHRA': 81, '+SH': 81, - '+SHRA': 81, '-SHRASN': 83, '-SHSNRA': 83, '+SHRABR': 84, - 'SHRASN': 84, '+SHRASN': 84, 'SHSNRA': 84, '+SHSNRA': 84, - '-SHSN': 85, 'SHSN': 86, '+SHSN': 86, '-GS': 87, - '-SHGS': 87, 'FZRAPLGS': 88, '-SNGS': 88, 'GSPLSN': 88, - 'GSPL': 88, 'PLGSSN': 88, 'GS': 88, 'SHGS': 88, - '+GS': 88, '+SHGS': 88, '-GR': 89, '-SHGR': 89, - '-SNGR': 90, 'GR': 90, 'SHGR': 90, '+GR': 90, - '+SHGR': 90, '-TSRA': 95, 'TSRA': 95, 'TSSN': 95, - 'TSPL': 95, '-TSDZ': 95, '-TSSN': 95, '-TSPL': 95, - 'TSPLSN': 95, 'TSSNPL': 95, '-TSSNPL': 95, 'TSRAGS': 96, - 'TSGS': 96, 'TSGR': 96, '+TSRA': 97, '+TSSN': 97, - '+TSPL': 97, '+TSPLSN': 97, 'TSSA': 98, 'TSDS': 98, - 'TSDU': 98, '+TSGS': 99, '+TSGR': 99} + wx_code_map = { + "": 0, + "M": 0, + "TSNO": 0, + "VA": 4, + "FU": 4, + "HZ": 5, + "DU": 6, + "BLDU": 7, + "SA": 7, + "BLSA": 7, + "VCBLSA": 7, + "VCBLDU": 7, + "BLPY": 7, + "PO": 8, + "VCPO": 8, + "VCDS": 9, + "VCSS": 9, + "BR": 10, + "BCBR": 10, + "BC": 11, + "MIFG": 12, + "VCTS": 13, + "VIRGA": 14, + "VCSH": 16, + "TS": 17, + "THDR": 17, + "VCTSHZ": 17, + "TSFZFG": 17, + "TSBR": 17, + "TSDZ": 17, + "SQ": 18, + "FC": 19, + "+FC": 19, + "DS": 31, + "SS": 31, + "DRSA": 31, + "DRDU": 31, + "TSUP": 32, + "+DS": 34, + "+SS": 34, + "-BLSN": 36, + "BLSN": 36, + "+BLSN": 36, + "VCBLSN": 36, + "DRSN": 38, + "+DRSN": 38, + "VCFG": 40, + "BCFG": 41, + "PRFG": 44, + "FG": 45, + "FZFG": 49, + "-VCTSDZ": 51, + "-DZ": 51, + "-DZBR": 51, + "VCTSDZ": 53, + "DZ": 53, + "+VCTSDZ": 55, + "+DZ": 55, + "-FZDZ": 56, + "-FZDZSN": 56, + "FZDZ": 57, + "+FZDZ": 57, + "FZDZSN": 57, + "-DZRA": 58, + "DZRA": 59, + "+DZRA": 59, + "-VCTSRA": 61, + "-RA": 61, + "-RABR": 61, + "VCTSRA": 63, + "RA": 63, + "RABR": 63, + "RAFG": 63, + "+VCTSRA": 65, + "+RA": 65, + "-FZRA": 66, + "-FZRASN": 66, + "-FZRABR": 66, + "-FZRAPL": 66, + "-FZRASNPL": 66, + "TSFZRAPL": 67, + "-TSFZRA": 67, + "FZRA": 67, + "+FZRA": 67, + "FZRASN": 67, + "TSFZRA": 67, + "-DZSN": 68, + "-RASN": 68, + "-SNRA": 68, + "-SNDZ": 68, + "RASN": 69, + "+RASN": 69, + "SNRA": 69, + "DZSN": 69, + "SNDZ": 69, + "+DZSN": 69, + "+SNDZ": 69, + "-VCTSSN": 71, + "-SN": 71, + "-SNBR": 71, + "VCTSSN": 73, + "SN": 73, + "+VCTSSN": 75, + "+SN": 75, + "VCTSUP": 76, + "IN": 76, + "-UP": 76, + "UP": 76, + "+UP": 76, + "-SNSG": 77, + "SG": 77, + "-SG": 77, + "IC": 78, + "-FZDZPL": 79, + "-FZDZPLSN": 79, + "FZDZPL": 79, + "-FZRAPLSN": 79, + "FZRAPL": 79, + "+FZRAPL": 79, + "-RAPL": 79, + "-RASNPL": 79, + "-RAPLSN": 79, + "+RAPL": 79, + "RAPL": 79, + "-SNPL": 79, + "SNPL": 79, + "-PL": 79, + "PL": 79, + "-PLSN": 79, + "-PLRA": 79, + "PLRA": 79, + "-PLDZ": 79, + "+PL": 79, + "PLSN": 79, + "PLUP": 79, + "+PLSN": 79, + "-SH": 80, + "-SHRA": 80, + "SH": 81, + "SHRA": 81, + "+SH": 81, + "+SHRA": 81, + "-SHRASN": 83, + "-SHSNRA": 83, + "+SHRABR": 84, + "SHRASN": 84, + "+SHRASN": 84, + "SHSNRA": 84, + "+SHSNRA": 84, + "-SHSN": 85, + "SHSN": 86, + "+SHSN": 86, + "-GS": 87, + "-SHGS": 87, + "FZRAPLGS": 88, + "-SNGS": 88, + "GSPLSN": 88, + "GSPL": 88, + "PLGSSN": 88, + "GS": 88, + "SHGS": 88, + "+GS": 88, + "+SHGS": 88, + "-GR": 89, + "-SHGR": 89, + "-SNGR": 90, + "GR": 90, + "SHGR": 90, + "+GR": 90, + "+SHGR": 90, + "-TSRA": 95, + "TSRA": 95, + "TSSN": 95, + "TSPL": 95, + "-TSDZ": 95, + "-TSSN": 95, + "-TSPL": 95, + "TSPLSN": 95, + "TSSNPL": 95, + "-TSSNPL": 95, + "TSRAGS": 96, + "TSGS": 96, + "TSGR": 96, + "+TSRA": 97, + "+TSSN": 97, + "+TSPL": 97, + "+TSPLSN": 97, + "TSSA": 98, + "TSDS": 98, + "TSDU": 98, + "+TSGS": 99, + "+TSGR": 99, + } diff --git a/src/metpy/testing.py b/src/metpy/testing.py index 103dc28febb..5a71069c6ed 100644 --- a/src/metpy/testing.py +++ b/src/metpy/testing.py @@ -18,6 +18,7 @@ from metpy.calc import wind_components from metpy.cbook import get_test_data from metpy.deprecation import MetpyDeprecationWarning + from .units import units @@ -27,10 +28,12 @@ def needs_cartopy(test_func): Will skip the decorated test, or any test using the decorated fixture, if ``cartopy`` is unable to be imported. """ + @functools.wraps(test_func) def wrapped(*args, **kwargs): - pytest.importorskip('cartopy') + pytest.importorskip("cartopy") return test_func(*args, **kwargs) + return wrapped @@ -40,10 +43,12 @@ def needs_pyproj(test_func): Will skip the decorated test, or any test using the decorated fixture, if ``pyproj`` is unable to be imported. """ + @functools.wraps(test_func) def wrapped(*args, **kwargs): - pytest.importorskip('pyproj') + pytest.importorskip("pyproj") return test_func(*args, **kwargs) + return wrapped @@ -63,12 +68,14 @@ def get_upper_air_data(date, station): dict : upper air data """ - sounding_key = f'{date:%Y-%m-%dT%HZ}_{station}' - sounding_files = {'2016-05-22T00Z_DDC': 'may22_sounding.txt', - '2013-01-20T12Z_OUN': 'jan20_sounding.txt', - '1999-05-04T00Z_OUN': 'may4_sounding.txt', - '2002-11-11T00Z_BNA': 'nov11_sounding.txt', - '2010-12-09T12Z_BOI': 'dec9_sounding.txt'} + sounding_key = f"{date:%Y-%m-%dT%HZ}_{station}" + sounding_files = { + "2016-05-22T00Z_DDC": "may22_sounding.txt", + "2013-01-20T12Z_OUN": "jan20_sounding.txt", + "1999-05-04T00Z_OUN": "may4_sounding.txt", + "2002-11-11T00Z_BNA": "nov11_sounding.txt", + "2010-12-09T12Z_BOI": "dec9_sounding.txt", + } fname = sounding_files[sounding_key] fobj = get_test_data(fname) @@ -76,7 +83,7 @@ def get_upper_air_data(date, station): def to_float(s): # Remove all whitespace and replace empty values with NaN if not s.strip(): - s = 'nan' + s = "nan" return float(s) # Skip dashes, column names, units, and more dashes @@ -89,8 +96,13 @@ def to_float(s): # Read all lines of data and append to lists only if there is some data for row in fobj: level = to_float(row[0:7]) - values = (to_float(row[7:14]), to_float(row[14:21]), to_float(row[21:28]), - to_float(row[42:49]), to_float(row[49:56])) + values = ( + to_float(row[7:14]), + to_float(row[14:21]), + to_float(row[21:28]), + to_float(row[42:49]), + to_float(row[49:56]), + ) if any(np.invert(np.isnan(values[1:]))): arr_data.append((level,) + values) @@ -106,8 +118,16 @@ def to_float(s): u, v = wind_components(spd, direc) - return {'pressure': p, 'height': z, 'temperature': t, - 'dewpoint': td, 'direction': direc, 'speed': spd, 'u_wind': u, 'v_wind': v} + return { + "pressure": p, + "height": z, + "temperature": t, + "dewpoint": td, + "direction": direc, + "speed": spd, + "u_wind": u, + "v_wind": v, + } def check_and_drop_units(actual, desired): @@ -139,22 +159,25 @@ def check_and_drop_units(actual, desired): actual = actual.metpy.unit_array # If the desired result has units, add dimensionless units if necessary, then # ensure that this is compatible to the desired result. - if hasattr(desired, 'units'): - if not hasattr(actual, 'units'): - actual = units.Quantity(actual, 'dimensionless') + if hasattr(desired, "units"): + if not hasattr(actual, "units"): + actual = units.Quantity(actual, "dimensionless") actual = actual.to(desired.units) # Otherwise, the desired result has no units. Convert the actual result to # dimensionless units if it is a united quantity. else: - if hasattr(actual, 'units'): - actual = actual.to('dimensionless') + if hasattr(actual, "units"): + actual = actual.to("dimensionless") except DimensionalityError: - raise AssertionError('Units are not compatible: {} should be {}'.format( - actual.units, getattr(desired, 'units', 'dimensionless'))) from None + raise AssertionError( + "Units are not compatible: {} should be {}".format( + actual.units, getattr(desired, "units", "dimensionless") + ) + ) from None - if hasattr(actual, 'magnitude'): + if hasattr(actual, "magnitude"): actual = actual.magnitude - if hasattr(desired, 'magnitude'): + if hasattr(desired, "magnitude"): desired = desired.magnitude return actual, desired @@ -166,8 +189,8 @@ def check_mask(actual, desired): This handles the fact that `~numpy.testing.assert_array_equal` ignores masked values in either of the arrays. This ensures that the masks are identical. """ - actual_mask = getattr(actual, 'mask', np.full(np.asarray(actual).shape, False)) - desired_mask = getattr(desired, 'mask', np.full(np.asarray(desired).shape, False)) + actual_mask = getattr(actual, "mask", np.full(np.asarray(actual).shape, False)) + desired_mask = getattr(desired, "mask", np.full(np.asarray(desired).shape, False)) np.testing.assert_array_equal(actual_mask, desired_mask) @@ -213,13 +236,14 @@ def assert_xarray_allclose(actual, desired): assert desired.attrs == actual.attrs -@pytest.fixture(scope='module', autouse=True) +@pytest.fixture(scope="module", autouse=True) def set_agg_backend(): """Fixture to ensure the Agg backend is active.""" import matplotlib.pyplot as plt + prev_backend = plt.get_backend() try: - plt.switch_backend('agg') + plt.switch_backend("agg") yield finally: plt.switch_backend(prev_backend) @@ -232,12 +256,15 @@ def check_and_silence_warning(warn_type): tests, but checks that the warning is present and makes sure the function still works as intended. """ + def dec(func): @functools.wraps(func) def wrapper(*args, **kwargs): with pytest.warns(warn_type): return func(*args, **kwargs) + return wrapper + return dec diff --git a/src/metpy/units.py b/src/metpy/units.py index f50f87c89fd..8380e5a0db8 100644 --- a/src/metpy/units.py +++ b/src/metpy/units.py @@ -33,34 +33,34 @@ autoconvert_offset_to_baseunit=True, preprocessors=[ functools.partial( - re.sub, - r'(?<=[A-Za-z])(?![A-Za-z])(? 0: coord_lists[geometric] = coord_lists[graticule] # Filter out multidimensional coordinates where not allowed - require_1d_coord = ['time', 'vertical', 'y', 'x'] + require_1d_coord = ["time", "vertical", "y", "x"] for axis in require_1d_coord: coord_lists[axis] = [coord for coord in coord_lists[axis] if coord.ndim <= 1] @@ -311,23 +328,28 @@ def _generate_coordinate_map(self): self._resolve_axis_duplicates(axis, coord_lists) # Collapse the coord_lists to a coord_map - return {axis: (coord_lists[axis][0] if len(coord_lists[axis]) > 0 else None) - for axis in coord_lists} + return { + axis: (coord_lists[axis][0] if len(coord_lists[axis]) > 0 else None) + for axis in coord_lists + } def _resolve_axis_duplicates(self, axis, coord_lists): """Handle coordinate duplication for an axis type if it arises.""" # If one and only one of the possible axes is a dimension, use it - dimension_coords = [coord_var for coord_var in coord_lists[axis] if - coord_var.name in coord_var.dims] + dimension_coords = [ + coord_var for coord_var in coord_lists[axis] if coord_var.name in coord_var.dims + ] if len(dimension_coords) == 1: coord_lists[axis] = dimension_coords return # Ambiguous axis, raise warning and do not parse - varname = (' "' + self._data_array.name + '"' - if self._data_array.name is not None else '') - warnings.warn('More than one ' + axis + ' coordinate present for variable' - + varname + '.') + varname = ( + ' "' + self._data_array.name + '"' if self._data_array.name is not None else "" + ) + warnings.warn( + "More than one " + axis + " coordinate present for variable" + varname + "." + ) coord_lists[axis] = [] def _metpy_axis_search(self, metpy_axis): @@ -335,7 +357,7 @@ def _metpy_axis_search(self, metpy_axis): # Search for coord with proper _metpy_axis coords = self._data_array.coords.values() for coord_var in coords: - if metpy_axis in coord_var.attrs.get('_metpy_axis', '').split(','): + if metpy_axis in coord_var.attrs.get("_metpy_axis", "").split(","): return coord_var # Opportunistically parse all coordinates, and assign if not already assigned @@ -346,9 +368,9 @@ def _metpy_axis_search(self, metpy_axis): # coordinates, and nothing else. coord_map = self._generate_coordinate_map() for axis, coord_var in coord_map.items(): - if (coord_var is not None - and not any(axis in coord.attrs.get('_metpy_axis', '').split(',') - for coord in coords)): + if coord_var is not None and not any( + axis in coord.attrs.get("_metpy_axis", "").split(",") for coord in coords + ): _assign_axis(coord_var.attrs, axis) @@ -362,7 +384,7 @@ def _axis(self, axis): if coord_var is not None: return coord_var else: - raise AttributeError(axis + ' attribute is not available.') + raise AttributeError(axis + " attribute is not available.") else: raise AttributeError("'" + axis + "' is not an interpretable axis.") @@ -388,32 +410,32 @@ def coordinates(self, *args): @property def time(self): """Return the time coordinate.""" - return self._axis('time') + return self._axis("time") @property def vertical(self): """Return the vertical coordinate.""" - return self._axis('vertical') + return self._axis("vertical") @property def y(self): """Return the y coordinate.""" - return self._axis('y') + return self._axis("y") @property def latitude(self): """Return the latitude coordinate (if it exists).""" - return self._axis('latitude') + return self._axis("latitude") @property def x(self): """Return the x coordinate.""" - return self._axis('x') + return self._axis("x") @property def longitude(self): """Return the longitude coordinate (if it exists).""" - return self._axis('longitude') + return self._axis("longitude") def coordinates_identical(self, other): """Return whether or not the coordinates of other match this DataArray's.""" @@ -432,8 +454,11 @@ def coordinates_identical(self, other): @property def time_deltas(self): """Return the time difference of the data in seconds (to microsecond precision).""" - return (np.diff(self._data_array.values).astype('timedelta64[us]').astype('int64') - / 1e6 * units.s) + return ( + np.diff(self._data_array.values).astype("timedelta64[us]").astype("int64") + / 1e6 + * units.s + ) def find_axis_name(self, axis): """Return the name of the axis corresponding to the given identifier. @@ -486,16 +511,19 @@ def find_axis_number(self, axis): # names using regular expressions from coordinate parsing to allow for # multidimensional lat/lon without y/x dimension coordinates, and basic # vertical dim recognition - if axis in ('vertical', 'y', 'x'): + if axis in ("vertical", "y", "x"): for i, dim in enumerate(self._data_array.dims): - if re.match(coordinate_criteria['regular_expression'][axis], - dim.lower()): + if re.match( + coordinate_criteria["regular_expression"][axis], dim.lower() + ): return i raise exc except ValueError: # Intercept ValueError when axis type found but not dimension coordinate - raise AttributeError(f'Requested {axis} dimension coordinate but {axis} ' - f'coordinate {name} is not a dimension') + raise AttributeError( + f"Requested {axis} dimension coordinate but {axis} " + f"coordinate {name} is not a dimension" + ) else: # Otherwise, not valid raise ValueError(_axis_identifier_error) @@ -528,7 +556,7 @@ def loc(self): def sel(self, indexers=None, method=None, tolerance=None, drop=False, **indexers_kwargs): """Wrap DataArray.sel to handle units and coordinate types.""" - indexers = either_dict_or_kwargs(indexers, indexers_kwargs, 'sel') + indexers = either_dict_or_kwargs(indexers, indexers_kwargs, "sel") indexers = _reassign_quantity_indexer(self._data_array, indexers) return self._data_array.sel(indexers, method=method, tolerance=tolerance, drop=drop) @@ -576,10 +604,14 @@ def assign_latitude_longitude(self, force=False): """ # Check for existing latitude and longitude coords - if (not force and (self._metpy_axis_search('latitude') is not None - or self._metpy_axis_search('longitude'))): - raise RuntimeError('Latitude/longitude coordinate(s) are present. If you wish to ' - 'overwrite these, specify force=True.') + if not force and ( + self._metpy_axis_search("latitude") is not None + or self._metpy_axis_search("longitude") + ): + raise RuntimeError( + "Latitude/longitude coordinate(s) are present. If you wish to " + "overwrite these, specify force=True." + ) # Build new latitude and longitude DataArrays latitude, longitude = _build_latitude_longitude(self._data_array) @@ -612,10 +644,13 @@ def assign_y_x(self, force=False, tolerance=None): """ # Check for existing latitude and longitude coords - if (not force and (self._metpy_axis_search('y') is not None - or self._metpy_axis_search('x'))): - raise RuntimeError('y/x coordinate(s) are present. If you wish to overwrite ' - 'these, specify force=True.') + if not force and ( + self._metpy_axis_search("y") is not None or self._metpy_axis_search("x") + ): + raise RuntimeError( + "y/x coordinate(s) are present. If you wish to overwrite " + "these, specify force=True." + ) # Build new y and x DataArrays y, x = _build_y_x(self._data_array, tolerance) @@ -625,7 +660,7 @@ def assign_y_x(self, force=False, tolerance=None): return new_dataarray.metpy.assign_coordinates(None) -@xr.register_dataset_accessor('metpy') +@xr.register_dataset_accessor("metpy") class MetPyDatasetAccessor: """Provide custom attributes and methods on XArray Datasets for MetPy functionality. @@ -669,8 +704,12 @@ def parse_cf(self, varname=None, coordinates=None): if np.iterable(varname) and not isinstance(varname, str): # If non-string iterable is given, apply recursively across the varnames - subset = xr.merge([self.parse_cf(single_varname, coordinates=coordinates) - for single_varname in varname]) + subset = xr.merge( + [ + self.parse_cf(single_varname, coordinates=coordinates) + for single_varname in varname + ] + ) subset.attrs = self._dataset.attrs return subset @@ -682,55 +721,60 @@ def parse_cf(self, varname=None, coordinates=None): # Attempt to build the crs coordinate crs = None - if 'grid_mapping' in var.attrs: + if "grid_mapping" in var.attrs: # Use given CF grid_mapping - proj_name = var.attrs['grid_mapping'] + proj_name = var.attrs["grid_mapping"] try: proj_var = self._dataset.variables[proj_name] except KeyError: log.warning( - 'Could not find variable corresponding to the value of ' - f'grid_mapping: {proj_name}') + "Could not find variable corresponding to the value of " + f"grid_mapping: {proj_name}" + ) else: crs = CFProjection(proj_var.attrs) - if crs is None and not check_axis(var, 'latitude', 'longitude'): + if crs is None and not check_axis(var, "latitude", "longitude"): # This isn't a lat or lon coordinate itself, so determine if we need to fall back # to creating a latitude_longitude CRS. We do so if there exists valid coordinates # for latitude and longitude, even if they are not the dimension coordinates of # the variable. def _has_coord(coord_type): - return any(check_axis(coord_var, coord_type) - for coord_var in var.coords.values()) - if _has_coord('latitude') and _has_coord('longitude'): - crs = CFProjection({'grid_mapping_name': 'latitude_longitude'}) - log.warning('Found valid latitude/longitude coordinates, assuming ' - 'latitude_longitude for projection grid_mapping variable') + return any( + check_axis(coord_var, coord_type) for coord_var in var.coords.values() + ) + + if _has_coord("latitude") and _has_coord("longitude"): + crs = CFProjection({"grid_mapping_name": "latitude_longitude"}) + log.warning( + "Found valid latitude/longitude coordinates, assuming " + "latitude_longitude for projection grid_mapping variable" + ) # Rebuild the coordinates of the dataarray, and return quantified DataArray var = self._rebuild_coords(var, crs) if crs is not None: - var = var.assign_coords(coords={'crs': crs}) + var = var.assign_coords(coords={"crs": crs}) return var def _rebuild_coords(self, var, crs): """Clean up the units on the coordinate variables.""" for coord_name, coord_var in var.coords.items(): - if (check_axis(coord_var, 'x', 'y') - and not check_axis(coord_var, 'longitude', 'latitude')): + if check_axis(coord_var, "x", "y") and not check_axis( + coord_var, "longitude", "latitude" + ): try: - var = var.metpy.convert_coordinate_units(coord_name, 'meters') + var = var.metpy.convert_coordinate_units(coord_name, "meters") except DimensionalityError: # Radians! Attempt to use perspective point height conversion if crs is not None: - height = crs['perspective_point_height'] + height = crs["perspective_point_height"] new_coord_var = coord_var.copy( - data=( - coord_var.metpy.unit_array - * (height * units.meter) - ).m_as('meter') + data=(coord_var.metpy.unit_array * (height * units.meter)).m_as( + "meter" + ) ) - new_coord_var.attrs['units'] = 'meter' + new_coord_var.attrs["units"] = "meter" var = var.assign_coords(coords={coord_name: new_coord_var}) return var @@ -752,7 +796,7 @@ def loc(self): def sel(self, indexers=None, method=None, tolerance=None, drop=False, **indexers_kwargs): """Wrap Dataset.sel to handle units.""" - indexers = either_dict_or_kwargs(indexers, indexers_kwargs, 'sel') + indexers = either_dict_or_kwargs(indexers, indexers_kwargs, "sel") indexers = _reassign_quantity_indexer(self._dataset, indexers) return self._dataset.sel(indexers, method=method, tolerance=tolerance, drop=drop) @@ -804,18 +848,23 @@ def assign_latitude_longitude(self, force=False): # while also checking for existing lat/lon coords grid_prototype = None for data_var in self._dataset.data_vars.values(): - if hasattr(data_var.metpy, 'y') and hasattr(data_var.metpy, 'x'): + if hasattr(data_var.metpy, "y") and hasattr(data_var.metpy, "x"): if grid_prototype is None: grid_prototype = data_var - if (not force and (hasattr(data_var.metpy, 'latitude') - or hasattr(data_var.metpy, 'longitude'))): - raise RuntimeError('Latitude/longitude coordinate(s) are present. If you ' - 'wish to overwrite these, specify force=True.') + if not force and ( + hasattr(data_var.metpy, "latitude") or hasattr(data_var.metpy, "longitude") + ): + raise RuntimeError( + "Latitude/longitude coordinate(s) are present. If you " + "wish to overwrite these, specify force=True." + ) # Calculate latitude and longitude from grid_prototype, if it exists, and assign if grid_prototype is None: - warnings.warn('No latitude and longitude assigned since horizontal coordinates ' - 'were not found') + warnings.warn( + "No latitude and longitude assigned since horizontal coordinates " + "were not found" + ) return self._dataset else: latitude, longitude = _build_latitude_longitude(grid_prototype) @@ -849,18 +898,23 @@ def assign_y_x(self, force=False, tolerance=None): # while also checking for existing y and x coords grid_prototype = None for data_var in self._dataset.data_vars.values(): - if hasattr(data_var.metpy, 'latitude') and hasattr(data_var.metpy, 'longitude'): + if hasattr(data_var.metpy, "latitude") and hasattr(data_var.metpy, "longitude"): if grid_prototype is None: grid_prototype = data_var - if (not force and (hasattr(data_var.metpy, 'y') - or hasattr(data_var.metpy, 'x'))): - raise RuntimeError('y/x coordinate(s) are present. If you wish to ' - 'overwrite these, specify force=True.') + if not force and ( + hasattr(data_var.metpy, "y") or hasattr(data_var.metpy, "x") + ): + raise RuntimeError( + "y/x coordinate(s) are present. If you wish to " + "overwrite these, specify force=True." + ) # Calculate y and x from grid_prototype, if it exists, and assign if grid_prototype is None: - warnings.warn('No y and x coordinates assigned since horizontal coordinates ' - 'were not found') + warnings.warn( + "No y and x coordinates assigned since horizontal coordinates " + "were not found" + ) return self._dataset else: y, x = _build_y_x(grid_prototype, tolerance) @@ -901,13 +955,11 @@ def mapping_func(da): return da.assign_attrs(**{attribute: new_value}) # Apply across all variables and coordinates - return ( - self._dataset - .map(mapping_func, keep_attrs=True) - .assign_coords({ + return self._dataset.map(mapping_func, keep_attrs=True).assign_coords( + { coord_name: mapping_func(coord_var) for coord_name, coord_var in self._dataset.coords.items() - }) + } ) def quantify(self): @@ -921,18 +973,20 @@ def dequantify(self): def _assign_axis(attributes, axis): """Assign the given axis to the _metpy_axis attribute.""" - existing_axes = attributes.get('_metpy_axis', '').split(',') - if ((axis == 'y' and 'latitude' in existing_axes) - or (axis == 'latitude' and 'y' in existing_axes)): + existing_axes = attributes.get("_metpy_axis", "").split(",") + if (axis == "y" and "latitude" in existing_axes) or ( + axis == "latitude" and "y" in existing_axes + ): # Special case for combined y/latitude handling - attributes['_metpy_axis'] = 'y,latitude' - elif ((axis == 'x' and 'longitude' in existing_axes) - or (axis == 'longitude' and 'x' in existing_axes)): + attributes["_metpy_axis"] = "y,latitude" + elif (axis == "x" and "longitude" in existing_axes) or ( + axis == "longitude" and "x" in existing_axes + ): # Special case for combined x/longitude handling - attributes['_metpy_axis'] = 'x,longitude' + attributes["_metpy_axis"] = "x,longitude" else: # Simply add it/overwrite past value - attributes['_metpy_axis'] = axis + attributes["_metpy_axis"] = axis return attributes @@ -954,30 +1008,35 @@ def check_axis(var, *axes): # - _CoordinateAxisType (from THREDDS) # - axis (CF option) # - positive (CF standard for non-pressure vertical coordinate) - for criterion in ('standard_name', '_CoordinateAxisType', 'axis', 'positive'): - if (var.attrs.get(criterion, 'absent') in - coordinate_criteria[criterion].get(axis, set())): + for criterion in ("standard_name", "_CoordinateAxisType", "axis", "positive"): + if var.attrs.get(criterion, "absent") in coordinate_criteria[criterion].get( + axis, set() + ): return True # Check for units, either by dimensionality or name try: - if (axis in coordinate_criteria['units'] and ( - ( - coordinate_criteria['units'][axis]['match'] == 'dimensionality' - and (units.get_dimensionality(var.metpy.units) - == units.get_dimensionality( - coordinate_criteria['units'][axis]['units'])) - ) or ( - coordinate_criteria['units'][axis]['match'] == 'name' - and str(var.metpy.units) - in coordinate_criteria['units'][axis]['units'] - ))): + if axis in coordinate_criteria["units"] and ( + ( + coordinate_criteria["units"][axis]["match"] == "dimensionality" + and ( + units.get_dimensionality(var.metpy.units) + == units.get_dimensionality( + coordinate_criteria["units"][axis]["units"] + ) + ) + ) + or ( + coordinate_criteria["units"][axis]["match"] == "name" + and str(var.metpy.units) in coordinate_criteria["units"][axis]["units"] + ) + ): return True except UndefinedUnitError: pass # Check if name matches regular expression (non-CF failsafe) - if re.match(coordinate_criteria['regular_expression'][axis], var.name.lower()): + if re.match(coordinate_criteria["regular_expression"][axis], var.name.lower()): return True # If no match has been made, return False (rather than None) @@ -989,9 +1048,9 @@ def _assign_crs(xarray_object, cf_attributes, cf_kwargs): # Handle argument options if cf_attributes is not None and len(cf_kwargs) > 0: - raise ValueError('Cannot specify both attribute dictionary and kwargs.') + raise ValueError("Cannot specify both attribute dictionary and kwargs.") elif cf_attributes is None and len(cf_kwargs) == 0: - raise ValueError('Must specify either attribute dictionary or kwargs.') + raise ValueError("Must specify either attribute dictionary or kwargs.") attrs = cf_attributes if cf_attributes is not None else cf_kwargs # Assign crs coordinate to xarray object @@ -1000,59 +1059,79 @@ def _assign_crs(xarray_object, cf_attributes, cf_kwargs): def _build_latitude_longitude(da): """Build latitude/longitude coordinates from DataArray's y/x coordinates.""" - y, x = da.metpy.coordinates('y', 'x') + y, x = da.metpy.coordinates("y", "x") xx, yy = np.meshgrid(x.values, y.values) lonlats = da.metpy.cartopy_geodetic.transform_points(da.metpy.cartopy_crs, xx, yy) - longitude = xr.DataArray(lonlats[..., 0], dims=(y.name, x.name), - coords={y.name: y, x.name: x}, - attrs={'units': 'degrees_east', 'standard_name': 'longitude'}) - latitude = xr.DataArray(lonlats[..., 1], dims=(y.name, x.name), - coords={y.name: y, x.name: x}, - attrs={'units': 'degrees_north', 'standard_name': 'latitude'}) + longitude = xr.DataArray( + lonlats[..., 0], + dims=(y.name, x.name), + coords={y.name: y, x.name: x}, + attrs={"units": "degrees_east", "standard_name": "longitude"}, + ) + latitude = xr.DataArray( + lonlats[..., 1], + dims=(y.name, x.name), + coords={y.name: y, x.name: x}, + attrs={"units": "degrees_north", "standard_name": "latitude"}, + ) return latitude, longitude def _build_y_x(da, tolerance): """Build y/x coordinates from DataArray's latitude/longitude coordinates.""" # Initial sanity checks - latitude, longitude = da.metpy.coordinates('latitude', 'longitude') + latitude, longitude = da.metpy.coordinates("latitude", "longitude") if latitude.dims != longitude.dims: - raise ValueError('Latitude and longitude must have same dimensionality') + raise ValueError("Latitude and longitude must have same dimensionality") elif latitude.ndim != 2: - raise ValueError('To build 1D y/x coordinates via assign_y_x, latitude/longitude ' - 'must be 2D') + raise ValueError( + "To build 1D y/x coordinates via assign_y_x, latitude/longitude " "must be 2D" + ) # Convert to projected y/x - xxyy = da.metpy.cartopy_crs.transform_points(da.metpy.cartopy_geodetic, - longitude.values, - latitude.values) + xxyy = da.metpy.cartopy_crs.transform_points( + da.metpy.cartopy_geodetic, longitude.values, latitude.values + ) # Handle tolerance - tolerance = 1 if tolerance is None else tolerance.m_as('m') + tolerance = 1 if tolerance is None else tolerance.m_as("m") # If within tolerance, take median to collapse to 1D try: - y_dim = latitude.metpy.find_axis_number('y') - x_dim = latitude.metpy.find_axis_number('x') + y_dim = latitude.metpy.find_axis_number("y") + x_dim = latitude.metpy.find_axis_number("x") except AttributeError: - warnings.warn('y and x dimensions unable to be identified. Assuming [..., y, x] ' - 'dimension order.') + warnings.warn( + "y and x dimensions unable to be identified. Assuming [..., y, x] " + "dimension order." + ) y_dim, x_dim = 0, 1 - if (np.all(np.ptp(xxyy[..., 0], axis=y_dim) < tolerance) - and np.all(np.ptp(xxyy[..., 1], axis=x_dim) < tolerance)): + if np.all(np.ptp(xxyy[..., 0], axis=y_dim) < tolerance) and np.all( + np.ptp(xxyy[..., 1], axis=x_dim) < tolerance + ): x = np.median(xxyy[..., 0], axis=y_dim) y = np.median(xxyy[..., 1], axis=x_dim) - x = xr.DataArray(x, name=latitude.dims[x_dim], dims=(latitude.dims[x_dim],), - coords={latitude.dims[x_dim]: x}, - attrs={'units': 'meter', 'standard_name': 'projection_x_coordinate'}) - y = xr.DataArray(y, name=latitude.dims[y_dim], dims=(latitude.dims[y_dim],), - coords={latitude.dims[y_dim]: y}, - attrs={'units': 'meter', 'standard_name': 'projection_y_coordinate'}) + x = xr.DataArray( + x, + name=latitude.dims[x_dim], + dims=(latitude.dims[x_dim],), + coords={latitude.dims[x_dim]: x}, + attrs={"units": "meter", "standard_name": "projection_x_coordinate"}, + ) + y = xr.DataArray( + y, + name=latitude.dims[y_dim], + dims=(latitude.dims[y_dim],), + coords={latitude.dims[y_dim]: y}, + attrs={"units": "meter", "standard_name": "projection_y_coordinate"}, + ) return y, x else: - raise ValueError('Projected y and x coordinates cannot be collapsed to 1D within ' - 'tolerance. Verify that your latitude and longitude coordinates ' - 'correpsond to your CRS coordinate.') + raise ValueError( + "Projected y and x coordinates cannot be collapsed to 1D within " + "tolerance. Verify that your latitude and longitude coordinates " + "correpsond to your CRS coordinate." + ) def preprocess_and_wrap(broadcast=None, wrap_like=None, match_unit=False, to_magnitude=False): @@ -1081,6 +1160,7 @@ def preprocess_and_wrap(broadcast=None, wrap_like=None, match_unit=False, to_mag If true, downcast xarray and Pint arguments to their magnitude. If false, downcast xarray arguments to Quantity, and do not change other array-like arguments. """ + def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): @@ -1089,12 +1169,10 @@ def wrapper(*args, **kwargs): # Auto-broadcast select xarray arguments, and update bound_args if broadcast is not None: arg_names_to_broadcast = tuple( - arg_name for arg_name in broadcast + arg_name + for arg_name in broadcast if arg_name in bound_args.arguments - and isinstance( - bound_args.arguments[arg_name], - (xr.DataArray, xr.Variable) - ) + and isinstance(bound_args.arguments[arg_name], (xr.DataArray, xr.Variable)) ) broadcasted_args = xr.broadcast( *(bound_args.arguments[arg_name] for arg_name in arg_names_to_broadcast) @@ -1107,8 +1185,8 @@ def wrapper(*args, **kwargs): for arg_name in bound_args.arguments: if isinstance(bound_args.arguments[arg_name], xr.Variable): warnings.warn( - f'Argument {arg_name} given as xarray Variable...casting to its data. ' - 'xarray DataArrays are recommended instead.' + f"Argument {arg_name} given as xarray Variable...casting to its data. " + "xarray DataArrays are recommended instead." ) bound_args.arguments[arg_name] = bound_args.arguments[arg_name].data @@ -1124,9 +1202,9 @@ def wrapper(*args, **kwargs): # Cast all DataArrays to Pint Quantities for arg_name in bound_args.arguments: if isinstance(bound_args.arguments[arg_name], xr.DataArray): - bound_args.arguments[arg_name] = ( - bound_args.arguments[arg_name].metpy.unit_array - ) + bound_args.arguments[arg_name] = bound_args.arguments[ + arg_name + ].metpy.unit_array # Optionally cast all Quantities to their magnitudes if to_magnitude: @@ -1150,25 +1228,29 @@ def wrapper(*args, **kwargs): return tuple(wrapping(*args) for args in zip(result, match)) else: return wrapping(result, match) + return wrapper + return decorator def _wrap_output_like_matching_units(result, match): """Convert result to be like match with matching units for output wrapper.""" output_xarray = isinstance(match, xr.DataArray) - match_units = str(match.metpy.units if output_xarray else getattr(match, 'units', '')) + match_units = str(match.metpy.units if output_xarray else getattr(match, "units", "")) if isinstance(result, xr.DataArray): result = result.metpy.convert_units(match_units) return result if output_xarray else result.metpy.unit_array else: result = ( - result.to(match_units) if isinstance(result, units.Quantity) + result.to(match_units) + if isinstance(result, units.Quantity) else units.Quantity(result, match_units) ) return ( - xr.DataArray(result, coords=match.coords, dims=match.dims) if output_xarray + xr.DataArray(result, coords=match.coords, dims=match.dims) + if output_xarray else result ) @@ -1180,37 +1262,39 @@ def _wrap_output_like_not_matching_units(result, match): return result if output_xarray else result.metpy.unit_array else: # Determine if need to upcast to Quantity - if ( - not isinstance(result, units.Quantity) - and ( - isinstance(match, units.Quantity) - or (output_xarray and isinstance(match.data, units.Quantity)) - ) + if not isinstance(result, units.Quantity) and ( + isinstance(match, units.Quantity) + or (output_xarray and isinstance(match.data, units.Quantity)) ): result = units.Quantity(result) return ( - xr.DataArray(result, coords=match.coords, dims=match.dims) if output_xarray + xr.DataArray(result, coords=match.coords, dims=match.dims) + if output_xarray else result ) def check_matching_coordinates(func): """Decorate a function to make sure all given DataArrays have matching coordinates.""" + @functools.wraps(func) def wrapper(*args, **kwargs): - data_arrays = ([a for a in args if isinstance(a, xr.DataArray)] - + [a for a in kwargs.values() if isinstance(a, xr.DataArray)]) + data_arrays = [a for a in args if isinstance(a, xr.DataArray)] + [ + a for a in kwargs.values() if isinstance(a, xr.DataArray) + ] if len(data_arrays) > 1: first = data_arrays[0] for other in data_arrays[1:]: if not first.metpy.coordinates_identical(other): - raise ValueError('Input DataArray arguments must be on same coordinates.') + raise ValueError("Input DataArray arguments must be on same coordinates.") return func(*args, **kwargs) + return wrapper def _reassign_quantity_indexer(data, indexers): """Reassign a units.Quantity indexer to units of relevant coordinate.""" + def _to_magnitude(val, unit): try: return val.m_as(unit) @@ -1218,10 +1302,14 @@ def _to_magnitude(val, unit): return val # Update indexers keys for axis type -> coord name replacement - indexers = {(key if not isinstance(data, xr.DataArray) or key in data.dims - or key not in metpy_axes else - next(data.metpy.coordinates(key)).name): indexers[key] - for key in indexers} + indexers = { + ( + key + if not isinstance(data, xr.DataArray) or key in data.dims or key not in metpy_axes + else next(data.metpy.coordinates(key)).name + ): indexers[key] + for key in indexers + } # Update indexers to handle quantities and slices of quantities reassigned_indexers = {} @@ -1240,7 +1328,7 @@ def _to_magnitude(val, unit): return reassigned_indexers -def grid_deltas_from_dataarray(f, kind='default'): +def grid_deltas_from_dataarray(f, kind="default"): """Calculate the horizontal deltas between grid points of a DataArray. Calculate the signed delta distance between grid points of a DataArray in the horizontal @@ -1271,48 +1359,66 @@ def grid_deltas_from_dataarray(f, kind='default'): from metpy.calc import lat_lon_grid_deltas # Determine behavior - if kind == 'default' and f.metpy.crs['grid_mapping_name'] == 'latitude_longitude': - kind = 'actual' - elif kind == 'default': - kind = 'nominal' - elif kind not in ('actual', 'nominal'): - raise ValueError('"kind" argument must be specified as "default", "actual", or ' - '"nominal"') - - if kind == 'actual': + if kind == "default" and f.metpy.crs["grid_mapping_name"] == "latitude_longitude": + kind = "actual" + elif kind == "default": + kind = "nominal" + elif kind not in ("actual", "nominal"): + raise ValueError( + '"kind" argument must be specified as "default", "actual", or ' '"nominal"' + ) + + if kind == "actual": # Get latitude/longitude coordinates and find dim order - latitude, longitude = xr.broadcast(*f.metpy.coordinates('latitude', 'longitude')) + latitude, longitude = xr.broadcast(*f.metpy.coordinates("latitude", "longitude")) try: - y_dim = latitude.metpy.find_axis_number('y') - x_dim = latitude.metpy.find_axis_number('x') + y_dim = latitude.metpy.find_axis_number("y") + x_dim = latitude.metpy.find_axis_number("x") except AttributeError: - warnings.warn('y and x dimensions unable to be identified. Assuming [..., y, x] ' - 'dimension order.') + warnings.warn( + "y and x dimensions unable to be identified. Assuming [..., y, x] " + "dimension order." + ) y_dim, x_dim = -2, -1 # Obtain grid deltas as xarray Variables (dx_var, dx_units), (dy_var, dy_units) = ( (xr.Variable(dims=latitude.dims, data=deltas.magnitude), deltas.units) - for deltas in lat_lon_grid_deltas(longitude, latitude, x_dim=x_dim, y_dim=y_dim, - initstring=f.metpy.cartopy_crs.proj4_init)) + for deltas in lat_lon_grid_deltas( + longitude, + latitude, + x_dim=x_dim, + y_dim=y_dim, + initstring=f.metpy.cartopy_crs.proj4_init, + ) + ) else: # Obtain y/x coordinate differences - y, x = f.metpy.coordinates('y', 'x') + y, x = f.metpy.coordinates("y", "x") dx_var = x.diff(x.dims[0]).variable - dx_units = units(x.attrs.get('units')) + dx_units = units(x.attrs.get("units")) dy_var = y.diff(y.dims[0]).variable - dy_units = units(y.attrs.get('units')) + dy_units = units(y.attrs.get("units")) # Broadcast to input and attach units - dx = dx_var.set_dims(f.dims, shape=[dx_var.sizes[dim] if dim in dx_var.dims else 1 - for dim in f.dims]).data * dx_units - dy = dy_var.set_dims(f.dims, shape=[dy_var.sizes[dim] if dim in dy_var.dims else 1 - for dim in f.dims]).data * dy_units + dx = ( + dx_var.set_dims( + f.dims, shape=[dx_var.sizes[dim] if dim in dx_var.dims else 1 for dim in f.dims] + ).data + * dx_units + ) + dy = ( + dy_var.set_dims( + f.dims, shape=[dy_var.sizes[dim] if dim in dy_var.dims else 1 for dim in f.dims] + ).data + * dy_units + ) return dx, dy def add_grid_arguments_from_xarray(func): """Fill in optional arguments like dx/dy from DataArray arguments.""" + @functools.wraps(func) def wrapper(*args, **kwargs): bound_args = signature(func).bind(*args, **kwargs) @@ -1321,53 +1427,51 @@ def wrapper(*args, **kwargs): # Search for DataArray with valid latitude and longitude coordinates to find grid # deltas and any other needed parameter dataarray_arguments = [ - value for value in bound_args.arguments.values() - if isinstance(value, xr.DataArray) + value for value in bound_args.arguments.values() if isinstance(value, xr.DataArray) ] grid_prototype = None for da in dataarray_arguments: - if hasattr(da.metpy, 'latitude') and hasattr(da.metpy, 'longitude'): + if hasattr(da.metpy, "latitude") and hasattr(da.metpy, "longitude"): grid_prototype = da break # Fill in x_dim/y_dim if ( grid_prototype is not None - and 'x_dim' in bound_args.arguments - and 'y_dim' in bound_args.arguments + and "x_dim" in bound_args.arguments + and "y_dim" in bound_args.arguments ): try: - bound_args.arguments['x_dim'] = grid_prototype.metpy.find_axis_number('x') - bound_args.arguments['y_dim'] = grid_prototype.metpy.find_axis_number('y') + bound_args.arguments["x_dim"] = grid_prototype.metpy.find_axis_number("x") + bound_args.arguments["y_dim"] = grid_prototype.metpy.find_axis_number("y") except AttributeError: # If axis number not found, fall back to default but warn. - warnings.warn('Horizontal dimension numbers not found. Defaulting to ' - '(..., Y, X) order.') + warnings.warn( + "Horizontal dimension numbers not found. Defaulting to " + "(..., Y, X) order." + ) # Fill in vertical_dim - if ( - grid_prototype is not None - and 'vertical_dim' in bound_args.arguments - ): + if grid_prototype is not None and "vertical_dim" in bound_args.arguments: try: - bound_args.arguments['vertical_dim'] = ( - grid_prototype.metpy.find_axis_number('vertical') + bound_args.arguments["vertical_dim"] = grid_prototype.metpy.find_axis_number( + "vertical" ) except AttributeError: # If axis number not found, fall back to default but warn. warnings.warn( - 'Vertical dimension number not found. Defaulting to (..., Z, Y, X) order.' + "Vertical dimension number not found. Defaulting to (..., Z, Y, X) order." ) # Fill in dz if ( grid_prototype is not None - and 'dz' in bound_args.arguments - and bound_args.arguments['dz'] is None + and "dz" in bound_args.arguments + and bound_args.arguments["dz"] is None ): try: vertical_coord = grid_prototype.metpy.vertical - bound_args.arguments['dz'] = np.diff(vertical_coord.metpy.unit_array) + bound_args.arguments["dz"] = np.diff(vertical_coord.metpy.unit_array) except AttributeError: # Skip, since this only comes up in advection, where dz is optional (may not # need vertical at all) @@ -1375,40 +1479,47 @@ def wrapper(*args, **kwargs): # Fill in dx/dy if ( - 'dx' in bound_args.arguments and bound_args.arguments['dx'] is None - and 'dy' in bound_args.arguments and bound_args.arguments['dy'] is None + "dx" in bound_args.arguments + and bound_args.arguments["dx"] is None + and "dy" in bound_args.arguments + and bound_args.arguments["dy"] is None ): if grid_prototype is not None: - bound_args.arguments['dx'], bound_args.arguments['dy'] = ( - grid_deltas_from_dataarray(grid_prototype, kind='actual') - ) - elif 'dz' in bound_args.arguments: + ( + bound_args.arguments["dx"], + bound_args.arguments["dy"], + ) = grid_deltas_from_dataarray(grid_prototype, kind="actual") + elif "dz" in bound_args.arguments: # Handle advection case, allowing dx/dy to be None but dz to not be None - if bound_args.arguments['dz'] is None: + if bound_args.arguments["dz"] is None: raise ValueError( - 'Must provide dx, dy, and/or dz arguments or input DataArray with ' - 'proper coordinates.' + "Must provide dx, dy, and/or dz arguments or input DataArray with " + "proper coordinates." ) else: - raise ValueError('Must provide dx/dy arguments or input DataArray with ' - 'latitude/longitude coordinates.') + raise ValueError( + "Must provide dx/dy arguments or input DataArray with " + "latitude/longitude coordinates." + ) # Fill in latitude - if 'latitude' in bound_args.arguments and bound_args.arguments['latitude'] is None: + if "latitude" in bound_args.arguments and bound_args.arguments["latitude"] is None: if grid_prototype is not None: - bound_args.arguments['latitude'] = ( - grid_prototype.metpy.latitude - ) + bound_args.arguments["latitude"] = grid_prototype.metpy.latitude else: - raise ValueError('Must provide latitude argument or input DataArray with ' - 'latitude/longitude coordinates.') + raise ValueError( + "Must provide latitude argument or input DataArray with " + "latitude/longitude coordinates." + ) return func(*bound_args.args, **bound_args.kwargs) + return wrapper def add_vertical_dim_from_xarray(func): """Fill in optional vertical_dim from DataArray argument.""" + @functools.wraps(func) def wrapper(*args, **kwargs): bound_args = signature(func).bind(*args, **kwargs) @@ -1416,27 +1527,24 @@ def wrapper(*args, **kwargs): # Search for DataArray in arguments dataarray_arguments = [ - value for value in bound_args.arguments.values() - if isinstance(value, xr.DataArray) + value for value in bound_args.arguments.values() if isinstance(value, xr.DataArray) ] # Fill in vertical_dim - if ( - len(dataarray_arguments) > 0 - and 'vertical_dim' in bound_args.arguments - ): + if len(dataarray_arguments) > 0 and "vertical_dim" in bound_args.arguments: try: - bound_args.arguments['vertical_dim'] = ( - dataarray_arguments[0].metpy.find_axis_number('vertical') - ) + bound_args.arguments["vertical_dim"] = dataarray_arguments[ + 0 + ].metpy.find_axis_number("vertical") except AttributeError: # If axis number not found, fall back to default but warn. warnings.warn( - 'Vertical dimension number not found. Defaulting to initial dimension.' + "Vertical dimension number not found. Defaulting to initial dimension." ) return func(*bound_args.args, **bound_args.kwargs) + return wrapper -__all__ = ('MetPyDataArrayAccessor', 'MetPyDatasetAccessor', 'grid_deltas_from_dataarray') +__all__ = ("MetPyDataArrayAccessor", "MetPyDatasetAccessor", "grid_deltas_from_dataarray") diff --git a/tests/calc/test_basic.py b/tests/calc/test_basic.py index 98ca54b43ba..625a711aed8 100644 --- a/tests/calc/test_basic.py +++ b/tests/calc/test_basic.py @@ -8,22 +8,38 @@ import pytest import xarray as xr -from metpy.calc import (add_height_to_pressure, add_pressure_to_height, - altimeter_to_sea_level_pressure, altimeter_to_station_pressure, - apparent_temperature, coriolis_parameter, geopotential_to_height, - heat_index, height_to_geopotential, height_to_pressure_std, - pressure_to_height_std, sigma_to_pressure, smooth_circular, - smooth_gaussian, smooth_n_point, smooth_rectangular, smooth_window, - wind_components, wind_direction, wind_speed, windchill) -from metpy.testing import (assert_almost_equal, assert_array_almost_equal, assert_array_equal) +from metpy.calc import ( + add_height_to_pressure, + add_pressure_to_height, + altimeter_to_sea_level_pressure, + altimeter_to_station_pressure, + apparent_temperature, + coriolis_parameter, + geopotential_to_height, + heat_index, + height_to_geopotential, + height_to_pressure_std, + pressure_to_height_std, + sigma_to_pressure, + smooth_circular, + smooth_gaussian, + smooth_n_point, + smooth_rectangular, + smooth_window, + wind_components, + wind_direction, + wind_speed, + windchill, +) +from metpy.testing import assert_almost_equal, assert_array_almost_equal, assert_array_equal from metpy.units import units def test_wind_comps_basic(): """Test the basic wind component calculation.""" - speed = np.array([4, 4, 4, 4, 25, 25, 25, 25, 10.]) * units.mph + speed = np.array([4, 4, 4, 4, 25, 25, 25, 25, 10.0]) * units.mph dirs = np.array([0, 45, 90, 135, 180, 225, 270, 315, 360]) * units.deg - s2 = np.sqrt(2.) + s2 = np.sqrt(2.0) u, v = wind_components(speed, dirs) @@ -50,32 +66,32 @@ def test_wind_comps_with_north_and_calm(): def test_wind_comps_scalar(): """Test wind components calculation with scalars.""" - u, v = wind_components(8 * units('m/s'), 150 * units.deg) - assert_almost_equal(u, -4 * units('m/s'), 3) - assert_almost_equal(v, 6.9282 * units('m/s'), 3) + u, v = wind_components(8 * units("m/s"), 150 * units.deg) + assert_almost_equal(u, -4 * units("m/s"), 3) + assert_almost_equal(v, 6.9282 * units("m/s"), 3) def test_speed(): """Test calculating wind speed.""" - u = np.array([4., 2., 0., 0.]) * units('m/s') - v = np.array([0., 2., 4., 0.]) * units('m/s') + u = np.array([4.0, 2.0, 0.0, 0.0]) * units("m/s") + v = np.array([0.0, 2.0, 4.0, 0.0]) * units("m/s") speed = wind_speed(u, v) - s2 = np.sqrt(2.) - true_speed = np.array([4., 2 * s2, 4., 0.]) * units('m/s') + s2 = np.sqrt(2.0) + true_speed = np.array([4.0, 2 * s2, 4.0, 0.0]) * units("m/s") assert_array_almost_equal(true_speed, speed, 4) def test_direction(): """Test calculating wind direction.""" - u = np.array([4., 2., 0., 0.]) * units('m/s') - v = np.array([0., 2., 4., 0.]) * units('m/s') + u = np.array([4.0, 2.0, 0.0, 0.0]) * units("m/s") + v = np.array([0.0, 2.0, 4.0, 0.0]) * units("m/s") direc = wind_direction(u, v) - true_dir = np.array([270., 225., 180., 0.]) * units.deg + true_dir = np.array([270.0, 225.0, 180.0, 0.0]) * units.deg assert_array_almost_equal(true_dir, direc, 4) @@ -83,15 +99,15 @@ def test_direction(): def test_direction_masked(): """Test calculating wind direction from masked wind components.""" mask = np.array([True, False, True, False]) - u = np.array([4., 2., 0., 0.]) - v = np.array([0., 2., 4., 0.]) + u = np.array([4.0, 2.0, 0.0, 0.0]) + v = np.array([0.0, 2.0, 4.0, 0.0]) - u_masked = units.Quantity(np.ma.array(u, mask=mask), units('m/s')) - v_masked = units.Quantity(np.ma.array(v, mask=mask), units('m/s')) + u_masked = units.Quantity(np.ma.array(u, mask=mask), units("m/s")) + v_masked = units.Quantity(np.ma.array(v, mask=mask), units("m/s")) direc = wind_direction(u_masked, v_masked) - true_dir = np.array([270., 225., 180., 0.]) + true_dir = np.array([270.0, 225.0, 180.0, 0.0]) true_dir_masked = units.Quantity(np.ma.array(true_dir, mask=mask), units.deg) assert_array_almost_equal(true_dir_masked, direc, 4) @@ -99,25 +115,25 @@ def test_direction_masked(): def test_direction_with_north_and_calm(): """Test how wind direction handles northerly and calm winds.""" - u = np.array([0., -0., 0.]) * units('m/s') - v = np.array([0., 0., -5.]) * units('m/s') + u = np.array([0.0, -0.0, 0.0]) * units("m/s") + v = np.array([0.0, 0.0, -5.0]) * units("m/s") direc = wind_direction(u, v) - true_dir = np.array([0., 0., 360.]) * units.deg + true_dir = np.array([0.0, 0.0, 360.0]) * units.deg assert_array_almost_equal(true_dir, direc, 4) def test_direction_dimensions(): """Verify wind_direction returns degrees.""" - d = wind_direction(3. * units('m/s'), 4. * units('m/s')) - assert str(d.units) == 'degree' + d = wind_direction(3.0 * units("m/s"), 4.0 * units("m/s")) + assert str(d.units) == "degree" def test_oceanographic_direction(): """Test oceanographic direction (to) convention.""" - d = wind_direction(5 * units('m/s'), -5 * units('m/s'), convention='to') + d = wind_direction(5 * units("m/s"), -5 * units("m/s"), convention="to") true_dir = 135 * units.deg assert_almost_equal(d, true_dir, 4) @@ -125,14 +141,14 @@ def test_oceanographic_direction(): def test_invalid_direction_convention(): """Test the error that is returned if the convention kwarg is not valid.""" with pytest.raises(ValueError): - wind_direction(1 * units('m/s'), 5 * units('m/s'), convention='test') + wind_direction(1 * units("m/s"), 5 * units("m/s"), convention="test") def test_speed_direction_roundtrip(): """Test round-tripping between speed/direction and components.""" # Test each quadrant of the whole circle - wspd = np.array([15., 5., 2., 10.]) * units.meters / units.seconds - wdir = np.array([160., 30., 225., 350.]) * units.degrees + wspd = np.array([15.0, 5.0, 2.0, 10.0]) * units.meters / units.seconds + wdir = np.array([160.0, 30.0, 225.0, 350.0]) * units.degrees u, v = wind_components(wspd, wdir) @@ -145,19 +161,19 @@ def test_speed_direction_roundtrip(): def test_scalar_speed(): """Test wind speed with scalars.""" - s = wind_speed(-3. * units('m/s'), -4. * units('m/s')) - assert_almost_equal(s, 5. * units('m/s'), 3) + s = wind_speed(-3.0 * units("m/s"), -4.0 * units("m/s")) + assert_almost_equal(s, 5.0 * units("m/s"), 3) def test_scalar_direction(): """Test wind direction with scalars.""" - d = wind_direction(3. * units('m/s'), 4. * units('m/s')) + d = wind_direction(3.0 * units("m/s"), 4.0 * units("m/s")) assert_almost_equal(d, 216.870 * units.deg, 3) def test_windchill_scalar(): """Test wind chill with scalars.""" - wc = windchill(-5 * units.degC, 35 * units('m/s')) + wc = windchill(-5 * units.degC, 35 * units("m/s")) assert_almost_equal(wc, -18.9357 * units.degC, 0) @@ -173,7 +189,7 @@ def test_windchill_basic(): def test_windchill_kelvin(): """Test wind chill when given Kelvin temperatures.""" - wc = windchill(268.15 * units.kelvin, 35 * units('m/s')) + wc = windchill(268.15 * units.kelvin, 35 * units("m/s")) assert_almost_equal(wc, -18.9357 * units.degC, 0) @@ -184,8 +200,13 @@ def test_windchill_invalid(): wc = windchill(temp, speed) # We don't care about the masked values - truth = units.Quantity(np.ma.array([2.6230789, np.nan, np.nan, np.nan, np.nan, np.nan], - mask=[False, True, True, True, True, True]), units.degF) + truth = units.Quantity( + np.ma.array( + [2.6230789, np.nan, np.nan, np.nan, np.nan, np.nan], + mask=[False, True, True, True, True, True], + ), + units.degF, + ) assert_array_almost_equal(truth, wc) @@ -247,18 +268,18 @@ def test_heat_index_undefined_flag(): def test_heat_index_units(): """Test units coming out of heat index.""" - temp = units.Quantity([35., 20.], units.degC) + temp = units.Quantity([35.0, 20.0], units.degC) rh = 70 * units.percent hi = heat_index(temp, rh) - assert_almost_equal(hi.to('degC'), units.Quantity([50.3405, np.nan], units.degC), 4) + assert_almost_equal(hi.to("degC"), units.Quantity([50.3405, np.nan], units.degC), 4) def test_heat_index_ratio(): """Test giving humidity as number [0, 1] to heat index.""" - temp = units.Quantity([35., 20.], units.degC) + temp = units.Quantity([35.0, 20.0], units.degC) rh = 0.7 hi = heat_index(temp, rh) - assert_almost_equal(hi.to('degC'), units.Quantity([50.3405, np.nan], units.degC), 4) + assert_almost_equal(hi.to("degC"), units.Quantity([50.3405, np.nan], units.degC), 4) def test_heat_index_vs_nws(): @@ -267,8 +288,9 @@ def test_heat_index_vs_nws(): temp = units.Quantity(np.array([86, 111, 40, 96]), units.degF) rh = np.ma.array([45, 27, 99, 60]) * units.percent hi = heat_index(temp, rh) - truth = units.Quantity(np.ma.array([87, 121, 40, 116], mask=[False, False, True, False]), - units.degF) + truth = units.Quantity( + np.ma.array([87, 121, 40, 116], mask=[False, False, True, False]), units.degF + ) assert_array_almost_equal(hi, truth, 0) @@ -278,31 +300,49 @@ def test_heat_index_kelvin(): rh = 0.7 hi = heat_index(temp, rh) # NB rounded up test value here vs the above two tests - assert_almost_equal(hi.to('degC'), 50.3406 * units.degC, 4) + assert_almost_equal(hi.to("degC"), 50.3406 * units.degC, 4) def test_height_to_geopotential(): """Test conversion from height to geopotential.""" height = units.Quantity([0, 1000, 2000, 3000], units.m) geopot = height_to_geopotential(height) - assert_array_almost_equal(geopot, units.Quantity([0., 9805, 19607, - 29406], units('m**2 / second**2')), 0) + assert_array_almost_equal( + geopot, units.Quantity([0.0, 9805, 19607, 29406], units("m**2 / second**2")), 0 + ) # See #1075 regarding previous destructive cancellation in floating point def test_height_to_geopotential_32bit(): """Test conversion to geopotential with 32-bit values.""" heights = np.linspace(20597, 20598, 11, dtype=np.float32) * units.m - truth = np.array([201336.67, 201337.66, 201338.62, 201339.61, 201340.58, 201341.56, - 201342.53, 201343.52, 201344.48, 201345.44, 201346.42], - dtype=np.float32) * units('J/kg') + truth = ( + np.array( + [ + 201336.67, + 201337.66, + 201338.62, + 201339.61, + 201340.58, + 201341.56, + 201342.53, + 201343.52, + 201344.48, + 201345.44, + 201346.42, + ], + dtype=np.float32, + ) + * units("J/kg") + ) assert_almost_equal(height_to_geopotential(heights), truth, 2) def test_geopotential_to_height(): """Test conversion from geopotential to height.""" - geopotential = units.Quantity([0., 9805.11102602, 19607.14506998, 29406.10358006], - units('m**2 / second**2')) + geopotential = units.Quantity( + [0.0, 9805.11102602, 19607.14506998, 29406.10358006], units("m**2 / second**2") + ) height = geopotential_to_height(geopotential) assert_array_almost_equal(height, units.Quantity([0, 1000, 2000, 3000], units.m), 0) @@ -310,51 +350,68 @@ def test_geopotential_to_height(): # See #1075 regarding previous destructive cancellation in floating point def test_geopotential_to_height_32bit(): """Test conversion from geopotential to height with 32-bit values.""" - geopot = np.arange(201590, 201600, dtype=np.float32) * units('J/kg') - truth = np.array([20623.000, 20623.102, 20623.203, 20623.307, 20623.408, - 20623.512, 20623.615, 20623.717, 20623.820, 20623.924], - dtype=np.float32) * units.m + geopot = np.arange(201590, 201600, dtype=np.float32) * units("J/kg") + truth = ( + np.array( + [ + 20623.000, + 20623.102, + 20623.203, + 20623.307, + 20623.408, + 20623.512, + 20623.615, + 20623.717, + 20623.820, + 20623.924, + ], + dtype=np.float32, + ) + * units.m + ) assert_almost_equal(geopotential_to_height(geopot), truth, 2) -# class TestIrrad(object): -# def test_basic(self): -# 'Test the basic solar irradiance calculation.' -# from datetime import date +class TestIrrad(object): + def test_basic(self): + "Test the basic solar irradiance calculation." + from datetime import date + + d = date(2008, 9, 28) + lat = 35.25 + hours = np.linspace(6, 18, 10) -# d = date(2008, 9, 28) -# lat = 35.25 -# hours = np.linspace(6,18,10) + s = solar_irradiance(lat, d, hours) + values = np.array([0.0, 344.1, 682.6, 933.9, 1067.6, 1067.6, 933.9, 682.6, 344.1, 0.0]) + assert_array_almost_equal(s, values, 1) -# s = solar_irradiance(lat, d, hours) -# values = np.array([0., 344.1, 682.6, 933.9, 1067.6, 1067.6, 933.9, -# 682.6, 344.1, 0.]) -# assert_array_almost_equal(s, values, 1) + def test_scalar(self): + from datetime import date -# def test_scalar(self): -# from datetime import date -# d = date(2008, 9, 28) -# lat = 35.25 -# hour = 9.5 -# s = solar_irradiance(lat, d, hour) -# assert_almost_equal(s, 852.1, 1) + d = date(2008, 9, 28) + lat = 35.25 + hour = 9.5 + s = solar_irradiance(lat, d, hour) + assert_almost_equal(s, 852.1, 1) -# def test_invalid(self): -# 'Test for values that should be masked.' -# from datetime import date -# d = date(2008, 9, 28) -# lat = 35.25 -# hours = np.linspace(0,22,12) -# s = solar_irradiance(lat, d, hours) + def test_invalid(self): + "Test for values that should be masked." + from datetime import date -# mask = np.array([ True, True, True, True, False, False, False, -# False, False, True, True, True]) -# assert_array_equal(s.mask, mask) + d = date(2008, 9, 28) + lat = 35.25 + hours = np.linspace(0, 22, 12) + s = solar_irradiance(lat, d, hours) + + mask = np.array( + [True, True, True, True, False, False, False, False, False, True, True, True] + ) + assert_array_equal(s.mask, mask) def test_pressure_to_heights_basic(): """Test basic pressure to height calculation for standard atmosphere.""" - pressures = np.array([975.2, 987.5, 956., 943.]) * units.mbar + pressures = np.array([975.2, 987.5, 956.0, 943.0]) * units.mbar heights = pressure_to_height_std(pressures) values = np.array([321.5, 216.5, 487.6, 601.7]) * units.meter assert_almost_equal(heights, values, 1) @@ -364,7 +421,7 @@ def test_heights_to_pressure_basic(): """Test basic height to pressure calculation for standard atmosphere.""" heights = np.array([321.5, 216.5, 487.6, 601.7]) * units.meter pressures = height_to_pressure_std(heights) - values = np.array([975.2, 987.5, 956., 943.]) * units.mbar + values = np.array([975.2, 987.5, 956.0, 943.0]) * units.mbar assert_almost_equal(pressures, values, 1) @@ -375,10 +432,11 @@ def test_pressure_to_heights_units(): def test_coriolis_force(): """Test basic coriolis force calculation.""" - lat = np.array([-90., -30., 0., 30., 90.]) * units.degrees + lat = np.array([-90.0, -30.0, 0.0, 30.0, 90.0]) * units.degrees cor = coriolis_parameter(lat) - values = np.array([-1.4584232E-4, -.72921159E-4, 0, .72921159E-4, - 1.4584232E-4]) * units('s^-1') + values = np.array([-1.4584232e-4, -0.72921159e-4, 0, 0.72921159e-4, 1.4584232e-4]) * units( + "s^-1" + ) assert_almost_equal(cor, values, 7) @@ -396,10 +454,10 @@ def test_add_pressure_to_height(): def test_sigma_to_pressure(): """Test sigma_to_pressure.""" - surface_pressure = 1000. * units.hPa - model_top_pressure = 0. * units.hPa - sigma = np.arange(0., 1.1, 0.1) - expected = np.arange(0., 1100., 100.) * units.hPa + surface_pressure = 1000.0 * units.hPa + model_top_pressure = 0.0 * units.hPa + sigma = np.arange(0.0, 1.1, 0.1) + expected = np.arange(0.0, 1100.0, 100.0) * units.hPa pressure = sigma_to_pressure(sigma, surface_pressure, model_top_pressure) assert_array_almost_equal(pressure, expected, 5) @@ -407,7 +465,7 @@ def test_sigma_to_pressure(): def test_warning_dir(): """Test that warning is raised wind direction > 2Pi.""" with pytest.warns(UserWarning): - wind_components(3. * units('m/s'), 270) + wind_components(3.0 * units("m/s"), 270) def test_coriolis_warning(): @@ -421,20 +479,21 @@ def test_coriolis_warning(): def test_coriolis_units(): """Test that coriolis returns units of 1/second.""" f = coriolis_parameter(50 * units.degrees) - assert f.units == units('1/second') + assert f.units == units("1/second") def test_apparent_temperature(): """Test the apparent temperature calculation.""" - temperature = np.array([[90, 90, 70], - [20, 20, 60]]) * units.degF - rel_humidity = np.array([[60, 20, 60], - [10, 10, 10]]) * units.percent - wind = np.array([[5, 3, 3], - [10, 1, 10]]) * units.mph - truth = units.Quantity(np.ma.array([[99.6777178, 86.3357671, 70], [8.8140662, 20, 60]], - mask=[[False, False, True], [False, True, True]]), - units.degF) + temperature = np.array([[90, 90, 70], [20, 20, 60]]) * units.degF + rel_humidity = np.array([[60, 20, 60], [10, 10, 10]]) * units.percent + wind = np.array([[5, 3, 3], [10, 1, 10]]) * units.mph + truth = units.Quantity( + np.ma.array( + [[99.6777178, 86.3357671, 70], [8.8140662, 20, 60]], + mask=[[False, False, True], [False, True, True]], + ), + units.degF, + ) res = apparent_temperature(temperature, rel_humidity, wind) assert_array_almost_equal(res, truth, 6) @@ -461,9 +520,9 @@ def test_apparent_temperature_scalar_no_modification(): def test_apparent_temperature_windchill(): """Test that apparent temperature works when a windchill is calculated.""" - temperature = -5. * units.degC - rel_humidity = 50. * units.percent - wind = 35. * units('m/s') + temperature = -5.0 * units.degC + rel_humidity = 50.0 * units.percent + wind = 35.0 * units("m/s") truth = -18.9357 * units.degC res = apparent_temperature(temperature, rel_humidity, wind) assert_almost_equal(res, truth, 0) @@ -473,17 +532,17 @@ def test_apparent_temperature_mask_undefined_false(): """Test that apparent temperature works when mask_undefined is False.""" temp = np.array([80, 55, 10]) * units.degF rh = np.array([40, 50, 25]) * units.percent - wind = np.array([5, 4, 10]) * units('m/s') + wind = np.array([5, 4, 10]) * units("m/s") app_temperature = apparent_temperature(temp, rh, wind, mask_undefined=False) - assert not hasattr(app_temperature, 'mask') + assert not hasattr(app_temperature, "mask") def test_apparent_temperature_mask_undefined_true(): """Test that apparent temperature works when mask_undefined is True.""" temp = np.array([80, 55, 10]) * units.degF rh = np.array([40, 50, 25]) * units.percent - wind = np.array([5, 4, 10]) * units('m/s') + wind = np.array([5, 4, 10]) * units("m/s") app_temperature = apparent_temperature(temp, rh, wind, mask_undefined=True) mask = [False, True, False] @@ -495,28 +554,132 @@ def test_smooth_gaussian(): m = 10 s = np.zeros((m, m)) for i in np.ndindex(s.shape): - s[i] = i[0] + i[1]**2 + s[i] = i[0] + i[1] ** 2 s = smooth_gaussian(s, 4) - s_true = np.array([[0.40077472, 1.59215426, 4.59665817, 9.59665817, 16.59665817, - 25.59665817, 36.59665817, 49.59665817, 64.51108392, 77.87487258], - [1.20939518, 2.40077472, 5.40527863, 10.40527863, 17.40527863, - 26.40527863, 37.40527863, 50.40527863, 65.31970438, 78.68349304], - [2.20489127, 3.39627081, 6.40077472, 11.40077472, 18.40077472, - 27.40077472, 38.40077472, 51.40077472, 66.31520047, 79.67898913], - [3.20489127, 4.39627081, 7.40077472, 12.40077472, 19.40077472, - 28.40077472, 39.40077472, 52.40077472, 67.31520047, 80.67898913], - [4.20489127, 5.39627081, 8.40077472, 13.40077472, 20.40077472, - 29.40077472, 40.40077472, 53.40077472, 68.31520047, 81.67898913], - [5.20489127, 6.39627081, 9.40077472, 14.40077472, 21.40077472, - 30.40077472, 41.40077472, 54.40077472, 69.31520047, 82.67898913], - [6.20489127, 7.39627081, 10.40077472, 15.40077472, 22.40077472, - 31.40077472, 42.40077472, 55.40077472, 70.31520047, 83.67898913], - [7.20489127, 8.39627081, 11.40077472, 16.40077472, 23.40077472, - 32.40077472, 43.40077472, 56.40077472, 71.31520047, 84.67898913], - [8.20038736, 9.3917669, 12.39627081, 17.39627081, 24.39627081, - 33.39627081, 44.39627081, 57.39627081, 72.31069656, 85.67448522], - [9.00900782, 10.20038736, 13.20489127, 18.20489127, 25.20489127, - 34.20489127, 45.20489127, 58.20489127, 73.11931702, 86.48310568]]) + s_true = np.array( + [ + [ + 0.40077472, + 1.59215426, + 4.59665817, + 9.59665817, + 16.59665817, + 25.59665817, + 36.59665817, + 49.59665817, + 64.51108392, + 77.87487258, + ], + [ + 1.20939518, + 2.40077472, + 5.40527863, + 10.40527863, + 17.40527863, + 26.40527863, + 37.40527863, + 50.40527863, + 65.31970438, + 78.68349304, + ], + [ + 2.20489127, + 3.39627081, + 6.40077472, + 11.40077472, + 18.40077472, + 27.40077472, + 38.40077472, + 51.40077472, + 66.31520047, + 79.67898913, + ], + [ + 3.20489127, + 4.39627081, + 7.40077472, + 12.40077472, + 19.40077472, + 28.40077472, + 39.40077472, + 52.40077472, + 67.31520047, + 80.67898913, + ], + [ + 4.20489127, + 5.39627081, + 8.40077472, + 13.40077472, + 20.40077472, + 29.40077472, + 40.40077472, + 53.40077472, + 68.31520047, + 81.67898913, + ], + [ + 5.20489127, + 6.39627081, + 9.40077472, + 14.40077472, + 21.40077472, + 30.40077472, + 41.40077472, + 54.40077472, + 69.31520047, + 82.67898913, + ], + [ + 6.20489127, + 7.39627081, + 10.40077472, + 15.40077472, + 22.40077472, + 31.40077472, + 42.40077472, + 55.40077472, + 70.31520047, + 83.67898913, + ], + [ + 7.20489127, + 8.39627081, + 11.40077472, + 16.40077472, + 23.40077472, + 32.40077472, + 43.40077472, + 56.40077472, + 71.31520047, + 84.67898913, + ], + [ + 8.20038736, + 9.3917669, + 12.39627081, + 17.39627081, + 24.39627081, + 33.39627081, + 44.39627081, + 57.39627081, + 72.31069656, + 85.67448522, + ], + [ + 9.00900782, + 10.20038736, + 13.20489127, + 18.20489127, + 25.20489127, + 34.20489127, + 45.20489127, + 58.20489127, + 73.11931702, + 86.48310568, + ], + ] + ) assert_array_almost_equal(s, s_true) @@ -525,13 +688,15 @@ def test_smooth_gaussian_small_n(): m = 5 s = np.zeros((m, m)) for i in np.ndindex(s.shape): - s[i] = i[0] + i[1]**2 + s[i] = i[0] + i[1] ** 2 s = smooth_gaussian(s, 1) - s_true = [[0.0141798077, 1.02126971, 4.02126971, 9.02126971, 15.9574606], - [1.00708990, 2.01417981, 5.01417981, 10.0141798, 16.9503707], - [2.00708990, 3.01417981, 6.01417981, 11.0141798, 17.9503707], - [3.00708990, 4.01417981, 7.01417981, 12.0141798, 18.9503707], - [4.00000000, 5.00708990, 8.00708990, 13.0070899, 19.9432808]] + s_true = [ + [0.0141798077, 1.02126971, 4.02126971, 9.02126971, 15.9574606], + [1.00708990, 2.01417981, 5.01417981, 10.0141798, 16.9503707], + [2.00708990, 3.01417981, 6.01417981, 11.0141798, 17.9503707], + [3.00708990, 4.01417981, 7.01417981, 12.0141798, 18.9503707], + [4.00000000, 5.00708990, 8.00708990, 13.0070899, 19.9432808], + ] assert_array_almost_equal(s, s_true) @@ -540,215 +705,331 @@ def test_smooth_gaussian_3d_units(): m = 5 s = np.zeros((3, m, m)) for i in np.ndindex(s.shape): - s[i] = i[1] + i[2]**2 + s[i] = i[1] + i[2] ** 2 s[0::2, :, :] = 10 * s[0::2, :, :] - s = s * units('m') + s = s * units("m") s = smooth_gaussian(s, 1) - s_true = ([[0.0141798077, 1.02126971, 4.02126971, 9.02126971, 15.9574606], - [1.00708990, 2.01417981, 5.01417981, 10.0141798, 16.9503707], - [2.00708990, 3.01417981, 6.01417981, 11.0141798, 17.9503707], - [3.00708990, 4.01417981, 7.01417981, 12.0141798, 18.9503707], - [4.00000000, 5.00708990, 8.00708990, 13.0070899, 19.9432808]]) * units('m') + s_true = ( + [ + [0.0141798077, 1.02126971, 4.02126971, 9.02126971, 15.9574606], + [1.00708990, 2.01417981, 5.01417981, 10.0141798, 16.9503707], + [2.00708990, 3.01417981, 6.01417981, 11.0141798, 17.9503707], + [3.00708990, 4.01417981, 7.01417981, 12.0141798, 18.9503707], + [4.00000000, 5.00708990, 8.00708990, 13.0070899, 19.9432808], + ] + ) * units("m") assert_array_almost_equal(s[1, :, :], s_true) def test_smooth_n_pt_5(): """Test the smooth_n_pt function using 5 points.""" - hght = np.array([[5640., 5640., 5640., 5640., 5640.], - [5684., 5676., 5666., 5659., 5651.], - [5728., 5712., 5692., 5678., 5662.], - [5772., 5748., 5718., 5697., 5673.], - [5816., 5784., 5744., 5716., 5684.]]) + hght = np.array( + [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5676.0, 5666.0, 5659.0, 5651.0], + [5728.0, 5712.0, 5692.0, 5678.0, 5662.0], + [5772.0, 5748.0, 5718.0, 5697.0, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ] + ) shght = smooth_n_point(hght, 5, 1) - s_true = np.array([[5640., 5640., 5640., 5640., 5640.], - [5684., 5675.75, 5666.375, 5658.875, 5651.], - [5728., 5711.5, 5692.75, 5677.75, 5662.], - [5772., 5747.25, 5719.125, 5696.625, 5673.], - [5816., 5784., 5744., 5716., 5684.]]) + s_true = np.array( + [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5675.75, 5666.375, 5658.875, 5651.0], + [5728.0, 5711.5, 5692.75, 5677.75, 5662.0], + [5772.0, 5747.25, 5719.125, 5696.625, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ] + ) assert_array_almost_equal(shght, s_true) def test_smooth_n_pt_5_units(): """Test the smooth_n_pt function using 5 points with units.""" - hght = np.array([[5640., 5640., 5640., 5640., 5640.], - [5684., 5676., 5666., 5659., 5651.], - [5728., 5712., 5692., 5678., 5662.], - [5772., 5748., 5718., 5697., 5673.], - [5816., 5784., 5744., 5716., 5684.]]) * units.meter + hght = ( + np.array( + [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5676.0, 5666.0, 5659.0, 5651.0], + [5728.0, 5712.0, 5692.0, 5678.0, 5662.0], + [5772.0, 5748.0, 5718.0, 5697.0, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ] + ) + * units.meter + ) shght = smooth_n_point(hght, 5, 1) - s_true = np.array([[5640., 5640., 5640., 5640., 5640.], - [5684., 5675.75, 5666.375, 5658.875, 5651.], - [5728., 5711.5, 5692.75, 5677.75, 5662.], - [5772., 5747.25, 5719.125, 5696.625, 5673.], - [5816., 5784., 5744., 5716., 5684.]]) * units.meter + s_true = ( + np.array( + [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5675.75, 5666.375, 5658.875, 5651.0], + [5728.0, 5711.5, 5692.75, 5677.75, 5662.0], + [5772.0, 5747.25, 5719.125, 5696.625, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ] + ) + * units.meter + ) assert_array_almost_equal(shght, s_true) def test_smooth_n_pt_9_units(): """Test the smooth_n_pt function using 9 points with units.""" - hght = np.array([[5640., 5640., 5640., 5640., 5640.], - [5684., 5676., 5666., 5659., 5651.], - [5728., 5712., 5692., 5678., 5662.], - [5772., 5748., 5718., 5697., 5673.], - [5816., 5784., 5744., 5716., 5684.]]) * units.meter + hght = ( + np.array( + [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5676.0, 5666.0, 5659.0, 5651.0], + [5728.0, 5712.0, 5692.0, 5678.0, 5662.0], + [5772.0, 5748.0, 5718.0, 5697.0, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ] + ) + * units.meter + ) shght = smooth_n_point(hght, 9, 1) - s_true = np.array([[5640., 5640., 5640., 5640., 5640.], - [5684., 5675.5, 5666.75, 5658.75, 5651.], - [5728., 5711., 5693.5, 5677.5, 5662.], - [5772., 5746.5, 5720.25, 5696.25, 5673.], - [5816., 5784., 5744., 5716., 5684.]]) * units.meter + s_true = ( + np.array( + [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5675.5, 5666.75, 5658.75, 5651.0], + [5728.0, 5711.0, 5693.5, 5677.5, 5662.0], + [5772.0, 5746.5, 5720.25, 5696.25, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ] + ) + * units.meter + ) assert_array_almost_equal(shght, s_true) def test_smooth_n_pt_9_repeat(): """Test the smooth_n_pt function using 9 points with two passes.""" - hght = np.array([[5640., 5640., 5640., 5640., 5640.], - [5684., 5676., 5666., 5659., 5651.], - [5728., 5712., 5692., 5678., 5662.], - [5772., 5748., 5718., 5697., 5673.], - [5816., 5784., 5744., 5716., 5684.]]) + hght = np.array( + [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5676.0, 5666.0, 5659.0, 5651.0], + [5728.0, 5712.0, 5692.0, 5678.0, 5662.0], + [5772.0, 5748.0, 5718.0, 5697.0, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ] + ) shght = smooth_n_point(hght, 9, 2) - s_true = np.array([[5640., 5640., 5640., 5640., 5640.], - [5684., 5675.4375, 5666.9375, 5658.8125, 5651.], - [5728., 5710.875, 5693.875, 5677.625, 5662.], - [5772., 5746.375, 5720.625, 5696.375, 5673.], - [5816., 5784., 5744., 5716., 5684.]]) + s_true = np.array( + [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5675.4375, 5666.9375, 5658.8125, 5651.0], + [5728.0, 5710.875, 5693.875, 5677.625, 5662.0], + [5772.0, 5746.375, 5720.625, 5696.375, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ] + ) assert_array_almost_equal(shght, s_true) def test_smooth_n_pt_wrong_number(): """Test the smooth_n_pt function using wrong number of points.""" - hght = np.array([[5640., 5640., 5640., 5640., 5640.], - [5684., 5676., 5666., 5659., 5651.], - [5728., 5712., 5692., 5678., 5662.], - [5772., 5748., 5718., 5697., 5673.], - [5816., 5784., 5744., 5716., 5684.]]) + hght = np.array( + [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5676.0, 5666.0, 5659.0, 5651.0], + [5728.0, 5712.0, 5692.0, 5678.0, 5662.0], + [5772.0, 5748.0, 5718.0, 5697.0, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ] + ) with pytest.raises(ValueError): smooth_n_point(hght, 7) def test_smooth_n_pt_3d_units(): """Test the smooth_n_point function with a 3D array with units.""" - hght = [[[5640.0, 5640.0, 5640.0, 5640.0, 5640.0], - [5684.0, 5676.0, 5666.0, 5659.0, 5651.0], - [5728.0, 5712.0, 5692.0, 5678.0, 5662.0], - [5772.0, 5748.0, 5718.0, 5697.0, 5673.0], - [5816.0, 5784.0, 5744.0, 5716.0, 5684.0]], - [[6768.0, 6768.0, 6768.0, 6768.0, 6768.0], - [6820.8, 6811.2, 6799.2, 6790.8, 6781.2], - [6873.6, 6854.4, 6830.4, 6813.6, 6794.4], - [6926.4, 6897.6, 6861.6, 6836.4, 6807.6], - [6979.2, 6940.8, 6892.8, 6859.2, 6820.8]]] * units.m + hght = [ + [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5676.0, 5666.0, 5659.0, 5651.0], + [5728.0, 5712.0, 5692.0, 5678.0, 5662.0], + [5772.0, 5748.0, 5718.0, 5697.0, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ], + [ + [6768.0, 6768.0, 6768.0, 6768.0, 6768.0], + [6820.8, 6811.2, 6799.2, 6790.8, 6781.2], + [6873.6, 6854.4, 6830.4, 6813.6, 6794.4], + [6926.4, 6897.6, 6861.6, 6836.4, 6807.6], + [6979.2, 6940.8, 6892.8, 6859.2, 6820.8], + ], + ] * units.m shght = smooth_n_point(hght, 9, 2) - s_true = [[[5640., 5640., 5640., 5640., 5640.], - [5684., 5675.4375, 5666.9375, 5658.8125, 5651.], - [5728., 5710.875, 5693.875, 5677.625, 5662.], - [5772., 5746.375, 5720.625, 5696.375, 5673.], - [5816., 5784., 5744., 5716., 5684.]], - [[6768., 6768., 6768., 6768., 6768.], - [6820.8, 6810.525, 6800.325, 6790.575, 6781.2], - [6873.6, 6853.05, 6832.65, 6813.15, 6794.4], - [6926.4, 6895.65, 6864.75, 6835.65, 6807.6], - [6979.2, 6940.8, 6892.8, 6859.2, 6820.8]]] * units.m + s_true = [ + [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5675.4375, 5666.9375, 5658.8125, 5651.0], + [5728.0, 5710.875, 5693.875, 5677.625, 5662.0], + [5772.0, 5746.375, 5720.625, 5696.375, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ], + [ + [6768.0, 6768.0, 6768.0, 6768.0, 6768.0], + [6820.8, 6810.525, 6800.325, 6790.575, 6781.2], + [6873.6, 6853.05, 6832.65, 6813.15, 6794.4], + [6926.4, 6895.65, 6864.75, 6835.65, 6807.6], + [6979.2, 6940.8, 6892.8, 6859.2, 6820.8], + ], + ] * units.m assert_array_almost_equal(shght, s_true) def test_smooth_n_pt_temperature(): """Test the smooth_n_pt function with temperature units.""" - t = np.array([[2.73, 3.43, 6.53, 7.13, 4.83], - [3.73, 4.93, 6.13, 6.63, 8.23], - [3.03, 4.83, 6.03, 7.23, 7.63], - [3.33, 4.63, 7.23, 6.73, 6.23], - [3.93, 3.03, 7.43, 9.23, 9.23]]) * units.degC + t = ( + np.array( + [ + [2.73, 3.43, 6.53, 7.13, 4.83], + [3.73, 4.93, 6.13, 6.63, 8.23], + [3.03, 4.83, 6.03, 7.23, 7.63], + [3.33, 4.63, 7.23, 6.73, 6.23], + [3.93, 3.03, 7.43, 9.23, 9.23], + ] + ) + * units.degC + ) smooth_t = smooth_n_point(t, 9, 1) - smooth_t_true = np.array([[2.73, 3.43, 6.53, 7.13, 4.83], - [3.73, 4.6425, 5.96125, 6.81124, 8.23], - [3.03, 4.81125, 6.1175, 6.92375, 7.63], - [3.33, 4.73625, 6.43, 7.3175, 6.23], - [3.93, 3.03, 7.43, 9.23, 9.23]]) * units.degC + smooth_t_true = ( + np.array( + [ + [2.73, 3.43, 6.53, 7.13, 4.83], + [3.73, 4.6425, 5.96125, 6.81124, 8.23], + [3.03, 4.81125, 6.1175, 6.92375, 7.63], + [3.33, 4.73625, 6.43, 7.3175, 6.23], + [3.93, 3.03, 7.43, 9.23, 9.23], + ] + ) + * units.degC + ) assert_array_almost_equal(smooth_t, smooth_t_true, 4) def test_smooth_gaussian_temperature(): """Test the smooth_gaussian function with temperature units.""" - t = np.array([[2.73, 3.43, 6.53, 7.13, 4.83], - [3.73, 4.93, 6.13, 6.63, 8.23], - [3.03, 4.83, 6.03, 7.23, 7.63], - [3.33, 4.63, 7.23, 6.73, 6.23], - [3.93, 3.03, 7.43, 9.23, 9.23]]) * units.degC + t = ( + np.array( + [ + [2.73, 3.43, 6.53, 7.13, 4.83], + [3.73, 4.93, 6.13, 6.63, 8.23], + [3.03, 4.83, 6.03, 7.23, 7.63], + [3.33, 4.63, 7.23, 6.73, 6.23], + [3.93, 3.03, 7.43, 9.23, 9.23], + ] + ) + * units.degC + ) smooth_t = smooth_gaussian(t, 3) - smooth_t_true = np.array([[2.8892, 3.7657, 6.2805, 6.8532, 5.3174], - [3.6852, 4.799, 6.0844, 6.7816, 7.7617], - [3.2762, 4.787, 6.117, 7.0792, 7.5181], - [3.4618, 4.6384, 6.886, 6.982, 6.6653], - [3.8115, 3.626, 7.1705, 8.8528, 8.9605]]) * units.degC + smooth_t_true = ( + np.array( + [ + [2.8892, 3.7657, 6.2805, 6.8532, 5.3174], + [3.6852, 4.799, 6.0844, 6.7816, 7.7617], + [3.2762, 4.787, 6.117, 7.0792, 7.5181], + [3.4618, 4.6384, 6.886, 6.982, 6.6653], + [3.8115, 3.626, 7.1705, 8.8528, 8.9605], + ] + ) + * units.degC + ) assert_array_almost_equal(smooth_t, smooth_t_true, 4) def test_smooth_window(): """Test smooth_window with default configuration.""" - hght = [[5640., 5640., 5640., 5640., 5640.], - [5684., 5676., 5666., 5659., 5651.], - [5728., 5712., 5692., 5678., 5662.], - [5772., 5748., 5718., 5697., 5673.], - [5816., 5784., 5744., 5716., 5684.]] * units.m + hght = [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5676.0, 5666.0, 5659.0, 5651.0], + [5728.0, 5712.0, 5692.0, 5678.0, 5662.0], + [5772.0, 5748.0, 5718.0, 5697.0, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ] * units.m smoothed = smooth_window(hght, np.array([[1, 0, 1], [0, 0, 0], [1, 0, 1]])) - truth = [[5640., 5640., 5640., 5640., 5640.], - [5684., 5675., 5667.5, 5658.5, 5651.], - [5728., 5710., 5695., 5677., 5662.], - [5772., 5745., 5722.5, 5695.5, 5673.], - [5816., 5784., 5744., 5716., 5684.]] * units.m + truth = [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5675.0, 5667.5, 5658.5, 5651.0], + [5728.0, 5710.0, 5695.0, 5677.0, 5662.0], + [5772.0, 5745.0, 5722.5, 5695.5, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ] * units.m assert_array_almost_equal(smoothed, truth) def test_smooth_window_1d_dataarray(): """Test smooth_window on 1D DataArray.""" temperature = xr.DataArray( - [37., 32., 34., 29., 28., 24., 26., 24., 27., 30.], - dims=('time',), - coords={'time': pd.date_range('2020-01-01', periods=10, freq='H')}, - attrs={'units': 'degF'}) + [37.0, 32.0, 34.0, 29.0, 28.0, 24.0, 26.0, 24.0, 27.0, 30.0], + dims=("time",), + coords={"time": pd.date_range("2020-01-01", periods=10, freq="H")}, + attrs={"units": "degF"}, + ) smoothed = smooth_window(temperature, window=np.ones(3) / 3, normalize_weights=False) truth = xr.DataArray( - [37., 34.33333333, 31.66666667, 30.33333333, 27., 26., 24.66666667, - 25.66666667, 27., 30.] * units.degF, - dims=('time',), - coords={'time': pd.date_range('2020-01-01', periods=10, freq='H')} + [ + 37.0, + 34.33333333, + 31.66666667, + 30.33333333, + 27.0, + 26.0, + 24.66666667, + 25.66666667, + 27.0, + 30.0, + ] + * units.degF, + dims=("time",), + coords={"time": pd.date_range("2020-01-01", periods=10, freq="H")}, ) xr.testing.assert_allclose(smoothed, truth) def test_smooth_rectangular(): """Test smooth_rectangular with default configuration.""" - hght = [[5640., 5640., 5640., 5640., 5640.], - [5684., 5676., 5666., 5659., 5651.], - [5728., 5712., 5692., 5678., 5662.], - [5772., 5748., 5718., 5697., 5673.], - [5816., 5784., 5744., 5716., 5684.]] * units.m + hght = [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5676.0, 5666.0, 5659.0, 5651.0], + [5728.0, 5712.0, 5692.0, 5678.0, 5662.0], + [5772.0, 5748.0, 5718.0, 5697.0, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ] * units.m smoothed = smooth_rectangular(hght, (5, 3)) - truth = [[5640., 5640., 5640., 5640., 5640.], - [5684., 5676., 5666., 5659., 5651.], - [5728., 5710.66667, 5694., 5677.33333, 5662.], - [5772., 5748., 5718., 5697., 5673.], - [5816., 5784., 5744., 5716., 5684.]] * units.m + truth = [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5676.0, 5666.0, 5659.0, 5651.0], + [5728.0, 5710.66667, 5694.0, 5677.33333, 5662.0], + [5772.0, 5748.0, 5718.0, 5697.0, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ] * units.m assert_array_almost_equal(smoothed, truth, 4) def test_smooth_circular(): """Test smooth_circular with default configuration.""" - hght = [[5640., 5640., 5640., 5640., 5640.], - [5684., 5676., 5666., 5659., 5651.], - [5728., 5712., 5692., 5678., 5662.], - [5772., 5748., 5718., 5697., 5673.], - [5816., 5784., 5744., 5716., 5684.]] * units.m + hght = [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5676.0, 5666.0, 5659.0, 5651.0], + [5728.0, 5712.0, 5692.0, 5678.0, 5662.0], + [5772.0, 5748.0, 5718.0, 5697.0, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ] * units.m smoothed = smooth_circular(hght, 2, 2) - truth = [[5640., 5640., 5640., 5640., 5640.], - [5684., 5676., 5666., 5659., 5651.], - [5728., 5712., 5693.98817, 5678., 5662.], - [5772., 5748., 5718., 5697., 5673.], - [5816., 5784., 5744., 5716., 5684.]] * units.m + truth = [ + [5640.0, 5640.0, 5640.0, 5640.0, 5640.0], + [5684.0, 5676.0, 5666.0, 5659.0, 5651.0], + [5728.0, 5712.0, 5693.98817, 5678.0, 5662.0], + [5772.0, 5748.0, 5718.0, 5697.0, 5673.0], + [5816.0, 5784.0, 5744.0, 5716.0, 5684.0], + ] * units.m assert_array_almost_equal(smoothed, truth, 4) @@ -757,7 +1038,7 @@ def test_smooth_window_with_bad_window(): temperature = [37, 32, 34, 29, 28, 24, 26, 24, 27, 30] * units.degF with pytest.raises(ValueError) as exc: smooth_window(temperature, np.ones(4)) - assert 'must be odd in all dimensions' in str(exc) + assert "must be odd in all dimensions" in str(exc) def test_altimeter_to_station_pressure_inhg(): diff --git a/tests/calc/test_calc_tools.py b/tests/calc/test_calc_tools.py index 299ae865bcd..270f8f44199 100644 --- a/tests/calc/test_calc_tools.py +++ b/tests/calc/test_calc_tools.py @@ -11,27 +11,50 @@ import pytest import xarray as xr -from metpy.calc import (angle_to_direction, find_bounding_indices, find_intersections, - first_derivative, get_layer, get_layer_heights, gradient, - laplacian, lat_lon_grid_deltas, nearest_intersection_idx, parse_angle, - pressure_to_height_std, reduce_point_density, resample_nn_1d, - second_derivative) -from metpy.calc.tools import (_delete_masked_points, _get_bound_pressure_height, - _greater_or_close, _less_or_close, _next_non_masked_element, - _remove_nans, azimuth_range_to_lat_lon, BASE_DEGREE_MULTIPLIER, - DIR_STRS, UND) -from metpy.testing import (assert_almost_equal, assert_array_almost_equal, assert_array_equal, - needs_pyproj) +from metpy.calc import ( + angle_to_direction, + find_bounding_indices, + find_intersections, + first_derivative, + get_layer, + get_layer_heights, + gradient, + laplacian, + lat_lon_grid_deltas, + nearest_intersection_idx, + parse_angle, + pressure_to_height_std, + reduce_point_density, + resample_nn_1d, + second_derivative, +) +from metpy.calc.tools import ( + BASE_DEGREE_MULTIPLIER, + DIR_STRS, + UND, + _delete_masked_points, + _get_bound_pressure_height, + _greater_or_close, + _less_or_close, + _next_non_masked_element, + _remove_nans, + azimuth_range_to_lat_lon, +) +from metpy.testing import ( + assert_almost_equal, + assert_array_almost_equal, + assert_array_equal, + needs_pyproj, +) from metpy.units import units from metpy.xarray import grid_deltas_from_dataarray - FULL_CIRCLE_DEGREES = np.arange(0, 360, BASE_DEGREE_MULTIPLIER.m) * units.degree def test_resample_nn(): """Test 1d nearest neighbor functionality.""" - a = np.arange(5.) + a = np.arange(5.0) b = np.array([2, 3.8]) truth = np.array([2, 4]) @@ -41,22 +64,25 @@ def test_resample_nn(): def test_nearest_intersection_idx(): """Test nearest index to intersection functionality.""" x = np.linspace(5, 30, 17) - y1 = 3 * x**2 + y1 = 3 * x ** 2 y2 = 100 * x - 650 truth = np.array([2, 12]) assert_array_equal(truth, nearest_intersection_idx(y1, y2)) -@pytest.mark.parametrize('direction, expected', [ - ('all', np.array([[8.88, 24.44], [238.84, 1794.53]])), - ('increasing', np.array([[24.44], [1794.53]])), - ('decreasing', np.array([[8.88], [238.84]])) -]) +@pytest.mark.parametrize( + "direction, expected", + [ + ("all", np.array([[8.88, 24.44], [238.84, 1794.53]])), + ("increasing", np.array([[24.44], [1794.53]])), + ("decreasing", np.array([[8.88], [238.84]])), + ], +) def test_find_intersections(direction, expected): """Test finding the intersection of two curves functionality.""" x = np.linspace(5, 30, 17) - y1 = 3 * x**2 + y1 = 3 * x ** 2 y2 = 100 * x - 650 # Note: Truth is what we will get with this sampling, not the mathematical intersection assert_array_almost_equal(expected, find_intersections(x, y1, y2, direction=direction), 2) @@ -68,8 +94,7 @@ def test_find_intersections_no_intersections(): y1 = 3 * x + 0 y2 = 5 * x + 5 # Note: Truth is what we will get with this sampling, not the mathematical intersection - truth = np.array([[], - []]) + truth = np.array([[], []]) assert_array_equal(truth, find_intersections(x, y1, y2)) @@ -79,25 +104,28 @@ def test_find_intersections_invalid_direction(): y1 = 3 * x ** 2 y2 = 100 * x - 650 with pytest.raises(ValueError): - find_intersections(x, y1, y2, direction='increaing') + find_intersections(x, y1, y2, direction="increaing") def test_find_intersections_units(): """Test handling of units when logarithmic interpolation is called.""" x = np.linspace(5, 30, 17) * units.hPa - y1 = 3 * x.m**2 + y1 = 3 * x.m ** 2 y2 = 100 * x.m - 650 truth = np.array([24.43, 1794.54]) - x_test, y_test = find_intersections(x, y1, y2, direction='increasing', log_x=True) + x_test, y_test = find_intersections(x, y1, y2, direction="increasing", log_x=True) assert_array_almost_equal(truth, np.array([x_test.m, y_test.m]).flatten(), 2) assert x_test.units == units.hPa -@pytest.mark.parametrize('direction, expected', [ - ('all', np.array([[0., 3.5, 4.33333333, 7., 9., 10., 11.5, 13.], np.zeros(8)])), - ('increasing', np.array([[0., 4.333, 7., 11.5], np.zeros(4)])), - ('decreasing', np.array([[3.5, 10.], np.zeros(2)])) -]) +@pytest.mark.parametrize( + "direction, expected", + [ + ("all", np.array([[0.0, 3.5, 4.33333333, 7.0, 9.0, 10.0, 11.5, 13.0], np.zeros(8)])), + ("increasing", np.array([[0.0, 4.333, 7.0, 11.5], np.zeros(4)])), + ("decreasing", np.array([[3.5, 10.0], np.zeros(2)])), + ], +) def test_find_intersections_intersections_in_data_at_ends(direction, expected): """Test finding intersections when intersections are in the data. @@ -110,11 +138,14 @@ def test_find_intersections_intersections_in_data_at_ends(direction, expected): assert_array_almost_equal(expected, find_intersections(x, y1, y2, direction=direction), 2) -@pytest.mark.parametrize('mask, expected_idx, expected_element', [ - ([False, False, False, False, False], 1, 1), - ([False, True, True, False, False], 3, 3), - ([False, True, True, True, True], None, None) -]) +@pytest.mark.parametrize( + "mask, expected_idx, expected_element", + [ + ([False, False, False, False, False], 1, 1), + ([False, True, True, False, False], 3, 3), + ([False, True, True, True, True], None, None), + ], +) def test_non_masked_elements(mask, expected_idx, expected_element): """Test with a valid element.""" a = ma.masked_array(np.arange(5), mask=mask) @@ -126,57 +157,133 @@ def test_non_masked_elements(mask, expected_idx, expected_element): @pytest.fixture def thin_point_data(): r"""Provide scattered points for testing.""" - xy = np.array([[0.8793620, 0.9005706], [0.5382446, 0.8766988], [0.6361267, 0.1198620], - [0.4127191, 0.0270573], [0.1486231, 0.3121822], [0.2607670, 0.4886657], - [0.7132257, 0.2827587], [0.4371954, 0.5660840], [0.1318544, 0.6468250], - [0.6230519, 0.0682618], [0.5069460, 0.2326285], [0.1324301, 0.5609478], - [0.7975495, 0.2109974], [0.7513574, 0.9870045], [0.9305814, 0.0685815], - [0.5271641, 0.7276889], [0.8116574, 0.4795037], [0.7017868, 0.5875983], - [0.5591604, 0.5579290], [0.1284860, 0.0968003], [0.2857064, 0.3862123]]) + xy = np.array( + [ + [0.8793620, 0.9005706], + [0.5382446, 0.8766988], + [0.6361267, 0.1198620], + [0.4127191, 0.0270573], + [0.1486231, 0.3121822], + [0.2607670, 0.4886657], + [0.7132257, 0.2827587], + [0.4371954, 0.5660840], + [0.1318544, 0.6468250], + [0.6230519, 0.0682618], + [0.5069460, 0.2326285], + [0.1324301, 0.5609478], + [0.7975495, 0.2109974], + [0.7513574, 0.9870045], + [0.9305814, 0.0685815], + [0.5271641, 0.7276889], + [0.8116574, 0.4795037], + [0.7017868, 0.5875983], + [0.5591604, 0.5579290], + [0.1284860, 0.0968003], + [0.2857064, 0.3862123], + ] + ) return xy -@pytest.mark.parametrize('radius, truth', - [(2.0, np.array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], dtype=np.bool)), - (1.0, np.array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], dtype=np.bool)), - (0.3, np.array([1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, - 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], dtype=np.bool)), - (0.1, np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=np.bool)) - ]) +@pytest.mark.parametrize( + "radius, truth", + [ + ( + 2.0, + np.array( + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], dtype=np.bool + ), + ), + ( + 1.0, + np.array( + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], dtype=np.bool + ), + ), + ( + 0.3, + np.array( + [1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], dtype=np.bool + ), + ), + ( + 0.1, + np.array( + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=np.bool + ), + ), + ], +) def test_reduce_point_density(thin_point_data, radius, truth): r"""Test that reduce_point_density works.""" assert_array_equal(reduce_point_density(thin_point_data, radius=radius), truth) -@pytest.mark.parametrize('radius, truth', - [(2.0, np.array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], dtype=np.bool)), - (1.0, np.array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], dtype=np.bool)), - (0.3, np.array([1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, - 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], dtype=np.bool)), - (0.1, np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=np.bool)) - ]) +@pytest.mark.parametrize( + "radius, truth", + [ + ( + 2.0, + np.array( + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], dtype=np.bool + ), + ), + ( + 1.0, + np.array( + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], dtype=np.bool + ), + ), + ( + 0.3, + np.array( + [1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], dtype=np.bool + ), + ), + ( + 0.1, + np.array( + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=np.bool + ), + ), + ], +) def test_reduce_point_density_units(thin_point_data, radius, truth): r"""Test that reduce_point_density works with units.""" - assert_array_equal(reduce_point_density(thin_point_data * units.dam, - radius=radius * units.dam), truth) - - -@pytest.mark.parametrize('radius, truth', - [(2.0, np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], dtype=np.bool)), - (0.7, np.array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 1, 0, 0, 0, 0, 0, 1], dtype=np.bool)), - (0.3, np.array([1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, - 0, 0, 0, 1, 0, 0, 0, 1, 0, 1], dtype=np.bool)), - (0.1, np.array([1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=np.bool)) - ]) + assert_array_equal( + reduce_point_density(thin_point_data * units.dam, radius=radius * units.dam), truth + ) + + +@pytest.mark.parametrize( + "radius, truth", + [ + ( + 2.0, + np.array( + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], dtype=np.bool + ), + ), + ( + 0.7, + np.array( + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1], dtype=np.bool + ), + ), + ( + 0.3, + np.array( + [1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1], dtype=np.bool + ), + ), + ( + 0.1, + np.array( + [1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=np.bool + ), + ), + ], +) def test_reduce_point_density_priority(thin_point_data, radius, truth): r"""Test that reduce_point_density works properly with priority.""" key = np.array([8, 6, 2, 8, 6, 4, 4, 8, 8, 6, 3, 4, 3, 0, 7, 4, 3, 2, 3, 3, 9]) @@ -186,8 +293,9 @@ def test_reduce_point_density_priority(thin_point_data, radius, truth): def test_reduce_point_density_1d(): r"""Test that reduce_point_density works with 1D points.""" x = np.array([1, 3, 4, 8, 9, 10]) - assert_array_equal(reduce_point_density(x, 2.5), - np.array([1, 0, 1, 1, 0, 0], dtype=np.bool)) + assert_array_equal( + reduce_point_density(x, 2.5), np.array([1, 0, 1, 1, 0, 0], dtype=np.bool) + ) def test_delete_masked_points(): @@ -207,42 +315,130 @@ def get_bounds_data(): return pressures, heights -@pytest.mark.parametrize('pressure, bound, hgts, interp, expected', [ - (get_bounds_data()[0], 900 * units.hPa, None, True, - (900 * units.hPa, 0.9880028 * units.kilometer)), - (get_bounds_data()[0], 900 * units.hPa, None, False, - (900 * units.hPa, 0.9880028 * units.kilometer)), - (get_bounds_data()[0], 870 * units.hPa, None, True, - (870 * units.hPa, 1.2665298 * units.kilometer)), - (get_bounds_data()[0], 870 * units.hPa, None, False, - (900 * units.hPa, 0.9880028 * units.kilometer)), - (get_bounds_data()[0], 0.9880028 * units.kilometer, None, True, - (900 * units.hPa, 0.9880028 * units.kilometer)), - (get_bounds_data()[0], 0.9880028 * units.kilometer, None, False, - (900 * units.hPa, 0.9880028 * units.kilometer)), - (get_bounds_data()[0], 1.2665298 * units.kilometer, None, True, - (870 * units.hPa, 1.2665298 * units.kilometer)), - (get_bounds_data()[0], 1.2665298 * units.kilometer, None, False, - (900 * units.hPa, 0.9880028 * units.kilometer)), - (get_bounds_data()[0], 900 * units.hPa, get_bounds_data()[1], True, - (900 * units.hPa, 0.9880028 * units.kilometer)), - (get_bounds_data()[0], 900 * units.hPa, get_bounds_data()[1], False, - (900 * units.hPa, 0.9880028 * units.kilometer)), - (get_bounds_data()[0], 870 * units.hPa, get_bounds_data()[1], True, - (870 * units.hPa, 1.2643214 * units.kilometer)), - (get_bounds_data()[0], 870 * units.hPa, get_bounds_data()[1], False, - (900 * units.hPa, 0.9880028 * units.kilometer)), - (get_bounds_data()[0], 0.9880028 * units.kilometer, get_bounds_data()[1], True, - (900 * units.hPa, 0.9880028 * units.kilometer)), - (get_bounds_data()[0], 0.9880028 * units.kilometer, get_bounds_data()[1], False, - (900 * units.hPa, 0.9880028 * units.kilometer)), - (get_bounds_data()[0], 1.2665298 * units.kilometer, get_bounds_data()[1], True, - (870.9869087 * units.hPa, 1.2665298 * units.kilometer)), - (get_bounds_data()[0], 1.2665298 * units.kilometer, get_bounds_data()[1], False, - (900 * units.hPa, 0.9880028 * units.kilometer)), - (get_bounds_data()[0], 0.98800289 * units.kilometer, get_bounds_data()[1], True, - (900 * units.hPa, 0.9880028 * units.kilometer)) -]) +@pytest.mark.parametrize( + "pressure, bound, hgts, interp, expected", + [ + ( + get_bounds_data()[0], + 900 * units.hPa, + None, + True, + (900 * units.hPa, 0.9880028 * units.kilometer), + ), + ( + get_bounds_data()[0], + 900 * units.hPa, + None, + False, + (900 * units.hPa, 0.9880028 * units.kilometer), + ), + ( + get_bounds_data()[0], + 870 * units.hPa, + None, + True, + (870 * units.hPa, 1.2665298 * units.kilometer), + ), + ( + get_bounds_data()[0], + 870 * units.hPa, + None, + False, + (900 * units.hPa, 0.9880028 * units.kilometer), + ), + ( + get_bounds_data()[0], + 0.9880028 * units.kilometer, + None, + True, + (900 * units.hPa, 0.9880028 * units.kilometer), + ), + ( + get_bounds_data()[0], + 0.9880028 * units.kilometer, + None, + False, + (900 * units.hPa, 0.9880028 * units.kilometer), + ), + ( + get_bounds_data()[0], + 1.2665298 * units.kilometer, + None, + True, + (870 * units.hPa, 1.2665298 * units.kilometer), + ), + ( + get_bounds_data()[0], + 1.2665298 * units.kilometer, + None, + False, + (900 * units.hPa, 0.9880028 * units.kilometer), + ), + ( + get_bounds_data()[0], + 900 * units.hPa, + get_bounds_data()[1], + True, + (900 * units.hPa, 0.9880028 * units.kilometer), + ), + ( + get_bounds_data()[0], + 900 * units.hPa, + get_bounds_data()[1], + False, + (900 * units.hPa, 0.9880028 * units.kilometer), + ), + ( + get_bounds_data()[0], + 870 * units.hPa, + get_bounds_data()[1], + True, + (870 * units.hPa, 1.2643214 * units.kilometer), + ), + ( + get_bounds_data()[0], + 870 * units.hPa, + get_bounds_data()[1], + False, + (900 * units.hPa, 0.9880028 * units.kilometer), + ), + ( + get_bounds_data()[0], + 0.9880028 * units.kilometer, + get_bounds_data()[1], + True, + (900 * units.hPa, 0.9880028 * units.kilometer), + ), + ( + get_bounds_data()[0], + 0.9880028 * units.kilometer, + get_bounds_data()[1], + False, + (900 * units.hPa, 0.9880028 * units.kilometer), + ), + ( + get_bounds_data()[0], + 1.2665298 * units.kilometer, + get_bounds_data()[1], + True, + (870.9869087 * units.hPa, 1.2665298 * units.kilometer), + ), + ( + get_bounds_data()[0], + 1.2665298 * units.kilometer, + get_bounds_data()[1], + False, + (900 * units.hPa, 0.9880028 * units.kilometer), + ), + ( + get_bounds_data()[0], + 0.98800289 * units.kilometer, + get_bounds_data()[1], + True, + (900 * units.hPa, 0.9880028 * units.kilometer), + ), + ], +) def test_get_bound_pressure_height(pressure, bound, hgts, interp, expected): """Test getting bounds in layers with various parameter combinations.""" bounds = _get_bound_pressure_height(pressure, bound, height=hgts, interpolate=interp) @@ -276,23 +472,71 @@ def test_get_bound_height_out_of_range(): _get_bound_pressure_height(p, 100 * units.meter, height=h) -@pytest.mark.parametrize('flip_order', [(True, False)]) +@pytest.mark.parametrize("flip_order", [(True, False)]) def test_get_layer_float32(flip_order): """Test that get_layer works properly with float32 data.""" - p = np.asarray([940.85083008, 923.78851318, 911.42022705, 896.07220459, - 876.89404297, 781.63330078], np.float32) * units('hPa') - hgt = np.asarray([563.671875, 700.93817139, 806.88098145, 938.51745605, - 1105.25854492, 2075.04443359], dtype=np.float32) * units.meter + p = ( + np.asarray( + [ + 940.85083008, + 923.78851318, + 911.42022705, + 896.07220459, + 876.89404297, + 781.63330078, + ], + np.float32, + ) + * units("hPa") + ) + hgt = ( + np.asarray( + [ + 563.671875, + 700.93817139, + 806.88098145, + 938.51745605, + 1105.25854492, + 2075.04443359, + ], + dtype=np.float32, + ) + * units.meter + ) - true_p_layer = np.asarray([940.85083008, 923.78851318, 911.42022705, 896.07220459, - 876.89404297, 831.86472819], np.float32) * units('hPa') - true_hgt_layer = np.asarray([563.671875, 700.93817139, 806.88098145, 938.51745605, - 1105.25854492, 1549.8079], dtype=np.float32) * units.meter + true_p_layer = ( + np.asarray( + [ + 940.85083008, + 923.78851318, + 911.42022705, + 896.07220459, + 876.89404297, + 831.86472819, + ], + np.float32, + ) + * units("hPa") + ) + true_hgt_layer = ( + np.asarray( + [ + 563.671875, + 700.93817139, + 806.88098145, + 938.51745605, + 1105.25854492, + 1549.8079, + ], + dtype=np.float32, + ) + * units.meter + ) if flip_order: p = p[::-1] hgt = hgt[::-1] - p_layer, hgt_layer = get_layer(p, hgt, height=hgt, depth=1000. * units.meter) + p_layer, hgt_layer = get_layer(p, hgt, height=hgt, depth=1000.0 * units.meter) assert_array_almost_equal(p_layer, true_p_layer, 4) assert_array_almost_equal(hgt_layer, true_hgt_layer, 4) @@ -320,20 +564,49 @@ def layer_test_data(): return pressure, temperature -@pytest.mark.parametrize('pressure, variable, heights, bottom, depth, interp, expected', [ - (layer_test_data()[0], layer_test_data()[1], None, None, 150 * units.hPa, True, - (np.array([1000, 900, 850]) * units.hPa, - np.array([25.0, 16.666666, 12.62262]) * units.degC)), - (layer_test_data()[0], layer_test_data()[1], None, None, 150 * units.hPa, False, - (np.array([1000, 900]) * units.hPa, np.array([25.0, 16.666666]) * units.degC)), - (layer_test_data()[0], layer_test_data()[1], None, 2 * units.km, 3 * units.km, True, - (np.array([794.85264282, 700., 600., 540.01696548]) * units.hPa, - np.array([7.93049516, 0., -8.33333333, -13.14758845]) * units.degC)) -]) +@pytest.mark.parametrize( + "pressure, variable, heights, bottom, depth, interp, expected", + [ + ( + layer_test_data()[0], + layer_test_data()[1], + None, + None, + 150 * units.hPa, + True, + ( + np.array([1000, 900, 850]) * units.hPa, + np.array([25.0, 16.666666, 12.62262]) * units.degC, + ), + ), + ( + layer_test_data()[0], + layer_test_data()[1], + None, + None, + 150 * units.hPa, + False, + (np.array([1000, 900]) * units.hPa, np.array([25.0, 16.666666]) * units.degC), + ), + ( + layer_test_data()[0], + layer_test_data()[1], + None, + 2 * units.km, + 3 * units.km, + True, + ( + np.array([794.85264282, 700.0, 600.0, 540.01696548]) * units.hPa, + np.array([7.93049516, 0.0, -8.33333333, -13.14758845]) * units.degC, + ), + ), + ], +) def test_get_layer(pressure, variable, heights, bottom, depth, interp, expected): """Test get_layer functionality.""" - p_layer, y_layer = get_layer(pressure, variable, height=heights, bottom=bottom, - depth=depth, interpolate=interp) + p_layer, y_layer = get_layer( + pressure, variable, height=heights, bottom=bottom, depth=depth, interpolate=interp + ) assert_array_almost_equal(p_layer, expected[0], 4) assert_array_almost_equal(y_layer, expected[1], 4) @@ -350,10 +623,10 @@ def test_greater_or_close(): def test_greater_or_close_mixed_types(): """Test _greater_or_close with mixed Quantity and array errors.""" with pytest.raises(ValueError): - _greater_or_close(1000. * units.mbar, 1000.) + _greater_or_close(1000.0 * units.mbar, 1000.0) with pytest.raises(ValueError): - _greater_or_close(1000., 1000. * units.mbar) + _greater_or_close(1000.0, 1000.0 * units.mbar) def test_less_or_close(): @@ -368,10 +641,10 @@ def test_less_or_close(): def test_less_or_close_mixed_types(): """Test _less_or_close with mixed Quantity and array errors.""" with pytest.raises(ValueError): - _less_or_close(1000. * units.mbar, 1000.) + _less_or_close(1000.0 * units.mbar, 1000.0) with pytest.raises(ValueError): - _less_or_close(1000., 1000. * units.mbar) + _less_or_close(1000.0, 1000.0 * units.mbar) def test_get_layer_heights_interpolation(): @@ -389,8 +662,9 @@ def test_get_layer_heights_no_interpolation(): """Test get_layer_heights without interpolation.""" heights = np.arange(10) * units.km data = heights.m * 2 * units.degC - heights, data = get_layer_heights(heights, 5000 * units.m, data, - bottom=1500 * units.m, interpolate=False) + heights, data = get_layer_heights( + heights, 5000 * units.m, data, bottom=1500 * units.m, interpolate=False + ) heights_true = np.array([2, 3, 4, 5, 6]) * units.km data_true = heights_true.m * 2 * units.degC assert_array_almost_equal(heights_true, heights, 6) @@ -412,8 +686,14 @@ def test_get_layer_heights_agl_bottom_no_interp(): """Test get_layer_heights with no interpolation and a bottom.""" heights_init = np.arange(300, 1200, 100) * units.m data = heights_init.m * 0.1 * units.degC - heights, data = get_layer_heights(heights_init, 500 * units.m, data, with_agl=True, - interpolate=False, bottom=200 * units.m) + heights, data = get_layer_heights( + heights_init, + 500 * units.m, + data, + with_agl=True, + interpolate=False, + bottom=200 * units.m, + ) # Regression test for #789 assert_array_equal(heights_init[0], 300 * units.m) heights_true = np.array([0.2, 0.3, 0.4, 0.5, 0.6, 0.7]) * units.km @@ -428,30 +708,58 @@ def test_lat_lon_grid_deltas_1d(): lat = np.arange(40, 50, 2.5) lon = np.arange(-100, -90, 2.5) dx, dy = lat_lon_grid_deltas(lon, lat) - dx_truth = np.array([[212943.5585, 212943.5585, 212943.5585], - [204946.2305, 204946.2305, 204946.2305], - [196558.8269, 196558.8269, 196558.8269], - [187797.3216, 187797.3216, 187797.3216]]) * units.meter - dy_truth = np.array([[277987.1857, 277987.1857, 277987.1857, 277987.1857], - [277987.1857, 277987.1857, 277987.1857, 277987.1857], - [277987.1857, 277987.1857, 277987.1857, 277987.1857]]) * units.meter + dx_truth = ( + np.array( + [ + [212943.5585, 212943.5585, 212943.5585], + [204946.2305, 204946.2305, 204946.2305], + [196558.8269, 196558.8269, 196558.8269], + [187797.3216, 187797.3216, 187797.3216], + ] + ) + * units.meter + ) + dy_truth = ( + np.array( + [ + [277987.1857, 277987.1857, 277987.1857, 277987.1857], + [277987.1857, 277987.1857, 277987.1857, 277987.1857], + [277987.1857, 277987.1857, 277987.1857, 277987.1857], + ] + ) + * units.meter + ) assert_almost_equal(dx, dx_truth, 4) assert_almost_equal(dy, dy_truth, 4) -@pytest.mark.parametrize('flip_order', [(False, True)]) +@pytest.mark.parametrize("flip_order", [(False, True)]) @needs_pyproj def test_lat_lon_grid_deltas_2d(flip_order): """Test for lat_lon_grid_deltas for variable grid with negative delta distances.""" lat = np.arange(40, 50, 2.5) lon = np.arange(-100, -90, 2.5) - dx_truth = np.array([[212943.5585, 212943.5585, 212943.5585], - [204946.2305, 204946.2305, 204946.2305], - [196558.8269, 196558.8269, 196558.8269], - [187797.3216, 187797.3216, 187797.3216]]) * units.meter - dy_truth = np.array([[277987.1857, 277987.1857, 277987.1857, 277987.1857], - [277987.1857, 277987.1857, 277987.1857, 277987.1857], - [277987.1857, 277987.1857, 277987.1857, 277987.1857]]) * units.meter + dx_truth = ( + np.array( + [ + [212943.5585, 212943.5585, 212943.5585], + [204946.2305, 204946.2305, 204946.2305], + [196558.8269, 196558.8269, 196558.8269], + [187797.3216, 187797.3216, 187797.3216], + ] + ) + * units.meter + ) + dy_truth = ( + np.array( + [ + [277987.1857, 277987.1857, 277987.1857, 277987.1857], + [277987.1857, 277987.1857, 277987.1857, 277987.1857], + [277987.1857, 277987.1857, 277987.1857, 277987.1857], + ] + ) + * units.meter + ) if flip_order: lon = lon[::-1] lat = lat[::-1] @@ -470,14 +778,35 @@ def test_lat_lon_grid_deltas_extra_dimensions(): lon, lat = np.meshgrid(np.arange(-100, -90, 2.5), np.arange(40, 50, 2.5)) lat = lat[None, None] lon = lon[None, None] - dx_truth = np.array([[[[212943.5585, 212943.5585, 212943.5585], - [204946.2305, 204946.2305, 204946.2305], - [196558.8269, 196558.8269, 196558.8269], - [187797.3216, 187797.3216, 187797.3216]]]]) * units.meter - dy_truth = (np.array([[[[277987.1857, 277987.1857, 277987.1857, 277987.1857], - [277987.1857, 277987.1857, 277987.1857, 277987.1857], - [277987.1857, 277987.1857, 277987.1857, 277987.1857]]]]) - * units.meter) + dx_truth = ( + np.array( + [ + [ + [ + [212943.5585, 212943.5585, 212943.5585], + [204946.2305, 204946.2305, 204946.2305], + [196558.8269, 196558.8269, 196558.8269], + [187797.3216, 187797.3216, 187797.3216], + ] + ] + ] + ) + * units.meter + ) + dy_truth = ( + np.array( + [ + [ + [ + [277987.1857, 277987.1857, 277987.1857, 277987.1857], + [277987.1857, 277987.1857, 277987.1857, 277987.1857], + [277987.1857, 277987.1857, 277987.1857, 277987.1857], + ] + ] + ] + ) + * units.meter + ) dx, dy = lat_lon_grid_deltas(lon, lat) assert_almost_equal(dx, dx_truth, 4) assert_almost_equal(dy, dy_truth, 4) @@ -487,10 +816,14 @@ def test_lat_lon_grid_deltas_extra_dimensions(): def test_lat_lon_grid_deltas_mismatched_shape(): """Test for lat_lon_grid_deltas for variable grid.""" lat = np.arange(40, 50, 2.5) - lon = np.array([[-100., -97.5, -95., -92.5], - [-100., -97.5, -95., -92.5], - [-100., -97.5, -95., -92.5], - [-100., -97.5, -95., -92.5]]) + lon = np.array( + [ + [-100.0, -97.5, -95.0, -92.5], + [-100.0, -97.5, -95.0, -92.5], + [-100.0, -97.5, -95.0, -92.5], + [-100.0, -97.5, -95.0, -92.5], + ] + ) with pytest.raises(ValueError): lat_lon_grid_deltas(lon, lat) @@ -501,14 +834,27 @@ def test_lat_lon_grid_deltas_geod_kwargs(): lat = np.arange(40, 50, 2.5) lon = np.arange(-100, -90, 2.5) dx, dy = lat_lon_grid_deltas(lon, lat, a=4370997) - dx_truth = np.array([[146095.76101984, 146095.76101984, 146095.76101984], - [140608.9751528, 140608.9751528, 140608.9751528], - [134854.56713287, 134854.56713287, 134854.56713287], - [128843.49645823, 128843.49645823, 128843.49645823]]) * units.meter - dy_truth = np.array([[190720.72311199, 190720.72311199, 190720.72311199, 190720.72311199], - [190720.72311199, 190720.72311199, 190720.72311199, 190720.72311199], - [190720.72311199, 190720.72311199, 190720.72311199, - 190720.72311199]]) * units.meter + dx_truth = ( + np.array( + [ + [146095.76101984, 146095.76101984, 146095.76101984], + [140608.9751528, 140608.9751528, 140608.9751528], + [134854.56713287, 134854.56713287, 134854.56713287], + [128843.49645823, 128843.49645823, 128843.49645823], + ] + ) + * units.meter + ) + dy_truth = ( + np.array( + [ + [190720.72311199, 190720.72311199, 190720.72311199, 190720.72311199], + [190720.72311199, 190720.72311199, 190720.72311199, 190720.72311199], + [190720.72311199, 190720.72311199, 190720.72311199, 190720.72311199], + ] + ) + * units.meter + ) assert_almost_equal(dx, dx_truth, 4) assert_almost_equal(dy, dy_truth, 4) @@ -516,18 +862,22 @@ def test_lat_lon_grid_deltas_geod_kwargs(): @pytest.fixture() def deriv_1d_data(): """Return 1-dimensional data for testing derivative functions.""" - return namedtuple('D_1D_Test_Data', 'x values')(np.array([0, 1.25, 3.75]) * units.cm, - np.array([13.5, 12, 10]) * units.degC) + return namedtuple("D_1D_Test_Data", "x values")( + np.array([0, 1.25, 3.75]) * units.cm, np.array([13.5, 12, 10]) * units.degC + ) @pytest.fixture() def deriv_2d_data(): """Return 2-dimensional data for analytic function for testing derivative functions.""" - ret = namedtuple('D_2D_Test_Data', 'x y x0 y0 a b f')( - np.array([0., 2., 7.]), np.array([1., 5., 11., 13.]), 3, 1.5, 0.5, 0.25, 0) + ret = namedtuple("D_2D_Test_Data", "x y x0 y0 a b f")( + np.array([0.0, 2.0, 7.0]), np.array([1.0, 5.0, 11.0, 13.0]), 3, 1.5, 0.5, 0.25, 0 + ) # Makes a value array with y changing along rows (axis 0) and x along columns (axis 1) - return ret._replace(f=ret.a * (ret.x - ret.x0)**2 + ret.b * (ret.y[:, None] - ret.y0)**2) + return ret._replace( + f=ret.a * (ret.x - ret.x0) ** 2 + ret.b * (ret.y[:, None] - ret.y0) ** 2 + ) @pytest.fixture() @@ -541,21 +891,25 @@ def test_first_derivative(deriv_1d_data): dv_dx = first_derivative(deriv_1d_data.values, x=deriv_1d_data.x) # Worked by hand and taken from Chapra and Canale 23.2 - truth = np.array([-1.333333, -1.06666667, -0.5333333]) * units('delta_degC / cm') + truth = np.array([-1.333333, -1.06666667, -0.5333333]) * units("delta_degC / cm") assert_array_almost_equal(dv_dx, truth, 5) def test_first_derivative_2d(deriv_2d_data): """Test first_derivative with a full 2D array.""" df_dx = first_derivative(deriv_2d_data.f, x=deriv_2d_data.x, axis=1) - df_dx_analytic = np.tile(2 * deriv_2d_data.a * (deriv_2d_data.x - deriv_2d_data.x0), - (deriv_2d_data.f.shape[0], 1)) + df_dx_analytic = np.tile( + 2 * deriv_2d_data.a * (deriv_2d_data.x - deriv_2d_data.x0), + (deriv_2d_data.f.shape[0], 1), + ) assert_array_almost_equal(df_dx, df_dx_analytic, 5) df_dy = first_derivative(deriv_2d_data.f, x=deriv_2d_data.y, axis=0) # Repeat each row, then flip to get variation along rows - df_dy_analytic = np.tile(2 * deriv_2d_data.b * (deriv_2d_data.y - deriv_2d_data.y0), - (deriv_2d_data.f.shape[1], 1)).T + df_dy_analytic = np.tile( + 2 * deriv_2d_data.b * (deriv_2d_data.y - deriv_2d_data.y0), + (deriv_2d_data.f.shape[1], 1), + ).T assert_array_almost_equal(df_dy, df_dy_analytic, 5) @@ -568,7 +922,7 @@ def test_first_derivative_too_small(deriv_1d_data): def test_first_derivative_scalar_delta(): """Test first_derivative with a scalar passed for a delta.""" df_dx = first_derivative(np.arange(3), delta=1) - assert_array_almost_equal(df_dx, np.array([1., 1., 1.]), 6) + assert_array_almost_equal(df_dx, np.array([1.0, 1.0, 1.0]), 6) def test_first_derivative_masked(): @@ -577,22 +931,25 @@ def test_first_derivative_masked(): data[3] = np.ma.masked df_dx = first_derivative(data, delta=1) - truth = np.ma.array([1., 1., 1., 1., 1., 1., 1.], - mask=[False, False, True, True, True, False, False]) + truth = np.ma.array( + [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + mask=[False, False, True, True, True, False, False], + ) assert_array_almost_equal(df_dx, truth) assert_array_equal(df_dx.mask, truth.mask) def test_first_derivative_masked_units(): """Test that first_derivative properly propagates masks with units.""" - data = units('K') * np.ma.arange(7) + data = units("K") * np.ma.arange(7) data[3] = np.ma.masked - x = units('m') * np.ma.arange(7) + x = units("m") * np.ma.arange(7) df_dx = first_derivative(data, x=x) - truth = units('K / m') * np.ma.array( - [1., 1., 1., 1., 1., 1., 1.], - mask=[False, False, True, True, True, False, False]) + truth = units("K / m") * np.ma.array( + [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + mask=[False, False, True, True, True, False, False], + ) assert_array_almost_equal(df_dx, truth) assert_array_equal(df_dx.mask, truth.mask) @@ -602,19 +959,21 @@ def test_second_derivative(deriv_1d_data): d2v_dx2 = second_derivative(deriv_1d_data.values, x=deriv_1d_data.x) # Worked by hand - truth = np.ones_like(deriv_1d_data.values) * 0.2133333 * units('delta_degC/cm**2') + truth = np.ones_like(deriv_1d_data.values) * 0.2133333 * units("delta_degC/cm**2") assert_array_almost_equal(d2v_dx2, truth, 5) def test_second_derivative_2d(deriv_2d_data): """Test second_derivative with a full 2D array.""" df2_dx2 = second_derivative(deriv_2d_data.f, x=deriv_2d_data.x, axis=1) - assert_array_almost_equal(df2_dx2, - np.ones_like(deriv_2d_data.f) * (2 * deriv_2d_data.a), 5) + assert_array_almost_equal( + df2_dx2, np.ones_like(deriv_2d_data.f) * (2 * deriv_2d_data.a), 5 + ) df2_dy2 = second_derivative(deriv_2d_data.f, x=deriv_2d_data.y, axis=0) - assert_array_almost_equal(df2_dy2, - np.ones_like(deriv_2d_data.f) * (2 * deriv_2d_data.b), 5) + assert_array_almost_equal( + df2_dy2, np.ones_like(deriv_2d_data.f) * (2 * deriv_2d_data.b), 5 + ) def test_second_derivative_too_small(deriv_1d_data): @@ -626,7 +985,7 @@ def test_second_derivative_too_small(deriv_1d_data): def test_second_derivative_scalar_delta(): """Test second_derivative with a scalar passed for a delta.""" df_dx = second_derivative(np.arange(3), delta=1) - assert_array_almost_equal(df_dx, np.array([0., 0., 0.]), 6) + assert_array_almost_equal(df_dx, np.array([0.0, 0.0, 0.0]), 6) def test_laplacian(deriv_1d_data): @@ -634,7 +993,7 @@ def test_laplacian(deriv_1d_data): laplac = laplacian(deriv_1d_data.values, coordinates=(deriv_1d_data.x,)) # Worked by hand - truth = np.ones_like(deriv_1d_data.values) * 0.2133333 * units('delta_degC/cm**2') + truth = np.ones_like(deriv_1d_data.values) * 0.2133333 * units("delta_degC/cm**2") assert_array_almost_equal(laplac, truth, 5) @@ -654,10 +1013,24 @@ def test_parse_angle_abbrieviated(): def test_parse_angle_ext(): """Test extended (unabbrieviated) directional text in degrees.""" - test_dir_strs = ['NORTH', 'NORTHnorthEast', 'North_East', 'East__North_East', - 'easT', 'east south east', 'south east', ' south southeast', - 'SOUTH', 'SOUTH SOUTH WEST', 'southWEST', 'WEST south_WEST', - 'WeSt', 'WestNorth West', 'North West', 'NORTH north_WeSt'] + test_dir_strs = [ + "NORTH", + "NORTHnorthEast", + "North_East", + "East__North_East", + "easT", + "east south east", + "south east", + " south southeast", + "SOUTH", + "SOUTH SOUTH WEST", + "southWEST", + "WEST south_WEST", + "WeSt", + "WestNorth West", + "North West", + "NORTH north_WeSt", + ] expected_angles_degrees = np.arange(0, 360, 22.5) * units.degree output_angles_degrees = parse_angle(test_dir_strs) assert_array_almost_equal(output_angles_degrees, expected_angles_degrees) @@ -665,10 +1038,24 @@ def test_parse_angle_ext(): def test_parse_angle_mix_multiple(): """Test list of extended (unabbrieviated) directional text in degrees in one go.""" - test_dir_strs = ['NORTH', 'nne', 'ne', 'east north east', - 'easT', 'east se', 'south east', ' south southeast', - 'SOUTH', 'SOUTH SOUTH WEST', 'sw', 'WEST south_WEST', - 'w', 'wnw', 'North West', 'nnw'] + test_dir_strs = [ + "NORTH", + "nne", + "ne", + "east north east", + "easT", + "east se", + "south east", + " south southeast", + "SOUTH", + "SOUTH SOUTH WEST", + "sw", + "WEST south_WEST", + "w", + "wnw", + "North West", + "nnw", + ] expected_angles_degrees = FULL_CIRCLE_DEGREES output_angles_degrees = parse_angle(test_dir_strs) assert_array_almost_equal(output_angles_degrees, expected_angles_degrees) @@ -684,7 +1071,7 @@ def test_parse_angle_none(): def test_parse_angle_invalid_number(): """Test list of extended (unabbrieviated) directional text in degrees in one go.""" - test_dir_strs = 365. + test_dir_strs = 365.0 expected_angles_degrees = np.nan output_angles_degrees = parse_angle(test_dir_strs) assert_array_almost_equal(output_angles_degrees, expected_angles_degrees) @@ -692,7 +1079,7 @@ def test_parse_angle_invalid_number(): def test_parse_angle_invalid_arr(): """Test list of extended (unabbrieviated) directional text in degrees in one go.""" - test_dir_strs = ['nan', None, np.nan, 35, 35.5, 'north', 'andrewiscool'] + test_dir_strs = ["nan", None, np.nan, 35, 35.5, "north", "andrewiscool"] expected_angles_degrees = [np.nan, np.nan, np.nan, np.nan, np.nan, 0, np.nan] output_angles_degrees = parse_angle(test_dir_strs) assert_array_almost_equal(output_angles_degrees, expected_angles_degrees) @@ -700,10 +1087,26 @@ def test_parse_angle_invalid_arr(): def test_parse_angle_mix_multiple_arr(): """Test list of extended (unabbrieviated) directional text in degrees in one go.""" - test_dir_strs = np.array(['NORTH', 'nne', 'ne', 'east north east', - 'easT', 'east se', 'south east', ' south southeast', - 'SOUTH', 'SOUTH SOUTH WEST', 'sw', 'WEST south_WEST', - 'w', 'wnw', 'North West', 'nnw']) + test_dir_strs = np.array( + [ + "NORTH", + "nne", + "ne", + "east north east", + "easT", + "east se", + "south east", + " south southeast", + "SOUTH", + "SOUTH SOUTH WEST", + "sw", + "WEST south_WEST", + "w", + "wnw", + "North West", + "nnw", + ] + ) expected_angles_degrees = FULL_CIRCLE_DEGREES output_angles_degrees = parse_angle(test_dir_strs) assert_array_almost_equal(output_angles_degrees, expected_angles_degrees) @@ -711,7 +1114,7 @@ def test_parse_angle_mix_multiple_arr(): def test_parse_angles_array(): """Test array of angles to parse.""" - angles = np.array(['N', 'S', 'E', 'W']) + angles = np.array(["N", "S", "E", "W"]) expected_angles = np.array([0, 180, 90, 270]) * units.degree calculated_angles = parse_angle(angles) assert_array_almost_equal(calculated_angles, expected_angles) @@ -719,7 +1122,7 @@ def test_parse_angles_array(): def test_parse_angles_series(): """Test pandas.Series of angles to parse.""" - angles = pd.Series(['N', 'S', 'E', 'W']) + angles = pd.Series(["N", "S", "E", "W"]) expected_angles = np.array([0, 180, 90, 270]) * units.degree calculated_angles = parse_angle(angles) assert_array_almost_equal(calculated_angles, expected_angles) @@ -727,7 +1130,7 @@ def test_parse_angles_series(): def test_parse_angles_single(): """Test single input into `parse_angles`.""" - calculated_angle = parse_angle('SOUTH SOUTH EAST') + calculated_angle = parse_angle("SOUTH SOUTH EAST") expected_angle = 157.5 * units.degree assert_almost_equal(calculated_angle, expected_angle) @@ -735,36 +1138,38 @@ def test_parse_angles_single(): def test_gradient_2d(deriv_2d_data): """Test gradient with 2D arrays.""" res = gradient(deriv_2d_data.f, coordinates=(deriv_2d_data.y, deriv_2d_data.x)) - truth = (np.array([[-0.25, -0.25, -0.25], - [1.75, 1.75, 1.75], - [4.75, 4.75, 4.75], - [5.75, 5.75, 5.75]]), - np.array([[-3, -1, 4], - [-3, -1, 4], - [-3, -1, 4], - [-3, -1, 4]])) + truth = ( + np.array( + [[-0.25, -0.25, -0.25], [1.75, 1.75, 1.75], [4.75, 4.75, 4.75], [5.75, 5.75, 5.75]] + ), + np.array([[-3, -1, 4], [-3, -1, 4], [-3, -1, 4], [-3, -1, 4]]), + ) assert_array_almost_equal(res, truth, 5) def test_gradient_4d(deriv_4d_data): """Test gradient with 4D arrays.""" res = gradient(deriv_4d_data, deltas=(1, 1, 1, 1)) - truth = tuple(factor * np.ones_like(deriv_4d_data) for factor in (48., 16., 4., 1.)) + truth = tuple(factor * np.ones_like(deriv_4d_data) for factor in (48.0, 16.0, 4.0, 1.0)) assert_array_almost_equal(res, truth, 8) def test_gradient_restricted_axes(deriv_2d_data): """Test 2D gradient with 3D arrays and manual specification of axes.""" - res = gradient(deriv_2d_data.f[..., None], coordinates=(deriv_2d_data.y, deriv_2d_data.x), - axes=(0, 1)) - truth = (np.array([[[-0.25], [-0.25], [-0.25]], - [[1.75], [1.75], [1.75]], - [[4.75], [4.75], [4.75]], - [[5.75], [5.75], [5.75]]]), - np.array([[[-3], [-1], [4]], - [[-3], [-1], [4]], - [[-3], [-1], [4]], - [[-3], [-1], [4]]])) + res = gradient( + deriv_2d_data.f[..., None], coordinates=(deriv_2d_data.y, deriv_2d_data.x), axes=(0, 1) + ) + truth = ( + np.array( + [ + [[-0.25], [-0.25], [-0.25]], + [[1.75], [1.75], [1.75]], + [[4.75], [4.75], [4.75]], + [[5.75], [5.75], [5.75]], + ] + ), + np.array([[[-3], [-1], [4]], [[-3], [-1], [4]], [[-3], [-1], [4]], [[-3], [-1], [4]]]), + ) assert_array_almost_equal(res, truth, 5) @@ -797,7 +1202,7 @@ def test_angle_to_direction(): def test_angle_to_direction_edge(): """Test single angle edge case (360 and no units) in degree.""" - expected_dirs = 'N' + expected_dirs = "N" output_dirs = angle_to_direction(360) assert_array_equal(output_dirs, expected_dirs) @@ -819,10 +1224,22 @@ def test_angle_to_direction_arr(): def test_angle_to_direction_full(): """Test the `full` keyword argument, expecting unabbrieviated output.""" expected_dirs = [ - 'North', 'North North East', 'North East', 'East North East', - 'East', 'East South East', 'South East', 'South South East', - 'South', 'South South West', 'South West', 'West South West', - 'West', 'West North West', 'North West', 'North North West' + "North", + "North North East", + "North East", + "East North East", + "East", + "East South East", + "South East", + "South South East", + "South", + "South South West", + "South West", + "West South West", + "West", + "West North West", + "North West", + "North North West", ] output_dirs = angle_to_direction(FULL_CIRCLE_DEGREES, full=True) assert_array_equal(output_dirs, expected_dirs) @@ -837,8 +1254,8 @@ def test_angle_to_direction_invalid_scalar(): def test_angle_to_direction_invalid_arr(): """Test array of invalid angles.""" - expected_dirs = ['NE', UND, UND, UND, 'N'] - output_dirs = angle_to_direction(['46', None, np.nan, None, '362.']) + expected_dirs = ["NE", UND, UND, UND, "N"] + output_dirs = angle_to_direction(["46", None, np.nan, None, "362."]) assert_array_equal(output_dirs, expected_dirs) @@ -846,7 +1263,7 @@ def test_angle_to_direction_level_4(): """Test non-existent level of complexity.""" with pytest.raises(ValueError) as exc: angle_to_direction(FULL_CIRCLE_DEGREES, level=4) - assert 'cannot be less than 1 or greater than 3' in str(exc.value) + assert "cannot be less than 1 or greater than 3" in str(exc.value) def test_angle_to_direction_level_3(): @@ -859,8 +1276,22 @@ def test_angle_to_direction_level_3(): def test_angle_to_direction_level_2(): """Test array of angles in degree.""" expected_dirs = [ - 'N', 'N', 'NE', 'NE', 'E', 'E', 'SE', 'SE', - 'S', 'S', 'SW', 'SW', 'W', 'W', 'NW', 'NW' + "N", + "N", + "NE", + "NE", + "E", + "E", + "SE", + "SE", + "S", + "S", + "SW", + "SW", + "W", + "W", + "NW", + "NW", ] output_dirs = angle_to_direction(FULL_CIRCLE_DEGREES, level=2) assert_array_equal(output_dirs, expected_dirs) @@ -869,8 +1300,23 @@ def test_angle_to_direction_level_2(): def test_angle_to_direction_level_1(): """Test array of angles in degree.""" expected_dirs = [ - 'N', 'N', 'N', 'E', 'E', 'E', 'E', 'S', 'S', 'S', 'S', - 'W', 'W', 'W', 'W', 'N'] + "N", + "N", + "N", + "E", + "E", + "E", + "E", + "S", + "S", + "S", + "S", + "W", + "W", + "W", + "W", + "N", + ] output_dirs = angle_to_direction(FULL_CIRCLE_DEGREES, level=1) assert_array_equal(output_dirs, expected_dirs) @@ -879,30 +1325,24 @@ def test_angle_to_direction_level_1(): def test_azimuth_range_to_lat_lon(): """Test converstion of azimuth and range to lat/lon grid.""" az = [332.2403, 334.6765, 337.2528, 339.73846, 342.26257] - rng = [2125., 64625., 127125., 189625., 252125., 314625.] + rng = [2125.0, 64625.0, 127125.0, 189625.0, 252125.0, 314625.0] clon = -89.98416666666667 clat = 32.27972222222222 output_lon, output_lat = azimuth_range_to_lat_lon(az, rng, clon, clat) - true_lon = [[-89.9946968, -90.3061798, -90.6211612, -90.9397425, -91.2620282, - -91.5881257], - [-89.9938369, -90.2799198, -90.5692874, -90.8620385, -91.1582743, - -91.4580996], - [-89.9929086, -90.251559, -90.5132417, -90.7780507, -91.0460827, - -91.3174374], - [-89.9919961, -90.2236737, -90.4581161, -90.6954113, -90.9356497, - -91.178925], - [-89.9910545, -90.1948876, -90.4011921, -90.6100481, -90.8215385, - -91.0357492]] - true_lat = [[32.2966329, 32.7936114, 33.2898102, 33.7852055, 34.2797726, - 34.773486], - [32.2969961, 32.804717, 33.3117799, 33.8181643, 34.3238488, - 34.8288114], - [32.2973461, 32.8154229, 33.3329617, 33.8499452, 34.3663556, - 34.8821746], - [32.29765, 32.8247204, 33.3513589, 33.8775516, 34.4032838, - 34.9285404], - [32.2979242, 32.8331062, 33.367954, 33.9024562, 34.4366016, - 34.9703782]] + true_lon = [ + [-89.9946968, -90.3061798, -90.6211612, -90.9397425, -91.2620282, -91.5881257], + [-89.9938369, -90.2799198, -90.5692874, -90.8620385, -91.1582743, -91.4580996], + [-89.9929086, -90.251559, -90.5132417, -90.7780507, -91.0460827, -91.3174374], + [-89.9919961, -90.2236737, -90.4581161, -90.6954113, -90.9356497, -91.178925], + [-89.9910545, -90.1948876, -90.4011921, -90.6100481, -90.8215385, -91.0357492], + ] + true_lat = [ + [32.2966329, 32.7936114, 33.2898102, 33.7852055, 34.2797726, 34.773486], + [32.2969961, 32.804717, 33.3117799, 33.8181643, 34.3238488, 34.8288114], + [32.2973461, 32.8154229, 33.3329617, 33.8499452, 34.3663556, 34.8821746], + [32.29765, 32.8247204, 33.3513589, 33.8775516, 34.4032838, 34.9285404], + [32.2979242, 32.8331062, 33.367954, 33.9024562, 34.4366016, 34.9703782], + ] assert_array_almost_equal(output_lon, true_lon, 6) assert_array_almost_equal(output_lat, true_lat, 6) @@ -911,31 +1351,25 @@ def test_azimuth_range_to_lat_lon(): def test_azimuth_range_to_lat_lon_diff_ellps(): """Test converstion of azimuth and range to lat/lon grid.""" az = [332.2403, 334.6765, 337.2528, 339.73846, 342.26257] - rng = [2125., 64625., 127125., 189625., 252125., 314625.] + rng = [2125.0, 64625.0, 127125.0, 189625.0, 252125.0, 314625.0] clon = -89.98416666666667 clat = 32.27972222222222 - kwargs = {'ellps': 'WGS84'} + kwargs = {"ellps": "WGS84"} output_lon, output_lat = azimuth_range_to_lat_lon(az, rng, clon, clat, **kwargs) - true_lon = [[-89.9946749, -90.3055083, -90.6198256, -90.9377279, -91.2593193, - -91.5847066], - [-89.9938168, -90.279303, -90.5680603, -90.860187, -91.1557841, - -91.4549558], - [-89.9928904, -90.2510012, -90.5121319, -90.7763758, -91.0438294, - -91.3145919], - [-89.9919799, -90.2231741, -90.4571217, -90.6939102, -90.9336298, - -91.1763737], - [-89.9910402, -90.194448, -90.4003169, -90.6087268, -90.8197603, - -91.0335027]] - true_lat = [[32.2966791, 32.794996, 33.2924932, 33.7891466, 34.2849315, - 34.7798223], - [32.2970433, 32.8061309, 33.3145188, 33.8221862, 34.3291116, - 34.835273], - [32.2973942, 32.816865, 33.3357544, 33.8540448, 34.3717184, - 34.8887564], - [32.297699, 32.826187, 33.3541984, 33.8817186, 34.4087331, - 34.9352264], - [32.2979739, 32.834595, 33.3708355, 33.906684, 34.4421288, - 34.9771578]] + true_lon = [ + [-89.9946749, -90.3055083, -90.6198256, -90.9377279, -91.2593193, -91.5847066], + [-89.9938168, -90.279303, -90.5680603, -90.860187, -91.1557841, -91.4549558], + [-89.9928904, -90.2510012, -90.5121319, -90.7763758, -91.0438294, -91.3145919], + [-89.9919799, -90.2231741, -90.4571217, -90.6939102, -90.9336298, -91.1763737], + [-89.9910402, -90.194448, -90.4003169, -90.6087268, -90.8197603, -91.0335027], + ] + true_lat = [ + [32.2966791, 32.794996, 33.2924932, 33.7891466, 34.2849315, 34.7798223], + [32.2970433, 32.8061309, 33.3145188, 33.8221862, 34.3291116, 34.835273], + [32.2973942, 32.816865, 33.3357544, 33.8540448, 34.3717184, 34.8887564], + [32.297699, 32.826187, 33.3541984, 33.8817186, 34.4087331, 34.9352264], + [32.2979739, 32.834595, 33.3708355, 33.906684, 34.4421288, 34.9771578], + ] assert_array_almost_equal(output_lon, true_lon, 6) assert_array_almost_equal(output_lat, true_lat, 6) @@ -944,7 +1378,7 @@ def test_3d_gradient_3d_data_no_axes(deriv_4d_data): """Test 3D gradient with 3D data and no axes parameter.""" test = deriv_4d_data[0] res = gradient(test, deltas=(1, 1, 1)) - truth = tuple(factor * np.ones_like(test) for factor in (16., 4., 1.)) + truth = tuple(factor * np.ones_like(test) for factor in (16.0, 4.0, 1.0)) assert_array_almost_equal(res, truth, 8) @@ -953,7 +1387,7 @@ def test_2d_gradient_3d_data_no_axes(deriv_4d_data): test = deriv_4d_data[0] with pytest.raises(ValueError) as exc: gradient(test, deltas=(1, 1)) - assert 'must match the number of dimensions' in str(exc.value) + assert "must match the number of dimensions" in str(exc.value) def test_3d_gradient_2d_data_no_axes(deriv_4d_data): @@ -961,44 +1395,44 @@ def test_3d_gradient_2d_data_no_axes(deriv_4d_data): test = deriv_4d_data[0, 0] with pytest.raises(ValueError) as exc: gradient(test, deltas=(1, 1, 1)) - assert 'must match the number of dimensions' in str(exc.value) + assert "must match the number of dimensions" in str(exc.value) def test_2d_gradient_4d_data_2_axes_3_deltas(deriv_4d_data): """Test 2D gradient of 4D data with 2 axes and 3 deltas.""" res = gradient(deriv_4d_data, deltas=(1, 1, 1), axes=(-2, -1)) - truth = tuple(factor * np.ones_like(deriv_4d_data) for factor in (4., 1.)) + truth = tuple(factor * np.ones_like(deriv_4d_data) for factor in (4.0, 1.0)) assert_array_almost_equal(res, truth, 8) def test_2d_gradient_4d_data_2_axes_2_deltas(deriv_4d_data): """Test 2D gradient of 4D data with 2 axes and 2 deltas.""" res = gradient(deriv_4d_data, deltas=(1, 1), axes=(0, 1)) - truth = tuple(factor * np.ones_like(deriv_4d_data) for factor in (48., 16.)) + truth = tuple(factor * np.ones_like(deriv_4d_data) for factor in (48.0, 16.0)) assert_array_almost_equal(res, truth, 8) def test_2d_gradient_4d_data_2_axes_1_deltas(deriv_4d_data): """Test for failure of 2D gradient of 4D data with 2 axes and 1 deltas.""" with pytest.raises(ValueError) as exc: - gradient(deriv_4d_data, deltas=(1, ), axes=(1, 2)) + gradient(deriv_4d_data, deltas=(1,), axes=(1, 2)) assert 'cannot be less than that of "axes"' in str(exc.value) def test_first_derivative_xarray_lonlat(test_da_lonlat): """Test first derivative with an xarray.DataArray on a lonlat grid in each axis usage.""" - deriv = first_derivative(test_da_lonlat, axis='lon') # dimension coordinate name - deriv_alt1 = first_derivative(test_da_lonlat, axis='x') # axis type + deriv = first_derivative(test_da_lonlat, axis="lon") # dimension coordinate name + deriv_alt1 = first_derivative(test_da_lonlat, axis="x") # axis type deriv_alt2 = first_derivative(test_da_lonlat, axis=-1) # axis number # Build the xarray of the desired values partial = xr.DataArray( np.array([-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06]), - coords=(('lat', test_da_lonlat['lat']),) + coords=(("lat", test_da_lonlat["lat"]),), ) _, truth = xr.broadcast(test_da_lonlat, partial) - truth.coords['crs'] = test_da_lonlat['crs'] - truth.attrs['units'] = 'kelvin / meter' + truth.coords["crs"] = test_da_lonlat["crs"] + truth.attrs["units"] = "kelvin / meter" truth = truth.metpy.quantify() # Assert result matches expectation @@ -1014,7 +1448,7 @@ def test_first_derivative_xarray_time_and_default_axis(test_da_xy): """Test first derivative with an xarray.DataArray over time as default first dimension.""" deriv = first_derivative(test_da_xy) truth = xr.full_like(test_da_xy, -0.000777000777) - truth.attrs['units'] = 'kelvin / second' + truth.attrs["units"] = "kelvin / second" truth = truth.metpy.quantify() xr.testing.assert_allclose(deriv, truth) @@ -1023,18 +1457,22 @@ def test_first_derivative_xarray_time_and_default_axis(test_da_xy): def test_first_derivative_xarray_time_subsecond_precision(): """Test time derivative with an xarray.DataArray where subsecond precision is needed.""" - test_da = xr.DataArray([299.5, 300, 300.5], - dims='time', - coords={'time': np.array(['2019-01-01T00:00:00.0', - '2019-01-01T00:00:00.1', - '2019-01-01T00:00:00.2'], - dtype='datetime64[ms]')}, - attrs={'units': 'kelvin'}) + test_da = xr.DataArray( + [299.5, 300, 300.5], + dims="time", + coords={ + "time": np.array( + ["2019-01-01T00:00:00.0", "2019-01-01T00:00:00.1", "2019-01-01T00:00:00.2"], + dtype="datetime64[ms]", + ) + }, + attrs={"units": "kelvin"}, + ) deriv = first_derivative(test_da) - truth = xr.full_like(test_da, 5.) - truth.attrs['units'] = 'kelvin / second' + truth = xr.full_like(test_da, 5.0) + truth.attrs["units"] = "kelvin / second" truth = truth.metpy.quantify() xr.testing.assert_allclose(deriv, truth) @@ -1043,16 +1481,16 @@ def test_first_derivative_xarray_time_subsecond_precision(): def test_second_derivative_xarray_lonlat(test_da_lonlat): """Test second derivative with an xarray.DataArray on a lonlat grid.""" - deriv = second_derivative(test_da_lonlat, axis='lat') + deriv = second_derivative(test_da_lonlat, axis="lat") # Build the xarray of the desired values partial = xr.DataArray( np.array([1.67155420e-14, 1.67155420e-14, 1.74268211e-14, 1.74268211e-14]), - coords=(('lat', test_da_lonlat['lat']),) + coords=(("lat", test_da_lonlat["lat"]),), ) _, truth = xr.broadcast(test_da_lonlat, partial) - truth.coords['crs'] = test_da_lonlat['crs'] - truth.attrs['units'] = 'kelvin / meter^2' + truth.coords["crs"] = test_da_lonlat["crs"] + truth.attrs["units"] = "kelvin / meter^2" truth = truth.metpy.quantify() xr.testing.assert_allclose(deriv, truth) @@ -1061,26 +1499,27 @@ def test_second_derivative_xarray_lonlat(test_da_lonlat): def test_gradient_xarray(test_da_xy): """Test the 3D gradient calculation with a 4D DataArray in each axis usage.""" - deriv_x, deriv_y, deriv_p = gradient(test_da_xy, axes=('x', 'y', 'isobaric')) - deriv_x_alt1, deriv_y_alt1, deriv_p_alt1 = gradient(test_da_xy, - axes=('x', 'y', 'vertical')) + deriv_x, deriv_y, deriv_p = gradient(test_da_xy, axes=("x", "y", "isobaric")) + deriv_x_alt1, deriv_y_alt1, deriv_p_alt1 = gradient( + test_da_xy, axes=("x", "y", "vertical") + ) deriv_x_alt2, deriv_y_alt2, deriv_p_alt2 = gradient(test_da_xy, axes=(3, 2, 1)) truth_x = xr.full_like(test_da_xy, -6.993007e-07) - truth_x.attrs['units'] = 'kelvin / meter' + truth_x.attrs["units"] = "kelvin / meter" truth_x = truth_x.metpy.quantify() truth_y = xr.full_like(test_da_xy, -2.797203e-06) - truth_y.attrs['units'] = 'kelvin / meter' + truth_y.attrs["units"] = "kelvin / meter" truth_y = truth_y.metpy.quantify() partial = xr.DataArray( np.array([0.04129204, 0.03330003, 0.02264402]), - coords=(('isobaric', test_da_xy['isobaric']),) + coords=(("isobaric", test_da_xy["isobaric"]),), ) _, truth_p = xr.broadcast(test_da_xy, partial) - truth_p.coords['crs'] = test_da_xy['crs'] - truth_p.attrs['units'] = 'kelvin / hectopascal' + truth_p.coords["crs"] = test_da_xy["crs"] + truth_p.attrs["units"] = "kelvin / hectopascal" truth_p = truth_p.metpy.quantify() # Assert results match expectations @@ -1106,11 +1545,11 @@ def test_gradient_xarray_implicit_axes(test_da_xy): deriv_y, deriv_x = gradient(data) truth_x = xr.full_like(data, -6.993007e-07) - truth_x.attrs['units'] = 'kelvin / meter' + truth_x.attrs["units"] = "kelvin / meter" truth_x = truth_x.metpy.quantify() truth_y = xr.full_like(data, -2.797203e-06) - truth_y.attrs['units'] = 'kelvin / meter' + truth_y.attrs["units"] = "kelvin / meter" truth_y = truth_y.metpy.quantify() xr.testing.assert_allclose(deriv_x, truth_x) @@ -1122,28 +1561,34 @@ def test_gradient_xarray_implicit_axes(test_da_xy): def test_gradient_xarray_implicit_axes_transposed(test_da_lonlat): """Test the 2D gradient with no axes specified but in x/y order.""" - test_da = test_da_lonlat.isel(isobaric=0).transpose('lon', 'lat') + test_da = test_da_lonlat.isel(isobaric=0).transpose("lon", "lat") deriv_x, deriv_y = gradient(test_da) truth_x = xr.DataArray( np.array( - [[-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06], - [-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06], - [-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06], - [-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06]] - ) * units('kelvin / meter'), + [ + [-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06], + [-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06], + [-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06], + [-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06], + ] + ) + * units("kelvin / meter"), dims=test_da.dims, - coords=test_da.coords + coords=test_da.coords, ) truth_y = xr.DataArray( np.array( - [[-1.15162805e-05, -1.15101023e-05, -1.15037894e-05, -1.14973413e-05], - [-1.15162805e-05, -1.15101023e-05, -1.15037894e-05, -1.14973413e-05], - [-1.15162805e-05, -1.15101023e-05, -1.15037894e-05, -1.14973413e-05], - [-1.15162805e-05, -1.15101023e-05, -1.15037894e-05, -1.14973413e-05]] - ) * units('kelvin / meter'), + [ + [-1.15162805e-05, -1.15101023e-05, -1.15037894e-05, -1.14973413e-05], + [-1.15162805e-05, -1.15101023e-05, -1.15037894e-05, -1.14973413e-05], + [-1.15162805e-05, -1.15101023e-05, -1.15037894e-05, -1.14973413e-05], + [-1.15162805e-05, -1.15101023e-05, -1.15037894e-05, -1.14973413e-05], + ] + ) + * units("kelvin / meter"), dims=test_da.dims, - coords=test_da.coords + coords=test_da.coords, ) xr.testing.assert_allclose(deriv_x, truth_x) @@ -1155,16 +1600,16 @@ def test_gradient_xarray_implicit_axes_transposed(test_da_lonlat): def test_laplacian_xarray_lonlat(test_da_lonlat): """Test laplacian with an xarray.DataArray on a lonlat grid.""" - laplac = laplacian(test_da_lonlat, axes=('lat', 'lon')) + laplac = laplacian(test_da_lonlat, axes=("lat", "lon")) # Build the xarray of the desired values partial = xr.DataArray( np.array([1.67155420e-14, 1.67155420e-14, 1.74268211e-14, 1.74268211e-14]), - coords=(('lat', test_da_lonlat['lat']),) + coords=(("lat", test_da_lonlat["lat"]),), ) _, truth = xr.broadcast(test_da_lonlat, partial) - truth.coords['crs'] = test_da_lonlat['crs'] - truth.attrs['units'] = 'kelvin / meter^2' + truth.coords["crs"] = test_da_lonlat["crs"] + truth.attrs["units"] = "kelvin / meter^2" truth = truth.metpy.quantify() xr.testing.assert_allclose(laplac, truth) @@ -1175,8 +1620,20 @@ def test_first_derivative_xarray_pint_conversion(test_da_lonlat): """Test first derivative with implicit xarray to pint quantity conversion.""" dx, _ = grid_deltas_from_dataarray(test_da_lonlat) deriv = first_derivative(test_da_lonlat, delta=dx, axis=-1) - truth = np.array([[[-3.30782978e-06] * 4, [-3.42816074e-06] * 4, [-3.57012948e-06] * 4, - [-3.73759364e-06] * 4]] * 3) * units('kelvin / meter') + truth = ( + np.array( + [ + [ + [-3.30782978e-06] * 4, + [-3.42816074e-06] * 4, + [-3.57012948e-06] * 4, + [-3.73759364e-06] * 4, + ] + ] + * 3 + ) + * units("kelvin / meter") + ) assert_array_almost_equal(deriv, truth, 12) @@ -1185,8 +1642,8 @@ def test_gradient_xarray_pint_conversion(test_da_xy): data = test_da_xy.isel(time=0, isobaric=2) deriv_y, deriv_x = gradient(data, coordinates=(data.metpy.y, data.metpy.x)) - truth_x = np.ones_like(data) * -6.993007e-07 * units('kelvin / meter') - truth_y = np.ones_like(data) * -2.797203e-06 * units('kelvin / meter') + truth_x = np.ones_like(data) * -6.993007e-07 * units("kelvin / meter") + truth_y = np.ones_like(data) * -2.797203e-06 * units("kelvin / meter") assert_array_almost_equal(deriv_x, truth_x, 12) assert_array_almost_equal(deriv_y, truth_y, 12) diff --git a/tests/calc/test_cross_sections.py b/tests/calc/test_cross_sections.py index 259a25d59d3..e33f354e83f 100644 --- a/tests/calc/test_cross_sections.py +++ b/tests/calc/test_cross_sections.py @@ -7,10 +7,14 @@ import pytest import xarray as xr -from metpy.calc import (absolute_momentum, cross_section_components, normal_component, - tangential_component, unit_vectors_from_cross_section) -from metpy.calc.cross_sections import (distances_from_cross_section, - latitude_from_cross_section) +from metpy.calc import ( + absolute_momentum, + cross_section_components, + normal_component, + tangential_component, + unit_vectors_from_cross_section, +) +from metpy.calc.cross_sections import distances_from_cross_section, latitude_from_cross_section from metpy.interpolate import cross_section from metpy.testing import assert_array_almost_equal, assert_xarray_allclose, needs_cartopy from metpy.units import units @@ -24,83 +28,73 @@ def test_cross_lonlat(): data_v = np.linspace(40, -40, 5 * 6 * 7).reshape((5, 6, 7)) * units.knots ds = xr.Dataset( { - 'u_wind': (['isobaric', 'lat', 'lon'], data_u), - 'v_wind': (['isobaric', 'lat', 'lon'], data_v) + "u_wind": (["isobaric", "lat", "lon"], data_u), + "v_wind": (["isobaric", "lat", "lon"], data_v), }, coords={ - 'isobaric': xr.DataArray( + "isobaric": xr.DataArray( np.linspace(1000, 500, 5), - name='isobaric', - dims=['isobaric'], - attrs={'units': 'hPa'} + name="isobaric", + dims=["isobaric"], + attrs={"units": "hPa"}, ), - 'lat': xr.DataArray( + "lat": xr.DataArray( np.linspace(30, 45, 6), - name='lat', - dims=['lat'], - attrs={'units': 'degrees_north'} + name="lat", + dims=["lat"], + attrs={"units": "degrees_north"}, ), - 'lon': xr.DataArray( + "lon": xr.DataArray( np.linspace(255, 275, 7), - name='lon', - dims=['lon'], - attrs={'units': 'degrees_east'} - ) - } + name="lon", + dims=["lon"], + attrs={"units": "degrees_east"}, + ), + }, ) start, end = (30.5, 255.5), (44.5, 274.5) - return cross_section(ds.metpy.parse_cf(), start, end, steps=7, interp_type='nearest') + return cross_section(ds.metpy.parse_cf(), start, end, steps=7, interp_type="nearest") @pytest.fixture() @needs_cartopy def test_cross_xy(): """Return cross section on a x/y grid with a time coordinate for use in tests.""" - data_u = np.linspace(-25, 25, 5 * 6 * 7).reshape((1, 5, 6, 7)) * units('m/s') - data_v = np.linspace(25, -25, 5 * 6 * 7).reshape((1, 5, 6, 7)) * units('m/s') + data_u = np.linspace(-25, 25, 5 * 6 * 7).reshape((1, 5, 6, 7)) * units("m/s") + data_v = np.linspace(25, -25, 5 * 6 * 7).reshape((1, 5, 6, 7)) * units("m/s") ds = xr.Dataset( { - 'u_wind': (['time', 'isobaric', 'y', 'x'], data_u), - 'v_wind': (['time', 'isobaric', 'y', 'x'], data_v), - 'lambert_conformal': ([], '') + "u_wind": (["time", "isobaric", "y", "x"], data_u), + "v_wind": (["time", "isobaric", "y", "x"], data_v), + "lambert_conformal": ([], ""), }, coords={ - 'time': xr.DataArray( - np.array([np.datetime64('2018-07-01T00:00')]), - name='time', - dims=['time'] + "time": xr.DataArray( + np.array([np.datetime64("2018-07-01T00:00")]), name="time", dims=["time"] ), - 'isobaric': xr.DataArray( + "isobaric": xr.DataArray( np.linspace(1000, 500, 5), - name='isobaric', - dims=['isobaric'], - attrs={'units': 'hPa'} + name="isobaric", + dims=["isobaric"], + attrs={"units": "hPa"}, ), - 'y': xr.DataArray( - np.linspace(-1500, 0, 6), - name='y', - dims=['y'], - attrs={'units': 'km'} + "y": xr.DataArray( + np.linspace(-1500, 0, 6), name="y", dims=["y"], attrs={"units": "km"} ), - 'x': xr.DataArray( - np.linspace(-500, 3000, 7), - name='x', - dims=['x'], - attrs={'units': 'km'} - ) - } + "x": xr.DataArray( + np.linspace(-500, 3000, 7), name="x", dims=["x"], attrs={"units": "km"} + ), + }, ) - ds['u_wind'].attrs = ds['v_wind'].attrs = { - 'grid_mapping': 'lambert_conformal' - } - ds['lambert_conformal'].attrs = { - 'grid_mapping_name': 'lambert_conformal_conic', - 'standard_parallel': 50.0, - 'longitude_of_central_meridian': -107.0, - 'latitude_of_projection_origin': 50.0, - 'earth_shape': 'spherical', - 'earth_radius': 6367470.21484375 + ds["u_wind"].attrs = ds["v_wind"].attrs = {"grid_mapping": "lambert_conformal"} + ds["lambert_conformal"].attrs = { + "grid_mapping_name": "lambert_conformal_conic", + "standard_parallel": 50.0, + "longitude_of_central_meridian": -107.0, + "latitude_of_projection_origin": 50.0, + "earth_shape": "spherical", + "earth_radius": 6367470.21484375, } start, end = ((36.46, -112.45), (42.95, -68.74)) @@ -109,32 +103,50 @@ def test_cross_xy(): def test_distances_from_cross_section_given_lonlat(test_cross_lonlat): """Test distances from cross section with lat/lon grid.""" - x, y = distances_from_cross_section(test_cross_lonlat['u_wind']) - - true_x_values = np.array([-0., 252585.3108187, 505170.6216374, 757755.93245611, - 1010341.24327481, 1262926.55409352, 1515511.86491222]) - true_y_values = np.array([-0., 283412.80349716, 566825.60699432, 850238.41049148, - 1133651.21398864, 1417064.0174858, 1700476.82098296]) - index = xr.DataArray(range(7), name='index', dims=['index']) + x, y = distances_from_cross_section(test_cross_lonlat["u_wind"]) + + true_x_values = np.array( + [ + -0.0, + 252585.3108187, + 505170.6216374, + 757755.93245611, + 1010341.24327481, + 1262926.55409352, + 1515511.86491222, + ] + ) + true_y_values = np.array( + [ + -0.0, + 283412.80349716, + 566825.60699432, + 850238.41049148, + 1133651.21398864, + 1417064.0174858, + 1700476.82098296, + ] + ) + index = xr.DataArray(range(7), name="index", dims=["index"]) true_x = xr.DataArray( true_x_values * units.meters, coords={ - 'crs': test_cross_lonlat['crs'], - 'lat': test_cross_lonlat['lat'], - 'lon': test_cross_lonlat['lon'], - 'index': index, + "crs": test_cross_lonlat["crs"], + "lat": test_cross_lonlat["lat"], + "lon": test_cross_lonlat["lon"], + "index": index, }, - dims=['index'] + dims=["index"], ) true_y = xr.DataArray( true_y_values * units.meters, coords={ - 'crs': test_cross_lonlat['crs'], - 'lat': test_cross_lonlat['lat'], - 'lon': test_cross_lonlat['lon'], - 'index': index, + "crs": test_cross_lonlat["crs"], + "lat": test_cross_lonlat["lat"], + "lon": test_cross_lonlat["lon"], + "index": index, }, - dims=['index'] + dims=["index"], ) assert_xarray_allclose(x, true_x) assert_xarray_allclose(y, true_y) @@ -142,182 +154,492 @@ def test_distances_from_cross_section_given_lonlat(test_cross_lonlat): def test_distances_from_cross_section_given_xy(test_cross_xy): """Test distances from cross section with x/y grid.""" - x, y = distances_from_cross_section(test_cross_xy['u_wind']) - xr.testing.assert_identical(test_cross_xy['x'], x) - xr.testing.assert_identical(test_cross_xy['y'], y) + x, y = distances_from_cross_section(test_cross_xy["u_wind"]) + xr.testing.assert_identical(test_cross_xy["x"], x) + xr.testing.assert_identical(test_cross_xy["y"], y) def test_distances_from_cross_section_given_bad_coords(test_cross_xy): """Ensure an AttributeError is raised when the cross section lacks neeed coordinates.""" with pytest.raises(AttributeError): - distances_from_cross_section(test_cross_xy['u_wind'].drop_vars('x')) + distances_from_cross_section(test_cross_xy["u_wind"].drop_vars("x")) def test_latitude_from_cross_section_given_lat(test_cross_lonlat): """Test latitude from cross section with latitude given.""" - latitude = latitude_from_cross_section(test_cross_lonlat['v_wind']) - xr.testing.assert_identical(test_cross_lonlat['lat'], latitude) + latitude = latitude_from_cross_section(test_cross_lonlat["v_wind"]) + xr.testing.assert_identical(test_cross_lonlat["lat"], latitude) def test_latitude_from_cross_section_given_y(test_cross_xy): """Test latitude from cross section with y given.""" - latitude = latitude_from_cross_section(test_cross_xy['v_wind']) - true_latitude_values = np.array([36.46, 38.64829115, 40.44833152, 41.81277354, 42.7011178, - 43.0845549, 42.95]) - index = xr.DataArray(range(7), name='index', dims=['index']) + latitude = latitude_from_cross_section(test_cross_xy["v_wind"]) + true_latitude_values = np.array( + [36.46, 38.64829115, 40.44833152, 41.81277354, 42.7011178, 43.0845549, 42.95] + ) + index = xr.DataArray(range(7), name="index", dims=["index"]) true_latitude = xr.DataArray( true_latitude_values * units.degrees_north, coords={ - 'crs': test_cross_xy['crs'], - 'y': test_cross_xy['y'], - 'x': test_cross_xy['x'], - 'index': index, + "crs": test_cross_xy["crs"], + "y": test_cross_xy["y"], + "x": test_cross_xy["x"], + "index": index, }, - dims=['index'] + dims=["index"], ) assert_xarray_allclose(latitude, true_latitude) def test_unit_vectors_from_cross_section_given_lonlat(test_cross_lonlat): """Test unit vector calculation from cross section with lat/lon grid.""" - unit_tangent, unit_normal = unit_vectors_from_cross_section(test_cross_lonlat['u_wind']) - true_unit_tangent = np.array([[0.66533859, 0.66533859, 0.66533859, 0.66533859, 0.66533859, - 0.66533859, 0.66533859], - [0.74654173, 0.74654173, 0.74654173, 0.74654173, 0.74654173, - 0.74654173, 0.74654173]]) - true_unit_normal = np.array([[-0.74654173, -0.74654173, -0.74654173, -0.74654173, - -0.74654173, -0.74654173, -0.74654173], - [0.66533859, 0.66533859, 0.66533859, 0.66533859, - 0.66533859, 0.66533859, 0.66533859]]) + unit_tangent, unit_normal = unit_vectors_from_cross_section(test_cross_lonlat["u_wind"]) + true_unit_tangent = np.array( + [ + [ + 0.66533859, + 0.66533859, + 0.66533859, + 0.66533859, + 0.66533859, + 0.66533859, + 0.66533859, + ], + [ + 0.74654173, + 0.74654173, + 0.74654173, + 0.74654173, + 0.74654173, + 0.74654173, + 0.74654173, + ], + ] + ) + true_unit_normal = np.array( + [ + [ + -0.74654173, + -0.74654173, + -0.74654173, + -0.74654173, + -0.74654173, + -0.74654173, + -0.74654173, + ], + [ + 0.66533859, + 0.66533859, + 0.66533859, + 0.66533859, + 0.66533859, + 0.66533859, + 0.66533859, + ], + ] + ) assert_array_almost_equal(true_unit_tangent, unit_tangent, 7) assert_array_almost_equal(true_unit_normal, unit_normal, 7) def test_unit_vectors_from_cross_section_given_xy(test_cross_xy): """Test unit vector calculation from cross section with x/y grid.""" - unit_tangent, unit_normal = unit_vectors_from_cross_section(test_cross_xy['u_wind']) - true_unit_tangent = np.array([[0.93567585, 0.929688, 0.92380315, 0.91844706, 0.91349795, - 0.90875771, 0.90400673], - [0.35286074, 0.36834796, 0.3828678, 0.39554392, 0.40684333, - 0.41732413, 0.42751822]]) - true_unit_normal = np.array([[-0.35286074, -0.36834796, -0.3828678, -0.39554392, - -0.40684333, -0.41732413, -0.42751822], - [0.93567585, 0.929688, 0.92380315, 0.91844706, 0.91349795, - 0.90875771, 0.90400673]]) + unit_tangent, unit_normal = unit_vectors_from_cross_section(test_cross_xy["u_wind"]) + true_unit_tangent = np.array( + [ + [0.93567585, 0.929688, 0.92380315, 0.91844706, 0.91349795, 0.90875771, 0.90400673], + [ + 0.35286074, + 0.36834796, + 0.3828678, + 0.39554392, + 0.40684333, + 0.41732413, + 0.42751822, + ], + ] + ) + true_unit_normal = np.array( + [ + [ + -0.35286074, + -0.36834796, + -0.3828678, + -0.39554392, + -0.40684333, + -0.41732413, + -0.42751822, + ], + [0.93567585, 0.929688, 0.92380315, 0.91844706, 0.91349795, 0.90875771, 0.90400673], + ] + ) assert_array_almost_equal(true_unit_tangent, unit_tangent, 7) assert_array_almost_equal(true_unit_normal, unit_normal, 7) def test_cross_section_components(test_cross_lonlat): """Test getting cross section components of a 2D vector field.""" - tang_wind, norm_wind = cross_section_components(test_cross_lonlat['u_wind'], - test_cross_lonlat['v_wind']) - true_tang_wind_values = np.array([[3.24812563, 2.9994653, 2.75080496, 2.50214463, - 2.47106208, 2.22240175, 1.97374141], - [1.94265887, 1.69399854, 1.4453382, 1.19667786, - 1.16559532, 0.91693499, 0.66827465], - [0.63719211, 0.38853177, 0.13987144, -0.1087889, - -0.13987144, -0.38853177, -0.63719211], - [-0.66827465, -0.91693499, -1.16559532, -1.41425566, - -1.4453382, -1.69399854, -1.94265887], - [-1.97374141, -2.22240175, -2.47106208, -2.71972242, - -2.75080496, -2.9994653, -3.24812563]]) - true_norm_wind_values = np.array([[56.47521297, 52.15175169, 47.82829041, 43.50482913, - 42.96439647, 38.64093519, 34.31747391], - [33.77704125, 29.45357997, 25.13011869, 20.80665741, - 20.26622475, 15.94276347, 11.61930219], - [11.07886953, 6.75540825, 2.43194697, -1.89151431, - -2.43194697, -6.75540825, -11.07886953], - [-11.61930219, -15.94276347, -20.26622475, -24.58968603, - -25.13011869, -29.45357997, -33.77704125], - [-34.31747391, -38.64093519, -42.96439647, -47.28785775, - -47.82829041, -52.15175169, -56.47521297]]) - true_tang_wind = xr.DataArray(true_tang_wind_values * units.knots, - coords=test_cross_lonlat['u_wind'].coords, - dims=test_cross_lonlat['u_wind'].dims, - attrs=test_cross_lonlat['u_wind'].attrs) - true_norm_wind = xr.DataArray(true_norm_wind_values * units.knots, - coords=test_cross_lonlat['u_wind'].coords, - dims=test_cross_lonlat['u_wind'].dims, - attrs=test_cross_lonlat['u_wind'].attrs) + tang_wind, norm_wind = cross_section_components( + test_cross_lonlat["u_wind"], test_cross_lonlat["v_wind"] + ) + true_tang_wind_values = np.array( + [ + [ + 3.24812563, + 2.9994653, + 2.75080496, + 2.50214463, + 2.47106208, + 2.22240175, + 1.97374141, + ], + [ + 1.94265887, + 1.69399854, + 1.4453382, + 1.19667786, + 1.16559532, + 0.91693499, + 0.66827465, + ], + [ + 0.63719211, + 0.38853177, + 0.13987144, + -0.1087889, + -0.13987144, + -0.38853177, + -0.63719211, + ], + [ + -0.66827465, + -0.91693499, + -1.16559532, + -1.41425566, + -1.4453382, + -1.69399854, + -1.94265887, + ], + [ + -1.97374141, + -2.22240175, + -2.47106208, + -2.71972242, + -2.75080496, + -2.9994653, + -3.24812563, + ], + ] + ) + true_norm_wind_values = np.array( + [ + [ + 56.47521297, + 52.15175169, + 47.82829041, + 43.50482913, + 42.96439647, + 38.64093519, + 34.31747391, + ], + [ + 33.77704125, + 29.45357997, + 25.13011869, + 20.80665741, + 20.26622475, + 15.94276347, + 11.61930219, + ], + [ + 11.07886953, + 6.75540825, + 2.43194697, + -1.89151431, + -2.43194697, + -6.75540825, + -11.07886953, + ], + [ + -11.61930219, + -15.94276347, + -20.26622475, + -24.58968603, + -25.13011869, + -29.45357997, + -33.77704125, + ], + [ + -34.31747391, + -38.64093519, + -42.96439647, + -47.28785775, + -47.82829041, + -52.15175169, + -56.47521297, + ], + ] + ) + true_tang_wind = xr.DataArray( + true_tang_wind_values * units.knots, + coords=test_cross_lonlat["u_wind"].coords, + dims=test_cross_lonlat["u_wind"].dims, + attrs=test_cross_lonlat["u_wind"].attrs, + ) + true_norm_wind = xr.DataArray( + true_norm_wind_values * units.knots, + coords=test_cross_lonlat["u_wind"].coords, + dims=test_cross_lonlat["u_wind"].dims, + attrs=test_cross_lonlat["u_wind"].attrs, + ) assert_xarray_allclose(tang_wind, true_tang_wind) assert_xarray_allclose(norm_wind, true_norm_wind) def test_tangential_component(test_cross_xy): """Test getting cross section tangential component of a 2D vector field.""" - tang_wind = tangential_component(test_cross_xy['u_wind'], test_cross_xy['v_wind']) - true_tang_wind_values = np.array([[[-14.56982141, -13.17102075, -11.83790134, - -10.59675064, -9.42888813, -8.31533355, -7.2410326], - [-8.71378435, -7.53076196, -6.40266576, -5.34269988, - -4.33810002, -3.37748418, -2.45334901], - [-2.85774728, -1.89050316, -0.96743019, -0.08864912, - 0.7526881, 1.5603652, 2.33433459], - [2.99828978, 3.74975563, 4.46780539, 5.16540164, - 5.84347621, 6.49821458, 7.12201819], - [8.85432685, 9.39001443, 9.90304096, 10.41945241, - 10.93426433, 11.43606396, 11.90970179]]]) - true_tang_wind = xr.DataArray(true_tang_wind_values * units('m/s'), - coords=test_cross_xy['u_wind'].coords, - dims=test_cross_xy['u_wind'].dims, - attrs=test_cross_xy['u_wind'].attrs) + tang_wind = tangential_component(test_cross_xy["u_wind"], test_cross_xy["v_wind"]) + true_tang_wind_values = np.array( + [ + [ + [ + -14.56982141, + -13.17102075, + -11.83790134, + -10.59675064, + -9.42888813, + -8.31533355, + -7.2410326, + ], + [ + -8.71378435, + -7.53076196, + -6.40266576, + -5.34269988, + -4.33810002, + -3.37748418, + -2.45334901, + ], + [ + -2.85774728, + -1.89050316, + -0.96743019, + -0.08864912, + 0.7526881, + 1.5603652, + 2.33433459, + ], + [ + 2.99828978, + 3.74975563, + 4.46780539, + 5.16540164, + 5.84347621, + 6.49821458, + 7.12201819, + ], + [ + 8.85432685, + 9.39001443, + 9.90304096, + 10.41945241, + 10.93426433, + 11.43606396, + 11.90970179, + ], + ] + ] + ) + true_tang_wind = xr.DataArray( + true_tang_wind_values * units("m/s"), + coords=test_cross_xy["u_wind"].coords, + dims=test_cross_xy["u_wind"].dims, + attrs=test_cross_xy["u_wind"].attrs, + ) assert_xarray_allclose(tang_wind, true_tang_wind) def test_normal_component(test_cross_xy): """Test getting cross section normal component of a 2D vector field.""" - norm_wind = normal_component(test_cross_xy['u_wind'], test_cross_xy['v_wind']) - true_norm_wind_values = np.array([[[32.21218429, 30.45650997, 28.59536112, 26.62832466, - 24.57166983, 22.43805311, 20.2347284], - [19.26516594, 17.41404337, 15.46613157, 13.42554447, - 11.30508283, 9.11378585, 6.85576955], - [6.3181476, 4.37157677, 2.33690202, 0.22276428, - -1.96150417, -4.2104814, -6.52318931], - [-6.62887075, -8.67088982, -10.79232752, -12.98001592, - -15.22809117, -17.53474865, -19.90214816], - [-19.5758891, -21.71335642, -23.92155707, -26.18279611, - -28.49467817, -30.8590159, -33.28110701]]]) - true_norm_wind = xr.DataArray(true_norm_wind_values * units('m/s'), - coords=test_cross_xy['u_wind'].coords, - dims=test_cross_xy['u_wind'].dims, - attrs=test_cross_xy['u_wind'].attrs) + norm_wind = normal_component(test_cross_xy["u_wind"], test_cross_xy["v_wind"]) + true_norm_wind_values = np.array( + [ + [ + [ + 32.21218429, + 30.45650997, + 28.59536112, + 26.62832466, + 24.57166983, + 22.43805311, + 20.2347284, + ], + [ + 19.26516594, + 17.41404337, + 15.46613157, + 13.42554447, + 11.30508283, + 9.11378585, + 6.85576955, + ], + [ + 6.3181476, + 4.37157677, + 2.33690202, + 0.22276428, + -1.96150417, + -4.2104814, + -6.52318931, + ], + [ + -6.62887075, + -8.67088982, + -10.79232752, + -12.98001592, + -15.22809117, + -17.53474865, + -19.90214816, + ], + [ + -19.5758891, + -21.71335642, + -23.92155707, + -26.18279611, + -28.49467817, + -30.8590159, + -33.28110701, + ], + ] + ] + ) + true_norm_wind = xr.DataArray( + true_norm_wind_values * units("m/s"), + coords=test_cross_xy["u_wind"].coords, + dims=test_cross_xy["u_wind"].dims, + attrs=test_cross_xy["u_wind"].attrs, + ) assert_xarray_allclose(norm_wind, true_norm_wind) def test_absolute_momentum_given_lonlat(test_cross_lonlat): """Test absolute momentum calculation.""" - momentum = absolute_momentum(test_cross_lonlat['u_wind'], test_cross_lonlat['v_wind']) - true_momentum_values = np.array([[29.05335956, 57.00676169, 88.89733786, 124.37969813, - 165.02883664, 206.55775948, 250.49678829], - [17.37641122, 45.32981335, 77.22038952, 112.70274979, - 153.3518883, 194.88081114, 238.81983995], - [5.69946288, 33.65286501, 65.54344118, 101.02580145, - 141.67493996, 183.2038628, 227.14289161], - [-5.97748546, 21.97591667, 53.86649284, 89.34885311, - 129.99799162, 171.52691446, 215.46594327], - [-17.6544338, 10.29896833, 42.1895445, 77.67190477, - 118.32104328, 159.84996612, 203.78899492]]) - - true_momentum = xr.DataArray(true_momentum_values * units('m/s'), - coords=test_cross_lonlat['u_wind'].coords, - dims=test_cross_lonlat['u_wind'].dims) + momentum = absolute_momentum(test_cross_lonlat["u_wind"], test_cross_lonlat["v_wind"]) + true_momentum_values = np.array( + [ + [ + 29.05335956, + 57.00676169, + 88.89733786, + 124.37969813, + 165.02883664, + 206.55775948, + 250.49678829, + ], + [ + 17.37641122, + 45.32981335, + 77.22038952, + 112.70274979, + 153.3518883, + 194.88081114, + 238.81983995, + ], + [ + 5.69946288, + 33.65286501, + 65.54344118, + 101.02580145, + 141.67493996, + 183.2038628, + 227.14289161, + ], + [ + -5.97748546, + 21.97591667, + 53.86649284, + 89.34885311, + 129.99799162, + 171.52691446, + 215.46594327, + ], + [ + -17.6544338, + 10.29896833, + 42.1895445, + 77.67190477, + 118.32104328, + 159.84996612, + 203.78899492, + ], + ] + ) + + true_momentum = xr.DataArray( + true_momentum_values * units("m/s"), + coords=test_cross_lonlat["u_wind"].coords, + dims=test_cross_lonlat["u_wind"].dims, + ) assert_xarray_allclose(momentum, true_momentum) def test_absolute_momentum_given_xy(test_cross_xy): """Test absolute momentum calculation.""" - momentum = absolute_momentum(test_cross_xy['u_wind'], test_cross_xy['v_wind']) - true_momentum_values = np.array([[[169.22222693, 146.36354006, 145.75559124, 171.8710635, - 215.04876817, 265.73797007, 318.34138347], - [156.27520858, 133.32107346, 132.62636169, 158.66828331, - 201.78218117, 252.41370282, 304.96242462], - [143.32819023, 120.27860686, 119.49713214, 145.46550311, - 188.51559418, 239.08943557, 291.58346576], - [130.38117188, 107.23614026, 106.36790259, 132.26272292, - 175.24900718, 225.76516831, 278.20450691], - [117.43415353, 94.19367366, 93.23867305, 119.05994273, - 161.98242018, 212.44090106, 264.82554806]]]) - true_momentum = xr.DataArray(true_momentum_values * units('m/s'), - coords=test_cross_xy['u_wind'].coords, - dims=test_cross_xy['u_wind'].dims) + momentum = absolute_momentum(test_cross_xy["u_wind"], test_cross_xy["v_wind"]) + true_momentum_values = np.array( + [ + [ + [ + 169.22222693, + 146.36354006, + 145.75559124, + 171.8710635, + 215.04876817, + 265.73797007, + 318.34138347, + ], + [ + 156.27520858, + 133.32107346, + 132.62636169, + 158.66828331, + 201.78218117, + 252.41370282, + 304.96242462, + ], + [ + 143.32819023, + 120.27860686, + 119.49713214, + 145.46550311, + 188.51559418, + 239.08943557, + 291.58346576, + ], + [ + 130.38117188, + 107.23614026, + 106.36790259, + 132.26272292, + 175.24900718, + 225.76516831, + 278.20450691, + ], + [ + 117.43415353, + 94.19367366, + 93.23867305, + 119.05994273, + 161.98242018, + 212.44090106, + 264.82554806, + ], + ] + ] + ) + true_momentum = xr.DataArray( + true_momentum_values * units("m/s"), + coords=test_cross_xy["u_wind"].coords, + dims=test_cross_xy["u_wind"].dims, + ) assert_xarray_allclose(momentum, true_momentum) diff --git a/tests/calc/test_indices.py b/tests/calc/test_indices.py index 568bd57a689..ee43072b480 100644 --- a/tests/calc/test_indices.py +++ b/tests/calc/test_indices.py @@ -7,54 +7,171 @@ import numpy as np -from metpy.calc import (bulk_shear, bunkers_storm_motion, critical_angle, - mean_pressure_weighted, precipitable_water, - significant_tornado, supercell_composite) +from metpy.calc import ( + bulk_shear, + bunkers_storm_motion, + critical_angle, + mean_pressure_weighted, + precipitable_water, + significant_tornado, + supercell_composite, +) from metpy.testing import assert_almost_equal, assert_array_equal, get_upper_air_data from metpy.units import concatenate, units def test_precipitable_water(): """Test precipitable water with observed sounding.""" - data = get_upper_air_data(datetime(2016, 5, 22, 0), 'DDC') - pw = precipitable_water(data['pressure'], data['dewpoint'], top=400 * units.hPa) - truth = (0.8899441949243486 * units('inches')).to('millimeters') + data = get_upper_air_data(datetime(2016, 5, 22, 0), "DDC") + pw = precipitable_water(data["pressure"], data["dewpoint"], top=400 * units.hPa) + truth = (0.8899441949243486 * units("inches")).to("millimeters") assert_array_equal(pw, truth) def test_precipitable_water_no_bounds(): """Test precipitable water with observed sounding and no bounds given.""" - data = get_upper_air_data(datetime(2016, 5, 22, 0), 'DDC') - dewpoint = data['dewpoint'] - pressure = data['pressure'] + data = get_upper_air_data(datetime(2016, 5, 22, 0), "DDC") + dewpoint = data["dewpoint"] + pressure = data["pressure"] inds = pressure >= 400 * units.hPa pw = precipitable_water(pressure[inds], dewpoint[inds]) - truth = (0.8899441949243486 * units('inches')).to('millimeters') + truth = (0.8899441949243486 * units("inches")).to("millimeters") assert_array_equal(pw, truth) def test_precipitable_water_bound_error(): """Test with no top bound given and data that produced floating point issue #596.""" - pressure = np.array([993., 978., 960.5, 927.6, 925., 895.8, 892., 876., 45.9, 39.9, 36., - 36., 34.3]) * units.hPa - dewpoint = np.array([25.5, 24.1, 23.1, 21.2, 21.1, 19.4, 19.2, 19.2, -87.1, -86.5, -86.5, - -86.5, -88.1]) * units.degC + pressure = ( + np.array( + [ + 993.0, + 978.0, + 960.5, + 927.6, + 925.0, + 895.8, + 892.0, + 876.0, + 45.9, + 39.9, + 36.0, + 36.0, + 34.3, + ] + ) + * units.hPa + ) + dewpoint = ( + np.array( + [25.5, 24.1, 23.1, 21.2, 21.1, 19.4, 19.2, 19.2, -87.1, -86.5, -86.5, -86.5, -88.1] + ) + * units.degC + ) pw = precipitable_water(pressure, dewpoint) - truth = 89.86955998646951 * units('millimeters') + truth = 89.86955998646951 * units("millimeters") assert_almost_equal(pw, truth, 8) def test_precipitable_water_nans(): """Test that PW returns appropriate number if NaNs are present.""" - pressure = np.array([1001, 1000, 997, 977.9, 977, 957, 937.8, 925, 906, 899.3, 887, 862.5, - 854, 850, 800, 793.9, 785, 777, 771, 762, 731.8, 726, 703, 700, 655, - 630, 621.2, 602, 570.7, 548, 546.8, 539, 513, 511, 485, 481, 468, - 448, 439, 424, 420, 412]) * units.hPa - dewpoint = np.array([-25.1, -26.1, -26.8, np.nan, -27.3, -28.2, np.nan, -27.2, -26.6, - np.nan, -27.4, np.nan, -23.5, -23.5, -25.1, np.nan, -22.9, -17.8, - -16.6, np.nan, np.nan, -16.4, np.nan, -18.5, -21., -23.7, np.nan, - -28.3, np.nan, -32.6, np.nan, -33.8, -35., -35.1, -38.1, -40., - -43.3, -44.6, -46.4, -47., -49.2, -50.7]) * units.degC + pressure = ( + np.array( + [ + 1001, + 1000, + 997, + 977.9, + 977, + 957, + 937.8, + 925, + 906, + 899.3, + 887, + 862.5, + 854, + 850, + 800, + 793.9, + 785, + 777, + 771, + 762, + 731.8, + 726, + 703, + 700, + 655, + 630, + 621.2, + 602, + 570.7, + 548, + 546.8, + 539, + 513, + 511, + 485, + 481, + 468, + 448, + 439, + 424, + 420, + 412, + ] + ) + * units.hPa + ) + dewpoint = ( + np.array( + [ + -25.1, + -26.1, + -26.8, + np.nan, + -27.3, + -28.2, + np.nan, + -27.2, + -26.6, + np.nan, + -27.4, + np.nan, + -23.5, + -23.5, + -25.1, + np.nan, + -22.9, + -17.8, + -16.6, + np.nan, + np.nan, + -16.4, + np.nan, + -18.5, + -21.0, + -23.7, + np.nan, + -28.3, + np.nan, + -32.6, + np.nan, + -33.8, + -35.0, + -35.1, + -38.1, + -40.0, + -43.3, + -44.6, + -46.4, + -47.0, + -49.2, + -50.7, + ] + ) + * units.degC + ) pw = precipitable_water(pressure, dewpoint) truth = 4.003709214463873 * units.mm assert_almost_equal(pw, truth, 8) @@ -62,110 +179,127 @@ def test_precipitable_water_nans(): def test_mean_pressure_weighted(): """Test pressure-weighted mean wind function with vertical interpolation.""" - data = get_upper_air_data(datetime(2016, 5, 22, 0), 'DDC') - u, v = mean_pressure_weighted(data['pressure'], - data['u_wind'], - data['v_wind'], - height=data['height'], - depth=6000 * units('meter')) - assert_almost_equal(u, 6.0208700094534775 * units('m/s'), 7) - assert_almost_equal(v, 7.966031839967931 * units('m/s'), 7) + data = get_upper_air_data(datetime(2016, 5, 22, 0), "DDC") + u, v = mean_pressure_weighted( + data["pressure"], + data["u_wind"], + data["v_wind"], + height=data["height"], + depth=6000 * units("meter"), + ) + assert_almost_equal(u, 6.0208700094534775 * units("m/s"), 7) + assert_almost_equal(v, 7.966031839967931 * units("m/s"), 7) def test_mean_pressure_weighted_elevated(): """Test pressure-weighted mean wind function with a base above the surface.""" - data = get_upper_air_data(datetime(2016, 5, 22, 0), 'DDC') - u, v = mean_pressure_weighted(data['pressure'], - data['u_wind'], - data['v_wind'], - height=data['height'], - depth=3000 * units('meter'), - bottom=data['height'][0] + 3000 * units('meter')) - assert_almost_equal(u, 8.270829843626476 * units('m/s'), 7) - assert_almost_equal(v, 1.7392601775853547 * units('m/s'), 7) + data = get_upper_air_data(datetime(2016, 5, 22, 0), "DDC") + u, v = mean_pressure_weighted( + data["pressure"], + data["u_wind"], + data["v_wind"], + height=data["height"], + depth=3000 * units("meter"), + bottom=data["height"][0] + 3000 * units("meter"), + ) + assert_almost_equal(u, 8.270829843626476 * units("m/s"), 7) + assert_almost_equal(v, 1.7392601775853547 * units("m/s"), 7) def test_bunkers_motion(): """Test Bunkers storm motion with observed sounding.""" - data = get_upper_air_data(datetime(2016, 5, 22, 0), 'DDC') - motion = concatenate(bunkers_storm_motion(data['pressure'], - data['u_wind'], data['v_wind'], - data['height'])) - truth = [1.4537892577864744, 2.0169333025630616, 10.587950761120482, 13.915130377372801, - 6.0208700094534775, 7.9660318399679308] * units('m/s') + data = get_upper_air_data(datetime(2016, 5, 22, 0), "DDC") + motion = concatenate( + bunkers_storm_motion(data["pressure"], data["u_wind"], data["v_wind"], data["height"]) + ) + truth = [ + 1.4537892577864744, + 2.0169333025630616, + 10.587950761120482, + 13.915130377372801, + 6.0208700094534775, + 7.9660318399679308, + ] * units("m/s") assert_almost_equal(motion.flatten(), truth, 8) def test_bulk_shear(): """Test bulk shear with observed sounding.""" - data = get_upper_air_data(datetime(2016, 5, 22, 0), 'DDC') - u, v = bulk_shear(data['pressure'], data['u_wind'], - data['v_wind'], height=data['height'], - depth=6000 * units('meter')) - truth = [29.899581266946115, -14.389225800205509] * units('knots') - assert_almost_equal(u.to('knots'), truth[0], 8) - assert_almost_equal(v.to('knots'), truth[1], 8) + data = get_upper_air_data(datetime(2016, 5, 22, 0), "DDC") + u, v = bulk_shear( + data["pressure"], + data["u_wind"], + data["v_wind"], + height=data["height"], + depth=6000 * units("meter"), + ) + truth = [29.899581266946115, -14.389225800205509] * units("knots") + assert_almost_equal(u.to("knots"), truth[0], 8) + assert_almost_equal(v.to("knots"), truth[1], 8) def test_bulk_shear_no_depth(): """Test bulk shear with observed sounding and no depth given. Issue #568.""" - data = get_upper_air_data(datetime(2016, 5, 22, 0), 'DDC') - u, v = bulk_shear(data['pressure'], data['u_wind'], - data['v_wind'], height=data['height']) - truth = [20.225018939, 22.602359692] * units('knots') - assert_almost_equal(u.to('knots'), truth[0], 8) - assert_almost_equal(v.to('knots'), truth[1], 8) + data = get_upper_air_data(datetime(2016, 5, 22, 0), "DDC") + u, v = bulk_shear(data["pressure"], data["u_wind"], data["v_wind"], height=data["height"]) + truth = [20.225018939, 22.602359692] * units("knots") + assert_almost_equal(u.to("knots"), truth[0], 8) + assert_almost_equal(v.to("knots"), truth[1], 8) def test_bulk_shear_elevated(): """Test bulk shear with observed sounding and a base above the surface.""" - data = get_upper_air_data(datetime(2016, 5, 22, 0), 'DDC') - u, v = bulk_shear(data['pressure'], data['u_wind'], - data['v_wind'], height=data['height'], - bottom=data['height'][0] + 3000 * units('meter'), - depth=3000 * units('meter')) - truth = [0.9655943923302139, -3.8405428777944466] * units('m/s') + data = get_upper_air_data(datetime(2016, 5, 22, 0), "DDC") + u, v = bulk_shear( + data["pressure"], + data["u_wind"], + data["v_wind"], + height=data["height"], + bottom=data["height"][0] + 3000 * units("meter"), + depth=3000 * units("meter"), + ) + truth = [0.9655943923302139, -3.8405428777944466] * units("m/s") assert_almost_equal(u, truth[0], 8) assert_almost_equal(v, truth[1], 8) def test_supercell_composite(): """Test supercell composite function.""" - mucape = [2000., 1000., 500., 2000.] * units('J/kg') - esrh = [400., 150., 45., 45.] * units('m^2/s^2') - ebwd = [30., 15., 5., 5.] * units('m/s') - truth = [16., 2.25, 0., 0.] + mucape = [2000.0, 1000.0, 500.0, 2000.0] * units("J/kg") + esrh = [400.0, 150.0, 45.0, 45.0] * units("m^2/s^2") + ebwd = [30.0, 15.0, 5.0, 5.0] * units("m/s") + truth = [16.0, 2.25, 0.0, 0.0] supercell_comp = supercell_composite(mucape, esrh, ebwd) assert_array_equal(supercell_comp, truth) def test_supercell_composite_scalar(): """Test supercell composite function with a single value.""" - mucape = 2000. * units('J/kg') - esrh = 400. * units('m^2/s^2') - ebwd = 30. * units('m/s') - truth = 16. + mucape = 2000.0 * units("J/kg") + esrh = 400.0 * units("m^2/s^2") + ebwd = 30.0 * units("m/s") + truth = 16.0 supercell_comp = supercell_composite(mucape, esrh, ebwd) assert_almost_equal(supercell_comp, truth, 6) def test_sigtor(): """Test significant tornado parameter function.""" - sbcape = [2000., 2000., 2000., 2000., 3000, 4000] * units('J/kg') - sblcl = [3000., 1500., 500., 1500., 1500, 800] * units('meter') - srh1 = [200., 200., 200., 200., 300, 400] * units('m^2/s^2') - shr6 = [20., 5., 20., 35., 20., 35] * units('m/s') - truth = [0., 0, 1.777778, 1.333333, 2., 10.666667] + sbcape = [2000.0, 2000.0, 2000.0, 2000.0, 3000, 4000] * units("J/kg") + sblcl = [3000.0, 1500.0, 500.0, 1500.0, 1500, 800] * units("meter") + srh1 = [200.0, 200.0, 200.0, 200.0, 300, 400] * units("m^2/s^2") + shr6 = [20.0, 5.0, 20.0, 35.0, 20.0, 35] * units("m/s") + truth = [0.0, 0, 1.777778, 1.333333, 2.0, 10.666667] sigtor = significant_tornado(sbcape, sblcl, srh1, shr6) assert_almost_equal(sigtor, truth, 6) def test_sigtor_scalar(): """Test significant tornado parameter function with a single value.""" - sbcape = 4000 * units('J/kg') - sblcl = 800 * units('meter') - srh1 = 400 * units('m^2/s^2') - shr6 = 35 * units('m/s') + sbcape = 4000 * units("J/kg") + sblcl = 800 * units("meter") + srh1 = 400 * units("m^2/s^2") + shr6 = 35 * units("m/s") truth = 10.666667 sigtor = significant_tornado(sbcape, sblcl, srh1, shr6) assert_almost_equal(sigtor, truth, 6) @@ -173,24 +307,39 @@ def test_sigtor_scalar(): def test_critical_angle(): """Test critical angle with observed sounding.""" - data = get_upper_air_data(datetime(2016, 5, 22, 0), 'DDC') - ca = critical_angle(data['pressure'], data['u_wind'], - data['v_wind'], data['height'], - u_storm=0 * units('m/s'), v_storm=0 * units('m/s')) - truth = [140.0626637513269] * units('degrees') + data = get_upper_air_data(datetime(2016, 5, 22, 0), "DDC") + ca = critical_angle( + data["pressure"], + data["u_wind"], + data["v_wind"], + data["height"], + u_storm=0 * units("m/s"), + v_storm=0 * units("m/s"), + ) + truth = [140.0626637513269] * units("degrees") assert_almost_equal(ca, truth, 8) def test_critical_angle_units(): """Test critical angle with observed sounding and different storm motion units.""" - data = get_upper_air_data(datetime(2016, 5, 22, 0), 'DDC') + data = get_upper_air_data(datetime(2016, 5, 22, 0), "DDC") # Set storm motion in m/s - ca_ms = critical_angle(data['pressure'], data['u_wind'], - data['v_wind'], data['height'], - u_storm=10 * units('m/s'), v_storm=10 * units('m/s')) + ca_ms = critical_angle( + data["pressure"], + data["u_wind"], + data["v_wind"], + data["height"], + u_storm=10 * units("m/s"), + v_storm=10 * units("m/s"), + ) # Set same storm motion in kt and m/s - ca_kt_ms = critical_angle(data['pressure'], data['u_wind'], - data['v_wind'], data['height'], - u_storm=10 * units('m/s'), v_storm=19.4384449244 * units('kt')) + ca_kt_ms = critical_angle( + data["pressure"], + data["u_wind"], + data["v_wind"], + data["height"], + u_storm=10 * units("m/s"), + v_storm=19.4384449244 * units("kt"), + ) # Make sure the resulting critical angles are equal assert_almost_equal(ca_ms, ca_kt_ms, 8) diff --git a/tests/calc/test_kinematics.py b/tests/calc/test_kinematics.py index 7ec51385da4..728a790746c 100644 --- a/tests/calc/test_kinematics.py +++ b/tests/calc/test_kinematics.py @@ -7,22 +7,43 @@ import pytest import xarray as xr -from metpy.calc import (absolute_vorticity, advection, ageostrophic_wind, - divergence, frontogenesis, geostrophic_wind, inertial_advective_wind, - lat_lon_grid_deltas, montgomery_streamfunction, - potential_temperature, potential_vorticity_baroclinic, - potential_vorticity_barotropic, q_vector, shearing_deformation, - static_stability, storm_relative_helicity, stretching_deformation, - total_deformation, vorticity, wind_components) -from metpy.constants import g, Re -from metpy.testing import (assert_almost_equal, assert_array_almost_equal, assert_array_equal, - get_test_data, needs_cartopy, needs_pyproj) +from metpy.calc import ( + absolute_vorticity, + advection, + ageostrophic_wind, + divergence, + frontogenesis, + geostrophic_wind, + inertial_advective_wind, + lat_lon_grid_deltas, + montgomery_streamfunction, + potential_temperature, + potential_vorticity_baroclinic, + potential_vorticity_barotropic, + q_vector, + shearing_deformation, + static_stability, + storm_relative_helicity, + stretching_deformation, + total_deformation, + vorticity, + wind_components, +) +from metpy.constants import Re, g +from metpy.testing import ( + assert_almost_equal, + assert_array_almost_equal, + assert_array_equal, + get_test_data, + needs_cartopy, + needs_pyproj, +) from metpy.units import concatenate, units def test_default_order(): """Test using the default array ordering.""" - u = np.ones((3, 3)) * units('m/s') + u = np.ones((3, 3)) * units("m/s") v = vorticity(u, u, dx=1 * units.meter, dy=1 * units.meter) true_v = np.zeros_like(u) / units.sec assert_array_equal(v, true_v) @@ -31,7 +52,7 @@ def test_default_order(): def test_zero_vorticity(): """Test vorticity calculation when zeros should be returned.""" a = np.arange(3) - u = np.c_[a, a, a] * units('m/s') + u = np.c_[a, a, a] * units("m/s") v = vorticity(u.T, u, dx=1 * units.meter, dy=1 * units.meter) true_v = np.zeros_like(u) / units.sec assert_array_equal(v, true_v) @@ -40,7 +61,7 @@ def test_zero_vorticity(): def test_vorticity(): """Test vorticity for simple case.""" a = np.arange(3) - u = np.c_[a, a, a] * units('m/s') + u = np.c_[a, a, a] * units("m/s") v = vorticity(u.T, u.T, dx=1 * units.meter, dy=1 * units.meter) true_v = np.ones_like(u) / units.sec assert_array_equal(v, true_v) @@ -48,10 +69,12 @@ def test_vorticity(): def test_vorticity_asym(): """Test vorticity calculation with a complicated field.""" - u = np.array([[2, 4, 8], [0, 2, 2], [4, 6, 8]]) * units('m/s') - v = np.array([[6, 4, 8], [2, 6, 0], [2, 2, 6]]) * units('m/s') + u = np.array([[2, 4, 8], [0, 2, 2], [4, 6, 8]]) * units("m/s") + v = np.array([[6, 4, 8], [2, 6, 0], [2, 2, 6]]) * units("m/s") vort = vorticity(u, v, dx=1 * units.meters, dy=2 * units.meters) - true_vort = np.array([[-2.5, 3.5, 13.], [8.5, -1.5, -11.], [-5.5, -1.5, 0.]]) / units.sec + true_vort = ( + np.array([[-2.5, 3.5, 13.0], [8.5, -1.5, -11.0], [-5.5, -1.5, 0.0]]) / units.sec + ) assert_array_equal(vort, true_vort) @@ -59,53 +82,53 @@ def test_vorticity_positional_grid_args_failure(): """Test that old API of positional grid arguments to vorticity fails.""" # pylint: disable=too-many-function-args a = np.arange(3) - u = np.c_[a, a, a] * units('m/s') - with pytest.raises(TypeError, match='too many positional arguments'): + u = np.c_[a, a, a] * units("m/s") + with pytest.raises(TypeError, match="too many positional arguments"): vorticity(u.T, u, 1 * units.meter, 1 * units.meter) def test_zero_divergence(): """Test divergence calculation when zeros should be returned.""" a = np.arange(3) - u = np.c_[a, a, a] * units('m/s') + u = np.c_[a, a, a] * units("m/s") c = divergence(u.T, u, dx=1 * units.meter, dy=1 * units.meter) - true_c = 2. * np.ones_like(u) / units.sec + true_c = 2.0 * np.ones_like(u) / units.sec assert_array_equal(c, true_c) def test_divergence(): """Test divergence for simple case.""" a = np.arange(3) - u = np.c_[a, a, a] * units('m/s') + u = np.c_[a, a, a] * units("m/s") c = divergence(u.T, u.T, dx=1 * units.meter, dy=1 * units.meter) true_c = np.ones_like(u) / units.sec assert_array_equal(c, true_c) -def test_horizontal_divergence(): - """Test taking the horizontal divergence of a 3D field.""" - u = np.array([[[1., 1., 1.], - [1., 0., 1.], - [1., 1., 1.]], - [[0., 0., 0.], - [0., 1., 0.], - [0., 0., 0.]]]) * units('m/s') - c = divergence(u, u, dx=1 * units.meter, dy=1 * units.meter) - true_c = np.array([[[0., -2., 0.], - [-2., 0., 2.], - [0., 2., 0.]], - [[0., 2., 0.], - [2., 0., -2.], - [0., -2., 0.]]]) * units('s^-1') - assert_array_equal(c, true_c) +# def test_horizontal_divergence(): +# """Test taking the horizontal divergence of a 3D field.""" +# u = np.array([[[1., 1., 1.], +# [1., 0., 1.], +# [1., 1., 1.]], +# [[0., 0., 0.], +# [0., 1., 0.], +# [0., 0., 0.]]]) * units('m/s') +# c = divergence(u, u, dx=1 * units.meter, dy=1 * units.meter) +# true_c = np.array([[[0., -2., 0.], +# [-2., 0., 2.], +# [0., 2., 0.]], +# [[0., 2., 0.], +# [2., 0., -2.], +# [0., -2., 0.]]]) * units('s^-1') +# assert_array_equal(c, true_c) def test_divergence_asym(): """Test divergence calculation with a complicated field.""" - u = np.array([[2, 4, 8], [0, 2, 2], [4, 6, 8]]) * units('m/s') - v = np.array([[6, 4, 8], [2, 6, 0], [2, 2, 6]]) * units('m/s') + u = np.array([[2, 4, 8], [0, 2, 2], [4, 6, 8]]) * units("m/s") + v = np.array([[6, 4, 8], [2, 6, 0], [2, 2, 6]]) * units("m/s") c = divergence(u, v, dx=1 * units.meters, dy=2 * units.meters) - true_c = np.array([[-2, 5.5, -2.5], [2., 0.5, -1.5], [3., -1.5, 8.5]]) / units.sec + true_c = np.array([[-2, 5.5, -2.5], [2.0, 0.5, -1.5], [3.0, -1.5, 8.5]]) / units.sec assert_array_equal(c, true_c) @@ -113,119 +136,132 @@ def test_divergence_positional_grid_args_failure(): """Test that old API of positional grid arguments to divergence fails.""" # pylint: disable=too-many-function-args a = np.arange(3) - u = np.c_[a, a, a] * units('m/s') - with pytest.raises(TypeError, match='too many positional arguments'): + u = np.c_[a, a, a] * units("m/s") + with pytest.raises(TypeError, match="too many positional arguments"): divergence(u, u, 1 * units.meter, 1 * units.meter) def test_shearing_deformation_asym(): """Test shearing deformation calculation with a complicated field.""" - u = np.array([[2, 4, 8], [0, 2, 2], [4, 6, 8]]) * units('m/s') - v = np.array([[6, 4, 8], [2, 6, 0], [2, 2, 6]]) * units('m/s') + u = np.array([[2, 4, 8], [0, 2, 2], [4, 6, 8]]) * units("m/s") + v = np.array([[6, 4, 8], [2, 6, 0], [2, 2, 6]]) * units("m/s") sh = shearing_deformation(u, v, 1 * units.meters, 2 * units.meters) - true_sh = np.array([[-7.5, -1.5, 1.], [9.5, -0.5, -11.], [1.5, 5.5, 12.]]) / units.sec + true_sh = np.array([[-7.5, -1.5, 1.0], [9.5, -0.5, -11.0], [1.5, 5.5, 12.0]]) / units.sec assert_array_equal(sh, true_sh) def test_stretching_deformation_asym(): """Test stretching deformation calculation with a complicated field.""" - u = np.array([[2, 4, 8], [0, 2, 2], [4, 6, 8]]) * units('m/s') - v = np.array([[6, 4, 8], [2, 6, 0], [2, 2, 6]]) * units('m/s') + u = np.array([[2, 4, 8], [0, 2, 2], [4, 6, 8]]) * units("m/s") + v = np.array([[6, 4, 8], [2, 6, 0], [2, 2, 6]]) * units("m/s") st = stretching_deformation(u, v, 1 * units.meters, 2 * units.meters) - true_st = np.array([[4., 0.5, 12.5], [4., 1.5, -0.5], [1., 5.5, -4.5]]) / units.sec + true_st = np.array([[4.0, 0.5, 12.5], [4.0, 1.5, -0.5], [1.0, 5.5, -4.5]]) / units.sec assert_array_equal(st, true_st) def test_total_deformation_asym(): """Test total deformation calculation with a complicated field.""" - u = np.array([[2, 4, 8], [0, 2, 2], [4, 6, 8]]) * units('m/s') - v = np.array([[6, 4, 8], [2, 6, 0], [2, 2, 6]]) * units('m/s') + u = np.array([[2, 4, 8], [0, 2, 2], [4, 6, 8]]) * units("m/s") + v = np.array([[6, 4, 8], [2, 6, 0], [2, 2, 6]]) * units("m/s") tdef = total_deformation(u, v, 1 * units.meters, 2 * units.meters) - true_tdef = np.array([[8.5, 1.58113883, 12.5399362], [10.30776406, 1.58113883, 11.0113578], - [1.80277562, 7.7781746, 12.8160056]]) / units.sec + true_tdef = ( + np.array( + [ + [8.5, 1.58113883, 12.5399362], + [10.30776406, 1.58113883, 11.0113578], + [1.80277562, 7.7781746, 12.8160056], + ] + ) + / units.sec + ) assert_almost_equal(tdef, true_tdef) def test_frontogenesis_asym(): """Test frontogensis calculation with a complicated field.""" - u = np.array([[2, 4, 8], [0, 2, 2], [4, 6, 8]]) * units('m/s') - v = np.array([[6, 4, 8], [2, 6, 0], [2, 2, 6]]) * units('m/s') - theta = np.array([[303, 295, 305], [308, 310, 312], [299, 293, 289]]) * units('K') + u = np.array([[2, 4, 8], [0, 2, 2], [4, 6, 8]]) * units("m/s") + v = np.array([[6, 4, 8], [2, 6, 0], [2, 2, 6]]) * units("m/s") + theta = np.array([[303, 295, 305], [308, 310, 312], [299, 293, 289]]) * units("K") fronto = frontogenesis(theta, u, v, 1 * units.meters, 2 * units.meters) - true_fronto = np.array([[-52.4746386, -37.3658646, -50.3996939], - [3.5777088, -2.1221867, -16.9941166], - [-23.1417334, 26.0499143, -158.4839684]] - ) * units.K / units.meter / units.sec + true_fronto = ( + np.array( + [ + [-52.4746386, -37.3658646, -50.3996939], + [3.5777088, -2.1221867, -16.9941166], + [-23.1417334, 26.0499143, -158.4839684], + ] + ) + * units.K + / units.meter + / units.sec + ) assert_almost_equal(fronto, true_fronto) def test_advection_uniform(): """Test advection calculation for a uniform 1D field.""" - u = np.ones((3,)) * units('m/s') + u = np.ones((3,)) * units("m/s") s = np.ones_like(u) * units.kelvin a = advection(s.T, u.T, dx=1 * units.meter) - truth = np.zeros_like(u) * units('K/sec') + truth = np.zeros_like(u) * units("K/sec") assert_array_equal(a, truth) def test_advection_1d_uniform_wind(): """Test advection for simple 1D case with uniform wind.""" - u = np.ones((3,)) * units('m/s') - s = np.array([1, 2, 3]) * units('kg') + u = np.ones((3,)) * units("m/s") + s = np.array([1, 2, 3]) * units("kg") a = advection(s.T, u.T, dx=1 * units.meter) - truth = -np.ones_like(u) * units('kg/sec') + truth = -np.ones_like(u) * units("kg/sec") assert_array_equal(a, truth) def test_advection_1d(): """Test advection calculation with varying wind and field.""" - u = np.array([1, 2, 3]) * units('m/s') - s = np.array([1, 2, 3]) * units('Pa') + u = np.array([1, 2, 3]) * units("m/s") + s = np.array([1, 2, 3]) * units("Pa") a = advection(s.T, u.T, dx=1 * units.meter) - truth = np.array([-1, -2, -3]) * units('Pa/sec') + truth = np.array([-1, -2, -3]) * units("Pa/sec") assert_array_equal(a, truth) def test_advection_2d_uniform(): """Test advection for uniform 2D field.""" - u = np.ones((3, 3)) * units('m/s') + u = np.ones((3, 3)) * units("m/s") s = np.ones_like(u) * units.kelvin a = advection(s.T, u.T, u.T, dx=1 * units.meter, dy=1 * units.meter) - truth = np.zeros_like(u) * units('K/sec') + truth = np.zeros_like(u) * units("K/sec") assert_array_equal(a, truth) def test_advection_2d(): """Test advection in varying 2D field.""" - u = np.ones((3, 3)) * units('m/s') - v = 2 * np.ones((3, 3)) * units('m/s') + u = np.ones((3, 3)) * units("m/s") + v = 2 * np.ones((3, 3)) * units("m/s") s = np.array([[1, 2, 1], [2, 4, 2], [1, 2, 1]]) * units.kelvin a = advection(s.T, v.T, u.T, dx=1 * units.meter, dy=1 * units.meter) - truth = np.array([[-6, -4, 2], [-8, 0, 8], [-2, 4, 6]]) * units('K/sec') + truth = np.array([[-6, -4, 2], [-8, 0, 8], [-2, 4, 6]]) * units("K/sec") assert_array_equal(a, truth) def test_advection_2d_asym(): """Test advection in asymmetric varying 2D field.""" - u = np.arange(9).reshape(3, 3) * units('m/s') + u = np.arange(9).reshape(3, 3) * units("m/s") v = 2 * u s = np.array([[1, 2, 4], [4, 8, 4], [8, 6, 4]]) * units.kelvin a = advection(s, u, v, dx=2 * units.meter, dy=1 * units.meter) - truth = np.array([[0, -20.75, -2.5], [-33., -16., 20.], [-48, 91., 8]]) * units('K/sec') + truth = np.array([[0, -20.75, -2.5], [-33.0, -16.0, 20.0], [-48, 91.0, 8]]) * units( + "K/sec" + ) assert_array_equal(a, truth) def test_geostrophic_wind(): """Test geostrophic wind calculation with basic conditions.""" - z = np.array([[48, 49, 48], [49, 50, 49], [48, 49, 48]]) * 10. * units.meter + z = np.array([[48, 49, 48], [49, 50, 49], [48, 49, 48]]) * 10.0 * units.meter latitude = 30 * units.degrees - ug, vg = geostrophic_wind( - z.T, - 100. * units.kilometer, - 100. * units.kilometer, - latitude - ) - true_u = np.array([[-26.897, 0, 26.897]] * 3) * units('m/s') + ug, vg = geostrophic_wind(z.T, 100.0 * units.kilometer, 100.0 * units.kilometer, latitude) + true_u = np.array([[-26.897, 0, 26.897]] * 3) * units("m/s") true_v = -true_u.T assert_array_almost_equal(ug, true_u.T, 2) assert_array_almost_equal(vg, true_v.T, 2) @@ -233,35 +269,25 @@ def test_geostrophic_wind(): def test_geostrophic_wind_asym(): """Test geostrophic wind calculation with a complicated field.""" - z = np.array([[1, 2, 4], [4, 8, 4], [8, 6, 4]]) * 20. * units.meter + z = np.array([[1, 2, 4], [4, 8, 4], [8, 6, 4]]) * 20.0 * units.meter latitude = 30 * units.degrees - ug, vg = geostrophic_wind( - z, - 2000. * units.kilometer, - 1000. * units.kilometer, - latitude - ) + ug, vg = geostrophic_wind(z, 2000.0 * units.kilometer, 1000.0 * units.kilometer, latitude) true_u = np.array( [[-6.724, -26.897, 0], [-9.414, -5.379, 0], [-12.103, 16.138, 0]] - ) * units('m/s') + ) * units("m/s") true_v = np.array( [[0.672, 2.017, 3.362], [10.759, 0, -10.759], [-2.690, -2.690, -2.690]] - ) * units('m/s') + ) * units("m/s") assert_array_almost_equal(ug, true_u, 2) assert_array_almost_equal(vg, true_v, 2) def test_geostrophic_geopotential(): """Test geostrophic wind calculation with geopotential.""" - z = np.array([[48, 49, 48], [49, 50, 49], [48, 49, 48]]) * 100. * units('m^2/s^2') + z = np.array([[48, 49, 48], [49, 50, 49], [48, 49, 48]]) * 100.0 * units("m^2/s^2") latitude = 30 * units.degrees - ug, vg = geostrophic_wind( - z.T, - 100. * units.kilometer, - 100. * units.kilometer, - latitude - ) - true_u = np.array([[-27.427, 0, 27.427]] * 3) * units('m/s') + ug, vg = geostrophic_wind(z.T, 100.0 * units.kilometer, 100.0 * units.kilometer, latitude) + true_u = np.array([[-27.427, 0, 27.427]] * 3) * units("m/s") true_v = -true_u.T assert_array_almost_equal(ug, true_u.T, 2) assert_array_almost_equal(vg, true_v.T, 2) @@ -269,16 +295,13 @@ def test_geostrophic_geopotential(): def test_geostrophic_3d(): """Test geostrophic wind calculation with 3D array.""" - z = np.array([[48, 49, 48], [49, 50, 49], [48, 49, 48]]) * 10. + z = np.array([[48, 49, 48], [49, 50, 49], [48, 49, 48]]) * 10.0 latitude = 30 * units.degrees z3d = np.dstack((z, z)) * units.meter ug, vg = geostrophic_wind( - z3d.T, - 100. * units.kilometer, - 100. * units.kilometer, - latitude + z3d.T, 100.0 * units.kilometer, 100.0 * units.kilometer, latitude ) - true_u = np.array([[-26.897, 0, 26.897]] * 3) * units('m/s') + true_u = np.array([[-26.897, 0, 26.897]] * 3) * units("m/s") true_v = -true_u.T true_u = concatenate((true_u[..., None], true_u[..., None]), axis=2) @@ -289,45 +312,62 @@ def test_geostrophic_3d(): def test_geostrophic_gempak(): """Test of geostrophic wind calculation against gempak values.""" - z = np.array([[5586387.00, 5584467.50, 5583147.50], - [5594407.00, 5592487.50, 5591307.50], - [5604707.50, 5603247.50, 5602527.50]]).T \ - * (9.80616 * units('m/s^2')) * 1e-3 + z = ( + np.array( + [ + [5586387.00, 5584467.50, 5583147.50], + [5594407.00, 5592487.50, 5591307.50], + [5604707.50, 5603247.50, 5602527.50], + ] + ).T + * (9.80616 * units("m/s^2")) + * 1e-3 + ) dx = np.deg2rad(0.25) * Re * np.cos(np.deg2rad(44)) dy = -np.deg2rad(0.25) * Re lat = 44 * units.degrees ug, vg = geostrophic_wind(z.T * units.m, dx.T, dy.T, lat) - true_u = np.array([[21.97512, 21.97512, 22.08005], - [31.89402, 32.69477, 33.73863], - [38.43922, 40.18805, 42.14609]]) - true_v = np.array([[-10.93621, -7.83859, -4.54839], - [-10.74533, -7.50152, -3.24262], - [-8.66612, -5.27816, -1.45282]]) - assert_almost_equal(ug[1, 1], true_u[1, 1] * units('m/s'), 2) - assert_almost_equal(vg[1, 1], true_v[1, 1] * units('m/s'), 2) + true_u = np.array( + [ + [21.97512, 21.97512, 22.08005], + [31.89402, 32.69477, 33.73863], + [38.43922, 40.18805, 42.14609], + ] + ) + true_v = np.array( + [ + [-10.93621, -7.83859, -4.54839], + [-10.74533, -7.50152, -3.24262], + [-8.66612, -5.27816, -1.45282], + ] + ) + assert_almost_equal(ug[1, 1], true_u[1, 1] * units("m/s"), 2) + assert_almost_equal(vg[1, 1], true_v[1, 1] * units("m/s"), 2) def test_no_ageostrophic_geopotential(): """Test the updated ageostrophic wind function.""" - z = np.array([[48, 49, 48], [49, 50, 49], [48, 49, 48]]) * 100. * units('m^2/s^2') - u = np.array([[-27.427, 0, 27.427]] * 3) * units('m/s') + z = np.array([[48, 49, 48], [49, 50, 49], [48, 49, 48]]) * 100.0 * units("m^2/s^2") + u = np.array([[-27.427, 0, 27.427]] * 3) * units("m/s") v = -u.T latitude = 30 * units.degrees - uag, vag = ageostrophic_wind(z.T, u.T, v.T, 100. * units.kilometer, - 100. * units.kilometer, latitude) - true = np.array([[0, 0, 0]] * 3) * units('m/s') + uag, vag = ageostrophic_wind( + z.T, u.T, v.T, 100.0 * units.kilometer, 100.0 * units.kilometer, latitude + ) + true = np.array([[0, 0, 0]] * 3) * units("m/s") assert_array_almost_equal(uag, true.T, 2) assert_array_almost_equal(vag, true.T, 2) def test_ageostrophic_geopotential(): """Test ageostrophic wind calculation with future input variable order.""" - z = np.array([[48, 49, 48], [49, 50, 49], [48, 49, 48]]) * 100. * units('m^2/s^2') - u = v = np.array([[0, 0, 0]] * 3) * units('m/s') + z = np.array([[48, 49, 48], [49, 50, 49], [48, 49, 48]]) * 100.0 * units("m^2/s^2") + u = v = np.array([[0, 0, 0]] * 3) * units("m/s") latitude = 30 * units.degrees - uag, vag = ageostrophic_wind(z.T, u.T, v.T, 100. * units.kilometer, - 100. * units.kilometer, latitude) - u_true = np.array([[27.427, 0, -27.427]] * 3) * units('m/s') + uag, vag = ageostrophic_wind( + z.T, u.T, v.T, 100.0 * units.kilometer, 100.0 * units.kilometer, latitude + ) + u_true = np.array([[27.427, 0, -27.427]] * 3) * units("m/s") v_true = -u_true.T assert_array_almost_equal(uag, u_true.T, 2) assert_array_almost_equal(vag, v_true.T, 2) @@ -335,85 +375,98 @@ def test_ageostrophic_geopotential(): def test_streamfunc(): """Test of Montgomery Streamfunction calculation.""" - t = 287. * units.kelvin - hgt = 5000. * units.meter + t = 287.0 * units.kelvin + hgt = 5000.0 * units.meter msf = montgomery_streamfunction(hgt, t) - assert_almost_equal(msf, 337468.2500 * units('m^2 s^-2'), 4) + assert_almost_equal(msf, 337468.2500 * units("m^2 s^-2"), 4) def test_storm_relative_helicity_no_storm_motion(): """Test storm relative helicity with no storm motion and differing input units.""" - u = np.array([0, 20, 10, 0]) * units('m/s') - v = np.array([20, 0, 0, 10]) * units('m/s') - u = u.to('knots') + u = np.array([0, 20, 10, 0]) * units("m/s") + v = np.array([20, 0, 0, 10]) * units("m/s") + u = u.to("knots") heights = np.array([0, 250, 500, 750]) * units.m - positive_srh, negative_srh, total_srh = storm_relative_helicity(heights, u, v, - depth=750 * units.meters) + positive_srh, negative_srh, total_srh = storm_relative_helicity( + heights, u, v, depth=750 * units.meters + ) - assert_almost_equal(positive_srh, 400. * units('meter ** 2 / second ** 2 '), 6) - assert_almost_equal(negative_srh, -100. * units('meter ** 2 / second ** 2 '), 6) - assert_almost_equal(total_srh, 300. * units('meter ** 2 / second ** 2 '), 6) + assert_almost_equal(positive_srh, 400.0 * units("meter ** 2 / second ** 2 "), 6) + assert_almost_equal(negative_srh, -100.0 * units("meter ** 2 / second ** 2 "), 6) + assert_almost_equal(total_srh, 300.0 * units("meter ** 2 / second ** 2 "), 6) def test_storm_relative_helicity_storm_motion(): """Test storm relative helicity with storm motion and differing input units.""" - u = np.array([5, 25, 15, 5]) * units('m/s') - v = np.array([30, 10, 10, 20]) * units('m/s') - u = u.to('knots') + u = np.array([5, 25, 15, 5]) * units("m/s") + v = np.array([30, 10, 10, 20]) * units("m/s") + u = u.to("knots") heights = np.array([0, 250, 500, 750]) * units.m - pos_srh, neg_srh, total_srh = storm_relative_helicity(heights, u, v, - depth=750 * units.meters, - storm_u=5 * units('m/s'), - storm_v=10 * units('m/s')) + pos_srh, neg_srh, total_srh = storm_relative_helicity( + heights, + u, + v, + depth=750 * units.meters, + storm_u=5 * units("m/s"), + storm_v=10 * units("m/s"), + ) - assert_almost_equal(pos_srh, 400. * units('meter ** 2 / second ** 2 '), 6) - assert_almost_equal(neg_srh, -100. * units('meter ** 2 / second ** 2 '), 6) - assert_almost_equal(total_srh, 300. * units('meter ** 2 / second ** 2 '), 6) + assert_almost_equal(pos_srh, 400.0 * units("meter ** 2 / second ** 2 "), 6) + assert_almost_equal(neg_srh, -100.0 * units("meter ** 2 / second ** 2 "), 6) + assert_almost_equal(total_srh, 300.0 * units("meter ** 2 / second ** 2 "), 6) def test_storm_relative_helicity_with_interpolation(): """Test storm relative helicity with interpolation.""" - u = np.array([-5, 15, 25, 15, -5]) * units('m/s') - v = np.array([40, 20, 10, 10, 30]) * units('m/s') - u = u.to('knots') + u = np.array([-5, 15, 25, 15, -5]) * units("m/s") + v = np.array([40, 20, 10, 10, 30]) * units("m/s") + u = u.to("knots") heights = np.array([0, 100, 200, 300, 400]) * units.m - pos_srh, neg_srh, total_srh = storm_relative_helicity(heights, u, v, - bottom=50 * units.meters, - depth=300 * units.meters, - storm_u=5 * units('m/s'), - storm_v=10 * units('m/s')) + pos_srh, neg_srh, total_srh = storm_relative_helicity( + heights, + u, + v, + bottom=50 * units.meters, + depth=300 * units.meters, + storm_u=5 * units("m/s"), + storm_v=10 * units("m/s"), + ) - assert_almost_equal(pos_srh, 400. * units('meter ** 2 / second ** 2 '), 6) - assert_almost_equal(neg_srh, -100. * units('meter ** 2 / second ** 2 '), 6) - assert_almost_equal(total_srh, 300. * units('meter ** 2 / second ** 2 '), 6) + assert_almost_equal(pos_srh, 400.0 * units("meter ** 2 / second ** 2 "), 6) + assert_almost_equal(neg_srh, -100.0 * units("meter ** 2 / second ** 2 "), 6) + assert_almost_equal(total_srh, 300.0 * units("meter ** 2 / second ** 2 "), 6) def test_storm_relative_helicity(): """Test function for SRH calculations on an eigth-circle hodograph.""" # Create larger arrays for everything except pressure to make a smoother graph hgt_int = np.arange(0, 2050, 50) - hgt_int = hgt_int * units('meter') + hgt_int = hgt_int * units("meter") dir_int = np.arange(180, 272.25, 2.25) spd_int = np.zeros(hgt_int.shape[0]) - spd_int[:] = 2. - u_int, v_int = wind_components(spd_int * units('m/s'), dir_int * units.degree) + spd_int[:] = 2.0 + u_int, v_int = wind_components(spd_int * units("m/s"), dir_int * units.degree) # Put in the correct value of SRH for a eighth-circle, 2 m/s hodograph # (SRH = 2 * area under hodo, in this case...) - srh_true_p = (.25 * np.pi * (2 ** 2)) * units('m^2/s^2') + srh_true_p = (0.25 * np.pi * (2 ** 2)) * units("m^2/s^2") # Since there's only positive SRH in this case, total SRH will be equal to positive SRH and # negative SRH will be zero. srh_true_t = srh_true_p - srh_true_n = 0 * units('m^2/s^2') - p_srh, n_srh, t_srh = storm_relative_helicity(hgt_int, u_int, v_int, - 1000 * units('meter'), - bottom=0 * units('meter'), - storm_u=0 * units.knot, - storm_v=0 * units.knot) + srh_true_n = 0 * units("m^2/s^2") + p_srh, n_srh, t_srh = storm_relative_helicity( + hgt_int, + u_int, + v_int, + 1000 * units("meter"), + bottom=0 * units("meter"), + storm_u=0 * units.knot, + storm_v=0 * units.knot, + ) assert_almost_equal(p_srh, srh_true_p, 2) assert_almost_equal(n_srh, srh_true_n, 2) assert_almost_equal(t_srh, srh_true_t, 2) @@ -421,22 +474,26 @@ def test_storm_relative_helicity(): def test_storm_relative_helicity_agl(): """Test storm relative helicity with heights above ground.""" - u = np.array([-5, 15, 25, 15, -5]) * units('m/s') - v = np.array([40, 20, 10, 10, 30]) * units('m/s') - u = u.to('knots') + u = np.array([-5, 15, 25, 15, -5]) * units("m/s") + v = np.array([40, 20, 10, 10, 30]) * units("m/s") + u = u.to("knots") heights = np.array([100, 200, 300, 400, 500]) * units.m - pos_srh, neg_srh, total_srh = storm_relative_helicity(heights, u, v, - bottom=50 * units.meters, - depth=300 * units.meters, - storm_u=5 * units('m/s'), - storm_v=10 * units('m/s')) + pos_srh, neg_srh, total_srh = storm_relative_helicity( + heights, + u, + v, + bottom=50 * units.meters, + depth=300 * units.meters, + storm_u=5 * units("m/s"), + storm_v=10 * units("m/s"), + ) # Check that heights isn't modified--checks for regression of #789 assert_almost_equal(heights[0], 100 * units.m, 6) - assert_almost_equal(pos_srh, 400. * units('meter ** 2 / second ** 2 '), 6) - assert_almost_equal(neg_srh, -100. * units('meter ** 2 / second ** 2 '), 6) - assert_almost_equal(total_srh, 300. * units('meter ** 2 / second ** 2 '), 6) + assert_almost_equal(pos_srh, 400.0 * units("meter ** 2 / second ** 2 "), 6) + assert_almost_equal(neg_srh, -100.0 * units("meter ** 2 / second ** 2 "), 6) + assert_almost_equal(total_srh, 300.0 * units("meter ** 2 / second ** 2 "), 6) def test_storm_relative_helicity_masked(): @@ -444,9 +501,14 @@ def test_storm_relative_helicity_masked(): h = units.Quantity(np.ma.array([20.72, 234.85, 456.69, 683.21]), units.meter) u = units.Quantity(np.ma.array(np.zeros((4,))), units.knot) v = units.Quantity(np.zeros_like(u), units.knot) - pos, neg, com = storm_relative_helicity(h, u, v, depth=500 * units.meter, - storm_u=15.77463015050421 * units('m/s'), - storm_v=21.179437759755647 * units('m/s')) + pos, neg, com = storm_relative_helicity( + h, + u, + v, + depth=500 * units.meter, + storm_u=15.77463015050421 * units("m/s"), + storm_v=21.179437759755647 * units("m/s"), + ) assert not np.ma.is_masked(pos) assert not np.ma.is_masked(neg) @@ -455,48 +517,55 @@ def test_storm_relative_helicity_masked(): def test_absolute_vorticity_asym(): """Test absolute vorticity calculation with a complicated field.""" - u = np.array([[2, 4, 8], [0, 2, 2], [4, 6, 8]]) * units('m/s') - v = np.array([[6, 4, 8], [2, 6, 0], [2, 2, 6]]) * units('m/s') + u = np.array([[2, 4, 8], [0, 2, 2], [4, 6, 8]]) * units("m/s") + v = np.array([[6, 4, 8], [2, 6, 0], [2, 2, 6]]) * units("m/s") lats = np.array([[30, 30, 30], [20, 20, 20], [10, 10, 10]]) * units.degrees vort = absolute_vorticity(u, v, 1 * units.meters, 2 * units.meters, lats) - true_vort = np.array([[-2.499927, 3.500073, 13.00007], - [8.500050, -1.499950, -10.99995], - [-5.499975, -1.499975, 2.532525e-5]]) / units.sec + true_vort = ( + np.array( + [ + [-2.499927, 3.500073, 13.00007], + [8.500050, -1.499950, -10.99995], + [-5.499975, -1.499975, 2.532525e-5], + ] + ) + / units.sec + ) assert_almost_equal(vort, true_vort, 5) -@pytest.fixture -@needs_pyproj -def pv_data(): - """Test data for all PV testing.""" - u = np.array([[[100, 90, 80, 70], - [90, 80, 70, 60], - [80, 70, 60, 50], - [70, 60, 50, 40]], - [[100, 90, 80, 70], - [90, 80, 70, 60], - [80, 70, 60, 50], - [70, 60, 50, 40]], - [[100, 90, 80, 70], - [90, 80, 70, 60], - [80, 70, 60, 50], - [70, 60, 50, 40]]]) * units('m/s') - - v = np.zeros_like(u) * units('m/s') - - lats = np.array([[40, 40, 40, 40], - [40.1, 40.1, 40.1, 40.1], - [40.2, 40.2, 40.2, 40.2], - [40.3, 40.3, 40.3, 40.3]]) * units.degrees - - lons = np.array([[40, 39.9, 39.8, 39.7], - [40, 39.9, 39.8, 39.7], - [40, 39.9, 39.8, 39.7], - [40, 39.9, 39.8, 39.7]]) * units.degrees +# @pytest.fixture +# @needs_pyproj +# def pv_data(): +# """Test data for all PV testing.""" +# u = np.array([[[100, 90, 80, 70], +# [90, 80, 70, 60], +# [80, 70, 60, 50], +# [70, 60, 50, 40]], +# [[100, 90, 80, 70], +# [90, 80, 70, 60], +# [80, 70, 60, 50], +# [70, 60, 50, 40]], +# [[100, 90, 80, 70], +# [90, 80, 70, 60], +# [80, 70, 60, 50], +# [70, 60, 50, 40]]]) * units('m/s') - dx, dy = lat_lon_grid_deltas(lons, lats) +# v = np.zeros_like(u) * units('m/s') + +# lats = np.array([[40, 40, 40, 40], +# [40.1, 40.1, 40.1, 40.1], +# [40.2, 40.2, 40.2, 40.2], +# [40.3, 40.3, 40.3, 40.3]]) * units.degrees + +# lons = np.array([[40, 39.9, 39.8, 39.7], +# [40, 39.9, 39.8, 39.7], +# [40, 39.9, 39.8, 39.7], +# [40, 39.9, 39.8, 39.7]]) * units.degrees - return u, v, lats, lons, dx, dy +# dx, dy = lat_lon_grid_deltas(lons, lats) + +# return u, v, lats, lons, dx, dy def test_potential_vorticity_baroclinic_unity_axis0(pv_data): @@ -513,17 +582,17 @@ def test_potential_vorticity_baroclinic_unity_axis0(pv_data): pressure[1] = 900 * units.hPa pressure[0] = 800 * units.hPa - pvor = potential_vorticity_baroclinic(potential_temperature, pressure, - u, v, dx[None, :, :], dy[None, :, :], - lats[None, :, :]) + pvor = potential_vorticity_baroclinic( + potential_temperature, pressure, u, v, dx[None, :, :], dy[None, :, :], lats[None, :, :] + ) - abs_vorticity = absolute_vorticity(u, v, dx[None, :, :], dy[None, :, :], - lats[None, :, :]) + abs_vorticity = absolute_vorticity(u, v, dx[None, :, :], dy[None, :, :], lats[None, :, :]) vort_difference = pvor - (abs_vorticity * g * (-1 * (units.kelvin / units.hPa))) - true_vort = np.zeros_like(u) * (units.kelvin * units.meter**2 - / (units.second * units.kilogram)) + true_vort = np.zeros_like(u) * ( + units.kelvin * units.meter ** 2 / (units.second * units.kilogram) + ) assert_almost_equal(vort_difference, true_vort, 10) @@ -542,17 +611,17 @@ def test_potential_vorticity_baroclinic_non_unity_derivative(pv_data): pressure[1] = 999 * units.hPa pressure[0] = 998 * units.hPa - pvor = potential_vorticity_baroclinic(potential_temperature, pressure, - u, v, dx[None, :, :], dy[None, :, :], - lats[None, :, :]) + pvor = potential_vorticity_baroclinic( + potential_temperature, pressure, u, v, dx[None, :, :], dy[None, :, :], lats[None, :, :] + ) - abs_vorticity = absolute_vorticity(u, v, dx[None, :, :], dy[None, :, :], - lats[None, :, :]) + abs_vorticity = absolute_vorticity(u, v, dx[None, :, :], dy[None, :, :], lats[None, :, :]) vort_difference = pvor - (abs_vorticity * g * (-100 * (units.kelvin / units.hPa))) - true_vort = np.zeros_like(u) * (units.kelvin * units.meter ** 2 - / (units.second * units.kilogram)) + true_vort = np.zeros_like(u) * ( + units.kelvin * units.meter ** 2 / (units.second * units.kilogram) + ) assert_almost_equal(vort_difference, true_vort, 10) @@ -572,107 +641,249 @@ def test_potential_vorticity_baroclinic_wrong_number_of_levels_axis_0(pv_data): pressure[0] = 800 * units.hPa with pytest.raises(ValueError): - potential_vorticity_baroclinic(potential_temperature[:1, :, :], pressure, u, v, - dx[None, :, :], dy[None, :, :], - lats[None, :, :]) + potential_vorticity_baroclinic( + potential_temperature[:1, :, :], + pressure, + u, + v, + dx[None, :, :], + dy[None, :, :], + lats[None, :, :], + ) with pytest.raises(ValueError): - potential_vorticity_baroclinic(u, v, dx[None, :, :], dy[None, :, :], - lats[None, :, :], potential_temperature, - pressure[:1, :, :]) + potential_vorticity_baroclinic( + u, + v, + dx[None, :, :], + dy[None, :, :], + lats[None, :, :], + potential_temperature, + pressure[:1, :, :], + ) @needs_pyproj def test_potential_vorticity_baroclinic_isentropic_real_data(): """Test potential vorticity calculation with real isentropic data.""" isentlevs = [328, 330, 332] * units.K - isentprs = np.array([[[245.88052, 245.79416, 245.68776, 245.52525, 245.31844], - [245.97734, 245.85878, 245.74838, 245.61089, 245.4683], - [246.4308, 246.24358, 246.08649, 245.93279, 245.80148], - [247.14348, 246.87215, 246.64842, 246.457, 246.32005], - [248.05727, 247.72388, 247.44029, 247.19205, 247.0112]], - [[239.66074, 239.60431, 239.53738, 239.42496, 239.27725], - [239.5676, 239.48225, 239.4114, 239.32259, 239.23781], - [239.79681, 239.6465, 239.53227, 239.43031, 239.35794], - [240.2442, 240.01723, 239.84442, 239.71255, 239.64021], - [240.85277, 240.57112, 240.34885, 240.17174, 240.0666]], - [[233.63297, 233.60493, 233.57542, 233.51053, 233.41898], - [233.35995, 233.3061, 233.27275, 233.23009, 233.2001], - [233.37685, 233.26152, 233.18793, 233.13496, 233.11841], - [233.57312, 233.38823, 233.26366, 233.18817, 233.17694], - [233.89297, 233.66039, 233.49615, 233.38635, 233.35281]]]) * units.hPa - isentu = np.array([[[28.94226812, 28.53362902, 27.98145564, 27.28696092, 26.46488305], - [28.15024259, 28.12645242, 27.95788749, 27.62007338, 27.10351611], - [26.27821641, 26.55765132, 26.7329775, 26.77170719, 26.64779014], - [24.07215607, 24.48837805, 24.86738637, 25.17622757, 25.38030319], - [22.25524153, 22.65568001, 23.07333679, 23.48542321, 23.86341343]], - [[28.50078095, 28.12605738, 27.6145395, 26.96565679, 26.1919881], - [27.73718892, 27.73189078, 27.58886228, 27.28329365, 26.80468118], - [25.943111, 26.23034592, 26.41833632, 26.47466534, 26.37320009], - [23.82858821, 24.24937503, 24.63505859, 24.95235053, 25.16669265], - [22.09498322, 22.5008718, 22.9247538, 23.34295878, 23.72623895]], - [[28.05929378, 27.71848573, 27.24762337, 26.64435265, 25.91909314], - [27.32413525, 27.33732915, 27.21983708, 26.94651392, 26.50584625], - [25.60800559, 25.90304052, 26.10369515, 26.17762349, 26.09861004], - [23.58502035, 24.01037201, 24.4027308, 24.72847348, 24.95308212], - [21.9347249, 22.34606359, 22.77617081, 23.20049435, 23.58906447]]])\ + isentprs = ( + np.array( + [ + [ + [245.88052, 245.79416, 245.68776, 245.52525, 245.31844], + [245.97734, 245.85878, 245.74838, 245.61089, 245.4683], + [246.4308, 246.24358, 246.08649, 245.93279, 245.80148], + [247.14348, 246.87215, 246.64842, 246.457, 246.32005], + [248.05727, 247.72388, 247.44029, 247.19205, 247.0112], + ], + [ + [239.66074, 239.60431, 239.53738, 239.42496, 239.27725], + [239.5676, 239.48225, 239.4114, 239.32259, 239.23781], + [239.79681, 239.6465, 239.53227, 239.43031, 239.35794], + [240.2442, 240.01723, 239.84442, 239.71255, 239.64021], + [240.85277, 240.57112, 240.34885, 240.17174, 240.0666], + ], + [ + [233.63297, 233.60493, 233.57542, 233.51053, 233.41898], + [233.35995, 233.3061, 233.27275, 233.23009, 233.2001], + [233.37685, 233.26152, 233.18793, 233.13496, 233.11841], + [233.57312, 233.38823, 233.26366, 233.18817, 233.17694], + [233.89297, 233.66039, 233.49615, 233.38635, 233.35281], + ], + ] + ) + * units.hPa + ) + isentu = ( + np.array( + [ + [ + [28.94226812, 28.53362902, 27.98145564, 27.28696092, 26.46488305], + [28.15024259, 28.12645242, 27.95788749, 27.62007338, 27.10351611], + [26.27821641, 26.55765132, 26.7329775, 26.77170719, 26.64779014], + [24.07215607, 24.48837805, 24.86738637, 25.17622757, 25.38030319], + [22.25524153, 22.65568001, 23.07333679, 23.48542321, 23.86341343], + ], + [ + [28.50078095, 28.12605738, 27.6145395, 26.96565679, 26.1919881], + [27.73718892, 27.73189078, 27.58886228, 27.28329365, 26.80468118], + [25.943111, 26.23034592, 26.41833632, 26.47466534, 26.37320009], + [23.82858821, 24.24937503, 24.63505859, 24.95235053, 25.16669265], + [22.09498322, 22.5008718, 22.9247538, 23.34295878, 23.72623895], + ], + [ + [28.05929378, 27.71848573, 27.24762337, 26.64435265, 25.91909314], + [27.32413525, 27.33732915, 27.21983708, 26.94651392, 26.50584625], + [25.60800559, 25.90304052, 26.10369515, 26.17762349, 26.09861004], + [23.58502035, 24.01037201, 24.4027308, 24.72847348, 24.95308212], + [21.9347249, 22.34606359, 22.77617081, 23.20049435, 23.58906447], + ], + ] + ) * (units.meters / units.sec) - isentv = np.array([[[-2.22336191, -2.82451946, -3.27190475, -3.53076527, -3.59311591], - [-2.12438321, -2.98895919, -3.73633746, -4.32254411, -4.70849598], - [-1.24050415, -2.31904635, -3.32284815, -4.20895826, -4.93036136], - [0.32254009, -0.89843808, -2.09621275, -3.2215678, -4.2290825], - [2.14238865, 0.88207403, -0.40652485, -1.67244834, -2.86837275]], - [[-1.99024801, -2.59146057, -3.04973279, -3.3296825, -3.42137476], - [-1.8711102, -2.71865804, -3.45952099, -4.05064148, -4.45309013], - [-0.99367383, -2.04299168, -3.02642031, -3.90252563, -4.62540783], - [0.547778, -0.63635567, -1.80391109, -2.90776869, -3.90375721], - [2.33967328, 1.12072805, -0.13066324, -1.3662872, -2.5404749]], - [[-1.75713411, -2.35840168, -2.82756083, -3.12859972, -3.24963361], - [-1.6178372, -2.44835688, -3.18270452, -3.77873886, -4.19768429], - [-0.7468435, -1.76693701, -2.72999246, -3.596093, -4.32045429], - [0.7730159, -0.37427326, -1.51160943, -2.59396958, -3.57843192], - [2.53695791, 1.35938207, 0.14519838, -1.06012605, -2.21257705]]])\ + ) + isentv = ( + np.array( + [ + [ + [-2.22336191, -2.82451946, -3.27190475, -3.53076527, -3.59311591], + [-2.12438321, -2.98895919, -3.73633746, -4.32254411, -4.70849598], + [-1.24050415, -2.31904635, -3.32284815, -4.20895826, -4.93036136], + [0.32254009, -0.89843808, -2.09621275, -3.2215678, -4.2290825], + [2.14238865, 0.88207403, -0.40652485, -1.67244834, -2.86837275], + ], + [ + [-1.99024801, -2.59146057, -3.04973279, -3.3296825, -3.42137476], + [-1.8711102, -2.71865804, -3.45952099, -4.05064148, -4.45309013], + [-0.99367383, -2.04299168, -3.02642031, -3.90252563, -4.62540783], + [0.547778, -0.63635567, -1.80391109, -2.90776869, -3.90375721], + [2.33967328, 1.12072805, -0.13066324, -1.3662872, -2.5404749], + ], + [ + [-1.75713411, -2.35840168, -2.82756083, -3.12859972, -3.24963361], + [-1.6178372, -2.44835688, -3.18270452, -3.77873886, -4.19768429], + [-0.7468435, -1.76693701, -2.72999246, -3.596093, -4.32045429], + [0.7730159, -0.37427326, -1.51160943, -2.59396958, -3.57843192], + [2.53695791, 1.35938207, 0.14519838, -1.06012605, -2.21257705], + ], + ] + ) * (units.meters / units.sec) - lats = np.array([57.5, 57., 56.5, 56., 55.5]) * units.degrees - lons = np.array([227.5, 228., 228.5, 229., 229.5]) * units.degrees + ) + lats = np.array([57.5, 57.0, 56.5, 56.0, 55.5]) * units.degrees + lons = np.array([227.5, 228.0, 228.5, 229.0, 229.5]) * units.degrees dx, dy = lat_lon_grid_deltas(lons, lats) - pvor = potential_vorticity_baroclinic(isentlevs[:, None, None], isentprs, - isentu, isentv, dx[None, :, :], dy[None, :, :], - lats[None, :, None]) - - true_pv = np.array([[[2.97116898e-06, 3.38486331e-06, 3.81432403e-06, 4.24722471e-06, - 4.64995688e-06], - [2.04235589e-06, 2.35739554e-06, 2.71138003e-06, 3.11803005e-06, - 3.54655984e-06], - [1.41179481e-06, 1.60663306e-06, 1.85439220e-06, 2.17827401e-06, - 2.55309150e-06], - [1.25933892e-06, 1.31915377e-06, 1.43444064e-06, 1.63067920e-06, - 1.88631658e-06], - [1.37533104e-06, 1.31658998e-06, 1.30424716e-06, 1.36777872e-06, - 1.49289942e-06]], - [[3.07674708e-06, 3.48172482e-06, 3.90371030e-06, 4.33207155e-06, - 4.73253199e-06], - [2.16369614e-06, 2.47112604e-06, 2.81747901e-06, 3.21722053e-06, - 3.63944011e-06], - [1.53925419e-06, 1.72853221e-06, 1.97026966e-06, 2.28774012e-06, - 2.65577906e-06], - [1.38675388e-06, 1.44221972e-06, 1.55296146e-06, 1.74439951e-06, - 1.99486345e-06], - [1.50312413e-06, 1.44039769e-06, 1.42422805e-06, 1.48403040e-06, - 1.60544869e-06]], - [[3.17979446e-06, 3.57430736e-06, 3.98713951e-06, 4.40950119e-06, - 4.80650246e-06], - [2.28618901e-06, 2.58455503e-06, 2.92172357e-06, 3.31292186e-06, - 3.72721632e-06], - [1.67022518e-06, 1.85294576e-06, 2.08747504e-06, 2.39710083e-06, - 2.75677598e-06], - [1.51817109e-06, 1.56879550e-06, 1.67430213e-06, 1.85997008e-06, - 2.10409000e-06], - [1.63449148e-06, 1.56773336e-06, 1.54753266e-06, 1.60313832e-06, - 1.72018062e-06]]]) * (units.kelvin * units.meter ** 2 - / (units.second * units.kilogram)) + pvor = potential_vorticity_baroclinic( + isentlevs[:, None, None], + isentprs, + isentu, + isentv, + dx[None, :, :], + dy[None, :, :], + lats[None, :, None], + ) + + true_pv = ( + np.array( + [ + [ + [ + 2.97116898e-06, + 3.38486331e-06, + 3.81432403e-06, + 4.24722471e-06, + 4.64995688e-06, + ], + [ + 2.04235589e-06, + 2.35739554e-06, + 2.71138003e-06, + 3.11803005e-06, + 3.54655984e-06, + ], + [ + 1.41179481e-06, + 1.60663306e-06, + 1.85439220e-06, + 2.17827401e-06, + 2.55309150e-06, + ], + [ + 1.25933892e-06, + 1.31915377e-06, + 1.43444064e-06, + 1.63067920e-06, + 1.88631658e-06, + ], + [ + 1.37533104e-06, + 1.31658998e-06, + 1.30424716e-06, + 1.36777872e-06, + 1.49289942e-06, + ], + ], + [ + [ + 3.07674708e-06, + 3.48172482e-06, + 3.90371030e-06, + 4.33207155e-06, + 4.73253199e-06, + ], + [ + 2.16369614e-06, + 2.47112604e-06, + 2.81747901e-06, + 3.21722053e-06, + 3.63944011e-06, + ], + [ + 1.53925419e-06, + 1.72853221e-06, + 1.97026966e-06, + 2.28774012e-06, + 2.65577906e-06, + ], + [ + 1.38675388e-06, + 1.44221972e-06, + 1.55296146e-06, + 1.74439951e-06, + 1.99486345e-06, + ], + [ + 1.50312413e-06, + 1.44039769e-06, + 1.42422805e-06, + 1.48403040e-06, + 1.60544869e-06, + ], + ], + [ + [ + 3.17979446e-06, + 3.57430736e-06, + 3.98713951e-06, + 4.40950119e-06, + 4.80650246e-06, + ], + [ + 2.28618901e-06, + 2.58455503e-06, + 2.92172357e-06, + 3.31292186e-06, + 3.72721632e-06, + ], + [ + 1.67022518e-06, + 1.85294576e-06, + 2.08747504e-06, + 2.39710083e-06, + 2.75677598e-06, + ], + [ + 1.51817109e-06, + 1.56879550e-06, + 1.67430213e-06, + 1.85997008e-06, + 2.10409000e-06, + ], + [ + 1.63449148e-06, + 1.56773336e-06, + 1.54753266e-06, + 1.60313832e-06, + 1.72018062e-06, + ], + ], + ] + ) + * (units.kelvin * units.meter ** 2 / (units.second * units.kilogram)) + ) assert_almost_equal(pvor, true_pv, 14) @@ -680,94 +891,224 @@ def test_potential_vorticity_baroclinic_isentropic_real_data(): @needs_pyproj def test_potential_vorticity_baroclinic_isobaric_real_data(): """Test potential vorticity calculation with real isentropic data.""" - pres = [20000., 25000., 30000.] * units.Pa - theta = np.array([[[344.45776, 344.5063, 344.574, 344.6499, 344.735], - [343.98444, 344.02536, 344.08682, 344.16284, 344.2629], - [343.58792, 343.60876, 343.65628, 343.72818, 343.82834], - [343.21542, 343.2204, 343.25833, 343.32935, 343.43414], - [342.85272, 342.84982, 342.88556, 342.95645, 343.0634]], - [[326.70923, 326.67603, 326.63416, 326.57153, 326.49155], - [326.77695, 326.73468, 326.6931, 326.6408, 326.58405], - [326.95062, 326.88986, 326.83627, 326.78134, 326.7308], - [327.1913, 327.10928, 327.03894, 326.97546, 326.92587], - [327.47235, 327.3778, 327.29468, 327.2188, 327.15973]], - [[318.47897, 318.30374, 318.1081, 317.8837, 317.63837], - [319.155, 318.983, 318.79745, 318.58905, 318.36212], - [319.8042, 319.64206, 319.4669, 319.2713, 319.0611], - [320.4621, 320.3055, 320.13373, 319.9425, 319.7401], - [321.1375, 320.98648, 320.81473, 320.62186, 320.4186]]]) * units.K - uwnd = np.array([[[25.309322, 25.169882, 24.94082, 24.61212, 24.181437], - [24.849028, 24.964956, 24.989666, 24.898415, 24.673553], - [23.666418, 24.003235, 24.269922, 24.435743, 24.474638], - [22.219162, 22.669518, 23.09492, 23.460283, 23.731855], - [21.065105, 21.506243, 21.967466, 22.420042, 22.830257]], - [[29.227198, 28.803436, 28.23203, 27.516447, 26.670708], - [28.402836, 28.376076, 28.199024, 27.848948, 27.315084], - [26.454042, 26.739328, 26.916056, 26.952703, 26.822044], - [24.17064, 24.59482, 24.979027, 25.290913, 25.495026], - [22.297522, 22.70384, 23.125736, 23.541069, 23.921045]], - [[27.429195, 26.97554, 26.360558, 25.594944, 24.7073], - [26.959536, 26.842077, 26.56688, 26.118752, 25.50171], - [25.460867, 25.599699, 25.62171, 25.50819, 25.249628], - [23.6418, 23.920736, 24.130007, 24.255558, 24.28613], - [21.915337, 22.283215, 22.607704, 22.879448, 23.093569]]])\ + pres = [20000.0, 25000.0, 30000.0] * units.Pa + theta = ( + np.array( + [ + [ + [344.45776, 344.5063, 344.574, 344.6499, 344.735], + [343.98444, 344.02536, 344.08682, 344.16284, 344.2629], + [343.58792, 343.60876, 343.65628, 343.72818, 343.82834], + [343.21542, 343.2204, 343.25833, 343.32935, 343.43414], + [342.85272, 342.84982, 342.88556, 342.95645, 343.0634], + ], + [ + [326.70923, 326.67603, 326.63416, 326.57153, 326.49155], + [326.77695, 326.73468, 326.6931, 326.6408, 326.58405], + [326.95062, 326.88986, 326.83627, 326.78134, 326.7308], + [327.1913, 327.10928, 327.03894, 326.97546, 326.92587], + [327.47235, 327.3778, 327.29468, 327.2188, 327.15973], + ], + [ + [318.47897, 318.30374, 318.1081, 317.8837, 317.63837], + [319.155, 318.983, 318.79745, 318.58905, 318.36212], + [319.8042, 319.64206, 319.4669, 319.2713, 319.0611], + [320.4621, 320.3055, 320.13373, 319.9425, 319.7401], + [321.1375, 320.98648, 320.81473, 320.62186, 320.4186], + ], + ] + ) + * units.K + ) + uwnd = ( + np.array( + [ + [ + [25.309322, 25.169882, 24.94082, 24.61212, 24.181437], + [24.849028, 24.964956, 24.989666, 24.898415, 24.673553], + [23.666418, 24.003235, 24.269922, 24.435743, 24.474638], + [22.219162, 22.669518, 23.09492, 23.460283, 23.731855], + [21.065105, 21.506243, 21.967466, 22.420042, 22.830257], + ], + [ + [29.227198, 28.803436, 28.23203, 27.516447, 26.670708], + [28.402836, 28.376076, 28.199024, 27.848948, 27.315084], + [26.454042, 26.739328, 26.916056, 26.952703, 26.822044], + [24.17064, 24.59482, 24.979027, 25.290913, 25.495026], + [22.297522, 22.70384, 23.125736, 23.541069, 23.921045], + ], + [ + [27.429195, 26.97554, 26.360558, 25.594944, 24.7073], + [26.959536, 26.842077, 26.56688, 26.118752, 25.50171], + [25.460867, 25.599699, 25.62171, 25.50819, 25.249628], + [23.6418, 23.920736, 24.130007, 24.255558, 24.28613], + [21.915337, 22.283215, 22.607704, 22.879448, 23.093569], + ], + ] + ) * (units.meters / units.sec) - vwnd = np.array([[[-0.3050951, -0.90105104, -1.4307652, -1.856761, -2.156073], - [-0.10017005, -0.82312256, -1.5097888, -2.1251845, -2.631675], - [0.6832816, -0.16461015, -1.0023694, -1.7991445, -2.5169075], - [2.0360851, 1.0960612, 0.13380499, -0.81640035, -1.718524], - [3.6074955, 2.654059, 1.6466523, 0.61709386, -0.39874703]], - [[-2.3738103, -2.9788015, -3.423631, -3.6743853, -3.7226477], - [-2.2792664, -3.159968, -3.917221, -4.507328, -4.8893175], - [-1.3700132, -2.4722757, -3.4953287, -4.3956766, -5.123884], - [0.2314668, -1.0151587, -2.2366724, -3.382317, -4.403803], - [2.0903401, 0.8078297, -0.5038105, -1.7920332, -3.0061343]], - [[-1.4415079, -1.7622383, -1.9080431, -1.8903408, -1.7376306], - [-1.5708634, -2.288579, -2.823628, -3.1583376, -3.285275], - [-0.9814599, -1.999404, -2.8674111, -3.550859, -4.0168552], - [0.07641177, -1.1033016, -2.1928647, -3.1449537, -3.9159832], - [1.2759045, 0.05043932, -1.1469103, -2.264961, -3.2550638]]])\ + ) + vwnd = ( + np.array( + [ + [ + [-0.3050951, -0.90105104, -1.4307652, -1.856761, -2.156073], + [-0.10017005, -0.82312256, -1.5097888, -2.1251845, -2.631675], + [0.6832816, -0.16461015, -1.0023694, -1.7991445, -2.5169075], + [2.0360851, 1.0960612, 0.13380499, -0.81640035, -1.718524], + [3.6074955, 2.654059, 1.6466523, 0.61709386, -0.39874703], + ], + [ + [-2.3738103, -2.9788015, -3.423631, -3.6743853, -3.7226477], + [-2.2792664, -3.159968, -3.917221, -4.507328, -4.8893175], + [-1.3700132, -2.4722757, -3.4953287, -4.3956766, -5.123884], + [0.2314668, -1.0151587, -2.2366724, -3.382317, -4.403803], + [2.0903401, 0.8078297, -0.5038105, -1.7920332, -3.0061343], + ], + [ + [-1.4415079, -1.7622383, -1.9080431, -1.8903408, -1.7376306], + [-1.5708634, -2.288579, -2.823628, -3.1583376, -3.285275], + [-0.9814599, -1.999404, -2.8674111, -3.550859, -4.0168552], + [0.07641177, -1.1033016, -2.1928647, -3.1449537, -3.9159832], + [1.2759045, 0.05043932, -1.1469103, -2.264961, -3.2550638], + ], + ] + ) * (units.meters / units.sec) - lats = np.array([57.5, 57., 56.5, 56., 55.5]) * units.degrees - lons = np.array([227.5, 228., 228.5, 229., 229.5]) * units.degrees + ) + lats = np.array([57.5, 57.0, 56.5, 56.0, 55.5]) * units.degrees + lons = np.array([227.5, 228.0, 228.5, 229.0, 229.5]) * units.degrees dx, dy = lat_lon_grid_deltas(lons, lats) - pvor = potential_vorticity_baroclinic(theta, pres[:, None, None], - uwnd, vwnd, dx[None, :, :], dy[None, :, :], - lats[None, :, None]) - - true_pv = np.array([[[4.29013406e-06, 4.61736108e-06, 4.97453387e-06, 5.36730237e-06, - 5.75500645e-06], - [3.48415057e-06, 3.72492697e-06, 4.00658450e-06, 4.35128065e-06, - 4.72701041e-06], - [2.87775662e-06, 3.01866087e-06, 3.21074864e-06, 3.47971854e-06, - 3.79924194e-06], - [2.70274738e-06, 2.71627883e-06, 2.78699880e-06, 2.94197238e-06, - 3.15685712e-06], - [2.81293318e-06, 2.70649941e-06, 2.65188277e-06, 2.68109532e-06, - 2.77737801e-06]], - [[2.43090597e-06, 2.79248225e-06, 3.16783697e-06, 3.54497301e-06, - 3.89481001e-06], - [1.61968826e-06, 1.88924405e-06, 2.19296648e-06, 2.54191855e-06, - 2.91119712e-06], - [1.09089606e-06, 1.25384007e-06, 1.46192044e-06, 1.73476959e-06, - 2.05268876e-06], - [9.72047256e-07, 1.02016741e-06, 1.11466014e-06, 1.27721014e-06, - 1.49122340e-06], - [1.07501523e-06, 1.02474621e-06, 1.01290749e-06, 1.06385170e-06, - 1.16674712e-06]], - [[6.10254835e-07, 7.31519400e-07, 8.55731472e-07, 9.74301226e-07, - 1.08453329e-06], - [3.17052987e-07, 3.98799900e-07, 4.91789955e-07, 5.96021549e-07, - 7.10773939e-07], - [1.81983099e-07, 2.26503437e-07, 2.83058115e-07, 3.56549337e-07, - 4.47098851e-07], - [1.54729567e-07, 1.73825926e-07, 2.01823376e-07, 2.44513805e-07, - 3.02525735e-07], - [1.55220676e-07, 1.63334569e-07, 1.76335524e-07, 1.98346439e-07, - 2.30155553e-07]]]) * (units.kelvin * units.meter ** 2 - / (units.second * units.kilogram)) + pvor = potential_vorticity_baroclinic( + theta, + pres[:, None, None], + uwnd, + vwnd, + dx[None, :, :], + dy[None, :, :], + lats[None, :, None], + ) + + true_pv = ( + np.array( + [ + [ + [ + 4.29013406e-06, + 4.61736108e-06, + 4.97453387e-06, + 5.36730237e-06, + 5.75500645e-06, + ], + [ + 3.48415057e-06, + 3.72492697e-06, + 4.00658450e-06, + 4.35128065e-06, + 4.72701041e-06, + ], + [ + 2.87775662e-06, + 3.01866087e-06, + 3.21074864e-06, + 3.47971854e-06, + 3.79924194e-06, + ], + [ + 2.70274738e-06, + 2.71627883e-06, + 2.78699880e-06, + 2.94197238e-06, + 3.15685712e-06, + ], + [ + 2.81293318e-06, + 2.70649941e-06, + 2.65188277e-06, + 2.68109532e-06, + 2.77737801e-06, + ], + ], + [ + [ + 2.43090597e-06, + 2.79248225e-06, + 3.16783697e-06, + 3.54497301e-06, + 3.89481001e-06, + ], + [ + 1.61968826e-06, + 1.88924405e-06, + 2.19296648e-06, + 2.54191855e-06, + 2.91119712e-06, + ], + [ + 1.09089606e-06, + 1.25384007e-06, + 1.46192044e-06, + 1.73476959e-06, + 2.05268876e-06, + ], + [ + 9.72047256e-07, + 1.02016741e-06, + 1.11466014e-06, + 1.27721014e-06, + 1.49122340e-06, + ], + [ + 1.07501523e-06, + 1.02474621e-06, + 1.01290749e-06, + 1.06385170e-06, + 1.16674712e-06, + ], + ], + [ + [ + 6.10254835e-07, + 7.31519400e-07, + 8.55731472e-07, + 9.74301226e-07, + 1.08453329e-06, + ], + [ + 3.17052987e-07, + 3.98799900e-07, + 4.91789955e-07, + 5.96021549e-07, + 7.10773939e-07, + ], + [ + 1.81983099e-07, + 2.26503437e-07, + 2.83058115e-07, + 3.56549337e-07, + 4.47098851e-07, + ], + [ + 1.54729567e-07, + 1.73825926e-07, + 2.01823376e-07, + 2.44513805e-07, + 3.02525735e-07, + ], + [ + 1.55220676e-07, + 1.63334569e-07, + 1.76335524e-07, + 1.98346439e-07, + 2.30155553e-07, + ], + ], + ] + ) + * (units.kelvin * units.meter ** 2 / (units.second * units.kilogram)) + ) assert_almost_equal(pvor, true_pv, 10) @@ -789,187 +1130,647 @@ def test_potential_vorticity_barotropic(pv_data): @needs_pyproj def test_inertial_advective_wind_diffluent(): """Test inertial advective wind with a diffluent flow.""" - lats = np.array([[50., 50., 50., 50., 50., 50., 50., 50., 50., 50., 50.], - [48., 48., 48., 48., 48., 48., 48., 48., 48., 48., 48.], - [46., 46., 46., 46., 46., 46., 46., 46., 46., 46., 46.], - [44., 44., 44., 44., 44., 44., 44., 44., 44., 44., 44.], - [42., 42., 42., 42., 42., 42., 42., 42., 42., 42., 42.], - [40., 40., 40., 40., 40., 40., 40., 40., 40., 40., 40.], - [38., 38., 38., 38., 38., 38., 38., 38., 38., 38., 38.], - [36., 36., 36., 36., 36., 36., 36., 36., 36., 36., 36.], - [34., 34., 34., 34., 34., 34., 34., 34., 34., 34., 34.], - [32., 32., 32., 32., 32., 32., 32., 32., 32., 32., 32.], - [30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30.]]) * units.degrees - - lons = np.array([[250., 254., 258., 262., 266., 270., 274., 278., 282., 286., 290.], - [250., 254., 258., 262., 266., 270., 274., 278., 282., 286., 290.], - [250., 254., 258., 262., 266., 270., 274., 278., 282., 286., 290.], - [250., 254., 258., 262., 266., 270., 274., 278., 282., 286., 290.], - [250., 254., 258., 262., 266., 270., 274., 278., 282., 286., 290.], - [250., 254., 258., 262., 266., 270., 274., 278., 282., 286., 290.], - [250., 254., 258., 262., 266., 270., 274., 278., 282., 286., 290.], - [250., 254., 258., 262., 266., 270., 274., 278., 282., 286., 290.], - [250., 254., 258., 262., 266., 270., 274., 278., 282., 286., 290.], - [250., 254., 258., 262., 266., 270., 274., 278., 282., 286., 290.], - [250., 254., 258., 262., 266., 270., 274., 278., 282., 286., - 290.]]) * units.degrees - - ug = np.array([[23.68206888, 23.28736773, 22.49796543, 21.70856314, 19.7350574, - 17.36685051, 14.20924133, 10.26222985, 7.49932181, 4.34171263, - 2.36820689], - [24.4118194, 24.00495574, 23.19122843, 22.37750111, 20.34318283, - 17.90200089, 14.64709164, 10.57845507, 7.73040948, 4.47550022, - 2.44118194], - [25.21967679, 24.79934884, 23.95869295, 23.11803706, 21.01639732, - 18.49442965, 15.13180607, 10.92852661, 7.98623098, 4.62360741, - 2.52196768], - [26.11573982, 25.68047749, 24.80995283, 23.93942817, 21.76311652, - 19.15154253, 15.66944389, 11.31682059, 8.26998428, 4.78788563, - 2.61157398], - [27.11207213, 26.66020426, 25.75646853, 24.85273279, 22.59339344, - 19.88218623, 16.26724328, 11.74856459, 8.58548951, 4.97054656, - 2.71120721], - [28.22319067, 27.75280415, 26.81203113, 25.87125811, 23.51932555, - 20.69700649, 16.9339144, 12.23004929, 8.93734371, 5.17425162, - 2.82231907], - [29.46670856, 28.97559675, 27.99337313, 27.01114951, 24.55559047, - 21.60891961, 17.68002514, 12.76890704, 9.33112438, 5.4022299, - 2.94667086], - [30.86419265, 30.34978944, 29.32098302, 28.2921766, 25.72016054, - 22.63374128, 18.51851559, 13.37448348, 9.77366101, 5.65843532, - 3.08641927], - [32.44232384, 31.90161845, 30.82020765, 29.73879686, 27.03526987, - 23.79103749, 19.46539431, 14.05834033, 10.27340255, 5.94775937, - 3.24423238], - [34.23449286, 33.66391798, 32.52276821, 31.38161845, 28.52874405, - 25.10529476, 20.54069571, 14.8349469, 10.84092274, 6.27632369, 3.42344929], - [36.28303453, 35.67831729, 34.46888281, 33.25944832, 30.23586211, - 26.60755866, 21.76982072, 15.7226483, 11.4896276, 6.65188966, - 3.62830345]]) * units('m/s') - - vg = np.array([[7.67648972e-01, 2.30294692e+00, 3.07059589e+00, - 5.37354281e+00, 8.44413870e+00, 1.07470856e+01, - 1.38176815e+01, 1.30500325e+01, 1.15147346e+01, - 9.97943664e+00, 5.37354281e+00], - [6.08116408e-01, 1.82434923e+00, 2.43246563e+00, - 4.25681486e+00, 6.68928049e+00, 8.51362972e+00, - 1.09460954e+01, 1.03379789e+01, 9.12174613e+00, - 7.90551331e+00, 4.25681486e+00], - [4.53862086e-01, 1.36158626e+00, 1.81544834e+00, - 3.17703460e+00, 4.99248295e+00, 6.35406920e+00, - 8.16951755e+00, 7.71565546e+00, 6.80793129e+00, - 5.90020712e+00, 3.17703460e+00], - [3.02572579e-01, 9.07717738e-01, 1.21029032e+00, - 2.11800806e+00, 3.32829837e+00, 4.23601611e+00, - 5.44630643e+00, 5.14373385e+00, 4.53858869e+00, - 3.93344353e+00, 2.11800806e+00], - [1.52025875e-01, 4.56077624e-01, 6.08103499e-01, - 1.06418112e+00, 1.67228462e+00, 2.12836225e+00, - 2.73646575e+00, 2.58443987e+00, 2.28038812e+00, - 1.97633637e+00, 1.06418112e+00], - [-5.44403782e-13, 0.00000000e+00, 0.00000000e+00, - 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, - 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, - 0.00000000e+00, 3.62935855e-13], - [-1.55819455e-01, -4.67458366e-01, -6.23277822e-01, - -1.09073619e+00, -1.71401401e+00, -2.18147238e+00, - -2.80475020e+00, -2.64893074e+00, -2.33729183e+00, - -2.02565292e+00, -1.09073619e+00], - [-3.17940982e-01, -9.53822947e-01, -1.27176393e+00, - -2.22558688e+00, -3.49735080e+00, -4.45117375e+00, - -5.72293768e+00, -5.40499670e+00, -4.76911473e+00, - -4.13323277e+00, -2.22558688e+00], - [-4.89187491e-01, -1.46756247e+00, -1.95674996e+00, - -3.42431243e+00, -5.38106240e+00, -6.84862487e+00, - -8.80537483e+00, -8.31618734e+00, -7.33781236e+00, - -6.35943738e+00, -3.42431243e+00], - [-6.72847961e-01, -2.01854388e+00, -2.69139184e+00, - -4.70993572e+00, -7.40132757e+00, -9.41987145e+00, - -1.21112633e+01, -1.14384153e+01, -1.00927194e+01, - -8.74702349e+00, -4.70993572e+00], - [-8.72878488e-01, -2.61863546e+00, -3.49151395e+00, - -6.11014941e+00, -9.60166336e+00, -1.22202988e+01, - -1.57118128e+01, -1.48389343e+01, -1.30931773e+01, - -1.13474203e+01, -6.11014941e+00]]) * units('m/s') - - uiaw_truth = np.array([[-1.42807415e+00, -8.84702475e-01, -1.16169714e+00, - -2.07178191e+00, -2.26651744e+00, -2.44307980e+00, - -2.13572115e+00, -1.07805246e+00, -7.66864343e-01, - -4.29350989e-01, 2.09863394e-01], - [-1.15466056e+00, -7.14539881e-01, -9.37868053e-01, - -1.67069607e+00, -1.82145232e+00, -1.95723406e+00, - -1.69677456e+00, -8.44795197e-01, -5.99128909e-01, - -3.31430392e-01, 1.74263065e-01], - [-8.85879800e-01, -5.47662808e-01, -7.18560490e-01, - -1.27868851e+00, -1.38965979e+00, -1.48894561e+00, - -1.28074427e+00, -6.29324678e-01, -4.45008769e-01, - -2.43265738e-01, 1.36907150e-01], - [-6.11536708e-01, -3.77851670e-01, -4.95655649e-01, - -8.81515449e-01, -9.56332337e-01, -1.02300759e+00, - -8.76092304e-01, -4.27259697e-01, -3.01610757e-01, - -1.63732062e-01, 9.57321746e-02], - [-3.20542252e-01, -1.98032517e-01, -2.59762867e-01, - -4.61930812e-01, -5.00960635e-01, -5.35715150e-01, - -4.58376210e-01, -2.23205436e-01, -1.57510625e-01, - -8.53847182e-02, 5.03061160e-02], - [-7.17595005e-13, -2.36529156e-13, -0.00000000e+00, - -0.00000000e+00, -0.00000000e+00, -0.00000000e+00, - -0.00000000e+00, -0.00000000e+00, -0.00000000e+00, - -2.93991041e-14, -6.68646808e-14], - [3.66014877e-01, 2.26381410e-01, 2.97076576e-01, - 5.28911772e-01, 5.75670994e-01, 6.17640324e-01, - 5.33241822e-01, 2.63664097e-01, 1.86703719e-01, - 1.02644563e-01, -5.59431414e-02], - [7.98146331e-01, 4.94284987e-01, 6.48956331e-01, - 1.15693361e+00, 1.26429103e+00, 1.36142948e+00, - 1.18700769e+00, 5.96585261e-01, 4.23976442e-01, - 2.36489429e-01, -1.18303960e-01], - [1.32422955e+00, 8.21548216e-01, 1.07935767e+00, - 1.92781420e+00, 2.11849275e+00, 2.29274320e+00, - 2.02576077e+00, 1.04018545e+00, 7.42658541e-01, - 4.21848012e-01, -1.87693140e-01], - [1.98305622e+00, 1.23316526e+00, 1.62158055e+00, - 2.90329131e+00, 3.21355659e+00, 3.50025687e+00, - 3.14455187e+00, 1.65685097e+00, 1.18935849e+00, - 6.89756719e-01, -2.64167230e-01], - [2.83017758e+00, 1.76405766e+00, 2.32173329e+00, - 4.16683333e+00, 4.64487467e+00, 5.09076175e+00, - 4.64597480e+00, 2.50595982e+00, 1.80748965e+00, - 1.06712544e+00, -3.52915793e-01]]) * units('m/s') - - viaw_truth = np.array([[-0.16767916, -0.49465351, -0.63718079, -1.07594125, -1.53705893, - -1.721506, -1.81093489, -1.23523645, -0.79647599, -0.39963532, - -0.11737541], - [-0.17337355, -0.51145198, -0.6588195, -1.1124803, -1.58925758, - -1.77996849, -1.87243438, -1.27718518, -0.82352438, -0.41320697, - -0.12136149], - [-0.18010801, -0.53131862, -0.68441043, -1.15569305, -1.65099008, - -1.84910889, -1.94516649, -1.32679566, -0.85551304, -0.42925742, - -0.12607561], - [-0.18806768, -0.55479966, -0.71465719, -1.20676763, -1.72395376, - -1.93082821, -2.03113097, -1.38543193, -0.89332149, -0.44822798, - -0.13164738], - [-0.19730148, -0.58203938, -0.74974564, -1.26601785, -1.80859693, - -2.02562856, -2.13085602, -1.45345426, -0.93718205, -0.4702352, - -0.13811104], - [-0.2078345, -0.61311178, -0.78977111, -1.33360472, -1.90514961, - -2.13376756, -2.24461263, -1.5310475, -0.98721389, -0.4953389, - -0.14548415], - [-0.21963486, -0.64792283, -0.83461247, -1.40932368, -2.01331954, - -2.25491789, -2.37205648, -1.6179768, -1.04326558, -0.52346308, - -0.1537444], - [-0.2325551, -0.68603755, -0.88370939, -1.49222857, -2.1317551, - -2.38756571, -2.5115951, -1.71315592, -1.10463673, -0.55425633, - -0.16278857], - [-0.24622751, -0.72637116, -0.93566454, -1.57995986, -2.25708551, - -2.52793577, -2.65925711, -1.81387599, -1.16958067, -0.58684223, - -0.17235926], - [-0.25987451, -0.76662981, -0.98752314, -1.66752812, -2.38218302, - -2.66804499, -2.80664473, -1.9144089, -1.23440393, -0.61936759, - -0.18191216], - [-0.27342538, -0.80660487, -1.03901645, -1.75447953, -2.50639932, - -2.80716724, -2.95299411, -2.01423364, -1.29877056, -0.65166382, - -0.19139777]]) * units('m/s') + lats = ( + np.array( + [ + [50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0], + [48.0, 48.0, 48.0, 48.0, 48.0, 48.0, 48.0, 48.0, 48.0, 48.0, 48.0], + [46.0, 46.0, 46.0, 46.0, 46.0, 46.0, 46.0, 46.0, 46.0, 46.0, 46.0], + [44.0, 44.0, 44.0, 44.0, 44.0, 44.0, 44.0, 44.0, 44.0, 44.0, 44.0], + [42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0], + [40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0], + [38.0, 38.0, 38.0, 38.0, 38.0, 38.0, 38.0, 38.0, 38.0, 38.0, 38.0], + [36.0, 36.0, 36.0, 36.0, 36.0, 36.0, 36.0, 36.0, 36.0, 36.0, 36.0], + [34.0, 34.0, 34.0, 34.0, 34.0, 34.0, 34.0, 34.0, 34.0, 34.0, 34.0], + [32.0, 32.0, 32.0, 32.0, 32.0, 32.0, 32.0, 32.0, 32.0, 32.0, 32.0], + [30.0, 30.0, 30.0, 30.0, 30.0, 30.0, 30.0, 30.0, 30.0, 30.0, 30.0], + ] + ) + * units.degrees + ) + + lons = ( + np.array( + [ + [250.0, 254.0, 258.0, 262.0, 266.0, 270.0, 274.0, 278.0, 282.0, 286.0, 290.0], + [250.0, 254.0, 258.0, 262.0, 266.0, 270.0, 274.0, 278.0, 282.0, 286.0, 290.0], + [250.0, 254.0, 258.0, 262.0, 266.0, 270.0, 274.0, 278.0, 282.0, 286.0, 290.0], + [250.0, 254.0, 258.0, 262.0, 266.0, 270.0, 274.0, 278.0, 282.0, 286.0, 290.0], + [250.0, 254.0, 258.0, 262.0, 266.0, 270.0, 274.0, 278.0, 282.0, 286.0, 290.0], + [250.0, 254.0, 258.0, 262.0, 266.0, 270.0, 274.0, 278.0, 282.0, 286.0, 290.0], + [250.0, 254.0, 258.0, 262.0, 266.0, 270.0, 274.0, 278.0, 282.0, 286.0, 290.0], + [250.0, 254.0, 258.0, 262.0, 266.0, 270.0, 274.0, 278.0, 282.0, 286.0, 290.0], + [250.0, 254.0, 258.0, 262.0, 266.0, 270.0, 274.0, 278.0, 282.0, 286.0, 290.0], + [250.0, 254.0, 258.0, 262.0, 266.0, 270.0, 274.0, 278.0, 282.0, 286.0, 290.0], + [250.0, 254.0, 258.0, 262.0, 266.0, 270.0, 274.0, 278.0, 282.0, 286.0, 290.0], + ] + ) + * units.degrees + ) + + ug = ( + np.array( + [ + [ + 23.68206888, + 23.28736773, + 22.49796543, + 21.70856314, + 19.7350574, + 17.36685051, + 14.20924133, + 10.26222985, + 7.49932181, + 4.34171263, + 2.36820689, + ], + [ + 24.4118194, + 24.00495574, + 23.19122843, + 22.37750111, + 20.34318283, + 17.90200089, + 14.64709164, + 10.57845507, + 7.73040948, + 4.47550022, + 2.44118194, + ], + [ + 25.21967679, + 24.79934884, + 23.95869295, + 23.11803706, + 21.01639732, + 18.49442965, + 15.13180607, + 10.92852661, + 7.98623098, + 4.62360741, + 2.52196768, + ], + [ + 26.11573982, + 25.68047749, + 24.80995283, + 23.93942817, + 21.76311652, + 19.15154253, + 15.66944389, + 11.31682059, + 8.26998428, + 4.78788563, + 2.61157398, + ], + [ + 27.11207213, + 26.66020426, + 25.75646853, + 24.85273279, + 22.59339344, + 19.88218623, + 16.26724328, + 11.74856459, + 8.58548951, + 4.97054656, + 2.71120721, + ], + [ + 28.22319067, + 27.75280415, + 26.81203113, + 25.87125811, + 23.51932555, + 20.69700649, + 16.9339144, + 12.23004929, + 8.93734371, + 5.17425162, + 2.82231907, + ], + [ + 29.46670856, + 28.97559675, + 27.99337313, + 27.01114951, + 24.55559047, + 21.60891961, + 17.68002514, + 12.76890704, + 9.33112438, + 5.4022299, + 2.94667086, + ], + [ + 30.86419265, + 30.34978944, + 29.32098302, + 28.2921766, + 25.72016054, + 22.63374128, + 18.51851559, + 13.37448348, + 9.77366101, + 5.65843532, + 3.08641927, + ], + [ + 32.44232384, + 31.90161845, + 30.82020765, + 29.73879686, + 27.03526987, + 23.79103749, + 19.46539431, + 14.05834033, + 10.27340255, + 5.94775937, + 3.24423238, + ], + [ + 34.23449286, + 33.66391798, + 32.52276821, + 31.38161845, + 28.52874405, + 25.10529476, + 20.54069571, + 14.8349469, + 10.84092274, + 6.27632369, + 3.42344929, + ], + [ + 36.28303453, + 35.67831729, + 34.46888281, + 33.25944832, + 30.23586211, + 26.60755866, + 21.76982072, + 15.7226483, + 11.4896276, + 6.65188966, + 3.62830345, + ], + ] + ) + * units("m/s") + ) + + vg = ( + np.array( + [ + [ + 7.67648972e-01, + 2.30294692e00, + 3.07059589e00, + 5.37354281e00, + 8.44413870e00, + 1.07470856e01, + 1.38176815e01, + 1.30500325e01, + 1.15147346e01, + 9.97943664e00, + 5.37354281e00, + ], + [ + 6.08116408e-01, + 1.82434923e00, + 2.43246563e00, + 4.25681486e00, + 6.68928049e00, + 8.51362972e00, + 1.09460954e01, + 1.03379789e01, + 9.12174613e00, + 7.90551331e00, + 4.25681486e00, + ], + [ + 4.53862086e-01, + 1.36158626e00, + 1.81544834e00, + 3.17703460e00, + 4.99248295e00, + 6.35406920e00, + 8.16951755e00, + 7.71565546e00, + 6.80793129e00, + 5.90020712e00, + 3.17703460e00, + ], + [ + 3.02572579e-01, + 9.07717738e-01, + 1.21029032e00, + 2.11800806e00, + 3.32829837e00, + 4.23601611e00, + 5.44630643e00, + 5.14373385e00, + 4.53858869e00, + 3.93344353e00, + 2.11800806e00, + ], + [ + 1.52025875e-01, + 4.56077624e-01, + 6.08103499e-01, + 1.06418112e00, + 1.67228462e00, + 2.12836225e00, + 2.73646575e00, + 2.58443987e00, + 2.28038812e00, + 1.97633637e00, + 1.06418112e00, + ], + [ + -5.44403782e-13, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 3.62935855e-13, + ], + [ + -1.55819455e-01, + -4.67458366e-01, + -6.23277822e-01, + -1.09073619e00, + -1.71401401e00, + -2.18147238e00, + -2.80475020e00, + -2.64893074e00, + -2.33729183e00, + -2.02565292e00, + -1.09073619e00, + ], + [ + -3.17940982e-01, + -9.53822947e-01, + -1.27176393e00, + -2.22558688e00, + -3.49735080e00, + -4.45117375e00, + -5.72293768e00, + -5.40499670e00, + -4.76911473e00, + -4.13323277e00, + -2.22558688e00, + ], + [ + -4.89187491e-01, + -1.46756247e00, + -1.95674996e00, + -3.42431243e00, + -5.38106240e00, + -6.84862487e00, + -8.80537483e00, + -8.31618734e00, + -7.33781236e00, + -6.35943738e00, + -3.42431243e00, + ], + [ + -6.72847961e-01, + -2.01854388e00, + -2.69139184e00, + -4.70993572e00, + -7.40132757e00, + -9.41987145e00, + -1.21112633e01, + -1.14384153e01, + -1.00927194e01, + -8.74702349e00, + -4.70993572e00, + ], + [ + -8.72878488e-01, + -2.61863546e00, + -3.49151395e00, + -6.11014941e00, + -9.60166336e00, + -1.22202988e01, + -1.57118128e01, + -1.48389343e01, + -1.30931773e01, + -1.13474203e01, + -6.11014941e00, + ], + ] + ) + * units("m/s") + ) + + uiaw_truth = ( + np.array( + [ + [ + -1.42807415e00, + -8.84702475e-01, + -1.16169714e00, + -2.07178191e00, + -2.26651744e00, + -2.44307980e00, + -2.13572115e00, + -1.07805246e00, + -7.66864343e-01, + -4.29350989e-01, + 2.09863394e-01, + ], + [ + -1.15466056e00, + -7.14539881e-01, + -9.37868053e-01, + -1.67069607e00, + -1.82145232e00, + -1.95723406e00, + -1.69677456e00, + -8.44795197e-01, + -5.99128909e-01, + -3.31430392e-01, + 1.74263065e-01, + ], + [ + -8.85879800e-01, + -5.47662808e-01, + -7.18560490e-01, + -1.27868851e00, + -1.38965979e00, + -1.48894561e00, + -1.28074427e00, + -6.29324678e-01, + -4.45008769e-01, + -2.43265738e-01, + 1.36907150e-01, + ], + [ + -6.11536708e-01, + -3.77851670e-01, + -4.95655649e-01, + -8.81515449e-01, + -9.56332337e-01, + -1.02300759e00, + -8.76092304e-01, + -4.27259697e-01, + -3.01610757e-01, + -1.63732062e-01, + 9.57321746e-02, + ], + [ + -3.20542252e-01, + -1.98032517e-01, + -2.59762867e-01, + -4.61930812e-01, + -5.00960635e-01, + -5.35715150e-01, + -4.58376210e-01, + -2.23205436e-01, + -1.57510625e-01, + -8.53847182e-02, + 5.03061160e-02, + ], + [ + -7.17595005e-13, + -2.36529156e-13, + -0.00000000e00, + -0.00000000e00, + -0.00000000e00, + -0.00000000e00, + -0.00000000e00, + -0.00000000e00, + -0.00000000e00, + -2.93991041e-14, + -6.68646808e-14, + ], + [ + 3.66014877e-01, + 2.26381410e-01, + 2.97076576e-01, + 5.28911772e-01, + 5.75670994e-01, + 6.17640324e-01, + 5.33241822e-01, + 2.63664097e-01, + 1.86703719e-01, + 1.02644563e-01, + -5.59431414e-02, + ], + [ + 7.98146331e-01, + 4.94284987e-01, + 6.48956331e-01, + 1.15693361e00, + 1.26429103e00, + 1.36142948e00, + 1.18700769e00, + 5.96585261e-01, + 4.23976442e-01, + 2.36489429e-01, + -1.18303960e-01, + ], + [ + 1.32422955e00, + 8.21548216e-01, + 1.07935767e00, + 1.92781420e00, + 2.11849275e00, + 2.29274320e00, + 2.02576077e00, + 1.04018545e00, + 7.42658541e-01, + 4.21848012e-01, + -1.87693140e-01, + ], + [ + 1.98305622e00, + 1.23316526e00, + 1.62158055e00, + 2.90329131e00, + 3.21355659e00, + 3.50025687e00, + 3.14455187e00, + 1.65685097e00, + 1.18935849e00, + 6.89756719e-01, + -2.64167230e-01, + ], + [ + 2.83017758e00, + 1.76405766e00, + 2.32173329e00, + 4.16683333e00, + 4.64487467e00, + 5.09076175e00, + 4.64597480e00, + 2.50595982e00, + 1.80748965e00, + 1.06712544e00, + -3.52915793e-01, + ], + ] + ) + * units("m/s") + ) + + viaw_truth = ( + np.array( + [ + [ + -0.16767916, + -0.49465351, + -0.63718079, + -1.07594125, + -1.53705893, + -1.721506, + -1.81093489, + -1.23523645, + -0.79647599, + -0.39963532, + -0.11737541, + ], + [ + -0.17337355, + -0.51145198, + -0.6588195, + -1.1124803, + -1.58925758, + -1.77996849, + -1.87243438, + -1.27718518, + -0.82352438, + -0.41320697, + -0.12136149, + ], + [ + -0.18010801, + -0.53131862, + -0.68441043, + -1.15569305, + -1.65099008, + -1.84910889, + -1.94516649, + -1.32679566, + -0.85551304, + -0.42925742, + -0.12607561, + ], + [ + -0.18806768, + -0.55479966, + -0.71465719, + -1.20676763, + -1.72395376, + -1.93082821, + -2.03113097, + -1.38543193, + -0.89332149, + -0.44822798, + -0.13164738, + ], + [ + -0.19730148, + -0.58203938, + -0.74974564, + -1.26601785, + -1.80859693, + -2.02562856, + -2.13085602, + -1.45345426, + -0.93718205, + -0.4702352, + -0.13811104, + ], + [ + -0.2078345, + -0.61311178, + -0.78977111, + -1.33360472, + -1.90514961, + -2.13376756, + -2.24461263, + -1.5310475, + -0.98721389, + -0.4953389, + -0.14548415, + ], + [ + -0.21963486, + -0.64792283, + -0.83461247, + -1.40932368, + -2.01331954, + -2.25491789, + -2.37205648, + -1.6179768, + -1.04326558, + -0.52346308, + -0.1537444, + ], + [ + -0.2325551, + -0.68603755, + -0.88370939, + -1.49222857, + -2.1317551, + -2.38756571, + -2.5115951, + -1.71315592, + -1.10463673, + -0.55425633, + -0.16278857, + ], + [ + -0.24622751, + -0.72637116, + -0.93566454, + -1.57995986, + -2.25708551, + -2.52793577, + -2.65925711, + -1.81387599, + -1.16958067, + -0.58684223, + -0.17235926, + ], + [ + -0.25987451, + -0.76662981, + -0.98752314, + -1.66752812, + -2.38218302, + -2.66804499, + -2.80664473, + -1.9144089, + -1.23440393, + -0.61936759, + -0.18191216, + ], + [ + -0.27342538, + -0.80660487, + -1.03901645, + -1.75447953, + -2.50639932, + -2.80716724, + -2.95299411, + -2.01423364, + -1.29877056, + -0.65166382, + -0.19139777, + ], + ] + ) + * units("m/s") + ) dx, dy = lat_lon_grid_deltas(lons, lats) uiaw, viaw = inertial_advective_wind(ug, vg, ug, vg, dx, dy, lats) @@ -981,30 +1782,50 @@ def test_inertial_advective_wind_diffluent(): @needs_pyproj def q_vector_data(): """Define data for use in Q-vector tests.""" - speed = np.ones((4, 4)) * 50. * units('knots') - wdir = np.array([[210., 190., 170., 150.], - [190., 180., 180., 170.], - [170., 180., 180., 190.], - [150., 170., 190., 210.]]) * units('degrees') + speed = np.ones((4, 4)) * 50.0 * units("knots") + wdir = ( + np.array( + [ + [210.0, 190.0, 170.0, 150.0], + [190.0, 180.0, 180.0, 170.0], + [170.0, 180.0, 180.0, 190.0], + [150.0, 170.0, 190.0, 210.0], + ] + ) + * units("degrees") + ) u, v = wind_components(speed, wdir) - temp = np.array([[[18., 18., 18., 18.], - [17., 17., 17., 17.], - [17., 17., 17., 17.], - [16., 16., 16., 16.]], - [[12., 11., 10., 9.], - [11., 10.5, 10.5, 10.], - [10., 10.5, 10.5, 11.], - [9., 10., 11., 12.]], - [[-10., -10., -10., -10.], - [-10., -10., -10., -10.], - [-11., -11., -11., -11.], - [-11., -11., -11., -11.]]]) * units('degC') - - p = np.array([850., 700., 500.]) * units('hPa') - - lats = np.linspace(35., 40., 4) * units('degrees') - lons = np.linspace(-100., -90., 4) * units('degrees') + temp = ( + np.array( + [ + [ + [18.0, 18.0, 18.0, 18.0], + [17.0, 17.0, 17.0, 17.0], + [17.0, 17.0, 17.0, 17.0], + [16.0, 16.0, 16.0, 16.0], + ], + [ + [12.0, 11.0, 10.0, 9.0], + [11.0, 10.5, 10.5, 10.0], + [10.0, 10.5, 10.5, 11.0], + [9.0, 10.0, 11.0, 12.0], + ], + [ + [-10.0, -10.0, -10.0, -10.0], + [-10.0, -10.0, -10.0, -10.0], + [-11.0, -11.0, -11.0, -11.0], + [-11.0, -11.0, -11.0, -11.0], + ], + ] + ) + * units("degC") + ) + + p = np.array([850.0, 700.0, 500.0]) * units("hPa") + + lats = np.linspace(35.0, 40.0, 4) * units("degrees") + lons = np.linspace(-100.0, -90.0, 4) * units("degrees") dx, dy = lat_lon_grid_deltas(lons, lats) return u, v, temp, p, dx, dy @@ -1017,16 +1838,28 @@ def test_q_vector_without_static_stability(q_vector_data): # Treating as 700 hPa data q1, q2 = q_vector(u, v, temp[1], p[1], dx, dy) - q1_truth = (np.array([[-2.7454089e-14, -3.0194267e-13, -3.0194267e-13, -2.7454089e-14], - [-1.8952185e-13, -2.2269905e-14, -2.2269905e-14, -1.8952185e-13], - [-1.9918390e-13, -2.3370829e-14, -2.3370829e-14, -1.9918390e-13], - [-5.6160772e-14, -3.5145951e-13, -3.5145951e-13, -5.6160772e-14]]) - * units('m^2 kg^-1 s^-1')) - q2_truth = (np.array([[-4.4976059e-14, -4.3582378e-13, 4.3582378e-13, 4.4976059e-14], - [-3.0124244e-13, -3.5724617e-14, 3.5724617e-14, 3.0124244e-13], - [3.1216232e-13, 3.6662900e-14, -3.6662900e-14, -3.1216232e-13], - [8.6038280e-14, 4.6968342e-13, -4.6968342e-13, -8.6038280e-14]]) - * units('m^2 kg^-1 s^-1')) + q1_truth = ( + np.array( + [ + [-2.7454089e-14, -3.0194267e-13, -3.0194267e-13, -2.7454089e-14], + [-1.8952185e-13, -2.2269905e-14, -2.2269905e-14, -1.8952185e-13], + [-1.9918390e-13, -2.3370829e-14, -2.3370829e-14, -1.9918390e-13], + [-5.6160772e-14, -3.5145951e-13, -3.5145951e-13, -5.6160772e-14], + ] + ) + * units("m^2 kg^-1 s^-1") + ) + q2_truth = ( + np.array( + [ + [-4.4976059e-14, -4.3582378e-13, 4.3582378e-13, 4.4976059e-14], + [-3.0124244e-13, -3.5724617e-14, 3.5724617e-14, 3.0124244e-13], + [3.1216232e-13, 3.6662900e-14, -3.6662900e-14, -3.1216232e-13], + [8.6038280e-14, 4.6968342e-13, -4.6968342e-13, -8.6038280e-14], + ] + ) + * units("m^2 kg^-1 s^-1") + ) assert_almost_equal(q1, q1_truth, 18) assert_almost_equal(q2, q2_truth, 18) @@ -1041,16 +1874,28 @@ def test_q_vector_with_static_stability(q_vector_data): # Treating as 700 hPa data q1, q2 = q_vector(u, v, temp[1], p[1], dx, dy, sigma[1]) - q1_truth = (np.array([[-1.4158140e-08, -1.6197987e-07, -1.6875014e-07, -1.6010616e-08], - [-9.3971386e-08, -1.1252476e-08, -1.1252476e-08, -9.7617234e-08], - [-1.0785670e-07, -1.2403513e-08, -1.2403513e-08, -1.0364793e-07], - [-2.9186946e-08, -1.7577703e-07, -1.6937879e-07, -2.6112047e-08]]) - * units('kg m^-2 s^-3')) - q2_truth = (np.array([[-2.3194263e-08, -2.3380160e-07, 2.4357380e-07, 2.6229040e-08], - [-1.4936626e-07, -1.8050836e-08, 1.8050836e-08, 1.5516129e-07], - [1.6903373e-07, 1.9457964e-08, -1.9457964e-08, -1.6243771e-07], - [4.4714390e-08, 2.3490489e-07, -2.2635441e-07, -4.0003646e-08]]) - * units('kg m^-2 s^-3')) + q1_truth = ( + np.array( + [ + [-1.4158140e-08, -1.6197987e-07, -1.6875014e-07, -1.6010616e-08], + [-9.3971386e-08, -1.1252476e-08, -1.1252476e-08, -9.7617234e-08], + [-1.0785670e-07, -1.2403513e-08, -1.2403513e-08, -1.0364793e-07], + [-2.9186946e-08, -1.7577703e-07, -1.6937879e-07, -2.6112047e-08], + ] + ) + * units("kg m^-2 s^-3") + ) + q2_truth = ( + np.array( + [ + [-2.3194263e-08, -2.3380160e-07, 2.4357380e-07, 2.6229040e-08], + [-1.4936626e-07, -1.8050836e-08, 1.8050836e-08, 1.5516129e-07], + [1.6903373e-07, 1.9457964e-08, -1.9457964e-08, -1.6243771e-07], + [4.4714390e-08, 2.3490489e-07, -2.2635441e-07, -4.0003646e-08], + ] + ) + * units("kg m^-2 s^-3") + ) assert_almost_equal(q1, q1_truth, 12) assert_almost_equal(q2, q2_truth, 12) @@ -1060,239 +1905,399 @@ def test_q_vector_with_static_stability(q_vector_data): @needs_cartopy def data_4d(): """Define 4D data (extracted from Irma GFS example) for testing kinematics functions.""" - data = xr.open_dataset(get_test_data('irma_gfs_example.nc', False)) + data = xr.open_dataset(get_test_data("irma_gfs_example.nc", False)) data = data.metpy.parse_cf() - data['Geopotential_height_isobaric'].attrs['units'] = 'm' - subset = data.drop_vars(( - 'LatLon_361X720-0p25S-180p00E', 'Vertical_velocity_pressure_isobaric', 'isobaric1', - 'Relative_humidity_isobaric', 'reftime' - - )).sel( - latitude=[46., 44., 42., 40.], - longitude=[262., 267., 272., 277.], - isobaric3=[50000., 70000., 85000.] - ).isel(time1=[0, 1, 2]) - return subset.rename({ - 'Geopotential_height_isobaric': 'height', - 'Temperature_isobaric': 'temperature', - 'isobaric3': 'pressure', - 'u-component_of_wind_isobaric': 'u', - 'v-component_of_wind_isobaric': 'v' - }) + data["Geopotential_height_isobaric"].attrs["units"] = "m" + subset = ( + data.drop_vars( + ( + "LatLon_361X720-0p25S-180p00E", + "Vertical_velocity_pressure_isobaric", + "isobaric1", + "Relative_humidity_isobaric", + "reftime", + ) + ) + .sel( + latitude=[46.0, 44.0, 42.0, 40.0], + longitude=[262.0, 267.0, 272.0, 277.0], + isobaric3=[50000.0, 70000.0, 85000.0], + ) + .isel(time1=[0, 1, 2]) + ) + return subset.rename( + { + "Geopotential_height_isobaric": "height", + "Temperature_isobaric": "temperature", + "isobaric3": "pressure", + "u-component_of_wind_isobaric": "u", + "v-component_of_wind_isobaric": "v", + } + ) def test_vorticity_4d(data_4d): """Test vorticity on a 4D (time, pressure, y, x) grid.""" vort = vorticity(data_4d.u, data_4d.v) - truth = np.array([[[[-5.83650490e-05, 3.17327814e-05, 4.57268332e-05, 2.00732350e-05], + truth = ( + np.array( + [ + [ + [ + [-5.83650490e-05, 3.17327814e-05, 4.57268332e-05, 2.00732350e-05], [2.14368312e-05, 1.95623237e-05, 4.15790182e-05, 6.90274641e-05], [6.18610861e-05, 6.93600880e-05, 8.36201998e-05, 8.25922654e-05], - [-4.44038452e-05, 1.56487106e-04, 1.42605312e-04, -6.03981765e-05]], - [[-8.26772499e-07, 4.24638141e-05, -1.02560273e-05, 1.40379447e-05], + [-4.44038452e-05, 1.56487106e-04, 1.42605312e-04, -6.03981765e-05], + ], + [ + [-8.26772499e-07, 4.24638141e-05, -1.02560273e-05, 1.40379447e-05], [1.16882545e-05, 1.06463071e-05, 2.84971990e-05, 6.22850560e-05], [1.83850591e-05, 7.36387780e-06, 3.76622760e-05, 4.67188878e-05], - [3.19856719e-05, 2.80735317e-05, 3.73822586e-05, 5.40379931e-05]], - [[-2.84629423e-05, 3.82238141e-06, 3.96173636e-06, 5.13752737e-05], + [3.19856719e-05, 2.80735317e-05, 3.73822586e-05, 5.40379931e-05], + ], + [ + [-2.84629423e-05, 3.82238141e-06, 3.96173636e-06, 5.13752737e-05], [-2.18549443e-05, 5.28657636e-06, 2.00254459e-05, 3.38246076e-05], [-1.97810827e-05, -2.51363102e-06, 2.87033130e-05, 3.01975044e-05], - [-2.34149501e-05, -1.82846160e-05, 2.95791089e-05, 3.41817364e-05]]], - [[[-3.66403309e-05, 2.45056689e-05, 8.30552352e-05, 2.42918324e-05], + [-2.34149501e-05, -1.82846160e-05, 2.95791089e-05, 3.41817364e-05], + ], + ], + [ + [ + [-3.66403309e-05, 2.45056689e-05, 8.30552352e-05, 2.42918324e-05], [3.30814959e-05, 2.30523398e-05, 4.66571426e-05, 7.60789420e-05], [7.65406561e-05, 4.98489001e-05, 6.61967180e-05, 9.90553670e-05], - [-3.83060455e-06, 8.82014475e-05, 1.11633279e-04, -4.43270102e-05]], - [[-2.47146999e-06, 3.95768075e-05, 3.76682359e-05, 3.79239346e-05], + [-3.83060455e-06, 8.82014475e-05, 1.11633279e-04, -4.43270102e-05], + ], + [ + [-2.47146999e-06, 3.95768075e-05, 3.76682359e-05, 3.79239346e-05], [-4.60129429e-06, 2.05601660e-05, 2.89144970e-05, 3.01301961e-05], [1.54778233e-05, 8.05069277e-06, 2.44051429e-05, 7.01730409e-05], - [2.07682219e-05, 2.16790897e-05, 3.38209456e-05, 9.11823021e-05]], - [[-7.12798691e-06, -2.81765143e-06, 1.93675069e-05, 6.21220857e-05], + [2.07682219e-05, 2.16790897e-05, 3.38209456e-05, 9.11823021e-05], + ], + [ + [-7.12798691e-06, -2.81765143e-06, 1.93675069e-05, 6.21220857e-05], [-1.80822794e-05, 7.10872010e-06, 1.40635809e-05, 1.80843580e-05], [-3.01135264e-06, 2.56664766e-06, 2.25038301e-05, 3.69789825e-05], - [-1.48940627e-05, -6.28397440e-06, 3.66706625e-05, 1.13280233e-05]]], - [[[-2.13814161e-05, 3.10846718e-05, 9.42880991e-05, 6.20302960e-05], + [-1.48940627e-05, -6.28397440e-06, 3.66706625e-05, 1.13280233e-05], + ], + ], + [ + [ + [-2.13814161e-05, 3.10846718e-05, 9.42880991e-05, 6.20302960e-05], [2.83685685e-05, 2.71376067e-05, 4.44470499e-05, 7.94154059e-05], [7.29341555e-05, 3.07015029e-05, 3.70538789e-05, 7.75632608e-05], - [6.86595116e-05, 2.25094524e-05, 7.15703850e-05, 6.90873953e-05]], - [[-5.70566101e-07, 5.63627148e-05, 2.91960395e-05, 3.62726492e-05], + [6.86595116e-05, 2.25094524e-05, 7.15703850e-05, 6.90873953e-05], + ], + [ + [-5.70566101e-07, 5.63627148e-05, 2.91960395e-05, 3.62726492e-05], [-6.17247194e-06, 2.63672993e-05, 3.27525843e-05, 2.87151996e-05], [8.82121811e-06, 6.46657237e-06, 2.03146030e-05, 4.99274322e-05], - [1.54560972e-05, 1.39161983e-06, -6.92832423e-06, 6.02698395e-05]], - [[3.01573325e-06, 2.95361596e-05, 3.30386503e-05, 4.50206712e-05], + [1.54560972e-05, 1.39161983e-06, -6.92832423e-06, 6.02698395e-05], + ], + [ + [3.01573325e-06, 2.95361596e-05, 3.30386503e-05, 4.50206712e-05], [-3.44201203e-06, 7.98411843e-06, 1.31230998e-05, 1.82704434e-05], [-5.97302093e-06, -6.76058488e-07, 5.89633276e-06, 1.82494546e-05], - [-2.96985363e-06, 3.86098537e-06, 5.24525482e-06, - 2.72933874e-05]]]]) * units('s^-1') + [-2.96985363e-06, 3.86098537e-06, 5.24525482e-06, 2.72933874e-05], + ], + ], + ] + ) + * units("s^-1") + ) assert_array_almost_equal(vort.data, truth, 12) def test_divergence_4d(data_4d): """Test divergence on a 4D (time, pressure, y, x) grid.""" div = divergence(data_4d.u, data_4d.v) - truth = np.array([[[[-8.43705083e-06, -5.42243991e-06, 1.42553766e-05, 2.81311077e-05], + truth = ( + np.array( + [ + [ + [ + [-8.43705083e-06, -5.42243991e-06, 1.42553766e-05, 2.81311077e-05], [2.95334911e-05, -8.91904163e-06, 1.18532270e-05, -6.26196756e-06], [-4.63583096e-05, -2.10525265e-05, 1.32571075e-05, 4.76118929e-05], - [-3.36862002e-05, 1.49431136e-05, -4.23301144e-05, 3.93742169e-05]], - [[1.69160375e-05, 1.54447811e-06, 4.11350021e-05, -5.08238612e-05], + [-3.36862002e-05, 1.49431136e-05, -4.23301144e-05, 3.93742169e-05], + ], + [ + [1.69160375e-05, 1.54447811e-06, 4.11350021e-05, -5.08238612e-05], [2.27239404e-05, -3.97652811e-06, 5.32329400e-06, 1.75756955e-05], [-6.16991733e-06, -1.77132521e-06, -1.46585782e-05, 2.66081211e-06], - [-3.06896618e-05, -8.23671871e-06, -2.56998533e-05, -2.79187158e-05]], - [[-1.47210200e-05, -2.26015888e-05, -2.10309987e-05, -3.88000930e-05], + [-3.06896618e-05, -8.23671871e-06, -2.56998533e-05, -2.79187158e-05], + ], + [ + [-1.47210200e-05, -2.26015888e-05, -2.10309987e-05, -3.88000930e-05], [2.87880179e-06, -9.97852896e-06, -1.02741993e-06, 4.53302920e-06], [1.48614763e-05, 3.64899207e-06, 5.07255670e-06, 5.85619901e-06], - [1.12477665e-05, 1.61231699e-05, 1.13583495e-05, 3.92603086e-06]]], - [[[-3.95659837e-06, -8.16293111e-06, 6.64912076e-06, 4.82899740e-05], + [1.12477665e-05, 1.61231699e-05, 1.13583495e-05, 3.92603086e-06], + ], + ], + [ + [ + [-3.95659837e-06, -8.16293111e-06, 6.64912076e-06, 4.82899740e-05], [2.29285761e-05, -7.41730432e-07, 4.88714205e-06, -3.26931553e-05], [-3.27492305e-05, -9.30212918e-06, 1.79834485e-06, 2.53811573e-05], - [-7.03628352e-05, 1.59599812e-05, -5.03928715e-05, 3.02766722e-05]], - [[1.67050619e-05, 1.24336555e-05, -2.22683301e-05, -1.89873955e-05], + [-7.03628352e-05, 1.59599812e-05, -5.03928715e-05, 3.02766722e-05], + ], + [ + [1.67050619e-05, 1.24336555e-05, -2.22683301e-05, -1.89873955e-05], [1.75618966e-05, -1.79165561e-06, 4.00327550e-06, -5.57201491e-06], [4.02631582e-06, -5.29814574e-06, 3.59245019e-06, 4.16299189e-06], - [-3.62032526e-06, 4.00602251e-06, -2.17495860e-05, 2.93910418e-05]], - [[-6.50182516e-06, -3.06444044e-07, -4.68103153e-05, 6.54271734e-06], + [-3.62032526e-06, 4.00602251e-06, -2.17495860e-05, 2.93910418e-05], + ], + [ + [-6.50182516e-06, -3.06444044e-07, -4.68103153e-05, 6.54271734e-06], [-9.22409986e-07, -6.80509227e-06, 1.57428914e-06, 2.13528516e-06], [5.77636627e-06, -2.32628120e-06, 1.63766780e-05, 1.30647979e-05], - [-3.84054963e-06, 1.81368329e-05, -2.12456769e-08, 1.39177255e-05]]], - [[[-3.09706856e-06, -1.58174014e-05, 1.11042898e-05, 1.01863872e-05], + [-3.84054963e-06, 1.81368329e-05, -2.12456769e-08, 1.39177255e-05], + ], + ], + [ + [ + [-3.09706856e-06, -1.58174014e-05, 1.11042898e-05, 1.01863872e-05], [2.22554541e-05, -2.18115261e-06, 2.95538179e-06, -1.27314237e-05], [-2.01806928e-06, -1.44611342e-05, -1.60090851e-06, 1.51875027e-05], - [-7.45883555e-05, -2.17664690e-05, -6.59935118e-06, -9.51280586e-06]], - [[-2.84262294e-06, 1.83370481e-05, -1.60080375e-05, 5.94414530e-06], + [-7.45883555e-05, -2.17664690e-05, -6.59935118e-06, -9.51280586e-06], + ], + [ + [-2.84262294e-06, 1.83370481e-05, -1.60080375e-05, 5.94414530e-06], [2.21275530e-05, 2.08417698e-06, -8.62049532e-06, -4.83025078e-06], [6.84132907e-06, -1.74271778e-06, 4.96579520e-06, -8.14051216e-06], - [-5.91652815e-06, -1.27122109e-06, 1.33424166e-05, 2.89090443e-05]], - [[3.56617289e-06, -3.61628942e-06, -2.14971623e-05, 9.09440121e-06], + [-5.91652815e-06, -1.27122109e-06, 1.33424166e-05, 2.89090443e-05], + ], + [ + [3.56617289e-06, -3.61628942e-06, -2.14971623e-05, 9.09440121e-06], [1.15504071e-05, -2.35438670e-07, -1.00682327e-05, -7.83169489e-06], [1.74820166e-06, -4.85659616e-07, 6.34687163e-06, -9.27089944e-06], - [9.23766788e-07, -2.85241737e-06, 1.68475020e-05, - -5.70982211e-06]]]]) * units('s^-1') + [9.23766788e-07, -2.85241737e-06, 1.68475020e-05, -5.70982211e-06], + ], + ], + ] + ) + * units("s^-1") + ) assert_array_almost_equal(div.data, truth, 12) def test_shearing_deformation_4d(data_4d): """Test shearing_deformation on a 4D (time, pressure, y, x) grid.""" shdef = shearing_deformation(data_4d.u, data_4d.v) - truth = np.array([[[[-2.32353766e-05, 3.38638896e-06, 2.68355706e-05, 1.06560395e-05], + truth = ( + np.array( + [ + [ + [ + [-2.32353766e-05, 3.38638896e-06, 2.68355706e-05, 1.06560395e-05], [-6.40834716e-05, 1.01157390e-05, 1.72783215e-05, -2.41362735e-05], [6.69848680e-07, -1.89007571e-05, -1.40877214e-05, 3.71581119e-05], - [6.36114984e-05, -1.08233745e-04, -9.64601102e-05, 7.32813538e-05]], - [[-2.42214688e-05, -1.01851671e-05, 5.54461375e-05, -3.51796268e-07], + [6.36114984e-05, -1.08233745e-04, -9.64601102e-05, 7.32813538e-05], + ], + [ + [-2.42214688e-05, -1.01851671e-05, 5.54461375e-05, -3.51796268e-07], [-2.71001778e-06, 9.30533079e-06, 2.03843188e-05, 3.34828205e-05], [-7.27917172e-06, 1.72622100e-05, -5.55179147e-06, -1.31598103e-05], - [-2.51927242e-05, 9.17056498e-06, -2.24631811e-06, -5.35695138e-05]], - [[-2.57651839e-05, 1.01219942e-05, 4.53600390e-05, 5.28799494e-07], + [-2.51927242e-05, 9.17056498e-06, -2.24631811e-06, -5.35695138e-05], + ], + [ + [-2.57651839e-05, 1.01219942e-05, 4.53600390e-05, 5.28799494e-07], [-1.55543764e-05, 6.18561775e-06, 2.36187660e-05, 2.52821250e-05], [-2.67211842e-06, 1.14466225e-05, 7.99438411e-06, 3.06434113e-05], - [1.17029675e-05, 2.71857215e-05, -1.93883537e-06, 1.03237850e-05]]], - [[[5.36878109e-06, 1.03810936e-05, -1.74133759e-05, 3.67019082e-05], + [1.17029675e-05, 2.71857215e-05, -1.93883537e-06, 1.03237850e-05], + ], + ], + [ + [ + [5.36878109e-06, 1.03810936e-05, -1.74133759e-05, 3.67019082e-05], [-3.68224309e-05, 8.02234002e-06, 2.97256283e-06, -2.41548002e-05], [-1.03217116e-07, -3.55295991e-07, 1.04215491e-06, 3.06178781e-05], - [1.78849776e-05, -3.14217684e-05, -5.31904971e-05, 6.33706906e-05]], - [[-2.54172738e-05, -1.89184694e-05, 7.52187239e-06, 7.78263224e-06], + [1.78849776e-05, -3.14217684e-05, -5.31904971e-05, 6.33706906e-05], + ], + [ + [-2.54172738e-05, -1.89184694e-05, 7.52187239e-06, 7.78263224e-06], [-1.58491234e-05, 8.86850501e-06, 1.94682285e-05, 6.28154949e-06], [2.87101909e-06, 1.02981542e-05, 1.49483081e-05, 1.11896135e-05], - [-6.24536223e-06, 5.02422684e-06, 3.65739211e-06, -4.43343790e-05]], - [[-1.97278534e-05, 7.98223535e-06, 1.93668724e-05, 1.17314603e-05], + [-6.24536223e-06, 5.02422684e-06, 3.65739211e-06, -4.43343790e-05], + ], + [ + [-1.97278534e-05, 7.98223535e-06, 1.93668724e-05, 1.17314603e-05], [-1.80800662e-05, 7.10682312e-06, 1.58638789e-05, -7.11095379e-06], [1.04957568e-05, 3.46915772e-06, 7.19233197e-06, 4.14864918e-05], - [1.30201499e-05, 7.22093417e-06, -1.46521398e-05, 5.00427528e-05]]], - [[[-8.31494391e-06, 2.74335132e-06, -2.40497584e-05, 1.75042968e-05], + [1.30201499e-05, 7.22093417e-06, -1.46521398e-05, 5.00427528e-05], + ], + ], + [ + [ + [-8.31494391e-06, 2.74335132e-06, -2.40497584e-05, 1.75042968e-05], [-1.88915366e-05, 3.28864180e-06, 1.34127052e-05, 1.23621458e-05], [4.61243898e-07, 1.85506704e-05, 1.67855055e-05, 9.59377214e-06], - [6.06292303e-06, 2.92575008e-05, -1.44159217e-05, 2.17975694e-05]], - [[-1.00168319e-05, -4.57753168e-05, 8.50542316e-06, 3.44821467e-05], + [6.06292303e-06, 2.92575008e-05, -1.44159217e-05, 2.17975694e-05], + ], + [ + [-1.00168319e-05, -4.57753168e-05, 8.50542316e-06, 3.44821467e-05], [-1.65225222e-05, -4.66989095e-06, 6.65190573e-06, 1.71105129e-06], [-4.23400810e-06, 1.50208950e-05, 1.80731201e-05, 5.36054473e-06], - [-2.10443897e-06, 1.80502669e-05, 4.39381815e-05, 5.78573040e-06]], - [[-2.08335492e-05, -3.03108476e-05, -3.85875023e-06, 2.70252773e-05], + [-2.10443897e-06, 1.80502669e-05, 4.39381815e-05, 5.78573040e-06], + ], + [ + [-2.08335492e-05, -3.03108476e-05, -3.85875023e-06, 2.70252773e-05], [-4.78804481e-06, 1.24351472e-06, 6.82854110e-06, 5.67152289e-06], [5.73158894e-06, 1.05747791e-05, 1.53497021e-05, 1.55510561e-05], - [1.23394357e-05, -1.98706807e-06, 1.56020711e-05, - 3.89964205e-05]]]]) * units('s^-1') + [1.23394357e-05, -1.98706807e-06, 1.56020711e-05, 3.89964205e-05], + ], + ], + ] + ) + * units("s^-1") + ) assert_array_almost_equal(shdef.data, truth, 12) def test_stretching_deformation_4d(data_4d): """Test stretching_deformation on a 4D (time, pressure, y, x) grid.""" stdef = stretching_deformation(data_4d.u, data_4d.v) - truth = np.array([[[[3.47764258e-05, 2.24655678e-05, -5.99204286e-06, -2.81311151e-05], + truth = ( + np.array( + [ + [ + [ + [3.47764258e-05, 2.24655678e-05, -5.99204286e-06, -2.81311151e-05], [-1.00806414e-05, 2.43815624e-05, 5.10566770e-06, 3.02039392e-05], [-5.93889988e-05, 4.15227142e-06, 3.93751112e-05, 5.52382202e-05], - [8.92010023e-05, 1.85531529e-05, 3.60056433e-05, -1.03321628e-04]], - [[2.96761128e-06, 1.36910447e-05, -4.34590663e-05, 1.80287489e-05], + [8.92010023e-05, 1.85531529e-05, 3.60056433e-05, -1.03321628e-04], + ], + [ + [2.96761128e-06, 1.36910447e-05, -4.34590663e-05, 1.80287489e-05], [1.86757141e-05, 5.47290208e-06, -9.06422655e-06, 8.11203944e-06], [1.34111946e-07, 1.26357749e-05, 2.72130530e-05, -3.62654235e-06], - [-1.35816208e-05, 1.87775033e-05, 5.84933984e-05, 5.04057146e-05]], - [[2.89236233e-05, 3.31889843e-05, 1.58664147e-05, 5.74675794e-06], + [-1.35816208e-05, 1.87775033e-05, 5.84933984e-05, 5.04057146e-05], + ], + [ + [2.89236233e-05, 3.31889843e-05, 1.58664147e-05, 5.74675794e-06], [1.68234432e-05, 1.52158343e-05, 5.26714302e-06, 1.21764691e-05], [8.55744700e-06, 7.69832273e-06, -4.83112472e-06, -1.57549242e-05], - [-5.86025733e-06, 8.47198887e-06, -3.49088418e-07, -3.92962147e-05]]], - [[[3.69582950e-05, 1.86470363e-05, -2.80150634e-06, -3.51977497e-05], + [-5.86025733e-06, 8.47198887e-06, -3.49088418e-07, -3.92962147e-05], + ], + ], + [ + [ + [3.69582950e-05, 1.86470363e-05, -2.80150634e-06, -3.51977497e-05], [-6.46847071e-06, 2.69781373e-05, 6.95914436e-06, 5.98289917e-06], [-4.00667252e-05, 5.05292624e-06, 4.75021137e-05, 6.24518714e-05], - [3.67260262e-05, 2.68548943e-06, 7.10293796e-05, -5.79403685e-05]], - [[-5.34297827e-06, -1.07157183e-06, 2.58835391e-05, 7.10885516e-06], + [3.67260262e-05, 2.68548943e-06, 7.10293796e-05, -5.79403685e-05], + ], + [ + [-5.34297827e-06, -1.07157183e-06, 2.58835391e-05, 7.10885516e-06], [1.26149579e-05, 1.35132438e-05, -2.75629799e-06, 4.32503739e-06], [8.52816121e-06, 1.49554343e-05, 6.30626925e-06, 9.11577765e-06], - [2.68336326e-06, 5.36356177e-06, 5.47773716e-05, 4.06465999e-05]], - [[1.73474509e-05, 5.98748586e-06, 4.13875024e-05, -2.90086556e-05], + [2.68336326e-06, 5.36356177e-06, 5.47773716e-05, 4.06465999e-05], + ], + [ + [1.73474509e-05, 5.98748586e-06, 4.13875024e-05, -2.90086556e-05], [4.23620645e-07, 1.02966295e-05, 5.15938898e-06, 7.09234799e-06], [5.32951540e-06, 6.67206038e-06, -7.92655166e-06, 1.03541254e-05], - [1.46155702e-05, 1.33856872e-07, 4.47179844e-06, -4.46031161e-05]]], - [[[3.02111317e-05, 2.69212573e-05, -4.64855995e-06, 2.98329788e-06], + [1.46155702e-05, 1.33856872e-07, 4.47179844e-06, -4.46031161e-05], + ], + ], + [ + [ + [3.02111317e-05, 2.69212573e-05, -4.64855995e-06, 2.98329788e-06], [-2.05441981e-06, 2.88664710e-05, 1.15095603e-05, -3.72867092e-06], [-1.41578887e-05, 1.61511593e-05, 3.08142052e-05, 5.12063542e-05], - [-4.81886157e-06, 1.96583134e-05, 4.92309570e-05, 6.43248731e-05]], - [[-1.85904008e-05, -1.13648603e-05, 2.94359552e-05, -8.00997936e-06], + [-4.81886157e-06, 1.96583134e-05, 4.92309570e-05, 6.43248731e-05], + ], + [ + [-1.85904008e-05, -1.13648603e-05, 2.94359552e-05, -8.00997936e-06], [1.62793512e-05, 8.39043368e-06, 7.12412372e-06, 7.32420819e-06], [9.09319575e-06, 1.67115147e-05, 5.41579051e-06, 1.03134035e-05], - [2.63717343e-06, 5.48753335e-06, 1.28924212e-05, 3.38671775e-05]], - [[-4.08263202e-06, 1.03302483e-05, 2.30465372e-05, -2.51046121e-05], + [2.63717343e-06, 5.48753335e-06, 1.28924212e-05, 3.38671775e-05], + ], + [ + [-4.08263202e-06, 1.03302483e-05, 2.30465372e-05, -2.51046121e-05], [8.40123079e-06, 9.21367536e-06, 6.57669664e-06, -9.62598559e-06], [6.70192818e-06, 9.41865112e-06, -1.75966046e-06, 4.68368828e-06], - [1.75811596e-05, 1.24562416e-05, -1.28654291e-05, - 7.34949445e-06]]]]) * units('s^-1') + [1.75811596e-05, 1.24562416e-05, -1.28654291e-05, 7.34949445e-06], + ], + ], + ] + ) + * units("s^-1") + ) assert_array_almost_equal(stdef.data, truth, 12) def test_total_deformation_4d(data_4d): """Test total_deformation on a 4D (time, pressure, y, x) grid.""" totdef = total_deformation(data_4d.u, data_4d.v) - truth = np.array([[[[4.18244250e-05, 2.27193611e-05, 2.74964075e-05, 3.00817356e-05], + truth = ( + np.array( + [ + [ + [ + [4.18244250e-05, 2.27193611e-05, 2.74964075e-05, 3.00817356e-05], [6.48714934e-05, 2.63967566e-05, 1.80168876e-05, 3.86631303e-05], [5.93927763e-05, 1.93514851e-05, 4.18194127e-05, 6.65731647e-05], - [1.09559306e-04, 1.09812399e-04, 1.02960960e-04, 1.26670895e-04]], - [[2.44025874e-05, 1.70640656e-05, 7.04483116e-05, 1.80321809e-05], + [1.09559306e-04, 1.09812399e-04, 1.02960960e-04, 1.26670895e-04], + ], + [ + [2.44025874e-05, 1.70640656e-05, 7.04483116e-05, 1.80321809e-05], [1.88713140e-05, 1.07954545e-05, 2.23087574e-05, 3.44514797e-05], [7.28040706e-06, 2.13926787e-05, 2.77735961e-05, 1.36503632e-05], - [2.86205132e-05, 2.08972221e-05, 5.85365151e-05, 7.35556175e-05]], - [[3.87352641e-05, 3.46981764e-05, 4.80549296e-05, 5.77103594e-06], + [2.86205132e-05, 2.08972221e-05, 5.85365151e-05, 7.35556175e-05], + ], + [ + [3.87352641e-05, 3.46981764e-05, 4.80549296e-05, 5.77103594e-06], [2.29121554e-05, 1.64250869e-05, 2.41989442e-05, 2.80615795e-05], [8.96493815e-06, 1.37945402e-05, 9.34076781e-06, 3.44562954e-05], - [1.30882414e-05, 2.84752182e-05, 1.97001150e-06, 4.06297062e-05]]], - [[[3.73462098e-05, 2.13419556e-05, 1.76372928e-05, 5.08518598e-05], + [1.30882414e-05, 2.84752182e-05, 1.97001150e-06, 4.06297062e-05], + ], + ], + [ + [ + [3.73462098e-05, 2.13419556e-05, 1.76372928e-05, 5.08518598e-05], [3.73862612e-05, 2.81456539e-05, 7.56741832e-06, 2.48847233e-05], [4.00668582e-05, 5.06540214e-06, 4.75135443e-05, 6.95535096e-05], - [4.08493993e-05, 3.15363185e-05, 8.87378259e-05, 8.58657716e-05]], - [[2.59727785e-05, 1.89487929e-05, 2.69543347e-05, 1.05406445e-05], + [4.08493993e-05, 3.15363185e-05, 8.87378259e-05, 8.58657716e-05], + ], + [ + [2.59727785e-05, 1.89487929e-05, 2.69543347e-05, 1.05406445e-05], [2.02566502e-05, 1.61634816e-05, 1.96623777e-05, 7.62652034e-06], [8.99846010e-06, 1.81581110e-05, 1.62240854e-05, 1.44327701e-05], - [6.79742509e-06, 7.34919385e-06, 5.48993347e-05, 6.01471798e-05]], - [[2.62701780e-05, 9.97827981e-06, 4.56946507e-05, 3.12910413e-05], + [6.79742509e-06, 7.34919385e-06, 5.48993347e-05, 6.01471798e-05], + ], + [ + [2.62701780e-05, 9.97827981e-06, 4.56946507e-05, 3.12910413e-05], [1.80850283e-05, 1.25110957e-05, 1.66817850e-05, 1.00432596e-05], [1.17713485e-05, 7.52006948e-06, 1.07032640e-05, 4.27590565e-05], - [1.95739418e-05, 7.22217474e-06, 1.53193401e-05, 6.70351779e-05]]], - [[[3.13344980e-05, 2.70606739e-05, 2.44948972e-05, 1.77567021e-05], + [1.95739418e-05, 7.22217474e-06, 1.53193401e-05, 6.70351779e-05], + ], + ], + [ + [ + [3.13344980e-05, 2.70606739e-05, 2.44948972e-05, 1.77567021e-05], [1.90029154e-05, 2.90531980e-05, 1.76740102e-05, 1.29122281e-05], [1.41654000e-05, 2.45964900e-05, 3.50894348e-05, 5.20973241e-05], - [7.74470545e-06, 3.52484133e-05, 5.12982059e-05, 6.79177688e-05]], - [[2.11172897e-05, 4.71650260e-05, 3.06401319e-05, 3.54002572e-05], + [7.74470545e-06, 3.52484133e-05, 5.12982059e-05, 6.79177688e-05], + ], + [ + [2.11172897e-05, 4.71650260e-05, 3.06401319e-05, 3.54002572e-05], [2.31950645e-05, 9.60246108e-06, 9.74684506e-06, 7.52141756e-06], [1.00306048e-05, 2.24700247e-05, 1.88671263e-05, 1.16233270e-05], - [3.37392161e-06, 1.88659788e-05, 4.57905920e-05, 3.43578287e-05]], - [[2.12298059e-05, 3.20228280e-05, 2.33673454e-05, 3.68864089e-05], + [3.37392161e-06, 1.88659788e-05, 4.57905920e-05, 3.43578287e-05], + ], + [ + [2.12298059e-05, 3.20228280e-05, 2.33673454e-05, 3.68864089e-05], [9.66985273e-06, 9.29721155e-06, 9.48060716e-06, 1.11725454e-05], [8.81855731e-06, 1.41611066e-05, 1.54502349e-05, 1.62410677e-05], - [2.14792655e-05, 1.26137383e-05, 2.02223611e-05, - 3.96829419e-05]]]]) * units('s^-1') + [2.14792655e-05, 1.26137383e-05, 2.02223611e-05, 3.96829419e-05], + ], + ], + ] + ) + * units("s^-1") + ) assert_array_almost_equal(totdef.data, truth, 12) @@ -1300,163 +2305,215 @@ def test_frontogenesis_4d(data_4d): """Test frontogenesis on a 4D (time, pressure, y, x) grid.""" thta = potential_temperature(data_4d.pressure, data_4d.temperature) frnt = frontogenesis(thta, data_4d.u, data_4d.v).transpose( - 'time1', - 'pressure', - 'latitude', - 'longitude' + "time1", "pressure", "latitude", "longitude" ) - truth = np.array([[[[4.23682195e-10, -6.42818314e-12, -2.16491106e-10, -3.81845902e-10], + truth = ( + np.array( + [ + [ + [ + [4.23682195e-10, -6.42818314e-12, -2.16491106e-10, -3.81845902e-10], [-5.28632893e-10, -6.99413155e-12, -4.77775880e-11, 2.95949984e-10], [7.82193227e-10, 3.55234312e-10, 2.14592821e-11, -5.20704165e-10], - [-3.51045184e-10, 2.06780694e-10, 1.68485199e-09, -1.46174872e-09]], - [[-7.24768625e-11, 1.07136516e-10, -1.33696585e-10, 3.43097590e-10], + [-3.51045184e-10, 2.06780694e-10, 1.68485199e-09, -1.46174872e-09], + ], + [ + [-7.24768625e-11, 1.07136516e-10, -1.33696585e-10, 3.43097590e-10], [-5.01911031e-11, 2.14208730e-11, -4.73005145e-11, 9.54558115e-11], [4.78125962e-11, 6.95838402e-11, 3.53993328e-10, -7.14986278e-11], - [6.14372837e-10, 1.41441177e-10, 8.45358549e-10, 1.36583089e-09]], - [[2.06344624e-11, 3.21877016e-10, 5.56930831e-10, 1.42479782e-10], + [6.14372837e-10, 1.41441177e-10, 8.45358549e-10, 1.36583089e-09], + ], + [ + [2.06344624e-11, 3.21877016e-10, 5.56930831e-10, 1.42479782e-10], [9.85250671e-11, 1.06837011e-10, 5.71410578e-11, -4.75266666e-12], [-6.42402291e-11, -2.11239598e-11, -1.21400141e-11, 2.11343115e-10], - [-6.98848390e-11, -4.11958693e-11, -1.75826411e-10, -1.78597026e-10]]], - [[[1.75135966e-10, -1.28928980e-11, -5.23466009e-11, -3.77702045e-10], + [-6.98848390e-11, -4.11958693e-11, -1.75826411e-10, -1.78597026e-10], + ], + ], + [ + [ + [1.75135966e-10, -1.28928980e-11, -5.23466009e-11, -3.77702045e-10], [-1.89751794e-10, -2.39181519e-11, 1.11100290e-11, 3.27299708e-10], [5.03778532e-10, 6.01896046e-11, 2.52803022e-10, 2.63665975e-10], - [8.58126215e-10, -1.03984888e-10, 1.36888801e-09, -2.55913184e-11]], - [[-4.65433709e-11, -4.29058715e-11, 1.37345453e-10, 1.99587747e-10], + [8.58126215e-10, -1.03984888e-10, 1.36888801e-09, -2.55913184e-11], + ], + [ + [-4.65433709e-11, -4.29058715e-11, 1.37345453e-10, 1.99587747e-10], [-7.54157254e-11, 3.17296645e-11, 1.98329738e-11, 7.03907102e-11], [2.07597572e-11, 9.76879251e-11, 3.64976958e-11, 9.05618888e-11], - [1.08070144e-10, 4.28175872e-12, 7.19436313e-10, -4.06647884e-10]], - [[3.53005192e-11, 6.97543696e-12, 7.69214961e-10, 1.72736149e-10], + [1.08070144e-10, 4.28175872e-12, 7.19436313e-10, -4.06647884e-10], + ], + [ + [3.53005192e-11, 6.97543696e-12, 7.69214961e-10, 1.72736149e-10], [8.60386822e-11, 6.45426820e-11, 5.98938946e-11, -3.48393954e-11], [-8.04261595e-11, 3.35202076e-11, -6.74886256e-11, 2.13321809e-10], - [3.19647867e-12, -1.19646868e-10, -3.89001592e-11, 2.32044147e-10]]], - [[[-1.06347076e-10, 1.43182380e-10, -1.67871828e-10, 7.55627348e-12], + [3.19647867e-12, -1.19646868e-10, -3.89001592e-11, 2.32044147e-10], + ], + ], + [ + [ + [-1.06347076e-10, 1.43182380e-10, -1.67871828e-10, 7.55627348e-12], [-2.10533376e-11, -1.36653603e-11, 5.14204938e-11, 4.52818256e-11], [9.35607979e-11, -1.90340153e-11, 5.14752944e-11, 3.57906394e-10], - [1.47969288e-09, -6.40884777e-11, -2.00932232e-10, 3.79333810e-10]], - [[1.39959107e-10, -3.68301267e-10, 2.35651001e-10, 1.53240049e-10], + [1.47969288e-09, -6.40884777e-11, -2.00932232e-10, 3.79333810e-10], + ], + [ + [1.39959107e-10, -3.68301267e-10, 2.35651001e-10, 1.53240049e-10], [-2.59723933e-10, 3.90319800e-11, 9.15095885e-11, 3.55206479e-11], [6.07203827e-12, 8.14448434e-11, 2.37589779e-11, 1.56707972e-10], - [6.01077213e-11, 1.43024438e-11, 2.19842020e-10, 6.06611960e-12]], - [[5.76777660e-11, 1.50880981e-10, 9.80083831e-11, 1.37735770e-10], + [6.01077213e-11, 1.43024438e-11, 2.19842020e-10, 6.06611960e-12], + ], + [ + [5.76777660e-11, 1.50880981e-10, 9.80083831e-11, 1.37735770e-10], [-5.69004435e-11, 1.61334326e-11, 8.32223545e-11, 1.07345248e-10], [-5.82285173e-11, 1.03267739e-12, 9.19171693e-12, 1.73823741e-10], - [-2.33302976e-11, 1.01795295e-10, 4.19754683e-12, - 5.18286088e-10]]]]) * units('K/m/s') + [-2.33302976e-11, 1.01795295e-10, 4.19754683e-12, 5.18286088e-10], + ], + ], + ] + ) + * units("K/m/s") + ) assert_array_almost_equal(frnt.data, truth, 16) def test_geostrophic_wind_4d(data_4d): """Test geostrophic_wind on a 4D (time, pressure, y, x) grid.""" u_g, v_g = geostrophic_wind(data_4d.height) - u_g_truth = np.array([[[[4.40351577, 12.52087174, 20.6458988, 3.17057524], - [14.11461945, 17.13672114, 22.06686549, 28.28270102], - [24.47454294, 22.86342357, 31.74065923, 41.48130088], - [35.59988608, 29.85398309, 50.68045123, 41.40714946]], - [[7.36263117, 11.15525254, 15.36136167, 8.9043257], - [8.36777239, 12.52326604, 13.39382587, 14.3316852], - [10.38214971, 13.05048176, 16.57141998, 20.60619861], - [13.53264547, 12.63889256, 25.51465438, 27.85194657]], - [[5.7558101, 8.87403945, 12.12136937, 6.95914488], - [5.63469768, 9.23443489, 9.46733739, 9.64257854], - [5.15675792, 8.93121196, 10.1444189, 10.03116286], - [4.2776387, 7.88423455, 14.54891694, 7.85445617]]], - [[[2.56196227, 12.12574931, 18.89599304, 9.31367555], - [11.14381317, 16.08241758, 22.90372798, 23.24530036], - [21.19957068, 18.21200016, 27.48622742, 37.93750724], - [32.9424023, 18.30589852, 32.72832352, 53.53662491]], - [[5.89065665, 10.24260864, 13.9982738, 7.63017804], - [7.7319368, 12.49290588, 13.8820136, 12.98573492], - [9.40028538, 12.48920597, 15.31293349, 18.73778037], - [10.88139947, 9.96423153, 18.47901734, 24.95509777]], - [[5.3790145, 9.32173797, 9.01530817, 3.68853401], - [5.42563016, 8.9380796, 9.35289746, 9.01581846], - [4.95407268, 8.35221797, 9.30403934, 11.10242176], - [3.90093595, 7.537516, 8.82237876, 9.57266969]]], - [[[4.077062, 9.91350661, 14.63954845, 11.45133198], - [9.22655905, 15.4118828, 20.8655254, 20.36949692], - [17.30011986, 16.30232673, 23.25100815, 32.47299215], - [28.67482075, 12.0424244, 21.34996542, 48.18503321]], - [[4.67946573, 7.67735341, 7.67258169, 7.43729616], - [6.37289243, 10.60261239, 12.10568057, 11.53061917], - [7.78079442, 11.18649202, 14.92802562, 16.19084404], - [8.87459397, 9.15376375, 15.95950978, 21.50271574]], - [[4.07034519, 6.49914852, 4.9842596, 5.1120617], - [4.20250871, 6.7603074, 8.51019946, 8.51714298], - [3.85757805, 6.9373935, 9.8250342, 10.52736698], - [2.97756024, 7.02083208, 8.67190524, - 10.98516411]]]]) * units('m/s') - v_g_truth = np.array([[[[-2.34336304e+01, -1.93589800e+01, -7.42980465e+00, - 1.23538955e+01], - [-2.05343103e+01, -1.59281972e+01, -7.22778771e+00, - 5.56691831e+00], - [-2.12483061e+01, -1.50276890e+01, -1.26159708e+00, - 2.00499696e+01], - [-2.82673839e+01, -1.22322398e+01, 2.74929719e+00, - 1.66772271e+01]], - [[-2.11572490e+01, -1.57068398e+01, -7.16428821e+00, - 4.47040576e+00], - [-1.85233793e+01, -1.38641633e+01, -7.23745352e+00, - 1.35674995e+00], - [-1.48069287e+01, -1.29873005e+01, -6.19402168e+00, - 5.57290769e+00], - [-1.63708722e+01, -1.07203268e+01, -3.25405588e+00, - 6.02794043e+00]], - [[-1.83721994e+01, -1.51434535e+01, -8.30361332e+00, - 2.14732109e+00], - [-1.60334603e+01, -1.37004633e+01, -8.52272656e+00, - -5.00249972e-01], - [-1.25811419e+01, -1.30858045e+01, -8.11893604e+00, - 2.31946331e+00], - [-1.07972595e+01, -1.12050147e+01, -8.05482698e+00, - -1.34669618e+00]]], - [[[-2.47128002e+01, -2.06093912e+01, -7.53605837e+00, - 1.45071982e+01], - [-2.04618167e+01, -1.66379272e+01, -6.94777385e+00, - 8.60864325e+00], - [-2.03847527e+01, -1.41640171e+01, -3.58588785e+00, - 1.13496351e+01], - [-3.06442215e+01, -1.34818877e+01, 3.63145087e+00, - 2.06957942e+01]], - [[-2.20117576e+01, -1.60592509e+01, -6.79979611e+00, - 5.76660686e+00], - [-1.88926841e+01, -1.40452204e+01, -7.10711240e+00, - 1.92164004e+00], - [-1.49428085e+01, -1.27155409e+01, -6.56034626e+00, - 3.52277542e+00], - [-1.56847892e+01, -1.10535722e+01, -3.82991450e+00, - 5.98618380e+00]], - [[-1.89418619e+01, -1.48982095e+01, -8.32871820e+00, - 7.66612047e-01], - [-1.57997569e+01, -1.38337366e+01, -9.12720817e+00, - -1.68017155e+00], - [-1.34002412e+01, -1.27868867e+01, -8.32854573e+00, - -2.52183180e-02], - [-1.10305552e+01, -1.16852908e+01, -7.77451018e+00, - 7.01786569e-01]]], - [[[-2.87198561e+01, -2.07541862e+01, -7.39120444e+00, - 1.13690891e+01], - [-2.50727626e+01, -1.76277299e+01, -6.48428638e+00, - 8.35756789e+00], - [-2.15686958e+01, -1.43774917e+01, -4.66795064e+00, - 7.55992752e+00], - [-3.08303915e+01, -1.46678239e+01, 2.17589132e+00, - 1.97007540e+01]], - [[-2.14034948e+01, -1.55089179e+01, -7.19566930e+00, - 3.53625110e+00], - [-1.85643117e+01, -1.42866005e+01, -7.10227950e+00, - 2.98865138e+00], - [-1.52824784e+01, -1.23952994e+01, -6.71565437e+00, - 1.75645659e+00], - [-1.53343446e+01, -1.06296646e+01, -4.49885811e+00, - 3.05807491e+00]], - [[-1.62094234e+01, -1.41161427e+01, -9.20541452e+00, - -1.47723905e+00], - [-1.41272620e+01, -1.33895366e+01, -9.16198151e+00, - -1.44459685e+00], - [-1.29925870e+01, -1.17892453e+01, -8.27421454e+00, - -2.44749477e+00], - [-1.08991833e+01, -1.03581717e+01, -7.35501458e+00, - -1.88971184e+00]]]]) * units('m/s') + u_g_truth = ( + np.array( + [ + [ + [ + [4.40351577, 12.52087174, 20.6458988, 3.17057524], + [14.11461945, 17.13672114, 22.06686549, 28.28270102], + [24.47454294, 22.86342357, 31.74065923, 41.48130088], + [35.59988608, 29.85398309, 50.68045123, 41.40714946], + ], + [ + [7.36263117, 11.15525254, 15.36136167, 8.9043257], + [8.36777239, 12.52326604, 13.39382587, 14.3316852], + [10.38214971, 13.05048176, 16.57141998, 20.60619861], + [13.53264547, 12.63889256, 25.51465438, 27.85194657], + ], + [ + [5.7558101, 8.87403945, 12.12136937, 6.95914488], + [5.63469768, 9.23443489, 9.46733739, 9.64257854], + [5.15675792, 8.93121196, 10.1444189, 10.03116286], + [4.2776387, 7.88423455, 14.54891694, 7.85445617], + ], + ], + [ + [ + [2.56196227, 12.12574931, 18.89599304, 9.31367555], + [11.14381317, 16.08241758, 22.90372798, 23.24530036], + [21.19957068, 18.21200016, 27.48622742, 37.93750724], + [32.9424023, 18.30589852, 32.72832352, 53.53662491], + ], + [ + [5.89065665, 10.24260864, 13.9982738, 7.63017804], + [7.7319368, 12.49290588, 13.8820136, 12.98573492], + [9.40028538, 12.48920597, 15.31293349, 18.73778037], + [10.88139947, 9.96423153, 18.47901734, 24.95509777], + ], + [ + [5.3790145, 9.32173797, 9.01530817, 3.68853401], + [5.42563016, 8.9380796, 9.35289746, 9.01581846], + [4.95407268, 8.35221797, 9.30403934, 11.10242176], + [3.90093595, 7.537516, 8.82237876, 9.57266969], + ], + ], + [ + [ + [4.077062, 9.91350661, 14.63954845, 11.45133198], + [9.22655905, 15.4118828, 20.8655254, 20.36949692], + [17.30011986, 16.30232673, 23.25100815, 32.47299215], + [28.67482075, 12.0424244, 21.34996542, 48.18503321], + ], + [ + [4.67946573, 7.67735341, 7.67258169, 7.43729616], + [6.37289243, 10.60261239, 12.10568057, 11.53061917], + [7.78079442, 11.18649202, 14.92802562, 16.19084404], + [8.87459397, 9.15376375, 15.95950978, 21.50271574], + ], + [ + [4.07034519, 6.49914852, 4.9842596, 5.1120617], + [4.20250871, 6.7603074, 8.51019946, 8.51714298], + [3.85757805, 6.9373935, 9.8250342, 10.52736698], + [2.97756024, 7.02083208, 8.67190524, 10.98516411], + ], + ], + ] + ) + * units("m/s") + ) + v_g_truth = ( + np.array( + [ + [ + [ + [-2.34336304e01, -1.93589800e01, -7.42980465e00, 1.23538955e01], + [-2.05343103e01, -1.59281972e01, -7.22778771e00, 5.56691831e00], + [-2.12483061e01, -1.50276890e01, -1.26159708e00, 2.00499696e01], + [-2.82673839e01, -1.22322398e01, 2.74929719e00, 1.66772271e01], + ], + [ + [-2.11572490e01, -1.57068398e01, -7.16428821e00, 4.47040576e00], + [-1.85233793e01, -1.38641633e01, -7.23745352e00, 1.35674995e00], + [-1.48069287e01, -1.29873005e01, -6.19402168e00, 5.57290769e00], + [-1.63708722e01, -1.07203268e01, -3.25405588e00, 6.02794043e00], + ], + [ + [-1.83721994e01, -1.51434535e01, -8.30361332e00, 2.14732109e00], + [-1.60334603e01, -1.37004633e01, -8.52272656e00, -5.00249972e-01], + [-1.25811419e01, -1.30858045e01, -8.11893604e00, 2.31946331e00], + [-1.07972595e01, -1.12050147e01, -8.05482698e00, -1.34669618e00], + ], + ], + [ + [ + [-2.47128002e01, -2.06093912e01, -7.53605837e00, 1.45071982e01], + [-2.04618167e01, -1.66379272e01, -6.94777385e00, 8.60864325e00], + [-2.03847527e01, -1.41640171e01, -3.58588785e00, 1.13496351e01], + [-3.06442215e01, -1.34818877e01, 3.63145087e00, 2.06957942e01], + ], + [ + [-2.20117576e01, -1.60592509e01, -6.79979611e00, 5.76660686e00], + [-1.88926841e01, -1.40452204e01, -7.10711240e00, 1.92164004e00], + [-1.49428085e01, -1.27155409e01, -6.56034626e00, 3.52277542e00], + [-1.56847892e01, -1.10535722e01, -3.82991450e00, 5.98618380e00], + ], + [ + [-1.89418619e01, -1.48982095e01, -8.32871820e00, 7.66612047e-01], + [-1.57997569e01, -1.38337366e01, -9.12720817e00, -1.68017155e00], + [-1.34002412e01, -1.27868867e01, -8.32854573e00, -2.52183180e-02], + [-1.10305552e01, -1.16852908e01, -7.77451018e00, 7.01786569e-01], + ], + ], + [ + [ + [-2.87198561e01, -2.07541862e01, -7.39120444e00, 1.13690891e01], + [-2.50727626e01, -1.76277299e01, -6.48428638e00, 8.35756789e00], + [-2.15686958e01, -1.43774917e01, -4.66795064e00, 7.55992752e00], + [-3.08303915e01, -1.46678239e01, 2.17589132e00, 1.97007540e01], + ], + [ + [-2.14034948e01, -1.55089179e01, -7.19566930e00, 3.53625110e00], + [-1.85643117e01, -1.42866005e01, -7.10227950e00, 2.98865138e00], + [-1.52824784e01, -1.23952994e01, -6.71565437e00, 1.75645659e00], + [-1.53343446e01, -1.06296646e01, -4.49885811e00, 3.05807491e00], + ], + [ + [-1.62094234e01, -1.41161427e01, -9.20541452e00, -1.47723905e00], + [-1.41272620e01, -1.33895366e01, -9.16198151e00, -1.44459685e00], + [-1.29925870e01, -1.17892453e01, -8.27421454e00, -2.44749477e00], + [-1.08991833e01, -1.03581717e01, -7.35501458e00, -1.88971184e00], + ], + ], + ] + ) + * units("m/s") + ) assert_array_almost_equal(u_g.data, u_g_truth, 6) assert_array_almost_equal(v_g.data, v_g_truth, 6) @@ -1465,115 +2522,140 @@ def test_inertial_advective_wind_4d(data_4d): """Test inertial_advective_wind on a 4D (time, pressure, y, x) grid.""" u_g, v_g = geostrophic_wind(data_4d.height) u_i, v_i = inertial_advective_wind(u_g, v_g, u_g, v_g) - u_i_truth = np.array([[[[-4.74579332, -6.36486064, -7.20354171, -11.08307751], - [-1.88515129, -4.33855679, -6.82871465, -9.38096911], - [2.308649, -6.93391208, -14.06293133, -20.60786775], - [-0.92388354, -13.76737076, -17.9039117, -23.71419254]], - [[-2.60558413, -3.48755492, -3.62050089, -4.18871134], - [-3.36965812, -2.57689219, -2.66529828, -3.34582207], - [-0.56309499, -2.3322732, -4.37379768, -6.6663065], - [1.70092943, -3.59623514, -5.94640587, -7.50380432]], - [[-1.60508844, -2.30572073, -2.39044749, -2.59511279], - [-2.18854472, -1.47967397, -1.57319604, -2.24386278], - [-1.10582176, -1.24627092, -2.02075175, -3.314856], - [-0.25911941, -1.62294229, -1.75103256, -1.21885814]]], - [[[-6.69345313, -6.73506869, -7.9082287, -12.43972804], - [-2.21048835, -5.05651724, -7.72691754, -11.18333726], - [2.66904547, -4.81530785, -9.54984823, -12.89835729], - [8.55752862, -7.70089375, -12.37978952, -10.22208691]], - [[-3.17485999, -3.54021424, -3.54593593, -4.29515483], - [-3.68981249, -2.85516457, -2.76603925, -3.31604629], - [-1.16624451, -2.17242275, -3.57186768, -5.25444633], - [1.41851647, -2.44637201, -4.63693023, -6.09680756]], - [[-3.2219496, -1.90321215, -1.16750878, -1.08832287], - [-2.0239913, -1.38273223, -1.39926438, -1.92743159], - [-1.31353175, -1.15761322, -1.72857968, -2.81015813], - [-0.96137414, -0.94030556, -1.52657711, -2.56457651]]], - [[[-5.10794084, -5.32937859, -5.93090309, -8.05663994], - [-5.25295525, -6.02259284, -7.06582462, -9.0763472], - [0.32747247, -4.38931301, -7.24210551, -8.856658], - [11.82591067, -3.51482111, -8.18935835, -3.90270871]], - [[-2.9420404, -1.93269048, -1.78193608, -2.21710641], - [-2.96678921, -2.48380116, -2.64978243, -3.39496054], - [-1.42507824, -2.23090734, -3.01660858, -3.95003961], - [0.38000295, -2.10863221, -3.40584443, -4.06614801]], - [[-1.84525414, -0.73542408, -0.62568812, -1.18458192], - [-0.90497548, -1.10518325, -1.44073904, -1.95278103], - [-0.97196521, -1.22914653, -1.48019684, -1.79349709], - [-1.29544691, -0.9808466, -1.24778616, - -1.95945874]]]]) * units('m/s') - v_i_truth = np.array([[[[1.03108918e+01, 5.87304544e+00, -3.23865690e+00, - -1.88225987e+01], - [9.87187503e+00, 5.33610060e+00, 4.80874417e+00, - 3.92484555e-02], - [6.37856912e+00, 6.46296166e+00, 8.14267044e+00, - 4.37232518e+00], - [-1.30385124e+00, 1.01032585e+01, 4.20243238e+00, - -1.97934081e+01]], - [[1.10360108e+00, 2.30280536e+00, -1.82078930e+00, - -3.54284012e+00], - [2.43663102e+00, 1.35818636e+00, 4.92919838e-01, - -9.85544117e-03], - [2.33985677e+00, 1.03370035e+00, 3.28069921e+00, - 4.50046765e-01], - [2.93689077e-01, 1.43848430e+00, 6.69758269e+00, - -4.27897434e+00]], - [[4.77869846e-01, 1.14482717e+00, -1.82404796e+00, - -1.95731131e+00], - [5.19464097e-01, 4.52949199e-01, -3.26412809e-01, - 6.88744088e-02], - [2.51097720e-01, 1.43448773e-01, 1.08982754e+00, - -9.69963394e-02], - [-3.37860948e-01, 2.48187099e-01, 2.41935519e+00, - -2.84847302e+00]]], - [[[9.00342804e+00, 6.74193832e+00, 5.48141003e-01, - -1.25005172e+01], - [9.56628265e+00, 4.57654669e+00, 3.34479904e+00, - -7.13103555e+00], - [5.46655351e+00, 2.14241047e+00, 7.51934330e+00, - 2.43229680e+00], - [-5.48082957e+00, -6.46852260e-01, 1.34334674e+01, - 1.61485491e+01]], - [[2.49375451e+00, 3.34815514e+00, -7.09673457e-01, - -3.42185701e+00], - [2.69963182e+00, 1.64621317e+00, 2.91799176e-01, - -1.12584231e+00], - [1.83462164e+00, 1.71608154e-01, 1.87927013e+00, - 7.54482898e-01], - [-4.86175507e-01, -1.06374611e+00, 4.20283383e+00, - 1.54789418e+00]], - [[1.05175282e+00, 2.36715709e-01, -4.35406547e-01, - -9.39935118e-01], - [5.26821709e-01, 1.34167595e-01, 6.74485663e-02, - 1.18351992e-01], - [9.51152970e-02, 3.63519903e-02, 2.14587938e-01, - 6.10557463e-01], - [-2.42904366e-01, -5.80309556e-02, -3.63185957e-02, - 2.28010678e-01]]], - [[[5.18112516e+00, 8.23347995e+00, 2.85922078e+00, - -5.58457816e+00], - [8.85157651e+00, 4.70839103e+00, 2.51314815e+00, - -5.64246393e+00], - [7.54770787e+00, 8.21372199e-02, 4.70293099e+00, - 3.47174970e+00], - [-1.92174464e+00, -5.91657547e+00, 1.00629730e+01, - 2.62854305e+01]], - [[2.20347520e+00, 3.00714687e+00, 1.59377661e+00, - -6.41826692e-01], - [2.15604582e+00, 1.86128202e+00, 1.28260457e+00, - -1.03918888e+00], - [1.50501488e+00, 5.74547239e-01, 1.52092784e+00, - -3.94591487e-01], - [2.83614456e-02, -8.95222937e-01, 2.49176874e+00, - 1.81097696e+00]], - [[6.98668139e-01, 2.56635250e-01, 1.74332893e+00, - 3.79321436e-01], - [2.39593746e-01, 4.88748160e-01, 1.16884612e+00, - -7.54110131e-03], - [-6.40285805e-02, 5.82931602e-01, 4.67005716e-01, - 3.76288542e-02], - [-2.10896883e-01, 5.17706856e-01, -4.13562541e-01, - 6.96975860e-01]]]]) * units('m/s') + u_i_truth = ( + np.array( + [ + [ + [ + [-4.74579332, -6.36486064, -7.20354171, -11.08307751], + [-1.88515129, -4.33855679, -6.82871465, -9.38096911], + [2.308649, -6.93391208, -14.06293133, -20.60786775], + [-0.92388354, -13.76737076, -17.9039117, -23.71419254], + ], + [ + [-2.60558413, -3.48755492, -3.62050089, -4.18871134], + [-3.36965812, -2.57689219, -2.66529828, -3.34582207], + [-0.56309499, -2.3322732, -4.37379768, -6.6663065], + [1.70092943, -3.59623514, -5.94640587, -7.50380432], + ], + [ + [-1.60508844, -2.30572073, -2.39044749, -2.59511279], + [-2.18854472, -1.47967397, -1.57319604, -2.24386278], + [-1.10582176, -1.24627092, -2.02075175, -3.314856], + [-0.25911941, -1.62294229, -1.75103256, -1.21885814], + ], + ], + [ + [ + [-6.69345313, -6.73506869, -7.9082287, -12.43972804], + [-2.21048835, -5.05651724, -7.72691754, -11.18333726], + [2.66904547, -4.81530785, -9.54984823, -12.89835729], + [8.55752862, -7.70089375, -12.37978952, -10.22208691], + ], + [ + [-3.17485999, -3.54021424, -3.54593593, -4.29515483], + [-3.68981249, -2.85516457, -2.76603925, -3.31604629], + [-1.16624451, -2.17242275, -3.57186768, -5.25444633], + [1.41851647, -2.44637201, -4.63693023, -6.09680756], + ], + [ + [-3.2219496, -1.90321215, -1.16750878, -1.08832287], + [-2.0239913, -1.38273223, -1.39926438, -1.92743159], + [-1.31353175, -1.15761322, -1.72857968, -2.81015813], + [-0.96137414, -0.94030556, -1.52657711, -2.56457651], + ], + ], + [ + [ + [-5.10794084, -5.32937859, -5.93090309, -8.05663994], + [-5.25295525, -6.02259284, -7.06582462, -9.0763472], + [0.32747247, -4.38931301, -7.24210551, -8.856658], + [11.82591067, -3.51482111, -8.18935835, -3.90270871], + ], + [ + [-2.9420404, -1.93269048, -1.78193608, -2.21710641], + [-2.96678921, -2.48380116, -2.64978243, -3.39496054], + [-1.42507824, -2.23090734, -3.01660858, -3.95003961], + [0.38000295, -2.10863221, -3.40584443, -4.06614801], + ], + [ + [-1.84525414, -0.73542408, -0.62568812, -1.18458192], + [-0.90497548, -1.10518325, -1.44073904, -1.95278103], + [-0.97196521, -1.22914653, -1.48019684, -1.79349709], + [-1.29544691, -0.9808466, -1.24778616, -1.95945874], + ], + ], + ] + ) + * units("m/s") + ) + v_i_truth = ( + np.array( + [ + [ + [ + [1.03108918e01, 5.87304544e00, -3.23865690e00, -1.88225987e01], + [9.87187503e00, 5.33610060e00, 4.80874417e00, 3.92484555e-02], + [6.37856912e00, 6.46296166e00, 8.14267044e00, 4.37232518e00], + [-1.30385124e00, 1.01032585e01, 4.20243238e00, -1.97934081e01], + ], + [ + [1.10360108e00, 2.30280536e00, -1.82078930e00, -3.54284012e00], + [2.43663102e00, 1.35818636e00, 4.92919838e-01, -9.85544117e-03], + [2.33985677e00, 1.03370035e00, 3.28069921e00, 4.50046765e-01], + [2.93689077e-01, 1.43848430e00, 6.69758269e00, -4.27897434e00], + ], + [ + [4.77869846e-01, 1.14482717e00, -1.82404796e00, -1.95731131e00], + [5.19464097e-01, 4.52949199e-01, -3.26412809e-01, 6.88744088e-02], + [2.51097720e-01, 1.43448773e-01, 1.08982754e00, -9.69963394e-02], + [-3.37860948e-01, 2.48187099e-01, 2.41935519e00, -2.84847302e00], + ], + ], + [ + [ + [9.00342804e00, 6.74193832e00, 5.48141003e-01, -1.25005172e01], + [9.56628265e00, 4.57654669e00, 3.34479904e00, -7.13103555e00], + [5.46655351e00, 2.14241047e00, 7.51934330e00, 2.43229680e00], + [-5.48082957e00, -6.46852260e-01, 1.34334674e01, 1.61485491e01], + ], + [ + [2.49375451e00, 3.34815514e00, -7.09673457e-01, -3.42185701e00], + [2.69963182e00, 1.64621317e00, 2.91799176e-01, -1.12584231e00], + [1.83462164e00, 1.71608154e-01, 1.87927013e00, 7.54482898e-01], + [-4.86175507e-01, -1.06374611e00, 4.20283383e00, 1.54789418e00], + ], + [ + [1.05175282e00, 2.36715709e-01, -4.35406547e-01, -9.39935118e-01], + [5.26821709e-01, 1.34167595e-01, 6.74485663e-02, 1.18351992e-01], + [9.51152970e-02, 3.63519903e-02, 2.14587938e-01, 6.10557463e-01], + [-2.42904366e-01, -5.80309556e-02, -3.63185957e-02, 2.28010678e-01], + ], + ], + [ + [ + [5.18112516e00, 8.23347995e00, 2.85922078e00, -5.58457816e00], + [8.85157651e00, 4.70839103e00, 2.51314815e00, -5.64246393e00], + [7.54770787e00, 8.21372199e-02, 4.70293099e00, 3.47174970e00], + [-1.92174464e00, -5.91657547e00, 1.00629730e01, 2.62854305e01], + ], + [ + [2.20347520e00, 3.00714687e00, 1.59377661e00, -6.41826692e-01], + [2.15604582e00, 1.86128202e00, 1.28260457e00, -1.03918888e00], + [1.50501488e00, 5.74547239e-01, 1.52092784e00, -3.94591487e-01], + [2.83614456e-02, -8.95222937e-01, 2.49176874e00, 1.81097696e00], + ], + [ + [6.98668139e-01, 2.56635250e-01, 1.74332893e00, 3.79321436e-01], + [2.39593746e-01, 4.88748160e-01, 1.16884612e00, -7.54110131e-03], + [-6.40285805e-02, 5.82931602e-01, 4.67005716e-01, 3.76288542e-02], + [-2.10896883e-01, 5.17706856e-01, -4.13562541e-01, 6.96975860e-01], + ], + ], + ] + ) + * units("m/s") + ) assert_array_almost_equal(u_i.data, u_i_truth, 6) assert_array_almost_equal(v_i.data, v_i_truth, 6) @@ -1582,87 +2664,139 @@ def test_q_vector_4d(data_4d): """Test q_vector on a 4D (time, pressure, y, x) grid.""" u_g, v_g = geostrophic_wind(data_4d.height) q1, q2 = q_vector(u_g, v_g, data_4d.temperature, data_4d.pressure) - q1_truth = np.array([[[[-8.98245364e-13, 2.03803219e-13, 2.88874668e-12, 2.18043424e-12], - [4.37446820e-13, 1.21145200e-13, 1.51859353e-12, 3.82803347e-12], - [-1.20538030e-12, 2.27477298e-12, 3.47570178e-12, 3.03123012e-12], - [-1.51597275e-12, 8.02915408e-12, 7.71292472e-12, -2.22078527e-12]], - [[5.72960497e-13, 1.04264321e-12, -1.75695523e-13, 1.20745997e-12], - [2.94807953e-13, 5.80261767e-13, 6.23668595e-13, 7.31474131e-13], - [-4.04218965e-14, 3.24794013e-13, 1.39539675e-12, 2.82242029e-12], - [3.27509076e-13, 5.61307677e-13, 1.13454829e-12, 4.63551274e-12]], - [[2.23877015e-13, 5.77177907e-13, 1.62133659e-12, 5.43858376e-13], - [2.65333917e-13, 2.41006445e-13, 3.72510595e-13, 7.35822030e-13], - [6.56644633e-14, 1.99773842e-13, 5.20573457e-13, 1.69706608e-12], - [4.15915138e-14, 1.19910880e-13, 1.03632944e-12, 1.99561829e-12]]], - [[[-2.68870846e-13, 1.35977736e-12, 4.17548337e-12, 1.50465522e-12], - [4.62457018e-14, 1.25888111e-13, 2.15928418e-12, 4.70656495e-12], - [-1.25393137e-12, 9.54737370e-13, 1.48443002e-12, 2.12375621e-12], - [-2.93284658e-12, 6.06555344e-12, 4.21151397e-12, -2.12250513e-12]], - [[4.23461674e-13, 1.39393686e-13, 5.89509120e-13, 2.55041326e-12], - [5.73125714e-13, 5.60965341e-13, 7.65040451e-13, 9.49571939e-13], - [2.17153819e-14, 3.97023968e-13, 1.09194718e-12, 1.90731542e-12], - [1.45101233e-13, 1.79588608e-13, 1.03018848e-12, 3.62186462e-12]], - [[5.32674437e-13, 5.13465061e-13, 1.15582657e-12, 1.04827520e-12], - [2.77261345e-13, 2.33645555e-13, 4.59592371e-13, 5.34293340e-13], - [1.47376125e-13, 1.95746242e-13, 3.45854003e-13, 7.47741411e-13], - [-2.14078421e-14, 1.75226662e-13, 4.85424103e-13, 1.10808035e-12]]], - [[[6.41348753e-13, 1.88256910e-12, 5.21213092e-12, 2.07707653e-12], - [1.30753737e-12, 4.77125469e-13, 2.15204760e-12, 3.07374453e-12], - [-2.30546806e-13, 2.49929428e-13, 8.82215204e-14, 2.45990265e-12], - [-7.25812141e-12, 8.47072439e-13, -2.06762495e-12, - -4.40132129e-12]], - [[6.03705941e-13, -6.71320661e-13, 9.10543636e-13, 5.82480651e-13], - [9.54081741e-13, 6.11781160e-13, 6.95995265e-13, 8.67169047e-13], - [7.86580678e-14, 5.27405484e-13, 7.45800341e-13, 1.33965768e-12], - [2.22480631e-13, -1.98920384e-13, 8.56608245e-13, 1.59793218e-12]], - [[4.47195537e-13, 2.18235390e-13, 3.30926531e-13, -4.06675908e-14], - [1.70517246e-13, 2.18234962e-13, 3.78622612e-13, 5.03962144e-13], - [2.59462161e-13, 2.65626826e-13, 2.04642555e-13, 6.02812047e-13], - [1.69339642e-13, 2.91716502e-13, -1.20043003e-14, - 4.43770388e-13]]]]) * units('m^2 kg^-1 s^-1') - q2_truth = np.array([[[[3.33980776e-12, -1.32969763e-13, 1.01454470e-12, 6.02652581e-12], - [2.52898242e-13, -1.71069245e-13, -8.24708561e-13, 1.66384429e-13], - [-3.50646511e-12, -1.68929195e-12, 7.76215111e-13, 1.54486058e-12], - [-1.75492099e-12, -3.86524071e-12, -1.89368596e-12, - -5.14689517e-12]], - [[-2.09848775e-13, -6.25683634e-13, -1.40009292e-13, 1.08972893e-12], - [-2.58259284e-13, -2.67211578e-13, -6.41928957e-14, 5.90625597e-13], - [-2.73346325e-13, -2.28248227e-13, -4.76577835e-13, - -8.48559875e-13], - [1.21003124e-12, -5.10541546e-13, 6.35947149e-14, 2.44893915e-12]], - [[-6.72309334e-14, -3.56791270e-13, -4.13553842e-14, 3.81212108e-13], - [-3.55860413e-13, -1.22880634e-13, -3.19443665e-14, - -4.71232601e-14], - [-2.82289531e-13, -1.20965929e-13, 1.14160715e-13, -6.85113982e-14], - [5.17465531e-14, -4.61129211e-13, 5.33793701e-13, 1.28285338e-12]]], - [[[1.71894904e-12, -1.35675428e-12, 1.48328005e-13, 3.22454170e-12], - [-2.12666583e-13, -1.17551681e-13, -6.93968059e-13, 1.76659826e-12], - [-2.67906914e-12, -3.78250861e-13, -9.88730956e-13, 2.88200442e-12], - [1.48225123e-12, 2.15004833e-13, -4.84554577e-12, 2.77242999e-12]], - [[-3.09626209e-13, -2.52138997e-13, 4.58311589e-14, 2.03206766e-12], - [-3.95662347e-13, -2.99828956e-13, 1.08715446e-14, 1.06096030e-12], - [-2.46502471e-13, -2.43524217e-13, -3.81250581e-13, - -1.70270366e-13], - [8.12479206e-13, -1.38629628e-13, -8.05591138e-13, - -7.80286006e-13]], - [[-2.19626566e-13, -1.52852503e-13, 4.07706963e-13, 1.52323163e-12], - [-2.56235985e-13, -1.20817691e-13, 6.51260820e-15, 3.49591511e-13], - [-2.44063890e-13, -1.21871642e-13, -9.09798480e-14, - -1.59903476e-13], - [-2.47929201e-13, -1.77269110e-13, -1.12991330e-13, - -6.06795348e-13]]], - [[[-6.48288201e-13, -1.96951432e-12, -5.53508048e-13, 1.94507133e-12], - [-2.00769011e-12, -3.72469047e-13, -4.59116219e-13, 1.11322705e-13], - [-3.83507643e-12, 1.18054543e-13, -4.24001455e-13, -5.88688871e-13], - [-1.84528711e-12, 1.54974343e-12, -7.36123184e-13, 1.06256777e-13]], - [[-4.58487019e-13, -1.89124158e-13, 2.58416604e-13, 8.14652306e-13], - [-6.09664269e-13, -3.51509413e-13, 2.39576397e-13, 5.80539044e-13], - [-1.68850738e-13, -3.49553817e-13, -2.26470205e-13, 7.79989044e-13], - [2.23081718e-13, 1.20195366e-13, -1.01508013e-12, -2.15527487e-13]], - [[-1.68054338e-13, -5.06878852e-14, 2.77697698e-13, 8.37521961e-13], - [-1.39462599e-13, -1.36628363e-13, 3.13920124e-14, 4.55413406e-13], - [-1.06658890e-13, -2.19817426e-13, -8.35968065e-14, 1.88190788e-13], - [-2.27182863e-13, -2.74607819e-13, -1.10587309e-13, - -3.88915866e-13]]]]) * units('m^2 kg^-1 s^-1') + q1_truth = ( + np.array( + [ + [ + [ + [-8.98245364e-13, 2.03803219e-13, 2.88874668e-12, 2.18043424e-12], + [4.37446820e-13, 1.21145200e-13, 1.51859353e-12, 3.82803347e-12], + [-1.20538030e-12, 2.27477298e-12, 3.47570178e-12, 3.03123012e-12], + [-1.51597275e-12, 8.02915408e-12, 7.71292472e-12, -2.22078527e-12], + ], + [ + [5.72960497e-13, 1.04264321e-12, -1.75695523e-13, 1.20745997e-12], + [2.94807953e-13, 5.80261767e-13, 6.23668595e-13, 7.31474131e-13], + [-4.04218965e-14, 3.24794013e-13, 1.39539675e-12, 2.82242029e-12], + [3.27509076e-13, 5.61307677e-13, 1.13454829e-12, 4.63551274e-12], + ], + [ + [2.23877015e-13, 5.77177907e-13, 1.62133659e-12, 5.43858376e-13], + [2.65333917e-13, 2.41006445e-13, 3.72510595e-13, 7.35822030e-13], + [6.56644633e-14, 1.99773842e-13, 5.20573457e-13, 1.69706608e-12], + [4.15915138e-14, 1.19910880e-13, 1.03632944e-12, 1.99561829e-12], + ], + ], + [ + [ + [-2.68870846e-13, 1.35977736e-12, 4.17548337e-12, 1.50465522e-12], + [4.62457018e-14, 1.25888111e-13, 2.15928418e-12, 4.70656495e-12], + [-1.25393137e-12, 9.54737370e-13, 1.48443002e-12, 2.12375621e-12], + [-2.93284658e-12, 6.06555344e-12, 4.21151397e-12, -2.12250513e-12], + ], + [ + [4.23461674e-13, 1.39393686e-13, 5.89509120e-13, 2.55041326e-12], + [5.73125714e-13, 5.60965341e-13, 7.65040451e-13, 9.49571939e-13], + [2.17153819e-14, 3.97023968e-13, 1.09194718e-12, 1.90731542e-12], + [1.45101233e-13, 1.79588608e-13, 1.03018848e-12, 3.62186462e-12], + ], + [ + [5.32674437e-13, 5.13465061e-13, 1.15582657e-12, 1.04827520e-12], + [2.77261345e-13, 2.33645555e-13, 4.59592371e-13, 5.34293340e-13], + [1.47376125e-13, 1.95746242e-13, 3.45854003e-13, 7.47741411e-13], + [-2.14078421e-14, 1.75226662e-13, 4.85424103e-13, 1.10808035e-12], + ], + ], + [ + [ + [6.41348753e-13, 1.88256910e-12, 5.21213092e-12, 2.07707653e-12], + [1.30753737e-12, 4.77125469e-13, 2.15204760e-12, 3.07374453e-12], + [-2.30546806e-13, 2.49929428e-13, 8.82215204e-14, 2.45990265e-12], + [-7.25812141e-12, 8.47072439e-13, -2.06762495e-12, -4.40132129e-12], + ], + [ + [6.03705941e-13, -6.71320661e-13, 9.10543636e-13, 5.82480651e-13], + [9.54081741e-13, 6.11781160e-13, 6.95995265e-13, 8.67169047e-13], + [7.86580678e-14, 5.27405484e-13, 7.45800341e-13, 1.33965768e-12], + [2.22480631e-13, -1.98920384e-13, 8.56608245e-13, 1.59793218e-12], + ], + [ + [4.47195537e-13, 2.18235390e-13, 3.30926531e-13, -4.06675908e-14], + [1.70517246e-13, 2.18234962e-13, 3.78622612e-13, 5.03962144e-13], + [2.59462161e-13, 2.65626826e-13, 2.04642555e-13, 6.02812047e-13], + [1.69339642e-13, 2.91716502e-13, -1.20043003e-14, 4.43770388e-13], + ], + ], + ] + ) + * units("m^2 kg^-1 s^-1") + ) + q2_truth = ( + np.array( + [ + [ + [ + [3.33980776e-12, -1.32969763e-13, 1.01454470e-12, 6.02652581e-12], + [2.52898242e-13, -1.71069245e-13, -8.24708561e-13, 1.66384429e-13], + [-3.50646511e-12, -1.68929195e-12, 7.76215111e-13, 1.54486058e-12], + [-1.75492099e-12, -3.86524071e-12, -1.89368596e-12, -5.14689517e-12], + ], + [ + [-2.09848775e-13, -6.25683634e-13, -1.40009292e-13, 1.08972893e-12], + [-2.58259284e-13, -2.67211578e-13, -6.41928957e-14, 5.90625597e-13], + [-2.73346325e-13, -2.28248227e-13, -4.76577835e-13, -8.48559875e-13], + [1.21003124e-12, -5.10541546e-13, 6.35947149e-14, 2.44893915e-12], + ], + [ + [-6.72309334e-14, -3.56791270e-13, -4.13553842e-14, 3.81212108e-13], + [-3.55860413e-13, -1.22880634e-13, -3.19443665e-14, -4.71232601e-14], + [-2.82289531e-13, -1.20965929e-13, 1.14160715e-13, -6.85113982e-14], + [5.17465531e-14, -4.61129211e-13, 5.33793701e-13, 1.28285338e-12], + ], + ], + [ + [ + [1.71894904e-12, -1.35675428e-12, 1.48328005e-13, 3.22454170e-12], + [-2.12666583e-13, -1.17551681e-13, -6.93968059e-13, 1.76659826e-12], + [-2.67906914e-12, -3.78250861e-13, -9.88730956e-13, 2.88200442e-12], + [1.48225123e-12, 2.15004833e-13, -4.84554577e-12, 2.77242999e-12], + ], + [ + [-3.09626209e-13, -2.52138997e-13, 4.58311589e-14, 2.03206766e-12], + [-3.95662347e-13, -2.99828956e-13, 1.08715446e-14, 1.06096030e-12], + [-2.46502471e-13, -2.43524217e-13, -3.81250581e-13, -1.70270366e-13], + [8.12479206e-13, -1.38629628e-13, -8.05591138e-13, -7.80286006e-13], + ], + [ + [-2.19626566e-13, -1.52852503e-13, 4.07706963e-13, 1.52323163e-12], + [-2.56235985e-13, -1.20817691e-13, 6.51260820e-15, 3.49591511e-13], + [-2.44063890e-13, -1.21871642e-13, -9.09798480e-14, -1.59903476e-13], + [-2.47929201e-13, -1.77269110e-13, -1.12991330e-13, -6.06795348e-13], + ], + ], + [ + [ + [-6.48288201e-13, -1.96951432e-12, -5.53508048e-13, 1.94507133e-12], + [-2.00769011e-12, -3.72469047e-13, -4.59116219e-13, 1.11322705e-13], + [-3.83507643e-12, 1.18054543e-13, -4.24001455e-13, -5.88688871e-13], + [-1.84528711e-12, 1.54974343e-12, -7.36123184e-13, 1.06256777e-13], + ], + [ + [-4.58487019e-13, -1.89124158e-13, 2.58416604e-13, 8.14652306e-13], + [-6.09664269e-13, -3.51509413e-13, 2.39576397e-13, 5.80539044e-13], + [-1.68850738e-13, -3.49553817e-13, -2.26470205e-13, 7.79989044e-13], + [2.23081718e-13, 1.20195366e-13, -1.01508013e-12, -2.15527487e-13], + ], + [ + [-1.68054338e-13, -5.06878852e-14, 2.77697698e-13, 8.37521961e-13], + [-1.39462599e-13, -1.36628363e-13, 3.13920124e-14, 4.55413406e-13], + [-1.06658890e-13, -2.19817426e-13, -8.35968065e-14, 1.88190788e-13], + [-2.27182863e-13, -2.74607819e-13, -1.10587309e-13, -3.88915866e-13], + ], + ], + ] + ) + * units("m^2 kg^-1 s^-1") + ) assert_array_almost_equal(q1.data, q1_truth, 18) assert_array_almost_equal(q2.data, q2_truth, 18) diff --git a/tests/calc/test_thermo.py b/tests/calc/test_thermo.py index 0d86574a20e..8ba10bcee55 100644 --- a/tests/calc/test_thermo.py +++ b/tests/calc/test_thermo.py @@ -7,34 +7,62 @@ import pytest import xarray as xr -from metpy.calc import (brunt_vaisala_frequency, brunt_vaisala_frequency_squared, - brunt_vaisala_period, cape_cin, density, dewpoint, - dewpoint_from_relative_humidity, dewpoint_from_specific_humidity, - dry_lapse, dry_static_energy, el, equivalent_potential_temperature, - exner_function, gradient_richardson_number, isentropic_interpolation, - isentropic_interpolation_as_dataset, lcl, lfc, lifted_index, - mixed_layer, mixed_layer_cape_cin, mixed_parcel, mixing_ratio, - mixing_ratio_from_relative_humidity, - mixing_ratio_from_specific_humidity, moist_lapse, - moist_static_energy, most_unstable_cape_cin, most_unstable_parcel, - parcel_profile, parcel_profile_with_lcl, - parcel_profile_with_lcl_as_dataset, potential_temperature, - psychrometric_vapor_pressure_wet, - relative_humidity_from_dewpoint, - relative_humidity_from_mixing_ratio, - relative_humidity_from_specific_humidity, - relative_humidity_wet_psychrometric, - saturation_equivalent_potential_temperature, - saturation_mixing_ratio, - saturation_vapor_pressure, - specific_humidity_from_dewpoint, - specific_humidity_from_mixing_ratio, static_stability, - surface_based_cape_cin, temperature_from_potential_temperature, - thickness_hydrostatic, - thickness_hydrostatic_from_relative_humidity, vapor_pressure, - vertical_velocity, vertical_velocity_pressure, - virtual_potential_temperature, virtual_temperature, - wet_bulb_temperature) +from metpy.calc import ( + brunt_vaisala_frequency, + brunt_vaisala_frequency_squared, + brunt_vaisala_period, + cape_cin, + density, + dewpoint, + dewpoint_from_relative_humidity, + dewpoint_from_specific_humidity, + dry_lapse, + dry_static_energy, + el, + equivalent_potential_temperature, + exner_function, + gradient_richardson_number, + isentropic_interpolation, + isentropic_interpolation_as_dataset, + lcl, + lfc, + lifted_index, + mixed_layer, + mixed_layer_cape_cin, + mixed_parcel, + mixing_ratio, + mixing_ratio_from_relative_humidity, + mixing_ratio_from_specific_humidity, + moist_lapse, + moist_static_energy, + most_unstable_cape_cin, + most_unstable_parcel, + parcel_profile, + parcel_profile_with_lcl, + parcel_profile_with_lcl_as_dataset, + potential_temperature, + psychrometric_vapor_pressure_wet, + relative_humidity_from_dewpoint, + relative_humidity_from_mixing_ratio, + relative_humidity_from_specific_humidity, + relative_humidity_wet_psychrometric, + saturation_equivalent_potential_temperature, + saturation_mixing_ratio, + saturation_vapor_pressure, + specific_humidity_from_dewpoint, + specific_humidity_from_mixing_ratio, + static_stability, + surface_based_cape_cin, + temperature_from_potential_temperature, + thickness_hydrostatic, + thickness_hydrostatic_from_relative_humidity, + vapor_pressure, + vertical_velocity, + vertical_velocity_pressure, + virtual_potential_temperature, + virtual_temperature, + wet_bulb_temperature, +) from metpy.calc.thermo import _find_append_zero_crossings from metpy.testing import assert_almost_equal, assert_array_almost_equal, assert_nan from metpy.units import masked_array, units @@ -42,34 +70,40 @@ def test_relative_humidity_from_dewpoint(): """Test Relative Humidity calculation.""" - assert_almost_equal(relative_humidity_from_dewpoint(25. * units.degC, 15. * units.degC), - 53.80 * units.percent, 2) + assert_almost_equal( + relative_humidity_from_dewpoint(25.0 * units.degC, 15.0 * units.degC), + 53.80 * units.percent, + 2, + ) def test_relative_humidity_from_dewpoint_with_f(): """Test Relative Humidity accepts temperature in Fahrenheit.""" - assert_almost_equal(relative_humidity_from_dewpoint(70. * units.degF, 55. * units.degF), - 58.935 * units.percent, 3) + assert_almost_equal( + relative_humidity_from_dewpoint(70.0 * units.degF, 55.0 * units.degF), + 58.935 * units.percent, + 3, + ) def test_relative_humidity_from_dewpoint_xarray(): """Test Relative Humidity with xarray data arrays (quantified and unquantified).""" - temp = xr.DataArray(25., attrs={'units': 'degC'}) - dewp = xr.DataArray([15.] * units.degC) + temp = xr.DataArray(25.0, attrs={"units": "degC"}) + dewp = xr.DataArray([15.0] * units.degC) assert_almost_equal(relative_humidity_from_dewpoint(temp, dewp), 53.80 * units.percent, 2) def test_exner_function(): """Test Exner function calculation.""" - pres = np.array([900., 500., 300., 100.]) * units.mbar + pres = np.array([900.0, 500.0, 300.0, 100.0]) * units.mbar truth = np.array([0.9703542, 0.8203834, 0.7090065, 0.518048]) * units.dimensionless assert_array_almost_equal(exner_function(pres), truth, 6) def test_potential_temperature(): """Test potential_temperature calculation.""" - temp = np.array([278., 283., 291., 298.]) * units.kelvin - pres = np.array([900., 500., 300., 100.]) * units.mbar + temp = np.array([278.0, 283.0, 291.0, 298.0]) * units.kelvin + pres = np.array([900.0, 500.0, 300.0, 100.0]) * units.mbar real_th = np.array([286.493, 344.961, 410.4335, 575.236]) * units.kelvin assert_array_almost_equal(potential_temperature(pres, temp), real_th, 3) @@ -79,86 +113,121 @@ def test_temperature_from_potential_temperature(): theta = np.array([286.12859679, 288.22362587, 290.31865495, 292.41368403]) * units.kelvin pres = np.array([850] * 4) * units.mbar real_t = np.array([273.15, 275.15, 277.15, 279.15]) * units.kelvin - assert_array_almost_equal(temperature_from_potential_temperature(pres, theta), - real_t, 2) + assert_array_almost_equal(temperature_from_potential_temperature(pres, theta), real_t, 2) def test_scalar(): """Test potential_temperature accepts scalar values.""" - assert_almost_equal(potential_temperature(1000. * units.mbar, 293. * units.kelvin), - 293. * units.kelvin, 4) - assert_almost_equal(potential_temperature(800. * units.mbar, 293. * units.kelvin), - 312.2828 * units.kelvin, 4) + assert_almost_equal( + potential_temperature(1000.0 * units.mbar, 293.0 * units.kelvin), + 293.0 * units.kelvin, + 4, + ) + assert_almost_equal( + potential_temperature(800.0 * units.mbar, 293.0 * units.kelvin), + 312.2828 * units.kelvin, + 4, + ) def test_fahrenheit(): """Test that potential_temperature handles temperature values in Fahrenheit.""" - assert_almost_equal(potential_temperature(800. * units.mbar, 68. * units.degF), - (312.444 * units.kelvin).to(units.degF), 2) + assert_almost_equal( + potential_temperature(800.0 * units.mbar, 68.0 * units.degF), + (312.444 * units.kelvin).to(units.degF), + 2, + ) def test_pot_temp_inhg(): """Test that potential_temperature can handle pressure not in mb (issue #165).""" - assert_almost_equal(potential_temperature(29.92 * units.inHg, 29 * units.degC), - 301.019735 * units.kelvin, 4) + assert_almost_equal( + potential_temperature(29.92 * units.inHg, 29 * units.degC), + 301.019735 * units.kelvin, + 4, + ) def test_dry_lapse(): """Test dry_lapse calculation.""" levels = np.array([1000, 900, 864.89]) * units.mbar temps = dry_lapse(levels, 303.15 * units.kelvin) - assert_array_almost_equal(temps, - np.array([303.15, 294.16, 290.83]) * units.kelvin, 2) + assert_array_almost_equal(temps, np.array([303.15, 294.16, 290.83]) * units.kelvin, 2) def test_dry_lapse_2_levels(): """Test dry_lapse calculation when given only two levels.""" - temps = dry_lapse(np.array([1000., 500.]) * units.mbar, 293. * units.kelvin) - assert_array_almost_equal(temps, [293., 240.3723] * units.kelvin, 4) + temps = dry_lapse(np.array([1000.0, 500.0]) * units.mbar, 293.0 * units.kelvin) + assert_array_almost_equal(temps, [293.0, 240.3723] * units.kelvin, 4) def test_moist_lapse(): """Test moist_lapse calculation.""" - temp = moist_lapse(np.array([1000., 800., 600., 500., 400.]) * units.mbar, - 293. * units.kelvin) + temp = moist_lapse( + np.array([1000.0, 800.0, 600.0, 500.0, 400.0]) * units.mbar, 293.0 * units.kelvin + ) true_temp = np.array([293, 284.64, 272.81, 264.42, 252.91]) * units.kelvin assert_array_almost_equal(temp, true_temp, 2) def test_moist_lapse_degc(): """Test moist_lapse with Celsius temperatures.""" - temp = moist_lapse(np.array([1000., 800., 600., 500., 400.]) * units.mbar, - 19.85 * units.degC) + temp = moist_lapse( + np.array([1000.0, 800.0, 600.0, 500.0, 400.0]) * units.mbar, 19.85 * units.degC + ) true_temp = np.array([293, 284.64, 272.81, 264.42, 252.91]) * units.kelvin assert_array_almost_equal(temp, true_temp, 2) def test_moist_lapse_ref_pres(): """Test moist_lapse with a reference pressure.""" - temp = moist_lapse(np.array([1050., 800., 600., 500., 400.]) * units.mbar, - 19.85 * units.degC, 1000. * units.mbar) + temp = moist_lapse( + np.array([1050.0, 800.0, 600.0, 500.0, 400.0]) * units.mbar, + 19.85 * units.degC, + 1000.0 * units.mbar, + ) true_temp = np.array([294.76, 284.64, 272.81, 264.42, 252.91]) * units.kelvin assert_array_almost_equal(temp, true_temp, 2) def test_parcel_profile(): """Test parcel profile calculation.""" - levels = np.array([1000., 900., 800., 700., 600., 500., 400.]) * units.mbar - true_prof = np.array([303.15, 294.16, 288.026, 283.073, 277.058, 269.402, - 258.966]) * units.kelvin + levels = np.array([1000.0, 900.0, 800.0, 700.0, 600.0, 500.0, 400.0]) * units.mbar + true_prof = ( + np.array([303.15, 294.16, 288.026, 283.073, 277.058, 269.402, 258.966]) * units.kelvin + ) - prof = parcel_profile(levels, 30. * units.degC, 20. * units.degC) + prof = parcel_profile(levels, 30.0 * units.degC, 20.0 * units.degC) assert_array_almost_equal(prof, true_prof, 2) def test_parcel_profile_lcl(): """Test parcel profile with lcl calculation.""" - p = np.array([1004., 1000., 943., 928., 925., 850., 839., 749., 700., 699.]) * units.hPa - t = np.array([24.2, 24., 20.2, 21.6, 21.4, 20.4, 20.2, 14.4, 13.2, 13.]) * units.degC + p = ( + np.array([1004.0, 1000.0, 943.0, 928.0, 925.0, 850.0, 839.0, 749.0, 700.0, 699.0]) + * units.hPa + ) + t = np.array([24.2, 24.0, 20.2, 21.6, 21.4, 20.4, 20.2, 14.4, 13.2, 13.0]) * units.degC td = np.array([21.9, 22.1, 19.2, 20.5, 20.4, 18.4, 17.4, 8.4, -2.8, -3.0]) * units.degC - true_prof = np.array([297.35, 297.01, 294.5, 293.48, 292.92, 292.81, 289.79, 289.32, - 285.15, 282.59, 282.53]) * units.kelvin + true_prof = ( + np.array( + [ + 297.35, + 297.01, + 294.5, + 293.48, + 292.92, + 292.81, + 289.79, + 289.32, + 285.15, + 282.59, + 282.53, + ] + ) + * units.kelvin + ) true_p = np.insert(p.m, 2, 970.699) * units.mbar true_t = np.insert(t.m, 2, 22.047) * units.degC true_td = np.insert(td.m, 2, 20.609) * units.degC @@ -172,54 +241,67 @@ def test_parcel_profile_lcl(): def test_parcel_profile_with_lcl_as_dataset(): """Test parcel profile with lcl calculation with xarray.""" - p = np.array([1004., 1000., 943., 928., 925., 850., 839., 749., 700., 699.]) * units.hPa - t = np.array([24.2, 24., 20.2, 21.6, 21.4, 20.4, 20.2, 14.4, 13.2, 13.]) * units.degC + p = ( + np.array([1004.0, 1000.0, 943.0, 928.0, 925.0, 850.0, 839.0, 749.0, 700.0, 699.0]) + * units.hPa + ) + t = np.array([24.2, 24.0, 20.2, 21.6, 21.4, 20.4, 20.2, 14.4, 13.2, 13.0]) * units.degC td = np.array([21.9, 22.1, 19.2, 20.5, 20.4, 18.4, 17.4, 8.4, -2.8, -3.0]) * units.degC result = parcel_profile_with_lcl_as_dataset(p, t, td) expected = xr.Dataset( { - 'ambient_temperature': ( - ('isobaric',), + "ambient_temperature": ( + ("isobaric",), np.insert(t.m, 2, 22.047) * units.degC, - {'standard_name': 'air_temperature'} + {"standard_name": "air_temperature"}, ), - 'ambient_dew_point': ( - ('isobaric',), + "ambient_dew_point": ( + ("isobaric",), np.insert(td.m, 2, 20.609) * units.degC, - {'standard_name': 'dew_point_temperature'} + {"standard_name": "dew_point_temperature"}, ), - 'parcel_temperature': ( - ('isobaric',), + "parcel_temperature": ( + ("isobaric",), [ - 297.35, 297.01, 294.5, 293.48, 292.92, 292.81, 289.79, 289.32, 285.15, - 282.59, 282.53 - ] * units.kelvin, - {'long_name': 'air_temperature_of_lifted_parcel'} - ) + 297.35, + 297.01, + 294.5, + 293.48, + 292.92, + 292.81, + 289.79, + 289.32, + 285.15, + 282.59, + 282.53, + ] + * units.kelvin, + {"long_name": "air_temperature_of_lifted_parcel"}, + ), }, coords={ - 'isobaric': ( - 'isobaric', + "isobaric": ( + "isobaric", np.insert(p.m, 2, 970.699), - {'units': 'hectopascal', 'standard_name': 'air_pressure'} + {"units": "hectopascal", "standard_name": "air_pressure"}, ) - } + }, ) xr.testing.assert_allclose(result, expected, atol=1e-2) for field in ( - 'ambient_temperature', - 'ambient_dew_point', - 'parcel_temperature', - 'isobaric' + "ambient_temperature", + "ambient_dew_point", + "parcel_temperature", + "isobaric", ): assert result[field].attrs == expected[field].attrs def test_parcel_profile_saturated(): """Test parcel_profile works when LCL in levels (issue #232).""" - levels = np.array([1000., 700., 500.]) * units.mbar + levels = np.array([1000.0, 700.0, 500.0]) * units.mbar true_prof = np.array([296.95, 284.381, 271.123]) * units.kelvin prof = parcel_profile(levels, 23.8 * units.degC, 23.8 * units.degC) @@ -228,7 +310,7 @@ def test_parcel_profile_saturated(): def test_sat_vapor_pressure(): """Test saturation_vapor_pressure calculation.""" - temp = np.array([5., 10., 18., 25.]) * units.degC + temp = np.array([5.0, 10.0, 18.0, 25.0]) * units.degC real_es = np.array([8.72, 12.27, 20.63, 31.67]) * units.mbar assert_array_almost_equal(saturation_vapor_pressure(temp), real_es, 2) @@ -241,15 +323,15 @@ def test_sat_vapor_pressure_scalar(): def test_sat_vapor_pressure_fahrenheit(): """Test saturation_vapor_pressure handles temperature in Fahrenheit.""" - temp = np.array([50., 68.]) * units.degF + temp = np.array([50.0, 68.0]) * units.degF real_es = np.array([12.2717, 23.3695]) * units.mbar assert_array_almost_equal(saturation_vapor_pressure(temp), real_es, 4) def test_basic_dewpoint_from_relative_humidity(): """Test dewpoint_from_relative_humidity function.""" - temp = np.array([30., 25., 10., 20., 25.]) * units.degC - rh = np.array([30., 45., 55., 80., 85.]) / 100. + temp = np.array([30.0, 25.0, 10.0, 20.0, 25.0]) * units.degC + rh = np.array([30.0, 45.0, 55.0, 80.0, 85.0]) / 100.0 real_td = np.array([11, 12, 1, 16, 22]) * units.degC assert_array_almost_equal(real_td, dewpoint_from_relative_humidity(temp, rh), 0) @@ -258,13 +340,13 @@ def test_basic_dewpoint_from_relative_humidity(): def test_scalar_dewpoint_from_relative_humidity(): """Test dewpoint_from_relative_humidity with scalar values.""" td = dewpoint_from_relative_humidity(10.6 * units.degC, 0.37) - assert_almost_equal(td, 26. * units.degF, 0) + assert_almost_equal(td, 26.0 * units.degF, 0) def test_percent_dewpoint_from_relative_humidity(): """Test dewpoint_from_relative_humidity with rh in percent.""" td = dewpoint_from_relative_humidity(10.6 * units.degC, 37 * units.percent) - assert_almost_equal(td, 26. * units.degF, 0) + assert_almost_equal(td, 26.0 * units.degF, 0) def test_warning_dewpoint_from_relative_humidity(): @@ -275,7 +357,7 @@ def test_warning_dewpoint_from_relative_humidity(): def test_dewpoint(): """Test dewpoint calculation.""" - assert_almost_equal(dewpoint(6.112 * units.mbar), 0. * units.degC, 2) + assert_almost_equal(dewpoint(6.112 * units.mbar), 0.0 * units.degC, 2) def test_dewpoint_weird_units(): @@ -284,26 +366,26 @@ def test_dewpoint_weird_units(): Revealed from odd dimensionless units and ending up using numpy.ma math functions instead of numpy ones. """ - assert_almost_equal(dewpoint(15825.6 * units('g * mbar / kg')), - 13.8564 * units.degC, 4) + assert_almost_equal(dewpoint(15825.6 * units("g * mbar / kg")), 13.8564 * units.degC, 4) def test_mixing_ratio(): """Test mixing ratio calculation.""" - p = 998. * units.mbar + p = 998.0 * units.mbar e = 73.75 * units.mbar assert_almost_equal(mixing_ratio(e, p), 0.04963, 2) def test_vapor_pressure(): """Test vapor pressure calculation.""" - assert_almost_equal(vapor_pressure(998. * units.mbar, 0.04963), - 73.74925 * units.mbar, 5) + assert_almost_equal(vapor_pressure(998.0 * units.mbar, 0.04963), 73.74925 * units.mbar, 5) def test_lcl(): """Test LCL calculation.""" - lcl_pressure, lcl_temperature = lcl(1000. * units.mbar, 30. * units.degC, 20. * units.degC) + lcl_pressure, lcl_temperature = lcl( + 1000.0 * units.mbar, 30.0 * units.degC, 20.0 * units.degC + ) assert_almost_equal(lcl_pressure, 864.761 * units.mbar, 2) assert_almost_equal(lcl_temperature, 17.676 * units.degC, 2) @@ -311,8 +393,9 @@ def test_lcl(): def test_lcl_kelvin(): """Test LCL temperature is returned as Kelvin, if temperature is Kelvin.""" temperature = 273.09723 * units.kelvin - lcl_pressure, lcl_temperature = lcl(1017.16 * units.mbar, temperature, - 264.5351 * units.kelvin) + lcl_pressure, lcl_temperature = lcl( + 1017.16 * units.mbar, temperature, 264.5351 * units.kelvin + ) assert_almost_equal(lcl_pressure, 889.416 * units.mbar, 2) assert_almost_equal(lcl_temperature, 262.827 * units.kelvin, 2) assert lcl_temperature.units == temperature.units @@ -321,14 +404,14 @@ def test_lcl_kelvin(): def test_lcl_convergence(): """Test LCL calculation convergence failure.""" with pytest.raises(RuntimeError): - lcl(1000. * units.mbar, 30. * units.degC, 20. * units.degC, max_iters=2) + lcl(1000.0 * units.mbar, 30.0 * units.degC, 20.0 * units.degC, max_iters=2) def test_lfc_basic(): """Test LFC calculation.""" - levels = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.mbar - temperatures = np.array([22.2, 14.6, 12., 9.4, 7., -49.]) * units.celsius - dewpoints = np.array([19., -11.2, -10.8, -10.4, -10., -53.2]) * units.celsius + levels = np.array([959.0, 779.2, 751.3, 724.3, 700.0, 269.0]) * units.mbar + temperatures = np.array([22.2, 14.6, 12.0, 9.4, 7.0, -49.0]) * units.celsius + dewpoints = np.array([19.0, -11.2, -10.8, -10.4, -10.0, -53.2]) * units.celsius lfc_pressure, lfc_temp = lfc(levels, temperatures, dewpoints) assert_almost_equal(lfc_pressure, 727.415 * units.mbar, 2) assert_almost_equal(lfc_temp, 9.705 * units.celsius, 2) @@ -336,11 +419,9 @@ def test_lfc_basic(): def test_lfc_kelvin(): """Test that LFC temperature returns Kelvin if Kelvin is provided.""" - pressure = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.mbar - temperature = (np.array([22.2, 14.6, 12., 9.4, 7., -49.] - ) + 273.15) * units.kelvin - dewpoint = (np.array([19., -11.2, -10.8, -10.4, -10., -53.2] - ) + 273.15) * units.kelvin + pressure = np.array([959.0, 779.2, 751.3, 724.3, 700.0, 269.0]) * units.mbar + temperature = (np.array([22.2, 14.6, 12.0, 9.4, 7.0, -49.0]) + 273.15) * units.kelvin + dewpoint = (np.array([19.0, -11.2, -10.8, -10.4, -10.0, -53.2]) + 273.15) * units.kelvin lfc_pressure, lfc_temp = lfc(pressure, temperature, dewpoint) assert_almost_equal(lfc_pressure, 727.415 * units.mbar, 2) assert_almost_equal(lfc_temp, 9.705 * units.degC, 2) @@ -349,9 +430,9 @@ def test_lfc_kelvin(): def test_lfc_ml(): """Test Mixed-Layer LFC calculation.""" - levels = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.mbar - temperatures = np.array([22.2, 14.6, 12., 9.4, 7., -49.]) * units.celsius - dewpoints = np.array([19., -11.2, -10.8, -10.4, -10., -53.2]) * units.celsius + levels = np.array([959.0, 779.2, 751.3, 724.3, 700.0, 269.0]) * units.mbar + temperatures = np.array([22.2, 14.6, 12.0, 9.4, 7.0, -49.0]) * units.celsius + dewpoints = np.array([19.0, -11.2, -10.8, -10.4, -10.0, -53.2]) * units.celsius __, t_mixed, td_mixed = mixed_parcel(levels, temperatures, dewpoints) mixed_parcel_prof = parcel_profile(levels, t_mixed, td_mixed) lfc_pressure, lfc_temp = lfc(levels, temperatures, dewpoints, mixed_parcel_prof) @@ -361,38 +442,174 @@ def test_lfc_ml(): def test_lfc_ml2(): """Test a mixed-layer LFC calculation that previously crashed.""" - levels = np.array([1024.95703125, 1016.61474609, 1005.33056641, 991.08544922, 973.4163208, - 951.3381958, 924.82836914, 898.25482178, 873.46124268, 848.69830322, - 823.92553711, 788.49304199, 743.44580078, 700.50970459, 659.62017822, - 620.70861816, 583.69421387, 548.49719238, 515.03826904, 483.24401855, - 453.0418396, 424.36477661, 397.1505127, 371.33441162, 346.85922241, - 323.66995239, 301.70935059, 280.92651367, 261.27053833, 242.69168091, - 225.14237976, 208.57781982, 192.95333862, 178.22599792, 164.39630127, - 151.54336548, 139.68635559, 128.74923706, 118.6588974, 109.35111237, - 100.76405334, 92.84288025, 85.53556824, 78.79430389, 72.57549286, - 66.83885193, 61.54678726, 56.66480637, 52.16108322]) * units.mbar - temperatures = np.array([6.00750732, 5.14892578, 4.177948, 3.00268555, 1.55535889, - -0.25527954, -1.93988037, -3.57766724, -4.40600586, -4.19238281, - -3.71185303, -4.47943115, -6.81280518, -8.08685303, -8.41287231, - -10.79302979, -14.13262939, -16.85784912, -19.51675415, - -22.28689575, -24.99938965, -27.79664612, -30.90414429, - -34.49435425, -38.438797, -42.27981567, -45.99230957, - -49.75340271, -53.58230591, -57.30686951, -60.76026917, - -63.92070007, -66.72470093, -68.97846985, -70.4264679, - -71.16407776, -71.53797913, -71.64375305, -71.52735901, - -71.53523254, -71.61097717, -71.92687988, -72.68682861, - -74.129776, -76.02471924, -76.88977051, -76.26008606, - -75.90351868, -76.15809631]) * units.celsius - dewpoints = np.array([4.50012302, 3.42483997, 2.78102994, 2.24474645, 1.593485, -0.9440815, - -3.8044982, -3.55629468, -9.7376976, -10.2950449, -9.67498302, - -10.30486488, -8.70559597, -8.71669006, -12.66509628, -18.6697197, - -23.00351334, -29.46240425, -36.82178497, -41.68824768, -44.50320816, - -48.54426575, -52.50753403, -51.09564209, -48.92690659, -49.97380829, - -51.57516098, -52.62096405, -54.24332809, -57.09109879, -60.5596199, - -63.93486404, -67.07530212, -70.01263428, -72.9258728, -76.12271881, - -79.49847412, -82.2350769, -83.91127014, -84.95665741, -85.61238861, - -86.16391754, -86.7653656, -87.34436035, -87.87495422, -88.34281921, - -88.74453735, -89.04680634, -89.26436615]) * units.celsius + levels = ( + np.array( + [ + 1024.95703125, + 1016.61474609, + 1005.33056641, + 991.08544922, + 973.4163208, + 951.3381958, + 924.82836914, + 898.25482178, + 873.46124268, + 848.69830322, + 823.92553711, + 788.49304199, + 743.44580078, + 700.50970459, + 659.62017822, + 620.70861816, + 583.69421387, + 548.49719238, + 515.03826904, + 483.24401855, + 453.0418396, + 424.36477661, + 397.1505127, + 371.33441162, + 346.85922241, + 323.66995239, + 301.70935059, + 280.92651367, + 261.27053833, + 242.69168091, + 225.14237976, + 208.57781982, + 192.95333862, + 178.22599792, + 164.39630127, + 151.54336548, + 139.68635559, + 128.74923706, + 118.6588974, + 109.35111237, + 100.76405334, + 92.84288025, + 85.53556824, + 78.79430389, + 72.57549286, + 66.83885193, + 61.54678726, + 56.66480637, + 52.16108322, + ] + ) + * units.mbar + ) + temperatures = ( + np.array( + [ + 6.00750732, + 5.14892578, + 4.177948, + 3.00268555, + 1.55535889, + -0.25527954, + -1.93988037, + -3.57766724, + -4.40600586, + -4.19238281, + -3.71185303, + -4.47943115, + -6.81280518, + -8.08685303, + -8.41287231, + -10.79302979, + -14.13262939, + -16.85784912, + -19.51675415, + -22.28689575, + -24.99938965, + -27.79664612, + -30.90414429, + -34.49435425, + -38.438797, + -42.27981567, + -45.99230957, + -49.75340271, + -53.58230591, + -57.30686951, + -60.76026917, + -63.92070007, + -66.72470093, + -68.97846985, + -70.4264679, + -71.16407776, + -71.53797913, + -71.64375305, + -71.52735901, + -71.53523254, + -71.61097717, + -71.92687988, + -72.68682861, + -74.129776, + -76.02471924, + -76.88977051, + -76.26008606, + -75.90351868, + -76.15809631, + ] + ) + * units.celsius + ) + dewpoints = ( + np.array( + [ + 4.50012302, + 3.42483997, + 2.78102994, + 2.24474645, + 1.593485, + -0.9440815, + -3.8044982, + -3.55629468, + -9.7376976, + -10.2950449, + -9.67498302, + -10.30486488, + -8.70559597, + -8.71669006, + -12.66509628, + -18.6697197, + -23.00351334, + -29.46240425, + -36.82178497, + -41.68824768, + -44.50320816, + -48.54426575, + -52.50753403, + -51.09564209, + -48.92690659, + -49.97380829, + -51.57516098, + -52.62096405, + -54.24332809, + -57.09109879, + -60.5596199, + -63.93486404, + -67.07530212, + -70.01263428, + -72.9258728, + -76.12271881, + -79.49847412, + -82.2350769, + -83.91127014, + -84.95665741, + -85.61238861, + -86.16391754, + -86.7653656, + -87.34436035, + -87.87495422, + -88.34281921, + -88.74453735, + -89.04680634, + -89.26436615, + ] + ) + * units.celsius + ) __, t_mixed, td_mixed = mixed_parcel(levels, temperatures, dewpoints) mixed_parcel_prof = parcel_profile(levels, t_mixed, td_mixed) lfc_pressure, lfc_temp = lfc(levels, temperatures, dewpoints, mixed_parcel_prof, td_mixed) @@ -402,10 +619,11 @@ def test_lfc_ml2(): def test_lfc_intersection(): """Test LFC calculation when LFC is below a tricky intersection.""" - p = np.array([1024.957, 930., 924.828, 898.255, 873.461, 848.698, 823.926, - 788.493]) * units('hPa') - t = np.array([6.008, -10., -6.94, -8.58, -4.41, -4.19, -3.71, -4.48]) * units('degC') - td = np.array([5., -10., -7., -9., -4.5, -4.2, -3.8, -4.5]) * units('degC') + p = np.array( + [1024.957, 930.0, 924.828, 898.255, 873.461, 848.698, 823.926, 788.493] + ) * units("hPa") + t = np.array([6.008, -10.0, -6.94, -8.58, -4.41, -4.19, -3.71, -4.48]) * units("degC") + td = np.array([5.0, -10.0, -7.0, -9.0, -4.5, -4.2, -3.8, -4.5]) * units("degC") _, mlt, mltd = mixed_parcel(p, t, td) ml_profile = parcel_profile(p, mlt, mltd) mllfc_p, mllfc_t = lfc(p, t, td, ml_profile, mltd) @@ -415,9 +633,9 @@ def test_lfc_intersection(): def test_no_lfc(): """Test LFC calculation when there is no LFC in the data.""" - levels = np.array([959., 867.9, 779.2, 647.5, 472.5, 321.9, 251.]) * units.mbar + levels = np.array([959.0, 867.9, 779.2, 647.5, 472.5, 321.9, 251.0]) * units.mbar temperatures = np.array([22.2, 17.4, 14.6, 1.4, -17.6, -39.4, -52.5]) * units.celsius - dewpoints = np.array([9., 4.3, -21.2, -26.7, -31., -53.3, -66.7]) * units.celsius + dewpoints = np.array([9.0, 4.3, -21.2, -26.7, -31.0, -53.3, -66.7]) * units.celsius lfc_pressure, lfc_temperature = lfc(levels, temperatures, dewpoints) assert_nan(lfc_pressure, levels.units) assert_nan(lfc_temperature, temperatures.units) @@ -425,12 +643,18 @@ def test_no_lfc(): def test_lfc_inversion(): """Test LFC when there is an inversion to be sure we don't pick that.""" - levels = np.array([963., 789., 782.3, 754.8, 728.1, 727., 700., - 571., 450., 300., 248.]) * units.mbar - temperatures = np.array([25.4, 18.4, 17.8, 15.4, 12.9, 12.8, - 10., -3.9, -16.3, -41.1, -51.5]) * units.celsius - dewpoints = np.array([20.4, 0.4, -0.5, -4.3, -8., -8.2, -9., - -23.9, -33.3, -54.1, -63.5]) * units.celsius + levels = ( + np.array([963.0, 789.0, 782.3, 754.8, 728.1, 727.0, 700.0, 571.0, 450.0, 300.0, 248.0]) + * units.mbar + ) + temperatures = ( + np.array([25.4, 18.4, 17.8, 15.4, 12.9, 12.8, 10.0, -3.9, -16.3, -41.1, -51.5]) + * units.celsius + ) + dewpoints = ( + np.array([20.4, 0.4, -0.5, -4.3, -8.0, -8.2, -9.0, -23.9, -33.3, -54.1, -63.5]) + * units.celsius + ) lfc_pressure, lfc_temp = lfc(levels, temperatures, dewpoints) assert_almost_equal(lfc_pressure, 705.9214 * units.mbar, 2) assert_almost_equal(lfc_temp, 10.6232 * units.celsius, 2) @@ -438,12 +662,18 @@ def test_lfc_inversion(): def test_lfc_equals_lcl(): """Test LFC when there is no cap and the lfc is equal to the lcl.""" - levels = np.array([912., 905.3, 874.4, 850., 815.1, 786.6, 759.1, - 748., 732.2, 700., 654.8]) * units.mbar - temperatures = np.array([29.4, 28.7, 25.2, 22.4, 19.4, 16.8, - 14.0, 13.2, 12.6, 11.4, 7.1]) * units.celsius - dewpoints = np.array([18.4, 18.1, 16.6, 15.4, 13.2, 11.4, 9.6, - 8.8, 0., -18.6, -22.9]) * units.celsius + levels = ( + np.array([912.0, 905.3, 874.4, 850.0, 815.1, 786.6, 759.1, 748.0, 732.2, 700.0, 654.8]) + * units.mbar + ) + temperatures = ( + np.array([29.4, 28.7, 25.2, 22.4, 19.4, 16.8, 14.0, 13.2, 12.6, 11.4, 7.1]) + * units.celsius + ) + dewpoints = ( + np.array([18.4, 18.1, 16.6, 15.4, 13.2, 11.4, 9.6, 8.8, 0.0, -18.6, -22.9]) + * units.celsius + ) lfc_pressure, lfc_temp = lfc(levels, temperatures, dewpoints) assert_almost_equal(lfc_pressure, 777.0333 * units.mbar, 2) assert_almost_equal(lfc_temp, 15.8714 * units.celsius, 2) @@ -453,31 +683,106 @@ def test_sensitive_sounding(): """Test quantities for a sensitive sounding (#902).""" # This sounding has a very small positive area in the low level. It's only captured # properly if the parcel profile includes the LCL, otherwise it breaks LFC and CAPE - p = units.Quantity([1004., 1000., 943., 928., 925., 850., 839., 749., 700., 699., - 603., 500., 404., 400., 363., 306., 300., 250., 213., 200., - 176., 150.], 'hectopascal') - t = units.Quantity([24.2, 24., 20.2, 21.6, 21.4, 20.4, 20.2, 14.4, 13.2, 13., 6.8, -3.3, - -13.1, -13.7, -17.9, -25.5, -26.9, -37.9, -46.7, -48.7, -52.1, -58.9], - 'degC') - td = units.Quantity([21.9, 22.1, 19.2, 20.5, 20.4, 18.4, 17.4, 8.4, -2.8, -3.0, -15.2, - -20.3, -29.1, -27.7, -24.9, -39.5, -41.9, -51.9, -60.7, -62.7, -65.1, - -71.9], 'degC') + p = units.Quantity( + [ + 1004.0, + 1000.0, + 943.0, + 928.0, + 925.0, + 850.0, + 839.0, + 749.0, + 700.0, + 699.0, + 603.0, + 500.0, + 404.0, + 400.0, + 363.0, + 306.0, + 300.0, + 250.0, + 213.0, + 200.0, + 176.0, + 150.0, + ], + "hectopascal", + ) + t = units.Quantity( + [ + 24.2, + 24.0, + 20.2, + 21.6, + 21.4, + 20.4, + 20.2, + 14.4, + 13.2, + 13.0, + 6.8, + -3.3, + -13.1, + -13.7, + -17.9, + -25.5, + -26.9, + -37.9, + -46.7, + -48.7, + -52.1, + -58.9, + ], + "degC", + ) + td = units.Quantity( + [ + 21.9, + 22.1, + 19.2, + 20.5, + 20.4, + 18.4, + 17.4, + 8.4, + -2.8, + -3.0, + -15.2, + -20.3, + -29.1, + -27.7, + -24.9, + -39.5, + -41.9, + -51.9, + -60.7, + -62.7, + -65.1, + -71.9, + ], + "degC", + ) lfc_pressure, lfc_temp = lfc(p, t, td) assert_almost_equal(lfc_pressure, 947.422 * units.mbar, 2) assert_almost_equal(lfc_temp, 20.498 * units.degC, 2) pos, neg = surface_based_cape_cin(p, t, td) - assert_almost_equal(pos, 0.1115 * units('J/kg'), 3) - assert_almost_equal(neg, -6.0806 * units('J/kg'), 3) + assert_almost_equal(pos, 0.1115 * units("J/kg"), 3) + assert_almost_equal(neg, -6.0806 * units("J/kg"), 3) def test_lfc_sfc_precision(): """Test LFC when there are precision issues with the parcel path.""" - levels = np.array([839., 819.4, 816., 807., 790.7, 763., 736.2, - 722., 710.1, 700.]) * units.mbar - temperatures = np.array([20.6, 22.3, 22.6, 22.2, 20.9, 18.7, 16.4, - 15.2, 13.9, 12.8]) * units.celsius - dewpoints = np.array([10.6, 8., 7.6, 6.2, 5.7, 4.7, 3.7, 3.2, 3., 2.8]) * units.celsius + levels = ( + np.array([839.0, 819.4, 816.0, 807.0, 790.7, 763.0, 736.2, 722.0, 710.1, 700.0]) + * units.mbar + ) + temperatures = ( + np.array([20.6, 22.3, 22.6, 22.2, 20.9, 18.7, 16.4, 15.2, 13.9, 12.8]) * units.celsius + ) + dewpoints = np.array([10.6, 8.0, 7.6, 6.2, 5.7, 4.7, 3.7, 3.2, 3.0, 2.8]) * units.celsius lfc_pressure, lfc_temp = lfc(levels, temperatures, dewpoints) assert_nan(lfc_pressure, levels.units) assert_nan(lfc_temp, temperatures.units) @@ -485,24 +790,124 @@ def test_lfc_sfc_precision(): def test_lfc_pos_area_below_lcl(): """Test LFC when there is positive area below the LCL (#1003).""" - p = [902.1554, 897.9034, 893.6506, 889.4047, 883.063, 874.6284, 866.2387, 857.887, - 849.5506, 841.2686, 833.0042, 824.7891, 812.5049, 796.2104, 776.0027, 751.9025, - 727.9612, 704.1409, 680.4028, 656.7156, 629.077, 597.4286, 565.6315, 533.5961, - 501.2452, 468.493, 435.2486, 401.4239, 366.9387, 331.7026, 295.6319, 258.6428, - 220.9178, 182.9384, 144.959, 106.9778, 69.00213] * units.hPa - t = [-3.039381, -3.703779, -4.15996, -4.562574, -5.131827, -5.856229, -6.568434, - -7.276881, -7.985013, -8.670911, -8.958063, -7.631381, -6.05927, -5.083627, - -5.11576, -5.687552, -5.453021, -4.981445, -5.236665, -6.324916, -8.434324, - -11.58795, -14.99297, -18.45947, -21.92021, -25.40522, -28.914, -32.78637, - -37.7179, -43.56836, -49.61077, -54.24449, -56.16666, -57.03775, -58.28041, - -60.86264, -64.21677] * units.degC - td = [-22.08774, -22.18181, -22.2508, -22.31323, -22.4024, -22.51582, -22.62526, - -22.72919, -22.82095, -22.86173, -22.49489, -21.66936, -21.67332, -21.94054, - -23.63561, -27.17466, -31.87395, -38.31725, -44.54717, -46.99218, -43.17544, - -37.40019, -34.3351, -36.42896, -42.1396, -46.95909, -49.36232, -48.94634, - -47.90178, -49.97902, -55.02753, -63.06276, -72.53742, -88.81377, -93.54573, - -92.92464, -91.57479] * units.degC - prof = parcel_profile(p, t[0], td[0]).to('degC') + p = [ + 902.1554, + 897.9034, + 893.6506, + 889.4047, + 883.063, + 874.6284, + 866.2387, + 857.887, + 849.5506, + 841.2686, + 833.0042, + 824.7891, + 812.5049, + 796.2104, + 776.0027, + 751.9025, + 727.9612, + 704.1409, + 680.4028, + 656.7156, + 629.077, + 597.4286, + 565.6315, + 533.5961, + 501.2452, + 468.493, + 435.2486, + 401.4239, + 366.9387, + 331.7026, + 295.6319, + 258.6428, + 220.9178, + 182.9384, + 144.959, + 106.9778, + 69.00213, + ] * units.hPa + t = [ + -3.039381, + -3.703779, + -4.15996, + -4.562574, + -5.131827, + -5.856229, + -6.568434, + -7.276881, + -7.985013, + -8.670911, + -8.958063, + -7.631381, + -6.05927, + -5.083627, + -5.11576, + -5.687552, + -5.453021, + -4.981445, + -5.236665, + -6.324916, + -8.434324, + -11.58795, + -14.99297, + -18.45947, + -21.92021, + -25.40522, + -28.914, + -32.78637, + -37.7179, + -43.56836, + -49.61077, + -54.24449, + -56.16666, + -57.03775, + -58.28041, + -60.86264, + -64.21677, + ] * units.degC + td = [ + -22.08774, + -22.18181, + -22.2508, + -22.31323, + -22.4024, + -22.51582, + -22.62526, + -22.72919, + -22.82095, + -22.86173, + -22.49489, + -21.66936, + -21.67332, + -21.94054, + -23.63561, + -27.17466, + -31.87395, + -38.31725, + -44.54717, + -46.99218, + -43.17544, + -37.40019, + -34.3351, + -36.42896, + -42.1396, + -46.95909, + -49.36232, + -48.94634, + -47.90178, + -49.97902, + -55.02753, + -63.06276, + -72.53742, + -88.81377, + -93.54573, + -92.92464, + -91.57479, + ] * units.degC + prof = parcel_profile(p, t[0], td[0]).to("degC") lfc_p, lfc_t = lfc(p, t, td, prof) assert_nan(lfc_p, p.units) assert_nan(lfc_t, t.units) @@ -510,36 +915,38 @@ def test_lfc_pos_area_below_lcl(): def test_saturation_mixing_ratio(): """Test saturation mixing ratio calculation.""" - p = 999. * units.mbar - t = 288. * units.kelvin - assert_almost_equal(saturation_mixing_ratio(p, t), .01068, 3) + p = 999.0 * units.mbar + t = 288.0 * units.kelvin + assert_almost_equal(saturation_mixing_ratio(p, t), 0.01068, 3) def test_saturation_mixing_ratio_with_xarray(): """Test saturation mixing ratio calculation with xarray.""" temperature = xr.DataArray( np.arange(10, 18).reshape((2, 2, 2)) * units.degC, - dims=('isobaric', 'y', 'x'), + dims=("isobaric", "y", "x"), coords={ - 'isobaric': (('isobaric',), [700., 850.], {'units': 'hPa'}), - 'y': (('y',), [0., 100.], {'units': 'kilometer'}), - 'x': (('x',), [0., 100.], {'units': 'kilometer'}) - } + "isobaric": (("isobaric",), [700.0, 850.0], {"units": "hPa"}), + "y": (("y",), [0.0, 100.0], {"units": "kilometer"}), + "x": (("x",), [0.0, 100.0], {"units": "kilometer"}), + }, ) result = saturation_mixing_ratio(temperature.metpy.vertical, temperature) - expected_values = [[[0.011098, 0.011879], [0.012708, 0.013589]], - [[0.011913, 0.012724], [0.013586, 0.014499]]] + expected_values = [ + [[0.011098, 0.011879], [0.012708, 0.013589]], + [[0.011913, 0.012724], [0.013586, 0.014499]], + ] assert_array_almost_equal(result.data, expected_values, 5) - xr.testing.assert_identical(result['isobaric'], temperature['isobaric']) - xr.testing.assert_identical(result['y'], temperature['y']) - xr.testing.assert_identical(result['x'], temperature['x']) + xr.testing.assert_identical(result["isobaric"], temperature["isobaric"]) + xr.testing.assert_identical(result["y"], temperature["y"]) + xr.testing.assert_identical(result["x"], temperature["x"]) def test_equivalent_potential_temperature(): """Test equivalent potential temperature calculation.""" p = 1000 * units.mbar - t = 293. * units.kelvin - td = 280. * units.kelvin + t = 293.0 * units.kelvin + td = 280.0 * units.kelvin ept = equivalent_potential_temperature(p, t, td) assert_almost_equal(ept, 311.18586467284007 * units.kelvin, 3) @@ -547,15 +954,13 @@ def test_equivalent_potential_temperature(): def test_equivalent_potential_temperature_masked(): """Test equivalent potential temperature calculation with masked arrays.""" p = 1000 * units.mbar - t = units.Quantity(np.ma.array([293., 294., 295.]), units.kelvin) + t = units.Quantity(np.ma.array([293.0, 294.0, 295.0]), units.kelvin) td = units.Quantity( - np.ma.array([280., 281., 282.], mask=[False, True, False]), - units.kelvin + np.ma.array([280.0, 281.0, 282.0], mask=[False, True, False]), units.kelvin ) ept = equivalent_potential_temperature(p, t, td) expected = units.Quantity( - np.ma.array([311.18586, 313.51781, 315.93971], mask=[False, True, False]), - units.kelvin + np.ma.array([311.18586, 313.51781, 315.93971], mask=[False, True, False]), units.kelvin ) assert isinstance(ept, units.Quantity) assert isinstance(ept.m, np.ma.MaskedArray) @@ -575,12 +980,9 @@ def test_saturation_equivalent_potential_temperature(): def test_saturation_equivalent_potential_temperature_masked(): """Test saturation equivalent potential temperature calculation with masked arrays.""" p = 1000 * units.mbar - t = units.Quantity(np.ma.array([293., 294., 295.]), units.kelvin) + t = units.Quantity(np.ma.array([293.0, 294.0, 295.0]), units.kelvin) s_ept = saturation_equivalent_potential_temperature(p, t) - expected = units.Quantity( - np.ma.array([335.02750, 338.95813, 343.08740]), - units.kelvin - ) + expected = units.Quantity(np.ma.array([335.02750, 338.95813, 343.08740]), units.kelvin) assert isinstance(s_ept, units.Quantity) assert isinstance(s_ept.m, np.ma.MaskedArray) assert_array_almost_equal(s_ept, expected, 3) @@ -588,35 +990,35 @@ def test_saturation_equivalent_potential_temperature_masked(): def test_virtual_temperature(): """Test virtual temperature calculation.""" - t = 288. * units.kelvin - qv = .0016 * units.dimensionless # kg/kg + t = 288.0 * units.kelvin + qv = 0.0016 * units.dimensionless # kg/kg tv = virtual_temperature(t, qv) assert_almost_equal(tv, 288.2796 * units.kelvin, 3) def test_virtual_potential_temperature(): """Test virtual potential temperature calculation.""" - p = 999. * units.mbar - t = 288. * units.kelvin - qv = .0016 * units.dimensionless # kg/kg + p = 999.0 * units.mbar + t = 288.0 * units.kelvin + qv = 0.0016 * units.dimensionless # kg/kg theta_v = virtual_potential_temperature(p, t, qv) assert_almost_equal(theta_v, 288.3620 * units.kelvin, 3) def test_density(): """Test density calculation.""" - p = 999. * units.mbar - t = 288. * units.kelvin - qv = .0016 * units.dimensionless # kg/kg + p = 999.0 * units.mbar + t = 288.0 * units.kelvin + qv = 0.0016 * units.dimensionless # kg/kg rho = density(p, t, qv).to(units.kilogram / units.meter ** 3) assert_almost_equal(rho, 1.2072 * (units.kilogram / units.meter ** 3), 3) def test_el(): """Test equilibrium layer calculation.""" - levels = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.mbar - temperatures = np.array([22.2, 14.6, 12., 9.4, 7., -38.]) * units.celsius - dewpoints = np.array([19., -11.2, -10.8, -10.4, -10., -53.2]) * units.celsius + levels = np.array([959.0, 779.2, 751.3, 724.3, 700.0, 269.0]) * units.mbar + temperatures = np.array([22.2, 14.6, 12.0, 9.4, 7.0, -38.0]) * units.celsius + dewpoints = np.array([19.0, -11.2, -10.8, -10.4, -10.0, -53.2]) * units.celsius el_pressure, el_temperature = el(levels, temperatures, dewpoints) assert_almost_equal(el_pressure, 470.4075 * units.mbar, 3) assert_almost_equal(el_temperature, -11.7027 * units.degC, 3) @@ -624,9 +1026,9 @@ def test_el(): def test_el_kelvin(): """Test that EL temperature returns Kelvin if Kelvin is provided.""" - levels = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.mbar - temperatures = (np.array([22.2, 14.6, 12., 9.4, 7., -38.]) + 273.15) * units.kelvin - dewpoints = (np.array([19., -11.2, -10.8, -10.4, -10., -53.2]) + 273.15) * units.kelvin + levels = np.array([959.0, 779.2, 751.3, 724.3, 700.0, 269.0]) * units.mbar + temperatures = (np.array([22.2, 14.6, 12.0, 9.4, 7.0, -38.0]) + 273.15) * units.kelvin + dewpoints = (np.array([19.0, -11.2, -10.8, -10.4, -10.0, -53.2]) + 273.15) * units.kelvin el_pressure, el_temp = el(levels, temperatures, dewpoints) assert_almost_equal(el_pressure, 470.4075 * units.mbar, 3) assert_almost_equal(el_temp, -11.7027 * units.degC, 3) @@ -635,9 +1037,9 @@ def test_el_kelvin(): def test_el_ml(): """Test equilibrium layer calculation for a mixed parcel.""" - levels = np.array([959., 779.2, 751.3, 724.3, 700., 400., 269.]) * units.mbar - temperatures = np.array([22.2, 14.6, 12., 9.4, 7., -25., -35.]) * units.celsius - dewpoints = np.array([19., -11.2, -10.8, -10.4, -10., -35., -53.2]) * units.celsius + levels = np.array([959.0, 779.2, 751.3, 724.3, 700.0, 400.0, 269.0]) * units.mbar + temperatures = np.array([22.2, 14.6, 12.0, 9.4, 7.0, -25.0, -35.0]) * units.celsius + dewpoints = np.array([19.0, -11.2, -10.8, -10.4, -10.0, -35.0, -53.2]) * units.celsius __, t_mixed, td_mixed = mixed_parcel(levels, temperatures, dewpoints) mixed_parcel_prof = parcel_profile(levels, t_mixed, td_mixed) el_pressure, el_temperature = el(levels, temperatures, dewpoints, mixed_parcel_prof) @@ -647,9 +1049,9 @@ def test_el_ml(): def test_no_el(): """Test equilibrium layer calculation when there is no EL in the data.""" - levels = np.array([959., 867.9, 779.2, 647.5, 472.5, 321.9, 251.]) * units.mbar + levels = np.array([959.0, 867.9, 779.2, 647.5, 472.5, 321.9, 251.0]) * units.mbar temperatures = np.array([22.2, 17.4, 14.6, 1.4, -17.6, -39.4, -52.5]) * units.celsius - dewpoints = np.array([19., 14.3, -11.2, -16.7, -21., -43.3, -56.7]) * units.celsius + dewpoints = np.array([19.0, 14.3, -11.2, -16.7, -21.0, -43.3, -56.7]) * units.celsius el_pressure, el_temperature = el(levels, temperatures, dewpoints) assert_nan(el_pressure, levels.units) assert_nan(el_temperature, temperatures.units) @@ -657,13 +1059,84 @@ def test_no_el(): def test_no_el_multi_crossing(): """Test el calculation with no el and severel parcel path-profile crossings.""" - levels = np.array([918., 911., 880., 873.9, 850., 848., 843.5, 818., 813.8, 785., - 773., 763., 757.5, 730.5, 700., 679., 654.4, 645., - 643.9]) * units.mbar - temperatures = np.array([24.2, 22.8, 19.6, 19.1, 17., 16.8, 16.5, 15., 14.9, 14.4, 16.4, - 16.2, 15.7, 13.4, 10.6, 8.4, 5.7, 4.6, 4.5]) * units.celsius - dewpoints = np.array([19.5, 17.8, 16.7, 16.5, 15.8, 15.7, 15.3, 13.1, 12.9, 11.9, 6.4, - 3.2, 2.6, -0.6, -4.4, -6.6, -9.3, -10.4, -10.5]) * units.celsius + levels = ( + np.array( + [ + 918.0, + 911.0, + 880.0, + 873.9, + 850.0, + 848.0, + 843.5, + 818.0, + 813.8, + 785.0, + 773.0, + 763.0, + 757.5, + 730.5, + 700.0, + 679.0, + 654.4, + 645.0, + 643.9, + ] + ) + * units.mbar + ) + temperatures = ( + np.array( + [ + 24.2, + 22.8, + 19.6, + 19.1, + 17.0, + 16.8, + 16.5, + 15.0, + 14.9, + 14.4, + 16.4, + 16.2, + 15.7, + 13.4, + 10.6, + 8.4, + 5.7, + 4.6, + 4.5, + ] + ) + * units.celsius + ) + dewpoints = ( + np.array( + [ + 19.5, + 17.8, + 16.7, + 16.5, + 15.8, + 15.7, + 15.3, + 13.1, + 12.9, + 11.9, + 6.4, + 3.2, + 2.6, + -0.6, + -4.4, + -6.6, + -9.3, + -10.4, + -10.5, + ] + ) + * units.celsius + ) el_pressure, el_temperature = el(levels, temperatures, dewpoints) assert_nan(el_pressure, levels.units) assert_nan(el_temperature, temperatures.units) @@ -672,8 +1145,14 @@ def test_no_el_multi_crossing(): def test_lfc_and_el_below_lcl(): """Test that LFC and EL are returned as NaN if both are below LCL.""" dewpoint = [264.5351, 261.13443, 259.0122, 252.30063, 248.58017, 242.66582] * units.kelvin - temperature = [273.09723, 268.40173, 263.56207, 260.257, 256.63538, - 252.91345] * units.kelvin + temperature = [ + 273.09723, + 268.40173, + 263.56207, + 260.257, + 256.63538, + 252.91345, + ] * units.kelvin pressure = [1017.16, 950, 900, 850, 800, 750] * units.hPa el_pressure, el_temperature = el(pressure, temperature, dewpoint) lfc_pressure, lfc_temperature = lfc(pressure, temperature, dewpoint) @@ -685,20 +1164,129 @@ def test_lfc_and_el_below_lcl(): def test_el_lfc_equals_lcl(): """Test equilibrium layer calculation when the lfc equals the lcl.""" - levels = np.array([912., 905.3, 874.4, 850., 815.1, 786.6, 759.1, 748., - 732.3, 700., 654.8, 606.8, 562.4, 501.8, 500., 482., - 400., 393.3, 317.1, 307., 300., 252.7, 250., 200., - 199.3, 197., 190., 172., 156.6, 150., 122.9, 112., - 106.2, 100.]) * units.mbar - temperatures = np.array([29.4, 28.7, 25.2, 22.4, 19.4, 16.8, 14.3, - 13.2, 12.6, 11.4, 7.1, 2.2, -2.7, -10.1, - -10.3, -12.4, -23.3, -24.4, -38., -40.1, -41.1, - -49.8, -50.3, -59.1, -59.1, -59.3, -59.7, -56.3, - -56.9, -57.1, -59.1, -60.1, -58.6, -56.9]) * units.celsius - dewpoints = np.array([18.4, 18.1, 16.6, 15.4, 13.2, 11.4, 9.6, 8.8, 0., - -18.6, -22.9, -27.8, -32.7, -40.1, -40.3, -42.4, -53.3, - -54.4, -68., -70.1, -70., -70., -70., -70., -70., -70., - -70., -70., -70., -70., -70., -70., -70., -70.]) * units.celsius + levels = ( + np.array( + [ + 912.0, + 905.3, + 874.4, + 850.0, + 815.1, + 786.6, + 759.1, + 748.0, + 732.3, + 700.0, + 654.8, + 606.8, + 562.4, + 501.8, + 500.0, + 482.0, + 400.0, + 393.3, + 317.1, + 307.0, + 300.0, + 252.7, + 250.0, + 200.0, + 199.3, + 197.0, + 190.0, + 172.0, + 156.6, + 150.0, + 122.9, + 112.0, + 106.2, + 100.0, + ] + ) + * units.mbar + ) + temperatures = ( + np.array( + [ + 29.4, + 28.7, + 25.2, + 22.4, + 19.4, + 16.8, + 14.3, + 13.2, + 12.6, + 11.4, + 7.1, + 2.2, + -2.7, + -10.1, + -10.3, + -12.4, + -23.3, + -24.4, + -38.0, + -40.1, + -41.1, + -49.8, + -50.3, + -59.1, + -59.1, + -59.3, + -59.7, + -56.3, + -56.9, + -57.1, + -59.1, + -60.1, + -58.6, + -56.9, + ] + ) + * units.celsius + ) + dewpoints = ( + np.array( + [ + 18.4, + 18.1, + 16.6, + 15.4, + 13.2, + 11.4, + 9.6, + 8.8, + 0.0, + -18.6, + -22.9, + -27.8, + -32.7, + -40.1, + -40.3, + -42.4, + -53.3, + -54.4, + -68.0, + -70.1, + -70.0, + -70.0, + -70.0, + -70.0, + -70.0, + -70.0, + -70.0, + -70.0, + -70.0, + -70.0, + -70.0, + -70.0, + -70.0, + -70.0, + ] + ) + * units.celsius + ) el_pressure, el_temperature = el(levels, temperatures, dewpoints) assert_almost_equal(el_pressure, 175.7187 * units.mbar, 3) assert_almost_equal(el_temperature, -57.0307 * units.degC, 3) @@ -706,15 +1294,87 @@ def test_el_lfc_equals_lcl(): def test_el_small_surface_instability(): """Test that no EL is found when there is a small pocket of instability at the sfc.""" - levels = np.array([959., 931.3, 925., 899.3, 892., 867.9, 850., 814., - 807.9, 790., 779.2, 751.3, 724.3, 700., 655., 647.5, - 599.4, 554.7, 550., 500.]) * units.mbar - temperatures = np.array([22.2, 20.2, 19.8, 18.4, 18., 17.4, 17., 15.4, 15.4, - 15.6, 14.6, 12., 9.4, 7., 2.2, 1.4, -4.2, -9.7, - -10.3, -14.9]) * units.degC - dewpoints = np.array([20., 18.5, 18.1, 17.9, 17.8, 15.3, 13.5, 6.4, 2.2, - -10.4, -10.2, -9.8, -9.4, -9., -15.8, -15.7, -14.8, -14., - -13.9, -17.9]) * units.degC + levels = ( + np.array( + [ + 959.0, + 931.3, + 925.0, + 899.3, + 892.0, + 867.9, + 850.0, + 814.0, + 807.9, + 790.0, + 779.2, + 751.3, + 724.3, + 700.0, + 655.0, + 647.5, + 599.4, + 554.7, + 550.0, + 500.0, + ] + ) + * units.mbar + ) + temperatures = ( + np.array( + [ + 22.2, + 20.2, + 19.8, + 18.4, + 18.0, + 17.4, + 17.0, + 15.4, + 15.4, + 15.6, + 14.6, + 12.0, + 9.4, + 7.0, + 2.2, + 1.4, + -4.2, + -9.7, + -10.3, + -14.9, + ] + ) + * units.degC + ) + dewpoints = ( + np.array( + [ + 20.0, + 18.5, + 18.1, + 17.9, + 17.8, + 15.3, + 13.5, + 6.4, + 2.2, + -10.4, + -10.2, + -9.8, + -9.4, + -9.0, + -15.8, + -15.7, + -14.8, + -14.0, + -13.9, + -17.9, + ] + ) + * units.degC + ) el_pressure, el_temperature = el(levels, temperatures, dewpoints) assert_nan(el_pressure, levels.units) assert_nan(el_temperature, temperatures.units) @@ -722,18 +1382,117 @@ def test_el_small_surface_instability(): def test_no_el_parcel_colder(): """Test no EL when parcel stays colder than environment. INL 20170925-12Z.""" - levels = np.array([974., 946., 925., 877.2, 866., 850., 814.6, 785., - 756.6, 739., 729.1, 700., 686., 671., 641., 613., - 603., 586., 571., 559.3, 539., 533., 500., 491., - 477.9, 413., 390., 378., 345., 336.]) * units.mbar - temperatures = np.array([10., 8.4, 7.6, 5.9, 7.2, 7.6, 6.8, 7.1, 7.7, - 7.8, 7.7, 5.6, 4.6, 3.4, 0.6, -0.9, -1.1, -3.1, - -4.7, -4.7, -6.9, -7.5, -11.1, -10.9, -12.1, -20.5, -23.5, - -24.7, -30.5, -31.7]) * units.celsius - dewpoints = np.array([8.9, 8.4, 7.6, 5.9, 7.2, 7., 5., 3.6, 0.3, - -4.2, -12.8, -12.4, -8.4, -8.6, -6.4, -7.9, -11.1, -14.1, - -8.8, -28.1, -18.9, -14.5, -15.2, -15.1, -21.6, -41.5, -45.5, - -29.6, -30.6, -32.1]) * units.celsius + levels = ( + np.array( + [ + 974.0, + 946.0, + 925.0, + 877.2, + 866.0, + 850.0, + 814.6, + 785.0, + 756.6, + 739.0, + 729.1, + 700.0, + 686.0, + 671.0, + 641.0, + 613.0, + 603.0, + 586.0, + 571.0, + 559.3, + 539.0, + 533.0, + 500.0, + 491.0, + 477.9, + 413.0, + 390.0, + 378.0, + 345.0, + 336.0, + ] + ) + * units.mbar + ) + temperatures = ( + np.array( + [ + 10.0, + 8.4, + 7.6, + 5.9, + 7.2, + 7.6, + 6.8, + 7.1, + 7.7, + 7.8, + 7.7, + 5.6, + 4.6, + 3.4, + 0.6, + -0.9, + -1.1, + -3.1, + -4.7, + -4.7, + -6.9, + -7.5, + -11.1, + -10.9, + -12.1, + -20.5, + -23.5, + -24.7, + -30.5, + -31.7, + ] + ) + * units.celsius + ) + dewpoints = ( + np.array( + [ + 8.9, + 8.4, + 7.6, + 5.9, + 7.2, + 7.0, + 5.0, + 3.6, + 0.3, + -4.2, + -12.8, + -12.4, + -8.4, + -8.6, + -6.4, + -7.9, + -11.1, + -14.1, + -8.8, + -28.1, + -18.9, + -14.5, + -15.2, + -15.1, + -21.6, + -41.5, + -45.5, + -29.6, + -30.6, + -32.1, + ] + ) + * units.celsius + ) el_pressure, el_temperature = el(levels, temperatures, dewpoints) assert_nan(el_pressure, levels.units) assert_nan(el_temperature, temperatures.units) @@ -741,24 +1500,124 @@ def test_no_el_parcel_colder(): def test_el_below_lcl(): """Test LFC when there is positive area below the LCL (#1003).""" - p = [902.1554, 897.9034, 893.6506, 889.4047, 883.063, 874.6284, 866.2387, 857.887, - 849.5506, 841.2686, 833.0042, 824.7891, 812.5049, 796.2104, 776.0027, 751.9025, - 727.9612, 704.1409, 680.4028, 656.7156, 629.077, 597.4286, 565.6315, 533.5961, - 501.2452, 468.493, 435.2486, 401.4239, 366.9387, 331.7026, 295.6319, 258.6428, - 220.9178, 182.9384, 144.959, 106.9778, 69.00213] * units.hPa - t = [-3.039381, -3.703779, -4.15996, -4.562574, -5.131827, -5.856229, -6.568434, - -7.276881, -7.985013, -8.670911, -8.958063, -7.631381, -6.05927, -5.083627, - -5.11576, -5.687552, -5.453021, -4.981445, -5.236665, -6.324916, -8.434324, - -11.58795, -14.99297, -18.45947, -21.92021, -25.40522, -28.914, -32.78637, - -37.7179, -43.56836, -49.61077, -54.24449, -56.16666, -57.03775, -58.28041, - -60.86264, -64.21677] * units.degC - td = [-22.08774, -22.18181, -22.2508, -22.31323, -22.4024, -22.51582, -22.62526, - -22.72919, -22.82095, -22.86173, -22.49489, -21.66936, -21.67332, -21.94054, - -23.63561, -27.17466, -31.87395, -38.31725, -44.54717, -46.99218, -43.17544, - -37.40019, -34.3351, -36.42896, -42.1396, -46.95909, -49.36232, -48.94634, - -47.90178, -49.97902, -55.02753, -63.06276, -72.53742, -88.81377, -93.54573, - -92.92464, -91.57479] * units.degC - prof = parcel_profile(p, t[0], td[0]).to('degC') + p = [ + 902.1554, + 897.9034, + 893.6506, + 889.4047, + 883.063, + 874.6284, + 866.2387, + 857.887, + 849.5506, + 841.2686, + 833.0042, + 824.7891, + 812.5049, + 796.2104, + 776.0027, + 751.9025, + 727.9612, + 704.1409, + 680.4028, + 656.7156, + 629.077, + 597.4286, + 565.6315, + 533.5961, + 501.2452, + 468.493, + 435.2486, + 401.4239, + 366.9387, + 331.7026, + 295.6319, + 258.6428, + 220.9178, + 182.9384, + 144.959, + 106.9778, + 69.00213, + ] * units.hPa + t = [ + -3.039381, + -3.703779, + -4.15996, + -4.562574, + -5.131827, + -5.856229, + -6.568434, + -7.276881, + -7.985013, + -8.670911, + -8.958063, + -7.631381, + -6.05927, + -5.083627, + -5.11576, + -5.687552, + -5.453021, + -4.981445, + -5.236665, + -6.324916, + -8.434324, + -11.58795, + -14.99297, + -18.45947, + -21.92021, + -25.40522, + -28.914, + -32.78637, + -37.7179, + -43.56836, + -49.61077, + -54.24449, + -56.16666, + -57.03775, + -58.28041, + -60.86264, + -64.21677, + ] * units.degC + td = [ + -22.08774, + -22.18181, + -22.2508, + -22.31323, + -22.4024, + -22.51582, + -22.62526, + -22.72919, + -22.82095, + -22.86173, + -22.49489, + -21.66936, + -21.67332, + -21.94054, + -23.63561, + -27.17466, + -31.87395, + -38.31725, + -44.54717, + -46.99218, + -43.17544, + -37.40019, + -34.3351, + -36.42896, + -42.1396, + -46.95909, + -49.36232, + -48.94634, + -47.90178, + -49.97902, + -55.02753, + -63.06276, + -72.53742, + -88.81377, + -93.54573, + -92.92464, + -91.57479, + ] * units.degC + prof = parcel_profile(p, t[0], td[0]).to("degC") el_p, el_t = el(p, t, td, prof) assert_nan(el_p, p.units) assert_nan(el_t, t.units) @@ -767,39 +1626,41 @@ def test_el_below_lcl(): def test_wet_psychrometric_vapor_pressure(): """Test calculation of vapor pressure from wet and dry bulb temperatures.""" p = 1013.25 * units.mbar - dry_bulb_temperature = 20. * units.degC - wet_bulb_temperature = 18. * units.degC - psychrometric_vapor_pressure = psychrometric_vapor_pressure_wet(p, dry_bulb_temperature, - wet_bulb_temperature) + dry_bulb_temperature = 20.0 * units.degC + wet_bulb_temperature = 18.0 * units.degC + psychrometric_vapor_pressure = psychrometric_vapor_pressure_wet( + p, dry_bulb_temperature, wet_bulb_temperature + ) assert_almost_equal(psychrometric_vapor_pressure, 19.3673 * units.mbar, 3) def test_wet_psychrometric_rh(): """Test calculation of relative humidity from wet and dry bulb temperatures.""" p = 1013.25 * units.mbar - dry_bulb_temperature = 20. * units.degC - wet_bulb_temperature = 18. * units.degC - psychrometric_rh = relative_humidity_wet_psychrometric(p, dry_bulb_temperature, - wet_bulb_temperature) + dry_bulb_temperature = 20.0 * units.degC + wet_bulb_temperature = 18.0 * units.degC + psychrometric_rh = relative_humidity_wet_psychrometric( + p, dry_bulb_temperature, wet_bulb_temperature + ) assert_almost_equal(psychrometric_rh, 82.8747 * units.percent, 3) def test_wet_psychrometric_rh_kwargs(): """Test calculation of relative humidity from wet and dry bulb temperatures.""" p = 1013.25 * units.mbar - dry_bulb_temperature = 20. * units.degC - wet_bulb_temperature = 18. * units.degC + dry_bulb_temperature = 20.0 * units.degC + wet_bulb_temperature = 18.0 * units.degC coeff = 6.1e-4 / units.kelvin - psychrometric_rh = relative_humidity_wet_psychrometric(p, dry_bulb_temperature, - wet_bulb_temperature, - psychrometer_coefficient=coeff) + psychrometric_rh = relative_humidity_wet_psychrometric( + p, dry_bulb_temperature, wet_bulb_temperature, psychrometer_coefficient=coeff + ) assert_almost_equal(psychrometric_rh, 82.9701 * units.percent, 3) def test_mixing_ratio_from_relative_humidity(): """Test relative humidity from mixing ratio.""" p = 1013.25 * units.mbar - temperature = 20. * units.degC + temperature = 20.0 * units.degC rh = 81.7219 * units.percent w = mixing_ratio_from_relative_humidity(p, temperature, rh) assert_almost_equal(w, 0.012 * units.dimensionless, 3) @@ -808,7 +1669,7 @@ def test_mixing_ratio_from_relative_humidity(): def test_rh_mixing_ratio(): """Test relative humidity from mixing ratio.""" p = 1013.25 * units.mbar - temperature = 20. * units.degC + temperature = 20.0 * units.degC w = 0.012 * units.dimensionless rh = relative_humidity_from_mixing_ratio(p, temperature, w) assert_almost_equal(rh, 81.7219 * units.percent, 3) @@ -845,7 +1706,7 @@ def test_specific_humidity_from_mixing_ratio_no_units(): def test_rh_specific_humidity(): """Test relative humidity from specific humidity.""" p = 1013.25 * units.mbar - temperature = 20. * units.degC + temperature = 20.0 * units.degC q = 0.012 * units.dimensionless rh = relative_humidity_from_specific_humidity(p, temperature, q) assert_almost_equal(rh, 82.7145 * units.percent, 3) @@ -853,35 +1714,35 @@ def test_rh_specific_humidity(): def test_cape_cin(): """Test the basic CAPE and CIN calculation.""" - p = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.mbar - temperature = np.array([22.2, 14.6, 12., 9.4, 7., -38.]) * units.celsius - dewpoint = np.array([19., -11.2, -10.8, -10.4, -10., -53.2]) * units.celsius + p = np.array([959.0, 779.2, 751.3, 724.3, 700.0, 269.0]) * units.mbar + temperature = np.array([22.2, 14.6, 12.0, 9.4, 7.0, -38.0]) * units.celsius + dewpoint = np.array([19.0, -11.2, -10.8, -10.4, -10.0, -53.2]) * units.celsius parcel_prof = parcel_profile(p, temperature[0], dewpoint[0]) cape, cin = cape_cin(p, temperature, dewpoint, parcel_prof) - assert_almost_equal(cape, 75.7340825 * units('joule / kilogram'), 2) - assert_almost_equal(cin, -89.8179205 * units('joule / kilogram'), 2) + assert_almost_equal(cape, 75.7340825 * units("joule / kilogram"), 2) + assert_almost_equal(cin, -89.8179205 * units("joule / kilogram"), 2) def test_cape_cin_no_el(): """Test that CAPE works with no EL.""" - p = np.array([959., 779.2, 751.3, 724.3]) * units.mbar - temperature = np.array([22.2, 14.6, 12., 9.4]) * units.celsius - dewpoint = np.array([19., -11.2, -10.8, -10.4]) * units.celsius - parcel_prof = parcel_profile(p, temperature[0], dewpoint[0]).to('degC') + p = np.array([959.0, 779.2, 751.3, 724.3]) * units.mbar + temperature = np.array([22.2, 14.6, 12.0, 9.4]) * units.celsius + dewpoint = np.array([19.0, -11.2, -10.8, -10.4]) * units.celsius + parcel_prof = parcel_profile(p, temperature[0], dewpoint[0]).to("degC") cape, cin = cape_cin(p, temperature, dewpoint, parcel_prof) - assert_almost_equal(cape, 0.08610409 * units('joule / kilogram'), 2) - assert_almost_equal(cin, -89.8179205 * units('joule / kilogram'), 2) + assert_almost_equal(cape, 0.08610409 * units("joule / kilogram"), 2) + assert_almost_equal(cin, -89.8179205 * units("joule / kilogram"), 2) def test_cape_cin_no_lfc(): """Test that CAPE is zero with no LFC.""" - p = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.mbar - temperature = np.array([22.2, 24.6, 22., 20.4, 18., -10.]) * units.celsius - dewpoint = np.array([19., -11.2, -10.8, -10.4, -10., -53.2]) * units.celsius - parcel_prof = parcel_profile(p, temperature[0], dewpoint[0]).to('degC') + p = np.array([959.0, 779.2, 751.3, 724.3, 700.0, 269.0]) * units.mbar + temperature = np.array([22.2, 24.6, 22.0, 20.4, 18.0, -10.0]) * units.celsius + dewpoint = np.array([19.0, -11.2, -10.8, -10.4, -10.0, -53.2]) * units.celsius + parcel_prof = parcel_profile(p, temperature[0], dewpoint[0]).to("degC") cape, cin = cape_cin(p, temperature, dewpoint, parcel_prof) - assert_almost_equal(cape, 0.0 * units('joule / kilogram'), 2) - assert_almost_equal(cin, 0.0 * units('joule / kilogram'), 2) + assert_almost_equal(cape, 0.0 * units("joule / kilogram"), 2) + assert_almost_equal(cin, 0.0 * units("joule / kilogram"), 2) def test_find_append_zero_crossings(): @@ -890,8 +1751,27 @@ def test_find_append_zero_crossings(): y = np.array([3, 2, 1, -1, 2, 2, 0, 1, 0, -1, 2]) * units.degC x2, y2 = _find_append_zero_crossings(x, y) - x_truth = np.array([0., 1., 2., 2.4494897, 3., 3.3019272, 4., 5., - 6., 7., 8., 9., 9.3216975, 10.]) * units.hPa + x_truth = ( + np.array( + [ + 0.0, + 1.0, + 2.0, + 2.4494897, + 3.0, + 3.3019272, + 4.0, + 5.0, + 6.0, + 7.0, + 8.0, + 9.0, + 9.3216975, + 10.0, + ] + ) + * units.hPa + ) y_truth = np.array([3, 2, 1, 0, -1, 0, 2, 2, 0, 1, 0, -1, 0, 2]) * units.degC assert_array_almost_equal(x2, x_truth, 6) assert_almost_equal(y2, y_truth, 6) @@ -899,29 +1779,29 @@ def test_find_append_zero_crossings(): def test_most_unstable_parcel(): """Test calculating the most unstable parcel.""" - levels = np.array([1000., 959., 867.9]) * units.mbar + levels = np.array([1000.0, 959.0, 867.9]) * units.mbar temperatures = np.array([18.2, 22.2, 17.4]) * units.celsius - dewpoints = np.array([19., 19., 14.3]) * units.celsius + dewpoints = np.array([19.0, 19.0, 14.3]) * units.celsius ret = most_unstable_parcel(levels, temperatures, dewpoints, depth=100 * units.hPa) assert_almost_equal(ret[0], 959.0 * units.hPa, 6) assert_almost_equal(ret[1], 22.2 * units.degC, 6) assert_almost_equal(ret[2], 19.0 * units.degC, 6) -@pytest.mark.filterwarnings('ignore:invalid value:RuntimeWarning') +@pytest.mark.filterwarnings("ignore:invalid value:RuntimeWarning") def test_isentropic_pressure(): """Test calculation of isentropic pressure function.""" - lev = [100000., 95000., 90000., 85000.] * units.Pa + lev = [100000.0, 95000.0, 90000.0, 85000.0] * units.Pa tmp = np.ones((4, 5, 5)) - tmp[0, :] = 296. - tmp[1, :] = 292. + tmp[0, :] = 296.0 + tmp[1, :] = 292.0 tmp[2, :] = 290 - tmp[3, :] = 288. + tmp[3, :] = 288.0 tmp[:, :, -1] = np.nan tmpk = tmp * units.kelvin - isentlev = [296.] * units.kelvin + isentlev = [296.0] * units.kelvin isentprs = isentropic_interpolation(isentlev, lev, tmpk) - trueprs = np.ones((1, 5, 5)) * (1000. * units.hPa) + trueprs = np.ones((1, 5, 5)) * (1000.0 * units.hPa) trueprs[:, :, -1] = np.nan assert isentprs[0].shape == (1, 5, 5) assert_almost_equal(isentprs[0], trueprs, 3) @@ -929,14 +1809,14 @@ def test_isentropic_pressure(): def test_isentropic_pressure_masked_column(): """Test calculation of isentropic pressure function with a masked column (#769).""" - lev = [100000., 95000.] * units.Pa + lev = [100000.0, 95000.0] * units.Pa tmp = np.ma.ones((len(lev), 5, 5)) - tmp[0, :] = 296. - tmp[1, :] = 292. + tmp[0, :] = 296.0 + tmp[1, :] = 292.0 tmp[:, :, -1] = np.ma.masked tmp = units.Quantity(tmp, units.kelvin) - isentprs = isentropic_interpolation([296.] * units.kelvin, lev, tmp) - trueprs = np.ones((1, 5, 5)) * (1000. * units.hPa) + isentprs = isentropic_interpolation([296.0] * units.kelvin, lev, tmp) + trueprs = np.ones((1, 5, 5)) * (1000.0 * units.hPa) trueprs[:, :, -1] = np.nan assert isentprs[0].shape == (1, 5, 5) assert_almost_equal(isentprs[0], trueprs, 3) @@ -944,86 +1824,86 @@ def test_isentropic_pressure_masked_column(): def test_isentropic_pressure_p_increase(): """Test calculation of isentropic pressure function, p increasing order.""" - lev = [85000, 90000., 95000., 100000.] * units.Pa + lev = [85000, 90000.0, 95000.0, 100000.0] * units.Pa tmp = np.ones((4, 5, 5)) - tmp[0, :] = 288. - tmp[1, :] = 290. - tmp[2, :] = 292. - tmp[3, :] = 296. + tmp[0, :] = 288.0 + tmp[1, :] = 290.0 + tmp[2, :] = 292.0 + tmp[3, :] = 296.0 tmpk = tmp * units.kelvin - isentlev = [296.] * units.kelvin + isentlev = [296.0] * units.kelvin isentprs = isentropic_interpolation(isentlev, lev, tmpk) - trueprs = 1000. * units.hPa + trueprs = 1000.0 * units.hPa assert_almost_equal(isentprs[0], trueprs, 3) def test_isentropic_pressure_additional_args(): """Test calculation of isentropic pressure function, additional args.""" - lev = [100000., 95000., 90000., 85000.] * units.Pa + lev = [100000.0, 95000.0, 90000.0, 85000.0] * units.Pa tmp = np.ones((4, 5, 5)) - tmp[0, :] = 296. - tmp[1, :] = 292. - tmp[2, :] = 290. - tmp[3, :] = 288. + tmp[0, :] = 296.0 + tmp[1, :] = 292.0 + tmp[2, :] = 290.0 + tmp[3, :] = 288.0 rh = np.ones((4, 5, 5)) - rh[0, :] = 100. - rh[1, :] = 80. - rh[2, :] = 40. - rh[3, :] = 20. + rh[0, :] = 100.0 + rh[1, :] = 80.0 + rh[2, :] = 40.0 + rh[3, :] = 20.0 relh = rh * units.percent tmpk = tmp * units.kelvin - isentlev = [296.] * units.kelvin + isentlev = [296.0] * units.kelvin isentprs = isentropic_interpolation(isentlev, lev, tmpk, relh) - truerh = 100. * units.percent + truerh = 100.0 * units.percent assert_almost_equal(isentprs[1], truerh, 3) def test_isentropic_pressure_tmp_out(): """Test calculation of isentropic pressure function, temperature output.""" - lev = [100000., 95000., 90000., 85000.] * units.Pa + lev = [100000.0, 95000.0, 90000.0, 85000.0] * units.Pa tmp = np.ones((4, 5, 5)) - tmp[0, :] = 296. - tmp[1, :] = 292. - tmp[2, :] = 290. - tmp[3, :] = 288. + tmp[0, :] = 296.0 + tmp[1, :] = 292.0 + tmp[2, :] = 290.0 + tmp[3, :] = 288.0 tmpk = tmp * units.kelvin - isentlev = [296.] * units.kelvin + isentlev = [296.0] * units.kelvin isentprs = isentropic_interpolation(isentlev, lev, tmpk, temperature_out=True) - truetmp = 296. * units.kelvin + truetmp = 296.0 * units.kelvin assert_almost_equal(isentprs[1], truetmp, 3) def test_isentropic_pressure_p_increase_rh_out(): """Test calculation of isentropic pressure function, p increasing order.""" - lev = [85000., 90000., 95000., 100000.] * units.Pa + lev = [85000.0, 90000.0, 95000.0, 100000.0] * units.Pa tmp = np.ones((4, 5, 5)) - tmp[0, :] = 288. - tmp[1, :] = 290. - tmp[2, :] = 292. - tmp[3, :] = 296. + tmp[0, :] = 288.0 + tmp[1, :] = 290.0 + tmp[2, :] = 292.0 + tmp[3, :] = 296.0 tmpk = tmp * units.kelvin rh = np.ones((4, 5, 5)) - rh[0, :] = 20. - rh[1, :] = 40. - rh[2, :] = 80. - rh[3, :] = 100. + rh[0, :] = 20.0 + rh[1, :] = 40.0 + rh[2, :] = 80.0 + rh[3, :] = 100.0 relh = rh * units.percent - isentlev = 296. * units.kelvin + isentlev = 296.0 * units.kelvin isentprs = isentropic_interpolation(isentlev, lev, tmpk, relh) - truerh = 100. * units.percent + truerh = 100.0 * units.percent assert_almost_equal(isentprs[1], truerh, 3) def test_isentropic_pressure_interp(): """Test calculation of isentropic pressure function.""" - lev = [100000., 95000., 90000., 85000.] * units.Pa + lev = [100000.0, 95000.0, 90000.0, 85000.0] * units.Pa tmp = np.ones((4, 5, 5)) - tmp[0, :] = 296. - tmp[1, :] = 292. + tmp[0, :] = 296.0 + tmp[1, :] = 292.0 tmp[2, :] = 290 - tmp[3, :] = 288. + tmp[3, :] = 288.0 tmpk = tmp * units.kelvin - isentlev = [296., 297] * units.kelvin + isentlev = [296.0, 297] * units.kelvin isentprs = isentropic_interpolation(isentlev, lev, tmpk) trueprs = 936.18057 * units.hPa assert_almost_equal(isentprs[0][1], trueprs, 3) @@ -1031,20 +1911,20 @@ def test_isentropic_pressure_interp(): def test_isentropic_pressure_addition_args_interp(): """Test calculation of isentropic pressure function, additional args.""" - lev = [100000., 95000., 90000., 85000.] * units.Pa + lev = [100000.0, 95000.0, 90000.0, 85000.0] * units.Pa tmp = np.ones((4, 5, 5)) - tmp[0, :] = 296. - tmp[1, :] = 292. - tmp[2, :] = 290. - tmp[3, :] = 288. + tmp[0, :] = 296.0 + tmp[1, :] = 292.0 + tmp[2, :] = 290.0 + tmp[3, :] = 288.0 rh = np.ones((4, 5, 5)) - rh[0, :] = 100. - rh[1, :] = 80. - rh[2, :] = 40. - rh[3, :] = 20. + rh[0, :] = 100.0 + rh[1, :] = 80.0 + rh[2, :] = 40.0 + rh[3, :] = 20.0 relh = rh * units.percent tmpk = tmp * units.kelvin - isentlev = [296., 297.] * units.kelvin + isentlev = [296.0, 297.0] * units.kelvin isentprs = isentropic_interpolation(isentlev, lev, tmpk, relh) truerh = 69.171 * units.percent assert_almost_equal(isentprs[1][1], truerh, 3) @@ -1052,14 +1932,14 @@ def test_isentropic_pressure_addition_args_interp(): def test_isentropic_pressure_tmp_out_interp(): """Test calculation of isentropic pressure function, temperature output.""" - lev = [100000., 95000., 90000., 85000.] * units.Pa + lev = [100000.0, 95000.0, 90000.0, 85000.0] * units.Pa tmp = np.ones((4, 5, 5)) - tmp[0, :] = 296. - tmp[1, :] = 292. - tmp[2, :] = 290. - tmp[3, :] = 288. + tmp[0, :] = 296.0 + tmp[1, :] = 292.0 + tmp[2, :] = 290.0 + tmp[3, :] = 288.0 tmpk = tmp * units.kelvin - isentlev = [296., 297.] * units.kelvin + isentlev = [296.0, 297.0] * units.kelvin isentprs = isentropic_interpolation(isentlev, lev, tmpk, temperature_out=True) truetmp = 291.4579 * units.kelvin assert_almost_equal(isentprs[1][1], truetmp, 3) @@ -1067,36 +1947,36 @@ def test_isentropic_pressure_tmp_out_interp(): def test_isentropic_pressure_data_bounds_error(): """Test calculation of isentropic pressure function, error for data out of bounds.""" - lev = [100000., 95000., 90000., 85000.] * units.Pa + lev = [100000.0, 95000.0, 90000.0, 85000.0] * units.Pa tmp = np.ones((4, 5, 5)) - tmp[0, :] = 296. - tmp[1, :] = 292. - tmp[2, :] = 290. - tmp[3, :] = 288. + tmp[0, :] = 296.0 + tmp[1, :] = 292.0 + tmp[2, :] = 290.0 + tmp[3, :] = 288.0 tmpk = tmp * units.kelvin - isentlev = [296., 350.] * units.kelvin + isentlev = [296.0, 350.0] * units.kelvin with pytest.raises(ValueError): isentropic_interpolation(isentlev, lev, tmpk) def test_isentropic_pressure_4d(): """Test calculation of isentropic pressure function.""" - lev = [100000., 95000., 90000., 85000.] * units.Pa + lev = [100000.0, 95000.0, 90000.0, 85000.0] * units.Pa tmp = np.ones((3, 4, 5, 5)) - tmp[:, 0, :] = 296. - tmp[:, 1, :] = 292. + tmp[:, 0, :] = 296.0 + tmp[:, 1, :] = 292.0 tmp[:, 2, :] = 290 - tmp[:, 3, :] = 288. + tmp[:, 3, :] = 288.0 tmpk = tmp * units.kelvin rh = np.ones((3, 4, 5, 5)) - rh[:, 0, :] = 100. - rh[:, 1, :] = 80. - rh[:, 2, :] = 40. - rh[:, 3, :] = 20. + rh[:, 0, :] = 100.0 + rh[:, 1, :] = 80.0 + rh[:, 2, :] = 40.0 + rh[:, 3, :] = 20.0 relh = rh * units.percent - isentlev = [296., 297., 300.] * units.kelvin + isentlev = [296.0, 297.0, 300.0] * units.kelvin isentprs = isentropic_interpolation(isentlev, lev, tmpk, relh, vertical_dim=1) - trueprs = 1000. * units.hPa + trueprs = 1000.0 * units.hPa trueprs2 = 936.18057 * units.hPa trueprs3 = 879.446 * units.hPa truerh = 69.171 * units.percent @@ -1104,156 +1984,291 @@ def test_isentropic_pressure_4d(): assert_almost_equal(isentprs[0][:, 0, :], trueprs, 3) assert_almost_equal(isentprs[0][:, 1, :], trueprs2, 3) assert_almost_equal(isentprs[0][:, 2, :], trueprs3, 3) - assert_almost_equal(isentprs[1][:, 1, ], truerh, 3) + assert_almost_equal( + isentprs[1][ + :, + 1, + ], + truerh, + 3, + ) def test_isentropic_interpolation_as_dataset(): """Test calculation of isentropic interpolation with xarray.""" data = xr.Dataset( { - 'temperature': ( - ('isobaric', 'y', 'x'), - [[[296.]], [[292.]], [[290.]], [[288.]]] * units.K + "temperature": ( + ("isobaric", "y", "x"), + [[[296.0]], [[292.0]], [[290.0]], [[288.0]]] * units.K, + ), + "rh": ( + ("isobaric", "y", "x"), + [[[100.0]], [[80.0]], [[40.0]], [[20.0]]] * units.percent, ), - 'rh': ( - ('isobaric', 'y', 'x'), - [[[100.]], [[80.]], [[40.]], [[20.]]] * units.percent - ) }, coords={ - 'isobaric': (('isobaric',), [1000., 950., 900., 850.], {'units': 'hPa'}), - 'time': '2020-01-01T00:00Z' - } + "isobaric": (("isobaric",), [1000.0, 950.0, 900.0, 850.0], {"units": "hPa"}), + "time": "2020-01-01T00:00Z", + }, ) - isentlev = [296., 297.] * units.kelvin - result = isentropic_interpolation_as_dataset(isentlev, data['temperature'], data['rh']) + isentlev = [296.0, 297.0] * units.kelvin + result = isentropic_interpolation_as_dataset(isentlev, data["temperature"], data["rh"]) expected = xr.Dataset( { - 'pressure': ( - ('isentropic_level', 'y', 'x'), - [[[1000.]], [[936.18057]]] * units.hPa, - {'standard_name': 'air_pressure'} + "pressure": ( + ("isentropic_level", "y", "x"), + [[[1000.0]], [[936.18057]]] * units.hPa, + {"standard_name": "air_pressure"}, ), - 'temperature': ( - ('isentropic_level', 'y', 'x'), - [[[296.]], [[291.4579]]] * units.K, - {'standard_name': 'air_temperature'} + "temperature": ( + ("isentropic_level", "y", "x"), + [[[296.0]], [[291.4579]]] * units.K, + {"standard_name": "air_temperature"}, ), - 'rh': ( - ('isentropic_level', 'y', 'x'), - [[[100.]], [[69.171]]] * units.percent - ) + "rh": (("isentropic_level", "y", "x"), [[[100.0]], [[69.171]]] * units.percent), }, coords={ - 'isentropic_level': ( - ('isentropic_level',), - [296., 297.], - {'units': 'kelvin', 'positive': 'up'} + "isentropic_level": ( + ("isentropic_level",), + [296.0, 297.0], + {"units": "kelvin", "positive": "up"}, ), - 'time': '2020-01-01T00:00Z' - } + "time": "2020-01-01T00:00Z", + }, ) xr.testing.assert_allclose(result, expected) - assert result['pressure'].attrs == expected['pressure'].attrs - assert result['temperature'].attrs == expected['temperature'].attrs - assert result['isentropic_level'].attrs == expected['isentropic_level'].attrs + assert result["pressure"].attrs == expected["pressure"].attrs + assert result["temperature"].attrs == expected["temperature"].attrs + assert result["isentropic_level"].attrs == expected["isentropic_level"].attrs -@pytest.mark.parametrize('array_class', (units.Quantity, masked_array)) +@pytest.mark.parametrize("array_class", (units.Quantity, masked_array)) def test_surface_based_cape_cin(array_class): """Test the surface-based CAPE and CIN calculation.""" - p = array_class([959., 779.2, 751.3, 724.3, 700., 269.], units.mbar) - temperature = array_class([22.2, 14.6, 12., 9.4, 7., -38.], units.celsius) - dewpoint = array_class([19., -11.2, -10.8, -10.4, -10., -53.2], units.celsius) + p = array_class([959.0, 779.2, 751.3, 724.3, 700.0, 269.0], units.mbar) + temperature = array_class([22.2, 14.6, 12.0, 9.4, 7.0, -38.0], units.celsius) + dewpoint = array_class([19.0, -11.2, -10.8, -10.4, -10.0, -53.2], units.celsius) cape, cin = surface_based_cape_cin(p, temperature, dewpoint) - assert_almost_equal(cape, 75.7340825 * units('joule / kilogram'), 2) - assert_almost_equal(cin, -136.607809 * units('joule / kilogram'), 2) + assert_almost_equal(cape, 75.7340825 * units("joule / kilogram"), 2) + assert_almost_equal(cin, -136.607809 * units("joule / kilogram"), 2) def test_surface_based_cape_cin_with_xarray(): """Test the surface-based CAPE and CIN calculation with xarray.""" data = xr.Dataset( { - 'temperature': (('isobaric',), [22.2, 14.6, 12., 9.4, 7., -38.] * units.degC), - 'dewpoint': (('isobaric',), [19., -11.2, -10.8, -10.4, -10., -53.2] * units.degC) + "temperature": (("isobaric",), [22.2, 14.6, 12.0, 9.4, 7.0, -38.0] * units.degC), + "dewpoint": ( + ("isobaric",), + [19.0, -11.2, -10.8, -10.4, -10.0, -53.2] * units.degC, + ), }, coords={ - 'isobaric': ( - ('isobaric',), - [959., 779.2, 751.3, 724.3, 700., 269.], - {'units': 'hPa'} + "isobaric": ( + ("isobaric",), + [959.0, 779.2, 751.3, 724.3, 700.0, 269.0], + {"units": "hPa"}, ) - } - ) - cape, cin = surface_based_cape_cin( - data['isobaric'], - data['temperature'], - data['dewpoint'] + }, ) - assert_almost_equal(cape, 75.7340825 * units('joule / kilogram'), 2) - assert_almost_equal(cin, -136.607809 * units('joule / kilogram'), 2) + cape, cin = surface_based_cape_cin(data["isobaric"], data["temperature"], data["dewpoint"]) + assert_almost_equal(cape, 75.7340825 * units("joule / kilogram"), 2) + assert_almost_equal(cin, -136.607809 * units("joule / kilogram"), 2) def test_profile_with_nans(): """Test a profile with nans to make sure it calculates functions appropriately (#1187).""" - pressure = np.array([1001, 1000, 997, 977.9, 977, 957, 937.8, 925, 906, 899.3, 887, 862.5, - 854, 850, 800, 793.9, 785, 777, 771, 762, 731.8, 726, 703, 700, 655, - 630, 621.2, 602, 570.7, 548, 546.8, 539, 513, 511, 485, 481, 468, - 448, 439, 424, 420, 412]) * units.hPa - temperature = np.array([-22.5, -22.7, -23.1, np.nan, -24.5, -25.1, np.nan, -24.5, -23.9, - np.nan, -24.7, np.nan, -21.3, -21.3, -22.7, np.nan, -20.7, -16.3, - -15.5, np.nan, np.nan, -15.3, np.nan, -17.3, -20.9, -22.5, - np.nan, -25.5, np.nan, -31.5, np.nan, -31.5, -34.1, -34.3, - -37.3, -37.7, -39.5, -42.1, -43.1, -45.1, -45.7, -46.7] - ) * units.degC - dewpoint = np.array([-25.1, -26.1, -26.8, np.nan, -27.3, -28.2, np.nan, -27.2, -26.6, - np.nan, -27.4, np.nan, -23.5, -23.5, -25.1, np.nan, -22.9, -17.8, - -16.6, np.nan, np.nan, -16.4, np.nan, -18.5, -21, -23.7, np.nan, - -28.3, np.nan, -32.6, np.nan, -33.8, -35, -35.1, -38.1, -40, - -43.3, -44.6, -46.4, -47, -49.2, -50.7]) * units.degC + pressure = ( + np.array( + [ + 1001, + 1000, + 997, + 977.9, + 977, + 957, + 937.8, + 925, + 906, + 899.3, + 887, + 862.5, + 854, + 850, + 800, + 793.9, + 785, + 777, + 771, + 762, + 731.8, + 726, + 703, + 700, + 655, + 630, + 621.2, + 602, + 570.7, + 548, + 546.8, + 539, + 513, + 511, + 485, + 481, + 468, + 448, + 439, + 424, + 420, + 412, + ] + ) + * units.hPa + ) + temperature = ( + np.array( + [ + -22.5, + -22.7, + -23.1, + np.nan, + -24.5, + -25.1, + np.nan, + -24.5, + -23.9, + np.nan, + -24.7, + np.nan, + -21.3, + -21.3, + -22.7, + np.nan, + -20.7, + -16.3, + -15.5, + np.nan, + np.nan, + -15.3, + np.nan, + -17.3, + -20.9, + -22.5, + np.nan, + -25.5, + np.nan, + -31.5, + np.nan, + -31.5, + -34.1, + -34.3, + -37.3, + -37.7, + -39.5, + -42.1, + -43.1, + -45.1, + -45.7, + -46.7, + ] + ) + * units.degC + ) + dewpoint = ( + np.array( + [ + -25.1, + -26.1, + -26.8, + np.nan, + -27.3, + -28.2, + np.nan, + -27.2, + -26.6, + np.nan, + -27.4, + np.nan, + -23.5, + -23.5, + -25.1, + np.nan, + -22.9, + -17.8, + -16.6, + np.nan, + np.nan, + -16.4, + np.nan, + -18.5, + -21, + -23.7, + np.nan, + -28.3, + np.nan, + -32.6, + np.nan, + -33.8, + -35, + -35.1, + -38.1, + -40, + -43.3, + -44.6, + -46.4, + -47, + -49.2, + -50.7, + ] + ) + * units.degC + ) lfc_p, _ = lfc(pressure, temperature, dewpoint) profile = parcel_profile(pressure, temperature[0], dewpoint[0]) cape, cin = cape_cin(pressure, temperature, dewpoint, profile) sbcape, sbcin = surface_based_cape_cin(pressure, temperature, dewpoint) mucape, mucin = most_unstable_cape_cin(pressure, temperature, dewpoint) assert_nan(lfc_p, units.hPa) - assert_almost_equal(cape, 0 * units('J/kg'), 0) - assert_almost_equal(cin, 0 * units('J/kg'), 0) - assert_almost_equal(sbcape, 0 * units('J/kg'), 0) - assert_almost_equal(sbcin, 0 * units('J/kg'), 0) - assert_almost_equal(mucape, 0 * units('J/kg'), 0) - assert_almost_equal(mucin, 0 * units('J/kg'), 0) + assert_almost_equal(cape, 0 * units("J/kg"), 0) + assert_almost_equal(cin, 0 * units("J/kg"), 0) + assert_almost_equal(sbcape, 0 * units("J/kg"), 0) + assert_almost_equal(sbcin, 0 * units("J/kg"), 0) + assert_almost_equal(mucape, 0 * units("J/kg"), 0) + assert_almost_equal(mucin, 0 * units("J/kg"), 0) def test_most_unstable_cape_cin_surface(): """Test the most unstable CAPE/CIN calculation when surface is most unstable.""" - pressure = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.mbar - temperature = np.array([22.2, 14.6, 12., 9.4, 7., -38.]) * units.celsius - dewpoint = np.array([19., -11.2, -10.8, -10.4, -10., -53.2]) * units.celsius + pressure = np.array([959.0, 779.2, 751.3, 724.3, 700.0, 269.0]) * units.mbar + temperature = np.array([22.2, 14.6, 12.0, 9.4, 7.0, -38.0]) * units.celsius + dewpoint = np.array([19.0, -11.2, -10.8, -10.4, -10.0, -53.2]) * units.celsius mucape, mucin = most_unstable_cape_cin(pressure, temperature, dewpoint) - assert_almost_equal(mucape, 75.7340825 * units('joule / kilogram'), 2) - assert_almost_equal(mucin, -136.607809 * units('joule / kilogram'), 2) + assert_almost_equal(mucape, 75.7340825 * units("joule / kilogram"), 2) + assert_almost_equal(mucin, -136.607809 * units("joule / kilogram"), 2) def test_most_unstable_cape_cin(): """Test the most unstable CAPE/CIN calculation.""" - pressure = np.array([1000., 959., 867.9, 850., 825., 800.]) * units.mbar - temperature = np.array([18.2, 22.2, 17.4, 10., 0., 15]) * units.celsius - dewpoint = np.array([19., 19., 14.3, 0., -10., 0.]) * units.celsius + pressure = np.array([1000.0, 959.0, 867.9, 850.0, 825.0, 800.0]) * units.mbar + temperature = np.array([18.2, 22.2, 17.4, 10.0, 0.0, 15]) * units.celsius + dewpoint = np.array([19.0, 19.0, 14.3, 0.0, -10.0, 0.0]) * units.celsius mucape, mucin = most_unstable_cape_cin(pressure, temperature, dewpoint) - assert_almost_equal(mucape, 157.1401 * units('joule / kilogram'), 4) - assert_almost_equal(mucin, -31.82547 * units('joule / kilogram'), 4) + assert_almost_equal(mucape, 157.1401 * units("joule / kilogram"), 4) + assert_almost_equal(mucin, -31.82547 * units("joule / kilogram"), 4) def test_mixed_parcel(): """Test the mixed parcel calculation.""" - pressure = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.hPa - temperature = np.array([22.2, 14.6, 12., 9.4, 7., -38.]) * units.degC - dewpoint = np.array([19., -11.2, -10.8, -10.4, -10., -53.2]) * units.degC - parcel_pressure, parcel_temperature, parcel_dewpoint = mixed_parcel(pressure, temperature, - dewpoint, - depth=250 * units.hPa) - assert_almost_equal(parcel_pressure, 959. * units.hPa, 6) + pressure = np.array([959.0, 779.2, 751.3, 724.3, 700.0, 269.0]) * units.hPa + temperature = np.array([22.2, 14.6, 12.0, 9.4, 7.0, -38.0]) * units.degC + dewpoint = np.array([19.0, -11.2, -10.8, -10.4, -10.0, -53.2]) * units.degC + parcel_pressure, parcel_temperature, parcel_dewpoint = mixed_parcel( + pressure, temperature, dewpoint, depth=250 * units.hPa + ) + assert_almost_equal(parcel_pressure, 959.0 * units.hPa, 6) assert_almost_equal(parcel_temperature, 28.7363771 * units.degC, 6) assert_almost_equal(parcel_dewpoint, 7.1534658 * units.degC, 6) @@ -1262,14 +2277,14 @@ def test_mixed_layer_cape_cin(multiple_intersections): """Test the calculation of mixed layer cape/cin.""" pressure, temperature, dewpoint = multiple_intersections mlcape, mlcin = mixed_layer_cape_cin(pressure, temperature, dewpoint) - assert_almost_equal(mlcape, 991.4484 * units('joule / kilogram'), 2) - assert_almost_equal(mlcin, -20.6552 * units('joule / kilogram'), 2) + assert_almost_equal(mlcape, 991.4484 * units("joule / kilogram"), 2) + assert_almost_equal(mlcin, -20.6552 * units("joule / kilogram"), 2) def test_mixed_layer(): """Test the mixed layer calculation.""" - pressure = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.hPa - temperature = np.array([22.2, 14.6, 12., 9.4, 7., -38.]) * units.degC + pressure = np.array([959.0, 779.2, 751.3, 724.3, 700.0, 269.0]) * units.hPa + temperature = np.array([22.2, 14.6, 12.0, 9.4, 7.0, -38.0]) * units.degC mixed_layer_temperature = mixed_layer(pressure, temperature, depth=250 * units.hPa)[0] assert_almost_equal(mixed_layer_temperature, 16.4024930 * units.degC, 6) @@ -1277,19 +2292,19 @@ def test_mixed_layer(): def test_dry_static_energy(): """Test the dry static energy calculation.""" dse = dry_static_energy(1000 * units.m, 25 * units.degC) - assert_almost_equal(dse, 309.4474 * units('kJ/kg'), 6) + assert_almost_equal(dse, 309.4474 * units("kJ/kg"), 6) def test_moist_static_energy(): """Test the moist static energy calculation.""" mse = moist_static_energy(1000 * units.m, 25 * units.degC, 0.012 * units.dimensionless) - assert_almost_equal(mse, 339.4594 * units('kJ/kg'), 6) + assert_almost_equal(mse, 339.4594 * units("kJ/kg"), 6) def test_thickness_hydrostatic(): """Test the thickness calculation for a moist layer.""" - pressure = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.hPa - temperature = np.array([22.2, 14.6, 12., 9.4, 7., -38.]) * units.degC + pressure = np.array([959.0, 779.2, 751.3, 724.3, 700.0, 269.0]) * units.hPa + temperature = np.array([22.2, 14.6, 12.0, 9.4, 7.0, -38.0]) * units.degC mixing = np.array([0.01458, 0.00209, 0.00224, 0.00240, 0.00256, 0.00010]) thickness = thickness_hydrostatic(pressure, temperature, mixing_ratio=mixing) assert_almost_equal(thickness, 9892.07 * units.m, 2) @@ -1297,11 +2312,16 @@ def test_thickness_hydrostatic(): def test_thickness_hydrostatic_subset(): """Test the thickness calculation with a subset of the moist layer.""" - pressure = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.hPa - temperature = np.array([22.2, 14.6, 12., 9.4, 7., -38.]) * units.degC + pressure = np.array([959.0, 779.2, 751.3, 724.3, 700.0, 269.0]) * units.hPa + temperature = np.array([22.2, 14.6, 12.0, 9.4, 7.0, -38.0]) * units.degC mixing = np.array([0.01458, 0.00209, 0.00224, 0.00240, 0.00256, 0.00010]) - thickness = thickness_hydrostatic(pressure, temperature, mixing_ratio=mixing, - bottom=850 * units.hPa, depth=150 * units.hPa) + thickness = thickness_hydrostatic( + pressure, + temperature, + mixing_ratio=mixing, + bottom=850 * units.hPa, + depth=150 * units.hPa, + ) assert_almost_equal(thickness, 1630.81 * units.m, 2) @@ -1317,81 +2337,92 @@ def test_thickness_hydrostatic_isothermal_subset(): """Test the thickness calculation for a dry isothermal layer subset at 0 degC.""" pressure = np.arange(1000, 500 - 1e-10, -10) * units.hPa temperature = np.zeros_like(pressure) * units.degC - thickness = thickness_hydrostatic(pressure, temperature, bottom=850 * units.hPa, - depth=350 * units.hPa) + thickness = thickness_hydrostatic( + pressure, temperature, bottom=850 * units.hPa, depth=350 * units.hPa + ) assert_almost_equal(thickness, 4242.68 * units.m, 2) def test_thickness_hydrostatic_from_relative_humidity(): """Test the thickness calculation for a moist layer using RH data.""" - pressure = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.hPa - temperature = np.array([22.2, 14.6, 12., 9.4, 7., -38.]) * units.degC + pressure = np.array([959.0, 779.2, 751.3, 724.3, 700.0, 269.0]) * units.hPa + temperature = np.array([22.2, 14.6, 12.0, 9.4, 7.0, -38.0]) * units.degC relative_humidity = np.array([81.69, 15.43, 18.95, 23.32, 28.36, 18.55]) * units.percent - thickness = thickness_hydrostatic_from_relative_humidity(pressure, temperature, - relative_humidity) + thickness = thickness_hydrostatic_from_relative_humidity( + pressure, temperature, relative_humidity + ) assert_almost_equal(thickness, 9892.07 * units.m, 2) def test_mixing_ratio_dimensions(): """Verify mixing ratio returns a dimensionless number.""" - p = 998. * units.mbar + p = 998.0 * units.mbar e = 73.75 * units.hPa - assert str(mixing_ratio(e, p).units) == 'dimensionless' + assert str(mixing_ratio(e, p).units) == "dimensionless" def test_saturation_mixing_ratio_dimensions(): """Verify saturation mixing ratio returns a dimensionless number.""" - p = 998. * units.mbar + p = 998.0 * units.mbar temp = 20 * units.celsius - assert str(saturation_mixing_ratio(p, temp).units) == 'dimensionless' + assert str(saturation_mixing_ratio(p, temp).units) == "dimensionless" def test_mixing_ratio_from_rh_dimensions(): """Verify mixing ratio from RH returns a dimensionless number.""" - p = 1000. * units.mbar - temperature = 0. * units.degC - rh = 100. * units.percent - assert (str(mixing_ratio_from_relative_humidity(p, temperature, rh).units) - == 'dimensionless') + p = 1000.0 * units.mbar + temperature = 0.0 * units.degC + rh = 100.0 * units.percent + assert ( + str(mixing_ratio_from_relative_humidity(p, temperature, rh).units) == "dimensionless" + ) @pytest.fixture def bv_data(): """Return height and potential temperature data for testing Brunt-Vaisala functions.""" - heights = [1000., 1500., 2000., 2500.] * units('m') - potential_temperatures = [[290., 290., 290., 290.], - [292., 293., 293., 292.], - [294., 296., 293., 293.], - [296., 295., 293., 296.]] * units('K') + heights = [1000.0, 1500.0, 2000.0, 2500.0] * units("m") + potential_temperatures = [ + [290.0, 290.0, 290.0, 290.0], + [292.0, 293.0, 293.0, 292.0], + [294.0, 296.0, 293.0, 293.0], + [296.0, 295.0, 293.0, 296.0], + ] * units("K") return heights, potential_temperatures def test_brunt_vaisala_frequency_squared(bv_data): """Test Brunt-Vaisala frequency squared function.""" - truth = [[1.35264138e-04, 2.02896207e-04, 3.04344310e-04, 1.69080172e-04], - [1.34337671e-04, 2.00818771e-04, 1.00409386e-04, 1.00753253e-04], - [1.33423810e-04, 6.62611486e-05, 0, 1.33879181e-04], - [1.32522297e-04, -1.99457288e-04, 0., 2.65044595e-04]] * units('s^-2') + truth = [ + [1.35264138e-04, 2.02896207e-04, 3.04344310e-04, 1.69080172e-04], + [1.34337671e-04, 2.00818771e-04, 1.00409386e-04, 1.00753253e-04], + [1.33423810e-04, 6.62611486e-05, 0, 1.33879181e-04], + [1.32522297e-04, -1.99457288e-04, 0.0, 2.65044595e-04], + ] * units("s^-2") bv_freq_sqr = brunt_vaisala_frequency_squared(bv_data[0], bv_data[1]) assert_almost_equal(bv_freq_sqr, truth, 6) def test_brunt_vaisala_frequency(bv_data): """Test Brunt-Vaisala frequency function.""" - truth = [[0.01163031, 0.01424416, 0.01744547, 0.01300308], - [0.01159041, 0.01417105, 0.01002045, 0.01003759], - [0.01155092, 0.00814010, 0., 0.01157062], - [0.01151183, np.nan, 0., 0.01628019]] * units('s^-1') + truth = [ + [0.01163031, 0.01424416, 0.01744547, 0.01300308], + [0.01159041, 0.01417105, 0.01002045, 0.01003759], + [0.01155092, 0.00814010, 0.0, 0.01157062], + [0.01151183, np.nan, 0.0, 0.01628019], + ] * units("s^-1") bv_freq = brunt_vaisala_frequency(bv_data[0], bv_data[1]) assert_almost_equal(bv_freq, truth, 6) def test_brunt_vaisala_period(bv_data): """Test Brunt-Vaisala period function.""" - truth = [[540.24223556, 441.10593821, 360.16149037, 483.20734521], - [542.10193894, 443.38165033, 627.03634320, 625.96540075], - [543.95528431, 771.88106656, np.nan, 543.02940230], - [545.80233643, np.nan, np.nan, 385.94053328]] * units('s') + truth = [ + [540.24223556, 441.10593821, 360.16149037, 483.20734521], + [542.10193894, 443.38165033, 627.03634320, 625.96540075], + [543.95528431, 771.88106656, np.nan, 543.02940230], + [545.80233643, np.nan, np.nan, 385.94053328], + ] * units("s") bv_period = brunt_vaisala_period(bv_data[0], bv_data[1]) assert_almost_equal(bv_period, truth, 6) @@ -1416,15 +2447,14 @@ def test_wet_bulb_temperature_1d(): def test_wet_bulb_temperature_2d(): """Test wet bulb calculation with 2d list.""" - pressures = [[1013, 1000, 990], - [1012, 999, 989]] * units.hPa - temperatures = [[25, 20, 15], - [24, 19, 14]] * units.degC - dewpoints = [[20, 15, 10], - [19, 14, 9]] * units.degC + pressures = [[1013, 1000, 990], [1012, 999, 989]] * units.hPa + temperatures = [[25, 20, 15], [24, 19, 14]] * units.degC + dewpoints = [[20, 15, 10], [19, 14, 9]] * units.degC val = wet_bulb_temperature(pressures, temperatures, dewpoints) - truth = [[21.4449794, 16.7368576, 12.0656909], - [20.5021631, 15.801218, 11.1361878]] * units.degC + truth = [ + [21.4449794, 16.7368576, 12.0656909], + [20.5021631, 15.801218, 11.1361878], + ] * units.degC # 21.58, 16.86, 12.18 # 20.6, 15.9, 11.2 from NWS Calculator assert_array_almost_equal(val, truth, 5) @@ -1432,34 +2462,36 @@ def test_wet_bulb_temperature_2d(): def test_static_stability_adiabatic(): """Test static stability calculation with a dry adiabatic profile.""" - pressures = [1000., 900., 800., 700., 600., 500.] * units.hPa + pressures = [1000.0, 900.0, 800.0, 700.0, 600.0, 500.0] * units.hPa temperature_start = 20 * units.degC temperatures = dry_lapse(pressures, temperature_start) sigma = static_stability(pressures, temperatures) - truth = np.zeros_like(pressures) * units('J kg^-1 hPa^-2') + truth = np.zeros_like(pressures) * units("J kg^-1 hPa^-2") # Should be zero with a dry adiabatic profile assert_almost_equal(sigma, truth, 6) def test_static_stability_cross_section(): """Test static stability calculation with a 2D cross-section.""" - pressures = [[850., 700., 500.], - [850., 700., 500.], - [850., 700., 500.]] * units.hPa - temperatures = [[17., 11., -10.], - [16., 10., -11.], - [11., 6., -12.]] * units.degC + pressures = [ + [850.0, 700.0, 500.0], + [850.0, 700.0, 500.0], + [850.0, 700.0, 500.0], + ] * units.hPa + temperatures = [[17.0, 11.0, -10.0], [16.0, 10.0, -11.0], [11.0, 6.0, -12.0]] * units.degC sigma = static_stability(pressures, temperatures, vertical_dim=1) - truth = [[0.02819452, 0.02016804, 0.00305262], - [0.02808841, 0.01999462, 0.00274956], - [0.02840196, 0.02366708, 0.0131604]] * units('J kg^-1 hPa^-2') + truth = [ + [0.02819452, 0.02016804, 0.00305262], + [0.02808841, 0.01999462, 0.00274956], + [0.02840196, 0.02366708, 0.0131604], + ] * units("J kg^-1 hPa^-2") assert_almost_equal(sigma, truth, 6) def test_dewpoint_specific_humidity(): """Test relative humidity from specific humidity.""" p = 1013.25 * units.mbar - temperature = 20. * units.degC + temperature = 20.0 * units.degC q = 0.012 * units.dimensionless td = dewpoint_from_specific_humidity(p, temperature, q) assert_almost_equal(td, 16.973 * units.degC, 3) @@ -1467,15 +2499,99 @@ def test_dewpoint_specific_humidity(): def test_lfc_not_below_lcl(): """Test sounding where LFC appears to be (but isn't) below LCL.""" - levels = np.array([1002.5, 1001.7, 1001., 1000.3, 999.7, 999., 998.2, 977.9, - 966.2, 952.3, 940.6, 930.5, 919.8, 909.1, 898.9, 888.4, - 878.3, 868.1, 858., 848., 837.2, 827., 816.7, 805.4]) * units.hPa - temperatures = np.array([17.9, 17.9, 17.8, 17.7, 17.7, 17.6, 17.5, 16., - 15.2, 14.5, 13.8, 13., 12.5, 11.9, 11.4, 11., - 10.3, 9.7, 9.2, 8.7, 8., 7.4, 6.8, 6.1]) * units.degC - dewpoints = np.array([13.6, 13.6, 13.5, 13.5, 13.5, 13.5, 13.4, 12.5, - 12.1, 11.8, 11.4, 11.3, 11., 9.3, 10., 8.7, 8.9, - 8.6, 8.1, 7.6, 7., 6.5, 6., 5.4]) * units.degC + levels = ( + np.array( + [ + 1002.5, + 1001.7, + 1001.0, + 1000.3, + 999.7, + 999.0, + 998.2, + 977.9, + 966.2, + 952.3, + 940.6, + 930.5, + 919.8, + 909.1, + 898.9, + 888.4, + 878.3, + 868.1, + 858.0, + 848.0, + 837.2, + 827.0, + 816.7, + 805.4, + ] + ) + * units.hPa + ) + temperatures = ( + np.array( + [ + 17.9, + 17.9, + 17.8, + 17.7, + 17.7, + 17.6, + 17.5, + 16.0, + 15.2, + 14.5, + 13.8, + 13.0, + 12.5, + 11.9, + 11.4, + 11.0, + 10.3, + 9.7, + 9.2, + 8.7, + 8.0, + 7.4, + 6.8, + 6.1, + ] + ) + * units.degC + ) + dewpoints = ( + np.array( + [ + 13.6, + 13.6, + 13.5, + 13.5, + 13.5, + 13.5, + 13.4, + 12.5, + 12.1, + 11.8, + 11.4, + 11.3, + 11.0, + 9.3, + 10.0, + 8.7, + 8.9, + 8.6, + 8.1, + 7.6, + 7.0, + 6.5, + 6.0, + 5.4, + ] + ) + * units.degC + ) lfc_pressure, lfc_temp = lfc(levels, temperatures, dewpoints) # Before patch, LFC pressure would show 1000.5912165339967 hPa assert_almost_equal(lfc_pressure, 811.8263397 * units.mbar, 3) @@ -1485,20 +2601,141 @@ def test_lfc_not_below_lcl(): @pytest.fixture def multiple_intersections(): """Create profile with multiple LFCs and ELs for testing.""" - levels = np.array([966., 937.2, 925., 904.6, 872.6, 853., 850., 836., 821., 811.6, 782.3, - 754.2, 726.9, 700., 648.9, 624.6, 601.1, 595., 587., 576., 555.7, - 534.2, 524., 500., 473.3, 400., 384.5, 358., 343., 308.3, 300., 276., - 273., 268.5, 250., 244.2, 233., 200.]) * units.mbar - temperatures = np.array([18.2, 16.8, 16.2, 15.1, 13.3, 12.2, 12.4, 14., 14.4, - 13.7, 11.4, 9.1, 6.8, 4.4, -1.4, -4.4, -7.3, -8.1, - -7.9, -7.7, -8.7, -9.8, -10.3, -13.5, -17.1, -28.1, -30.7, - -35.3, -37.1, -43.5, -45.1, -49.9, -50.4, -51.1, -54.1, -55., - -56.7, -57.5]) * units.degC - dewpoints = np.array([16.9, 15.9, 15.5, 14.2, 12.1, 10.8, 8.6, 0., -3.6, -4.4, - -6.9, -9.5, -12., -14.6, -15.8, -16.4, -16.9, -17.1, -27.9, -42.7, - -44.1, -45.6, -46.3, -45.5, -47.1, -52.1, -50.4, -47.3, -57.1, - -57.9, -58.1, -60.9, -61.4, -62.1, -65.1, -65.6, - -66.7, -70.5]) * units.degC + levels = ( + np.array( + [ + 966.0, + 937.2, + 925.0, + 904.6, + 872.6, + 853.0, + 850.0, + 836.0, + 821.0, + 811.6, + 782.3, + 754.2, + 726.9, + 700.0, + 648.9, + 624.6, + 601.1, + 595.0, + 587.0, + 576.0, + 555.7, + 534.2, + 524.0, + 500.0, + 473.3, + 400.0, + 384.5, + 358.0, + 343.0, + 308.3, + 300.0, + 276.0, + 273.0, + 268.5, + 250.0, + 244.2, + 233.0, + 200.0, + ] + ) + * units.mbar + ) + temperatures = ( + np.array( + [ + 18.2, + 16.8, + 16.2, + 15.1, + 13.3, + 12.2, + 12.4, + 14.0, + 14.4, + 13.7, + 11.4, + 9.1, + 6.8, + 4.4, + -1.4, + -4.4, + -7.3, + -8.1, + -7.9, + -7.7, + -8.7, + -9.8, + -10.3, + -13.5, + -17.1, + -28.1, + -30.7, + -35.3, + -37.1, + -43.5, + -45.1, + -49.9, + -50.4, + -51.1, + -54.1, + -55.0, + -56.7, + -57.5, + ] + ) + * units.degC + ) + dewpoints = ( + np.array( + [ + 16.9, + 15.9, + 15.5, + 14.2, + 12.1, + 10.8, + 8.6, + 0.0, + -3.6, + -4.4, + -6.9, + -9.5, + -12.0, + -14.6, + -15.8, + -16.4, + -16.9, + -17.1, + -27.9, + -42.7, + -44.1, + -45.6, + -46.3, + -45.5, + -47.1, + -52.1, + -50.4, + -47.3, + -57.1, + -57.9, + -58.1, + -60.9, + -61.4, + -62.1, + -65.1, + -65.6, + -66.7, + -70.5, + ] + ) + * units.degC + ) return levels, temperatures, dewpoints @@ -1512,9 +2749,8 @@ def test_multiple_lfcs_simple(multiple_intersections): """ levels, temperatures, dewpoints = multiple_intersections lfc_pressure_top, lfc_temp_top = lfc(levels, temperatures, dewpoints) - lfc_pressure_bottom, lfc_temp_bottom = lfc(levels, temperatures, dewpoints, - which='bottom') - lfc_pressure_all, _ = lfc(levels, temperatures, dewpoints, which='all') + lfc_pressure_bottom, lfc_temp_bottom = lfc(levels, temperatures, dewpoints, which="bottom") + lfc_pressure_all, _ = lfc(levels, temperatures, dewpoints, which="all") assert_almost_equal(lfc_pressure_top, 705.4346277 * units.mbar, 3) assert_almost_equal(lfc_temp_top, 4.8922235 * units.degC, 3) assert_almost_equal(lfc_pressure_bottom, 884.1954356 * units.mbar, 3) @@ -1525,7 +2761,7 @@ def test_multiple_lfcs_simple(multiple_intersections): def test_multiple_lfs_wide(multiple_intersections): """Test 'wide' LFC for sounding with multiple LFCs.""" levels, temperatures, dewpoints = multiple_intersections - lfc_pressure_wide, lfc_temp_wide = lfc(levels, temperatures, dewpoints, which='wide') + lfc_pressure_wide, lfc_temp_wide = lfc(levels, temperatures, dewpoints, which="wide") assert_almost_equal(lfc_pressure_wide, 705.4346277 * units.hPa, 3) assert_almost_equal(lfc_temp_wide, 4.8922235 * units.degC, 3) @@ -1534,9 +2770,9 @@ def test_invalid_which(multiple_intersections): """Test error message for invalid which option for LFC and EL.""" levels, temperatures, dewpoints = multiple_intersections with pytest.raises(ValueError): - lfc(levels, temperatures, dewpoints, which='test') + lfc(levels, temperatures, dewpoints, which="test") with pytest.raises(ValueError): - el(levels, temperatures, dewpoints, which='test') + el(levels, temperatures, dewpoints, which="test") def test_multiple_els_simple(multiple_intersections): @@ -1549,8 +2785,8 @@ def test_multiple_els_simple(multiple_intersections): """ levels, temperatures, dewpoints = multiple_intersections el_pressure_top, el_temp_top = el(levels, temperatures, dewpoints) - el_pressure_bottom, el_temp_bottom = el(levels, temperatures, dewpoints, which='bottom') - el_pressure_all, _ = el(levels, temperatures, dewpoints, which='all') + el_pressure_bottom, el_temp_bottom = el(levels, temperatures, dewpoints, which="bottom") + el_pressure_all, _ = el(levels, temperatures, dewpoints, which="all") assert_almost_equal(el_pressure_top, 228.0575059 * units.mbar, 3) assert_almost_equal(el_temp_top, -56.8123126 * units.degC, 3) assert_almost_equal(el_pressure_bottom, 849.7942185 * units.mbar, 3) @@ -1561,7 +2797,7 @@ def test_multiple_els_simple(multiple_intersections): def test_multiple_el_wide(multiple_intersections): """Test 'wide' EL for sounding with multiple ELs.""" levels, temperatures, dewpoints = multiple_intersections - el_pressure_wide, el_temp_wide = el(levels, temperatures, dewpoints, which='wide') + el_pressure_wide, el_temp_wide = el(levels, temperatures, dewpoints, which="wide") assert_almost_equal(el_pressure_wide, 228.0575059 * units.hPa, 3) assert_almost_equal(el_temp_wide, -56.8123126 * units.degC, 3) @@ -1569,7 +2805,7 @@ def test_multiple_el_wide(multiple_intersections): def test_muliple_el_most_cape(multiple_intersections): """Test 'most_cape' EL for sounding with multiple ELs.""" levels, temperatures, dewpoints = multiple_intersections - el_pressure_wide, el_temp_wide = el(levels, temperatures, dewpoints, which='most_cape') + el_pressure_wide, el_temp_wide = el(levels, temperatures, dewpoints, which="most_cape") assert_almost_equal(el_pressure_wide, 228.0575059 * units.hPa, 3) assert_almost_equal(el_temp_wide, -56.8123126 * units.degC, 3) @@ -1577,21 +2813,48 @@ def test_muliple_el_most_cape(multiple_intersections): def test_muliple_lfc_most_cape(multiple_intersections): """Test 'most_cape' LFC for sounding with multiple LFCs.""" levels, temperatures, dewpoints = multiple_intersections - lfc_pressure_wide, lfc_temp_wide = lfc(levels, temperatures, dewpoints, which='most_cape') + lfc_pressure_wide, lfc_temp_wide = lfc(levels, temperatures, dewpoints, which="most_cape") assert_almost_equal(lfc_pressure_wide, 705.4346277 * units.hPa, 3) assert_almost_equal(lfc_temp_wide, 4.8922235 * units.degC, 3) def test_el_lfc_most_cape_bottom(): """Test 'most_cape' LFC/EL when the bottom combination produces the most CAPE.""" - levels = np.array([966., 937.2, 904.6, 872.6, 853., 850., 836., 821., 811.6, 782.3, - 754.2, 726.9, 700., 648.9]) * units.mbar - temperatures = np.array([18.2, 16.5, 15.1, 11.5, 11.0, 12.4, 14., 14.4, - 13.7, 11.4, 9.1, 6.8, 3.8, 1.5]) * units.degC - dewpoints = np.array([16.9, 15.9, 14.2, 11, 9.5, 8.6, 0., -3.6, -4.4, - -6.9, -9.5, -12., -14.6, -15.8]) * units.degC - lfc_pres, lfc_temp = lfc(levels, temperatures, dewpoints, which='most_cape') - el_pres, el_temp = el(levels, temperatures, dewpoints, which='most_cape') + levels = ( + np.array( + [ + 966.0, + 937.2, + 904.6, + 872.6, + 853.0, + 850.0, + 836.0, + 821.0, + 811.6, + 782.3, + 754.2, + 726.9, + 700.0, + 648.9, + ] + ) + * units.mbar + ) + temperatures = ( + np.array( + [18.2, 16.5, 15.1, 11.5, 11.0, 12.4, 14.0, 14.4, 13.7, 11.4, 9.1, 6.8, 3.8, 1.5] + ) + * units.degC + ) + dewpoints = ( + np.array( + [16.9, 15.9, 14.2, 11, 9.5, 8.6, 0.0, -3.6, -4.4, -6.9, -9.5, -12.0, -14.6, -15.8] + ) + * units.degC + ) + lfc_pres, lfc_temp = lfc(levels, temperatures, dewpoints, which="most_cape") + el_pres, el_temp = el(levels, temperatures, dewpoints, which="most_cape") assert_almost_equal(lfc_pres, 900.7395292 * units.hPa, 3) assert_almost_equal(lfc_temp, 14.672512 * units.degC, 3) assert_almost_equal(el_pres, 849.7942184 * units.hPa, 3) @@ -1601,85 +2864,126 @@ def test_el_lfc_most_cape_bottom(): def test_cape_cin_top_el_lfc(multiple_intersections): """Test using LFC/EL options for CAPE/CIN.""" levels, temperatures, dewpoints = multiple_intersections - parcel_prof = parcel_profile(levels, temperatures[0], dewpoints[0]).to('degC') - cape, cin = cape_cin(levels, temperatures, dewpoints, parcel_prof, which_lfc='top') - assert_almost_equal(cape, 1262.8618 * units('joule / kilogram'), 3) - assert_almost_equal(cin, -97.6499 * units('joule / kilogram'), 3) + parcel_prof = parcel_profile(levels, temperatures[0], dewpoints[0]).to("degC") + cape, cin = cape_cin(levels, temperatures, dewpoints, parcel_prof, which_lfc="top") + assert_almost_equal(cape, 1262.8618 * units("joule / kilogram"), 3) + assert_almost_equal(cin, -97.6499 * units("joule / kilogram"), 3) def test_cape_cin_bottom_el_lfc(multiple_intersections): """Test using LFC/EL options for CAPE/CIN.""" levels, temperatures, dewpoints = multiple_intersections - parcel_prof = parcel_profile(levels, temperatures[0], dewpoints[0]).to('degC') - cape, cin = cape_cin(levels, temperatures, dewpoints, parcel_prof, which_el='bottom') - assert_almost_equal(cape, 2.1967 * units('joule / kilogram'), 3) - assert_almost_equal(cin, -8.1545 * units('joule / kilogram'), 3) + parcel_prof = parcel_profile(levels, temperatures[0], dewpoints[0]).to("degC") + cape, cin = cape_cin(levels, temperatures, dewpoints, parcel_prof, which_el="bottom") + assert_almost_equal(cape, 2.1967 * units("joule / kilogram"), 3) + assert_almost_equal(cin, -8.1545 * units("joule / kilogram"), 3) def test_cape_cin_wide_el_lfc(multiple_intersections): """Test using LFC/EL options for CAPE/CIN.""" levels, temperatures, dewpoints = multiple_intersections - parcel_prof = parcel_profile(levels, temperatures[0], dewpoints[0]).to('degC') - cape, cin = cape_cin(levels, temperatures, dewpoints, parcel_prof, which_lfc='wide', - which_el='wide') - assert_almost_equal(cape, 1262.8618 * units('joule / kilogram'), 3) - assert_almost_equal(cin, -97.6499 * units('joule / kilogram'), 3) + parcel_prof = parcel_profile(levels, temperatures[0], dewpoints[0]).to("degC") + cape, cin = cape_cin( + levels, temperatures, dewpoints, parcel_prof, which_lfc="wide", which_el="wide" + ) + assert_almost_equal(cape, 1262.8618 * units("joule / kilogram"), 3) + assert_almost_equal(cin, -97.6499 * units("joule / kilogram"), 3) def test_cape_cin_custom_profile(): """Test the CAPE and CIN calculation with a custom profile passed to LFC and EL.""" - p = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.mbar - temperature = np.array([22.2, 14.6, 12., 9.4, 7., -38.]) * units.celsius - dewpoint = np.array([19., -11.2, -10.8, -10.4, -10., -53.2]) * units.celsius + p = np.array([959.0, 779.2, 751.3, 724.3, 700.0, 269.0]) * units.mbar + temperature = np.array([22.2, 14.6, 12.0, 9.4, 7.0, -38.0]) * units.celsius + dewpoint = np.array([19.0, -11.2, -10.8, -10.4, -10.0, -53.2]) * units.celsius parcel_prof = parcel_profile(p, temperature[0], dewpoint[0]) + 5 * units.delta_degC cape, cin = cape_cin(p, temperature, dewpoint, parcel_prof) - assert_almost_equal(cape, 1443.505086499895 * units('joule / kilogram'), 2) - assert_almost_equal(cin, 0.0 * units('joule / kilogram'), 2) + assert_almost_equal(cape, 1443.505086499895 * units("joule / kilogram"), 2) + assert_almost_equal(cin, 0.0 * units("joule / kilogram"), 2) def test_parcel_profile_below_lcl(): """Test parcel profile calculation when pressures do not reach LCL (#827).""" - pressure = np.array([981, 949.2, 925., 913.9, 903, 879.4, 878, 864, 855, - 850, 846.3, 838, 820, 814.5, 799, 794]) * units.hPa - truth = np.array([276.35, 273.76110242, 271.74910213, 270.81364639, - 269.88711359, 267.85332225, 267.73145436, 266.5050728, - 265.70916946, 265.264412, 264.93408677, 264.18931638, - 262.55585912, 262.0516423, 260.61745662, - 260.15057861]) * units.kelvin + pressure = ( + np.array( + [ + 981, + 949.2, + 925.0, + 913.9, + 903, + 879.4, + 878, + 864, + 855, + 850, + 846.3, + 838, + 820, + 814.5, + 799, + 794, + ] + ) + * units.hPa + ) + truth = ( + np.array( + [ + 276.35, + 273.76110242, + 271.74910213, + 270.81364639, + 269.88711359, + 267.85332225, + 267.73145436, + 266.5050728, + 265.70916946, + 265.264412, + 264.93408677, + 264.18931638, + 262.55585912, + 262.0516423, + 260.61745662, + 260.15057861, + ] + ) + * units.kelvin + ) profile = parcel_profile(pressure, 3.2 * units.degC, -10.8 * units.degC) assert_almost_equal(profile, truth, 6) def test_vertical_velocity_pressure_dry_air(): """Test conversion of w to omega assuming dry air.""" - w = 1 * units('cm/s') - omega_truth = -1.250690495 * units('microbar/second') - omega_test = vertical_velocity_pressure(w, 1000. * units.mbar, 273.15 * units.K) + w = 1 * units("cm/s") + omega_truth = -1.250690495 * units("microbar/second") + omega_test = vertical_velocity_pressure(w, 1000.0 * units.mbar, 273.15 * units.K) assert_almost_equal(omega_test, omega_truth, 6) def test_vertical_velocity_dry_air(): """Test conversion of w to omega assuming dry air.""" - omega = 1 * units('microbar/second') - w_truth = -0.799558327 * units('cm/s') - w_test = vertical_velocity(omega, 1000. * units.mbar, 273.15 * units.K) + omega = 1 * units("microbar/second") + w_truth = -0.799558327 * units("cm/s") + w_test = vertical_velocity(omega, 1000.0 * units.mbar, 273.15 * units.K) assert_almost_equal(w_test, w_truth, 6) def test_vertical_velocity_pressure_moist_air(): """Test conversion of w to omega assuming moist air.""" - w = -1 * units('cm/s') - omega_truth = 1.032100858 * units('microbar/second') - omega_test = vertical_velocity_pressure(w, 850. * units.mbar, 280. * units.K, - 8 * units('g/kg')) + w = -1 * units("cm/s") + omega_truth = 1.032100858 * units("microbar/second") + omega_test = vertical_velocity_pressure( + w, 850.0 * units.mbar, 280.0 * units.K, 8 * units("g/kg") + ) assert_almost_equal(omega_test, omega_truth, 6) def test_vertical_velocity_moist_air(): """Test conversion of w to omega assuming moist air.""" - omega = -1 * units('microbar/second') - w_truth = 0.968897557 * units('cm/s') - w_test = vertical_velocity(omega, 850. * units.mbar, 280. * units.K, 8 * units('g/kg')) + omega = -1 * units("microbar/second") + w_truth = 0.968897557 * units("cm/s") + w_test = vertical_velocity(omega, 850.0 * units.mbar, 280.0 * units.K, 8 * units("g/kg")) assert_almost_equal(w_test, w_truth, 6) @@ -1701,26 +3005,188 @@ def test_lcl_convergence_issue(): def test_cape_cin_value_error(): """Test a profile that originally caused a ValueError in #1190.""" - pressure = np.array([1012.0, 1009.0, 1002.0, 1000.0, 925.0, 896.0, 855.0, 850.0, 849.0, - 830.0, 775.0, 769.0, 758.0, 747.0, 741.0, 731.0, 712.0, 700.0, 691.0, - 671.0, 636.0, 620.0, 610.0, 601.0, 594.0, 587.0, 583.0, 580.0, 571.0, - 569.0, 554.0, 530.0, 514.0, 506.0, 502.0, 500.0, 492.0, 484.0, 475.0, - 456.0, 449.0, 442.0, 433.0, 427.0, 400.0, 395.0, 390.0, 351.0, 300.0, - 298.0, 294.0, 274.0, 250.0]) * units.hPa - temperature = np.array([27.8, 25.8, 24.2, 24, 18.8, 16, 13, 12.6, 12.6, 11.6, 9.2, 8.6, - 8.4, 9.2, 10, 9.4, 7.4, 6.2, 5.2, 3.2, -0.3, -2.3, -3.3, -4.5, - -5.5, -6.1, -6.1, -6.1, -6.3, -6.3, -7.7, -9.5, -9.9, -10.3, - -10.9, -11.1, -11.9, -12.7, -13.7, -16.1, -16.9, -17.9, -19.1, - -19.9, -23.9, -24.7, -25.3, -29.5, -39.3, -39.7, -40.5, -44.3, - -49.3]) * units.degC - dewpoint = np.array([19.8, 16.8, 16.2, 16, 13.8, 12.8, 10.1, 9.7, 9.7, - 8.6, 4.2, 3.9, 0.4, -5.8, -32, -34.6, -35.6, -34.8, - -32.8, -10.8, -9.3, -10.3, -9.3, -10.5, -10.5, -10, -16.1, - -19.1, -23.3, -18.3, -17.7, -20.5, -27.9, -32.3, -33.9, -34.1, - -35.9, -26.7, -37.7, -43.1, -33.9, -40.9, -46.1, -34.9, -33.9, - -33.7, -33.3, -42.5, -50.3, -49.7, -49.5, -58.3, -61.3]) * units.degC + pressure = ( + np.array( + [ + 1012.0, + 1009.0, + 1002.0, + 1000.0, + 925.0, + 896.0, + 855.0, + 850.0, + 849.0, + 830.0, + 775.0, + 769.0, + 758.0, + 747.0, + 741.0, + 731.0, + 712.0, + 700.0, + 691.0, + 671.0, + 636.0, + 620.0, + 610.0, + 601.0, + 594.0, + 587.0, + 583.0, + 580.0, + 571.0, + 569.0, + 554.0, + 530.0, + 514.0, + 506.0, + 502.0, + 500.0, + 492.0, + 484.0, + 475.0, + 456.0, + 449.0, + 442.0, + 433.0, + 427.0, + 400.0, + 395.0, + 390.0, + 351.0, + 300.0, + 298.0, + 294.0, + 274.0, + 250.0, + ] + ) + * units.hPa + ) + temperature = ( + np.array( + [ + 27.8, + 25.8, + 24.2, + 24, + 18.8, + 16, + 13, + 12.6, + 12.6, + 11.6, + 9.2, + 8.6, + 8.4, + 9.2, + 10, + 9.4, + 7.4, + 6.2, + 5.2, + 3.2, + -0.3, + -2.3, + -3.3, + -4.5, + -5.5, + -6.1, + -6.1, + -6.1, + -6.3, + -6.3, + -7.7, + -9.5, + -9.9, + -10.3, + -10.9, + -11.1, + -11.9, + -12.7, + -13.7, + -16.1, + -16.9, + -17.9, + -19.1, + -19.9, + -23.9, + -24.7, + -25.3, + -29.5, + -39.3, + -39.7, + -40.5, + -44.3, + -49.3, + ] + ) + * units.degC + ) + dewpoint = ( + np.array( + [ + 19.8, + 16.8, + 16.2, + 16, + 13.8, + 12.8, + 10.1, + 9.7, + 9.7, + 8.6, + 4.2, + 3.9, + 0.4, + -5.8, + -32, + -34.6, + -35.6, + -34.8, + -32.8, + -10.8, + -9.3, + -10.3, + -9.3, + -10.5, + -10.5, + -10, + -16.1, + -19.1, + -23.3, + -18.3, + -17.7, + -20.5, + -27.9, + -32.3, + -33.9, + -34.1, + -35.9, + -26.7, + -37.7, + -43.1, + -33.9, + -40.9, + -46.1, + -34.9, + -33.9, + -33.7, + -33.3, + -42.5, + -50.3, + -49.7, + -49.5, + -58.3, + -61.3, + ] + ) + * units.degC + ) cape, cin = surface_based_cape_cin(pressure, temperature, dewpoint) - expected_cape, expected_cin = [2010.4136 * units('joules/kg'), 0.0 * units('joules/kg')] + expected_cape, expected_cin = [2010.4136 * units("joules/kg"), 0.0 * units("joules/kg")] assert_almost_equal(cape, expected_cape, 3) assert_almost_equal(cin, expected_cin, 3) @@ -1739,24 +3205,150 @@ def test_lcl_grid_surface_LCLs(): def test_lifted_index(): """Test the Lifted Index calculation.""" - pressure = np.array([1014., 1000., 997., 981.2, 947.4, 925., 914.9, 911., - 902., 883., 850., 822.3, 816., 807., 793.2, 770., - 765.1, 753., 737.5, 737., 713., 700., 688., 685., - 680., 666., 659.8, 653., 643., 634., 615., 611.8, - 566.2, 516., 500., 487., 484.2, 481., 475., 460., - 400.]) * units.hPa - temperature = np.array([24.2, 24.2, 24., 23.1, 21., 19.6, 18.7, 18.4, - 19.2, 19.4, 17.2, 15.3, 14.8, 14.4, 13.4, 11.6, - 11.1, 10., 8.8, 8.8, 8.2, 7., 5.6, 5.6, - 5.6, 4.4, 3.8, 3.2, 3., 3.2, 1.8, 1.5, - -3.4, -9.3, -11.3, -13.1, -13.1, -13.1, -13.7, -15.1, - -23.5]) * units.degC - dewpoint = np.array([23.2, 23.1, 22.8, 22., 20.2, 19., 17.6, 17., - 16.8, 15.5, 14., 11.7, 11.2, 8.4, 7., 4.6, - 5., 6., 4.2, 4.1, -1.8, -2., -1.4, -0.4, - -3.4, -5.6, -4.3, -2.8, -7., -25.8, -31.2, -31.4, - -34.1, -37.3, -32.3, -34.1, -37.3, -41.1, -37.7, -58.1, - -57.5]) * units.degC + pressure = ( + np.array( + [ + 1014.0, + 1000.0, + 997.0, + 981.2, + 947.4, + 925.0, + 914.9, + 911.0, + 902.0, + 883.0, + 850.0, + 822.3, + 816.0, + 807.0, + 793.2, + 770.0, + 765.1, + 753.0, + 737.5, + 737.0, + 713.0, + 700.0, + 688.0, + 685.0, + 680.0, + 666.0, + 659.8, + 653.0, + 643.0, + 634.0, + 615.0, + 611.8, + 566.2, + 516.0, + 500.0, + 487.0, + 484.2, + 481.0, + 475.0, + 460.0, + 400.0, + ] + ) + * units.hPa + ) + temperature = ( + np.array( + [ + 24.2, + 24.2, + 24.0, + 23.1, + 21.0, + 19.6, + 18.7, + 18.4, + 19.2, + 19.4, + 17.2, + 15.3, + 14.8, + 14.4, + 13.4, + 11.6, + 11.1, + 10.0, + 8.8, + 8.8, + 8.2, + 7.0, + 5.6, + 5.6, + 5.6, + 4.4, + 3.8, + 3.2, + 3.0, + 3.2, + 1.8, + 1.5, + -3.4, + -9.3, + -11.3, + -13.1, + -13.1, + -13.1, + -13.7, + -15.1, + -23.5, + ] + ) + * units.degC + ) + dewpoint = ( + np.array( + [ + 23.2, + 23.1, + 22.8, + 22.0, + 20.2, + 19.0, + 17.6, + 17.0, + 16.8, + 15.5, + 14.0, + 11.7, + 11.2, + 8.4, + 7.0, + 4.6, + 5.0, + 6.0, + 4.2, + 4.1, + -1.8, + -2.0, + -1.4, + -0.4, + -3.4, + -5.6, + -4.3, + -2.8, + -7.0, + -25.8, + -31.2, + -31.4, + -34.1, + -37.3, + -32.3, + -34.1, + -37.3, + -41.1, + -37.7, + -58.1, + -57.5, + ] + ) + * units.degC + ) parcel_prof = parcel_profile(pressure, temperature[0], dewpoint[0]) LI = lifted_index(pressure, temperature, parcel_prof) assert_almost_equal(LI, -7.9176350 * units.delta_degree_Celsius, 2) @@ -1764,10 +3356,10 @@ def test_lifted_index(): def test_gradient_richardson_number(): """Test gradient Richardson number calculation.""" - theta = units('K') * np.asarray([254.5, 258.3, 262.2]) - u_wnd = units('m/s') * np.asarray([-2., -1.1, 0.23]) - v_wnd = units('m/s') * np.asarray([3.3, 4.2, 5.2]) - height = units('km') * np.asarray([0.2, 0.4, 0.6]) + theta = units("K") * np.asarray([254.5, 258.3, 262.2]) + u_wnd = units("m/s") * np.asarray([-2.0, -1.1, 0.23]) + v_wnd = units("m/s") * np.asarray([3.3, 4.2, 5.2]) + height = units("km") * np.asarray([0.2, 0.4, 0.6]) result = gradient_richardson_number(height, theta, u_wnd, v_wnd) expected = np.asarray([24.2503551, 13.6242603, 8.4673744]) @@ -1779,21 +3371,18 @@ def test_gradient_richardson_number_with_xarray(): """Test gradient Richardson number calculation using xarray.""" data = xr.Dataset( { - 'theta': (('height',), [254.5, 258.3, 262.2] * units.K), - 'u_wind': (('height',), [-2., -1.1, 0.23] * units('m/s')), - 'v_wind': (('height',), [3.3, 4.2, 5.2] * units('m/s')), - 'Ri_g': (('height',), [24.2503551, 13.6242603, 8.4673744]) + "theta": (("height",), [254.5, 258.3, 262.2] * units.K), + "u_wind": (("height",), [-2.0, -1.1, 0.23] * units("m/s")), + "v_wind": (("height",), [3.3, 4.2, 5.2] * units("m/s")), + "Ri_g": (("height",), [24.2503551, 13.6242603, 8.4673744]), }, - coords={'height': (('height',), [0.2, 0.4, 0.6], {'units': 'kilometer'})} + coords={"height": (("height",), [0.2, 0.4, 0.6], {"units": "kilometer"})}, ) result = gradient_richardson_number( - data['height'], - data['theta'], - data['u_wind'], - data['v_wind'] + data["height"], data["theta"], data["u_wind"], data["v_wind"] ) assert isinstance(result, xr.DataArray) - xr.testing.assert_identical(result['height'], data['Ri_g']['height']) - assert_array_almost_equal(result.data.m_as(''), data['Ri_g'].data) + xr.testing.assert_identical(result["height"], data["Ri_g"]["height"]) + assert_array_almost_equal(result.data.m_as(""), data["Ri_g"].data) diff --git a/tests/calc/test_turbulence.py b/tests/calc/test_turbulence.py index 807edb97b8c..72f84d97ae7 100644 --- a/tests/calc/test_turbulence.py +++ b/tests/calc/test_turbulence.py @@ -20,7 +20,7 @@ def uvw_and_known_tke(): v = -u w = 2 * u # 0.5 * sqrt(2 + 2 + 8) - e_true = np.sqrt(12) / 2. + e_true = np.sqrt(12) / 2.0 return u, v, w, e_true @@ -188,7 +188,7 @@ def uvw_and_known_kf_zero_mean(): u = np.array([-2, -1, 0, 1, 2]) v = -u w = 2 * u - kf_true = {'uv': -2, 'uw': 4, 'vw': -4} + kf_true = {"uv": -2, "uw": 4, "vw": -4} return u, v, w, kf_true @@ -198,39 +198,36 @@ def uvw_and_known_kf_nonzero_mean(): u = np.array([-2, -1, 0, 1, 5]) v = -u w = 2 * u - kf_true = {'uv': -5.84, 'uw': 11.68, 'vw': -11.68} + kf_true = {"uv": -5.84, "uw": 11.68, "vw": -11.68} return u, v, w, kf_true def test_kf_1d_zero_mean(uvw_and_known_kf_zero_mean): """Test kinematic flux calculation in 1D with zero-mean time series.""" u, v, w, kf_true = uvw_and_known_kf_zero_mean - assert_array_equal(kinematic_flux(u, v, perturbation=False), - kf_true['uv']) - assert_array_equal(kinematic_flux(u, w, perturbation=False), - kf_true['uw']) - assert_array_equal(kinematic_flux(v, w, perturbation=False), - kf_true['vw']) + assert_array_equal(kinematic_flux(u, v, perturbation=False), kf_true["uv"]) + assert_array_equal(kinematic_flux(u, w, perturbation=False), kf_true["uw"]) + assert_array_equal(kinematic_flux(v, w, perturbation=False), kf_true["vw"]) # given u, v, and w have a zero mean, the kf computed with # perturbation=True and perturbation=False should be the same - assert_array_equal(kinematic_flux(u, v, perturbation=False), - kinematic_flux(u, v, perturbation=True)) - assert_array_equal(kinematic_flux(u, w, perturbation=False), - kinematic_flux(u, w, perturbation=True)) - assert_array_equal(kinematic_flux(v, w, perturbation=False), - kinematic_flux(v, w, perturbation=True)) + assert_array_equal( + kinematic_flux(u, v, perturbation=False), kinematic_flux(u, v, perturbation=True) + ) + assert_array_equal( + kinematic_flux(u, w, perturbation=False), kinematic_flux(u, w, perturbation=True) + ) + assert_array_equal( + kinematic_flux(v, w, perturbation=False), kinematic_flux(v, w, perturbation=True) + ) def test_kf_1d_nonzero_mean(uvw_and_known_kf_nonzero_mean): """Test kinematic flux calculation in 1D with non-zero-mean time series.""" u, v, w, kf_true = uvw_and_known_kf_nonzero_mean - assert_array_equal(kinematic_flux(u, v, perturbation=False), - kf_true['uv']) - assert_array_equal(kinematic_flux(u, w, perturbation=False), - kf_true['uw']) - assert_array_equal(kinematic_flux(v, w, perturbation=False), - kf_true['vw']) + assert_array_equal(kinematic_flux(u, v, perturbation=False), kf_true["uv"]) + assert_array_equal(kinematic_flux(u, w, perturbation=False), kf_true["uw"]) + assert_array_equal(kinematic_flux(v, w, perturbation=False), kf_true["vw"]) def test_kf_2d_axis_last_zero_mean(uvw_and_known_kf_zero_mean): @@ -240,20 +237,23 @@ def test_kf_2d_axis_last_zero_mean(uvw_and_known_kf_zero_mean): v = np.array([v, v, v]) w = np.array([w, w, w]) - assert_array_equal(kinematic_flux(u, v, perturbation=False, axis=-1), - kf_true['uv']) - assert_array_equal(kinematic_flux(u, w, perturbation=False, axis=-1), - kf_true['uw']) - assert_array_equal(kinematic_flux(v, w, perturbation=False, axis=-1), - kf_true['vw']) + assert_array_equal(kinematic_flux(u, v, perturbation=False, axis=-1), kf_true["uv"]) + assert_array_equal(kinematic_flux(u, w, perturbation=False, axis=-1), kf_true["uw"]) + assert_array_equal(kinematic_flux(v, w, perturbation=False, axis=-1), kf_true["vw"]) # given u, v, and w have a zero mean, the kf computed with # perturbation=True and perturbation=False should be the same - assert_array_equal(kinematic_flux(u, v, perturbation=False, axis=-1), - kinematic_flux(u, v, perturbation=True, axis=-1)) - assert_array_equal(kinematic_flux(u, w, perturbation=False, axis=-1), - kinematic_flux(u, w, perturbation=True, axis=-1)) - assert_array_equal(kinematic_flux(v, w, perturbation=False, axis=-1), - kinematic_flux(v, w, perturbation=True, axis=-1)) + assert_array_equal( + kinematic_flux(u, v, perturbation=False, axis=-1), + kinematic_flux(u, v, perturbation=True, axis=-1), + ) + assert_array_equal( + kinematic_flux(u, w, perturbation=False, axis=-1), + kinematic_flux(u, w, perturbation=True, axis=-1), + ) + assert_array_equal( + kinematic_flux(v, w, perturbation=False, axis=-1), + kinematic_flux(v, w, perturbation=True, axis=-1), + ) def test_kf_2d_axis_last_nonzero_mean(uvw_and_known_kf_nonzero_mean): @@ -263,12 +263,9 @@ def test_kf_2d_axis_last_nonzero_mean(uvw_and_known_kf_nonzero_mean): v = np.array([v, v, v]) w = np.array([w, w, w]) - assert_array_equal(kinematic_flux(u, v, perturbation=False, axis=-1), - kf_true['uv']) - assert_array_equal(kinematic_flux(u, w, perturbation=False, axis=-1), - kf_true['uw']) - assert_array_equal(kinematic_flux(v, w, perturbation=False, axis=-1), - kf_true['vw']) + assert_array_equal(kinematic_flux(u, v, perturbation=False, axis=-1), kf_true["uv"]) + assert_array_equal(kinematic_flux(u, w, perturbation=False, axis=-1), kf_true["uw"]) + assert_array_equal(kinematic_flux(v, w, perturbation=False, axis=-1), kf_true["vw"]) def test_kf_2d_axis_first_zero_mean(uvw_and_known_kf_zero_mean): @@ -278,20 +275,23 @@ def test_kf_2d_axis_first_zero_mean(uvw_and_known_kf_zero_mean): v = np.array([v, v, v]).transpose() w = np.array([w, w, w]).transpose() - assert_array_equal(kinematic_flux(u, v, perturbation=False, axis=0), - kf_true['uv']) - assert_array_equal(kinematic_flux(u, w, perturbation=False, axis=0), - kf_true['uw']) - assert_array_equal(kinematic_flux(v, w, perturbation=False, axis=0), - kf_true['vw']) + assert_array_equal(kinematic_flux(u, v, perturbation=False, axis=0), kf_true["uv"]) + assert_array_equal(kinematic_flux(u, w, perturbation=False, axis=0), kf_true["uw"]) + assert_array_equal(kinematic_flux(v, w, perturbation=False, axis=0), kf_true["vw"]) # given u, v, and w have a zero mean, the kf computed with # perturbation=True and perturbation=False should be the same - assert_array_equal(kinematic_flux(u, v, perturbation=False, axis=0), - kinematic_flux(u, v, perturbation=True, axis=0)) - assert_array_equal(kinematic_flux(u, w, perturbation=False, axis=0), - kinematic_flux(u, w, perturbation=True, axis=0)) - assert_array_equal(kinematic_flux(v, w, perturbation=False, axis=0), - kinematic_flux(v, w, perturbation=True, axis=0)) + assert_array_equal( + kinematic_flux(u, v, perturbation=False, axis=0), + kinematic_flux(u, v, perturbation=True, axis=0), + ) + assert_array_equal( + kinematic_flux(u, w, perturbation=False, axis=0), + kinematic_flux(u, w, perturbation=True, axis=0), + ) + assert_array_equal( + kinematic_flux(v, w, perturbation=False, axis=0), + kinematic_flux(v, w, perturbation=True, axis=0), + ) def test_kf_2d_axis_first_nonzero_mean(uvw_and_known_kf_nonzero_mean): @@ -301,12 +301,9 @@ def test_kf_2d_axis_first_nonzero_mean(uvw_and_known_kf_nonzero_mean): v = np.array([v, v, v]).transpose() w = np.array([w, w, w]).transpose() - assert_array_equal(kinematic_flux(u, v, perturbation=False, axis=0), - kf_true['uv']) - assert_array_equal(kinematic_flux(u, w, perturbation=False, axis=0), - kf_true['uw']) - assert_array_equal(kinematic_flux(v, w, perturbation=False, axis=0), - kf_true['vw']) + assert_array_equal(kinematic_flux(u, v, perturbation=False, axis=0), kf_true["uv"]) + assert_array_equal(kinematic_flux(u, w, perturbation=False, axis=0), kf_true["uw"]) + assert_array_equal(kinematic_flux(v, w, perturbation=False, axis=0), kf_true["vw"]) # @@ -318,7 +315,7 @@ def uvw_and_known_u_star_zero_mean(): u = np.array([-2, -1, 0, 1, 2]) v = -u w = 2 * u - u_star_true = {'uw': 2.0, 'uwvw': 2.3784142300054421} + u_star_true = {"uw": 2.0, "uwvw": 2.3784142300054421} return u, v, w, u_star_true @@ -328,26 +325,22 @@ def uvw_and_known_u_star_nonzero_mean(): u = np.array([-2, -1, 0, 1, 5]) v = -u w = 2 * u - u_star_true = {'uw': 3.4176014981270124, 'uwvw': 4.0642360178166017} + u_star_true = {"uw": 3.4176014981270124, "uwvw": 4.0642360178166017} return u, v, w, u_star_true def test_u_star_1d_zero_mean(uvw_and_known_u_star_zero_mean): """Test friction velocity in 1D with a zero-mean time series.""" u, v, w, u_star_true = uvw_and_known_u_star_zero_mean - assert_almost_equal(friction_velocity(u, w, perturbation=False), - u_star_true['uw']) - assert_almost_equal(friction_velocity(u, w, v=v, perturbation=False), - u_star_true['uwvw']) + assert_almost_equal(friction_velocity(u, w, perturbation=False), u_star_true["uw"]) + assert_almost_equal(friction_velocity(u, w, v=v, perturbation=False), u_star_true["uwvw"]) def test_u_star_1d_nonzero_mean(uvw_and_known_u_star_nonzero_mean): """Test friction velocity in 1D with a non-zero-mean time series.""" u, v, w, u_star_true = uvw_and_known_u_star_nonzero_mean - assert_almost_equal(friction_velocity(u, w, perturbation=False), - u_star_true['uw']) - assert_almost_equal(friction_velocity(u, w, v=v, perturbation=False), - u_star_true['uwvw']) + assert_almost_equal(friction_velocity(u, w, perturbation=False), u_star_true["uw"]) + assert_almost_equal(friction_velocity(u, w, v=v, perturbation=False), u_star_true["uwvw"]) def test_u_star_2d_axis_last_zero_mean(uvw_and_known_u_star_zero_mean): @@ -357,10 +350,12 @@ def test_u_star_2d_axis_last_zero_mean(uvw_and_known_u_star_zero_mean): v = np.array([v, v, v]) w = np.array([w, w, w]) - assert_almost_equal(friction_velocity(u, w, perturbation=False, - axis=-1), u_star_true['uw']) - assert_almost_equal(friction_velocity(u, w, v=v, perturbation=False, - axis=-1), u_star_true['uwvw']) + assert_almost_equal( + friction_velocity(u, w, perturbation=False, axis=-1), u_star_true["uw"] + ) + assert_almost_equal( + friction_velocity(u, w, v=v, perturbation=False, axis=-1), u_star_true["uwvw"] + ) def test_u_star_2d_axis_last_nonzero_mean(uvw_and_known_u_star_nonzero_mean): @@ -370,10 +365,12 @@ def test_u_star_2d_axis_last_nonzero_mean(uvw_and_known_u_star_nonzero_mean): v = np.array([v, v, v]) w = np.array([w, w, w]) - assert_almost_equal(friction_velocity(u, w, perturbation=False, - axis=-1), u_star_true['uw']) - assert_almost_equal(friction_velocity(u, w, v=v, perturbation=False, - axis=-1), u_star_true['uwvw']) + assert_almost_equal( + friction_velocity(u, w, perturbation=False, axis=-1), u_star_true["uw"] + ) + assert_almost_equal( + friction_velocity(u, w, v=v, perturbation=False, axis=-1), u_star_true["uwvw"] + ) def test_u_star_2d_axis_first_zero_mean(uvw_and_known_u_star_zero_mean): @@ -383,10 +380,10 @@ def test_u_star_2d_axis_first_zero_mean(uvw_and_known_u_star_zero_mean): v = np.array([v, v, v]).transpose() w = np.array([w, w, w]).transpose() - assert_almost_equal(friction_velocity(u, w, perturbation=False, - axis=0), u_star_true['uw']) - assert_almost_equal(friction_velocity(u, w, v=v, perturbation=False, - axis=0), u_star_true['uwvw']) + assert_almost_equal(friction_velocity(u, w, perturbation=False, axis=0), u_star_true["uw"]) + assert_almost_equal( + friction_velocity(u, w, v=v, perturbation=False, axis=0), u_star_true["uwvw"] + ) def test_u_star_2d_axis_first_nonzero_mean(uvw_and_known_u_star_nonzero_mean): @@ -396,7 +393,7 @@ def test_u_star_2d_axis_first_nonzero_mean(uvw_and_known_u_star_nonzero_mean): v = np.array([v, v, v]).transpose() w = np.array([w, w, w]).transpose() - assert_almost_equal(friction_velocity(u, w, perturbation=False, - axis=0), u_star_true['uw']) - assert_almost_equal(friction_velocity(u, w, v=v, perturbation=False, - axis=0), u_star_true['uwvw']) + assert_almost_equal(friction_velocity(u, w, perturbation=False, axis=0), u_star_true["uw"]) + assert_almost_equal( + friction_velocity(u, w, v=v, perturbation=False, axis=0), u_star_true["uwvw"] + ) diff --git a/tests/interpolate/test_geometry.py b/tests/interpolate/test_geometry.py index 8dd4921107d..63b2fefd031 100644 --- a/tests/interpolate/test_geometry.py +++ b/tests/interpolate/test_geometry.py @@ -10,12 +10,22 @@ from numpy.testing import assert_almost_equal, assert_array_almost_equal, assert_array_equal from scipy.spatial import Delaunay -from metpy.interpolate.geometry import (area, circumcenter, circumcircle_radius, dist_2, - distance, find_local_boundary, find_natural_neighbors, - find_nn_triangles_point, get_point_count_within_r, - get_points_within_r, order_edges, triangle_area) - -logging.getLogger('metpy.interpolate.geometry').setLevel(logging.ERROR) +from metpy.interpolate.geometry import ( + area, + circumcenter, + circumcircle_radius, + dist_2, + distance, + find_local_boundary, + find_natural_neighbors, + find_nn_triangles_point, + get_point_count_within_r, + get_points_within_r, + order_edges, + triangle_area, +) + +logging.getLogger("metpy.interpolate.geometry").setLevel(logging.ERROR) def test_get_points_within_r(): @@ -132,7 +142,7 @@ def test_circumcenter(): cc = circumcenter(pt0, pt1, pt2) - truth = [5., 5.] + truth = [5.0, 5.0] assert_array_almost_equal(truth, cc) @@ -150,19 +160,39 @@ def test_find_natural_neighbors(): neighbors, tri_info = find_natural_neighbors(tri, test_points) # Need to check point indices rather than simplex indices - neighbors_truth = [[(1, 5, 0), (5, 1, 6)], - [(11, 17, 16), (17, 11, 12)], - [(23, 19, 24), (19, 23, 18), (23, 17, 18), (17, 23, 22)], - [(7, 13, 12), (13, 7, 8), (9, 13, 8), (13, 9, 14), - (13, 19, 18), (19, 13, 14), (17, 13, 18), (13, 17, 12)], - np.zeros((0, 3), dtype=np.int32)] - - centers_truth = [[(2.0, 2.0), (2.0, 2.0)], - [(6.0, 10.0), (6.0, 10.0)], - [(14.0, 14.0), (14.0, 14.0), (10.0, 14.0), (10.0, 14.0)], - [(10.0, 6.0), (10.0, 6.0), (14.0, 6.0), (14.0, 6.0), - (14.0, 10.0), (14.0, 10.0), (10.0, 10.0), (10.0, 10.0)], - np.zeros((0, 2), dtype=np.int32)] + neighbors_truth = [ + [(1, 5, 0), (5, 1, 6)], + [(11, 17, 16), (17, 11, 12)], + [(23, 19, 24), (19, 23, 18), (23, 17, 18), (17, 23, 22)], + [ + (7, 13, 12), + (13, 7, 8), + (9, 13, 8), + (13, 9, 14), + (13, 19, 18), + (19, 13, 14), + (17, 13, 18), + (13, 17, 12), + ], + np.zeros((0, 3), dtype=np.int32), + ] + + centers_truth = [ + [(2.0, 2.0), (2.0, 2.0)], + [(6.0, 10.0), (6.0, 10.0)], + [(14.0, 14.0), (14.0, 14.0), (10.0, 14.0), (10.0, 14.0)], + [ + (10.0, 6.0), + (10.0, 6.0), + (14.0, 6.0), + (14.0, 6.0), + (14.0, 10.0), + (14.0, 10.0), + (10.0, 10.0), + (10.0, 10.0), + ], + np.zeros((0, 2), dtype=np.int32), + ] for i, true_neighbor in enumerate(neighbors_truth): assert set(true_neighbor) == {tuple(v) for v in tri.simplices[neighbors[i]]} diff --git a/tests/interpolate/test_grid.py b/tests/interpolate/test_grid.py index 9eec547cbfb..9c9e3fbfc39 100644 --- a/tests/interpolate/test_grid.py +++ b/tests/interpolate/test_grid.py @@ -12,12 +12,19 @@ import scipy from metpy.cbook import get_test_data -from metpy.interpolate.grid import (generate_grid, generate_grid_coords, get_boundary_coords, - get_xy_range, get_xy_steps, interpolate_to_grid, - interpolate_to_isosurface, inverse_distance_to_grid, - natural_neighbor_to_grid) +from metpy.interpolate.grid import ( + generate_grid, + generate_grid_coords, + get_boundary_coords, + get_xy_range, + get_xy_steps, + interpolate_to_grid, + interpolate_to_isosurface, + inverse_distance_to_grid, + natural_neighbor_to_grid, +) -logging.getLogger('metpy.interpolate.grid').setLevel(logging.ERROR) +logging.getLogger("metpy.interpolate.grid").setLevel(logging.ERROR) @pytest.fixture() @@ -34,8 +41,9 @@ def test_data(): r"""Return data used for tests in this file.""" x = np.array([8, 67, 79, 10, 52, 53, 98, 34, 15, 58], dtype=float) y = np.array([24, 87, 48, 94, 98, 66, 14, 24, 60, 16], dtype=float) - z = np.array([0.064, 4.489, 6.241, 0.1, 2.704, 2.809, 9.604, 1.156, - 0.225, 3.364], dtype=float) + z = np.array( + [0.064, 4.489, 6.241, 0.1, 2.704, 2.809, 9.604, 1.156, 0.225, 3.364], dtype=float + ) return x, y, z @@ -43,9 +51,9 @@ def test_data(): @pytest.fixture() def test_grid(): r"""Return grid locations used for tests in this file.""" - with get_test_data('interpolation_test_grid.npz') as fobj: + with get_test_data("interpolation_test_grid.npz") as fobj: data = np.load(fobj) - return data['xg'], data['yg'] + return data["xg"], data["yg"] def test_get_boundary_coords(): @@ -55,12 +63,12 @@ def test_get_boundary_coords(): bbox = get_boundary_coords(x, y) - truth = {'east': 9, 'north': 9, 'south': 0, 'west': 0} + truth = {"east": 9, "north": 9, "south": 0, "west": 0} assert bbox == truth bbox = get_boundary_coords(x, y, 10) - truth = {'east': 19, 'north': 19, 'south': -10, 'west': -10} + truth = {"east": 19, "north": 19, "south": -10, "west": -10} assert bbox == truth @@ -105,13 +113,9 @@ def test_generate_grid(): gx, gy = generate_grid(3, bbox) - truth_x = np.array([[0.0, 4.5, 9.0], - [0.0, 4.5, 9.0], - [0.0, 4.5, 9.0]]) + truth_x = np.array([[0.0, 4.5, 9.0], [0.0, 4.5, 9.0], [0.0, 4.5, 9.0]]) - truth_y = np.array([[0.0, 0.0, 0.0], - [4.5, 4.5, 4.5], - [9.0, 9.0, 9.0]]) + truth_y = np.array([[0.0, 0.0, 0.0], [4.5, 4.5, 4.5], [9.0, 9.0, 9.0]]) assert_array_almost_equal(gx, truth_x) assert_array_almost_equal(gy, truth_y) @@ -126,20 +130,22 @@ def test_generate_grid_coords(): gx, gy = generate_grid(3, bbox) - truth = [[0.0, 0.0], - [4.5, 0.0], - [9.0, 0.0], - [0.0, 4.5], - [4.5, 4.5], - [9.0, 4.5], - [0.0, 9.0], - [4.5, 9.0], - [9.0, 9.0]] + truth = [ + [0.0, 0.0], + [4.5, 0.0], + [9.0, 0.0], + [0.0, 4.5], + [4.5, 4.5], + [9.0, 4.5], + [0.0, 9.0], + [4.5, 9.0], + [9.0, 9.0], + ] pts = generate_grid_coords(gx, gy) assert_array_almost_equal(truth, pts) - assert pts.flags['C_CONTIGUOUS'] # need output to be C-contiguous + assert pts.flags["C_CONTIGUOUS"] # need output to be C-contiguous def test_natural_neighbor_to_grid(test_data, test_grid): @@ -149,97 +155,129 @@ def test_natural_neighbor_to_grid(test_data, test_grid): img = natural_neighbor_to_grid(xp, yp, z, xg, yg) - with get_test_data('nn_bbox0to100.npz') as fobj: - truth = np.load(fobj)['img'] + with get_test_data("nn_bbox0to100.npz") as fobj: + truth = np.load(fobj)["img"] assert_array_almost_equal(truth, img) -interp_methods = ['cressman', 'barnes'] +interp_methods = ["cressman", "barnes"] -@pytest.mark.parametrize('method', interp_methods) +@pytest.mark.parametrize("method", interp_methods) def test_inverse_distance_to_grid(method, test_data, test_grid): r"""Test inverse distance interpolation to grid function.""" xp, yp, z = test_data xg, yg = test_grid extra_kw = {} - if method == 'cressman': - extra_kw['r'] = 20 - extra_kw['min_neighbors'] = 1 - test_file = 'cressman_r20_mn1.npz' - elif method == 'barnes': - extra_kw['r'] = 40 - extra_kw['kappa'] = 100 - test_file = 'barnes_r40_k100.npz' + if method == "cressman": + extra_kw["r"] = 20 + extra_kw["min_neighbors"] = 1 + test_file = "cressman_r20_mn1.npz" + elif method == "barnes": + extra_kw["r"] = 40 + extra_kw["kappa"] = 100 + test_file = "barnes_r40_k100.npz" img = inverse_distance_to_grid(xp, yp, z, xg, yg, kind=method, **extra_kw) with get_test_data(test_file) as fobj: - truth = np.load(fobj)['img'] + truth = np.load(fobj)["img"] assert_array_almost_equal(truth, img) -interp_methods = ['natural_neighbor', 'cressman', 'barnes', - 'linear', 'nearest', 'rbf', - pytest.param('cubic', - marks=pytest.mark.skipif( - scipy.__version__ < '1.2.0', - reason='Need Scipy >=1.2 for fixed cubic interpolation.'))] +interp_methods = [ + "natural_neighbor", + "cressman", + "barnes", + "linear", + "nearest", + "rbf", + pytest.param( + "cubic", + marks=pytest.mark.skipif( + scipy.__version__ < "1.2.0", + reason="Need Scipy >=1.2 for fixed cubic interpolation.", + ), + ), +] -boundary_types = [{'west': 80.0, 'south': 140.0, 'east': 980.0, 'north': 980.0}, - None] +boundary_types = [{"west": 80.0, "south": 140.0, "east": 980.0, "north": 980.0}, None] def test_interpolate_to_isosurface(): r"""Test interpolation to level function.""" - pv = np.array([[[4.29013406, 4.61736108, 4.97453387, 5.36730237, 5.75500645], - [3.48415057, 3.72492697, 4.0065845, 4.35128065, 4.72701041], - [2.87775662, 3.01866087, 3.21074864, 3.47971854, 3.79924194], - [2.70274738, 2.71627883, 2.7869988, 2.94197238, 3.15685712], - [2.81293318, 2.70649941, 2.65188277, 2.68109532, 2.77737801]], - [[2.43090597, 2.79248225, 3.16783697, 3.54497301, 3.89481001], - [1.61968826, 1.88924405, 2.19296648, 2.54191855, 2.91119712], - [1.09089606, 1.25384007, 1.46192044, 1.73476959, 2.05268876], - [0.97204726, 1.02016741, 1.11466014, 1.27721014, 1.4912234], - [1.07501523, 1.02474621, 1.01290749, 1.0638517, 1.16674712]], - [[0.61025484, 0.7315194, 0.85573147, 0.97430123, 1.08453329], - [0.31705299, 0.3987999, 0.49178996, 0.59602155, 0.71077394], - [0.1819831, 0.22650344, 0.28305811, 0.35654934, 0.44709885], - [0.15472957, 0.17382593, 0.20182338, 0.2445138, 0.30252574], - [0.15522068, 0.16333457, 0.17633552, 0.19834644, 0.23015555]]]) - - thta = np.array([[[344.45776, 344.5063, 344.574, 344.6499, 344.735], - [343.98444, 344.02536, 344.08682, 344.16284, 344.2629], - [343.58792, 343.60876, 343.65628, 343.72818, 343.82834], - [343.21542, 343.2204, 343.25833, 343.32935, 343.43414], - [342.85272, 342.84982, 342.88556, 342.95645, 343.0634]], - [[326.70923, 326.67603, 326.63416, 326.57153, 326.49155], - [326.77695, 326.73468, 326.6931, 326.6408, 326.58405], - [326.95062, 326.88986, 326.83627, 326.78134, 326.7308], - [327.1913, 327.10928, 327.03894, 326.97546, 326.92587], - [327.47235, 327.3778, 327.29468, 327.2188, 327.15973]], - [[318.47897, 318.30374, 318.1081, 317.8837, 317.63837], - [319.155, 318.983, 318.79745, 318.58905, 318.36212], - [319.8042, 319.64206, 319.4669, 319.2713, 319.0611], - [320.4621, 320.3055, 320.13373, 319.9425, 319.7401], - [321.1375, 320.98648, 320.81473, 320.62186, 320.4186]]]) + pv = np.array( + [ + [ + [4.29013406, 4.61736108, 4.97453387, 5.36730237, 5.75500645], + [3.48415057, 3.72492697, 4.0065845, 4.35128065, 4.72701041], + [2.87775662, 3.01866087, 3.21074864, 3.47971854, 3.79924194], + [2.70274738, 2.71627883, 2.7869988, 2.94197238, 3.15685712], + [2.81293318, 2.70649941, 2.65188277, 2.68109532, 2.77737801], + ], + [ + [2.43090597, 2.79248225, 3.16783697, 3.54497301, 3.89481001], + [1.61968826, 1.88924405, 2.19296648, 2.54191855, 2.91119712], + [1.09089606, 1.25384007, 1.46192044, 1.73476959, 2.05268876], + [0.97204726, 1.02016741, 1.11466014, 1.27721014, 1.4912234], + [1.07501523, 1.02474621, 1.01290749, 1.0638517, 1.16674712], + ], + [ + [0.61025484, 0.7315194, 0.85573147, 0.97430123, 1.08453329], + [0.31705299, 0.3987999, 0.49178996, 0.59602155, 0.71077394], + [0.1819831, 0.22650344, 0.28305811, 0.35654934, 0.44709885], + [0.15472957, 0.17382593, 0.20182338, 0.2445138, 0.30252574], + [0.15522068, 0.16333457, 0.17633552, 0.19834644, 0.23015555], + ], + ] + ) + + thta = np.array( + [ + [ + [344.45776, 344.5063, 344.574, 344.6499, 344.735], + [343.98444, 344.02536, 344.08682, 344.16284, 344.2629], + [343.58792, 343.60876, 343.65628, 343.72818, 343.82834], + [343.21542, 343.2204, 343.25833, 343.32935, 343.43414], + [342.85272, 342.84982, 342.88556, 342.95645, 343.0634], + ], + [ + [326.70923, 326.67603, 326.63416, 326.57153, 326.49155], + [326.77695, 326.73468, 326.6931, 326.6408, 326.58405], + [326.95062, 326.88986, 326.83627, 326.78134, 326.7308], + [327.1913, 327.10928, 327.03894, 326.97546, 326.92587], + [327.47235, 327.3778, 327.29468, 327.2188, 327.15973], + ], + [ + [318.47897, 318.30374, 318.1081, 317.8837, 317.63837], + [319.155, 318.983, 318.79745, 318.58905, 318.36212], + [319.8042, 319.64206, 319.4669, 319.2713, 319.0611], + [320.4621, 320.3055, 320.13373, 319.9425, 319.7401], + [321.1375, 320.98648, 320.81473, 320.62186, 320.4186], + ], + ] + ) dt_thta = interpolate_to_isosurface(pv, thta, 2) - truth = np.array([[324.761318, 323.4567137, 322.3276748, 321.3501466, 320.5223535], - [330.286922, 327.7779134, 325.797487, 324.3984446, 323.1793418], - [335.4152061, 333.9585512, 332.0114516, 329.3572419, 326.4791125], - [336.7088576, 336.4165698, 335.6255217, 334.0758288, 331.9684081], - [335.6583567, 336.3500714, 336.6844744, 336.3286052, 335.3874244]]) + truth = np.array( + [ + [324.761318, 323.4567137, 322.3276748, 321.3501466, 320.5223535], + [330.286922, 327.7779134, 325.797487, 324.3984446, 323.1793418], + [335.4152061, 333.9585512, 332.0114516, 329.3572419, 326.4791125], + [336.7088576, 336.4165698, 335.6255217, 334.0758288, 331.9684081], + [335.6583567, 336.3500714, 336.6844744, 336.3286052, 335.3874244], + ] + ) assert_array_almost_equal(truth, dt_thta) -@pytest.mark.parametrize('method', interp_methods) -@pytest.mark.parametrize('boundary_coords', boundary_types) +@pytest.mark.parametrize("method", interp_methods) +@pytest.mark.parametrize("boundary_coords", boundary_types) def test_interpolate_to_grid(method, test_coords, boundary_coords): r"""Test main grid interpolation function.""" xp, yp = test_coords @@ -247,69 +285,92 @@ def test_interpolate_to_grid(method, test_coords, boundary_coords): xp *= 10 yp *= 10 - z = np.array([0.064, 4.489, 6.241, 0.1, 2.704, 2.809, 9.604, 1.156, - 0.225, 3.364]) + z = np.array([0.064, 4.489, 6.241, 0.1, 2.704, 2.809, 9.604, 1.156, 0.225, 3.364]) extra_kw = {} - if method == 'cressman': - extra_kw['search_radius'] = 200 - extra_kw['minimum_neighbors'] = 1 - elif method == 'barnes': - extra_kw['search_radius'] = 400 - extra_kw['minimum_neighbors'] = 1 - extra_kw['gamma'] = 1 + if method == "cressman": + extra_kw["search_radius"] = 200 + extra_kw["minimum_neighbors"] = 1 + elif method == "barnes": + extra_kw["search_radius"] = 400 + extra_kw["minimum_neighbors"] = 1 + extra_kw["gamma"] = 1 if boundary_coords is not None: - extra_kw['boundary_coords'] = boundary_coords + extra_kw["boundary_coords"] = boundary_coords _, _, img = interpolate_to_grid(xp, yp, z, hres=10, interp_type=method, **extra_kw) - with get_test_data(f'{method}_test.npz') as fobj: - truth = np.load(fobj)['img'] + with get_test_data(f"{method}_test.npz") as fobj: + truth = np.load(fobj)["img"] assert_array_almost_equal(truth, img) def test_interpolate_to_isosurface_from_below(): r"""Test interpolation to level function.""" - pv = np.array([[[1.75, 1.875, 2., 2.125, 2.25], - [1.9, 2.025, 2.15, 2.275, 2.4], - [2.05, 2.175, 2.3, 2.425, 2.55], - [2.2, 2.325, 2.45, 2.575, 2.7], - [2.35, 2.475, 2.6, 2.725, 2.85]], - [[1.5, 1.625, 1.75, 1.875, 2.], - [1.65, 1.775, 1.9, 2.025, 2.15], - [1.8, 1.925, 2.05, 2.175, 2.3], - [1.95, 2.075, 2.2, 2.325, 2.45], - [2.1, 2.225, 2.35, 2.475, 2.6]], - [[1.25, 1.375, 1.5, 1.625, 1.75], - [1.4, 1.525, 1.65, 1.775, 1.9], - [1.55, 1.675, 1.8, 1.925, 2.05], - [1.7, 1.825, 1.95, 2.075, 2.2], - [1.85, 1.975, 2.1, 2.225, 2.35]]]) - - thta = np.array([[[330., 350., 370., 390., 410.], - [340., 360., 380., 400., 420.], - [350., 370., 390., 410., 430.], - [360., 380., 400., 420., 440.], - [370., 390., 410., 430., 450.]], - [[320., 340., 360., 380., 400.], - [330., 350., 370., 390., 410.], - [340., 360., 380., 400., 420.], - [350., 370., 390., 410., 430.], - [360., 380., 400., 420., 440.]], - [[310., 330., 350., 370., 390.], - [320., 340., 360., 380., 400.], - [330., 350., 370., 390., 410.], - [340., 360., 380., 400., 420.], - [350., 370., 390., 410., 430.]]]) + pv = np.array( + [ + [ + [1.75, 1.875, 2.0, 2.125, 2.25], + [1.9, 2.025, 2.15, 2.275, 2.4], + [2.05, 2.175, 2.3, 2.425, 2.55], + [2.2, 2.325, 2.45, 2.575, 2.7], + [2.35, 2.475, 2.6, 2.725, 2.85], + ], + [ + [1.5, 1.625, 1.75, 1.875, 2.0], + [1.65, 1.775, 1.9, 2.025, 2.15], + [1.8, 1.925, 2.05, 2.175, 2.3], + [1.95, 2.075, 2.2, 2.325, 2.45], + [2.1, 2.225, 2.35, 2.475, 2.6], + ], + [ + [1.25, 1.375, 1.5, 1.625, 1.75], + [1.4, 1.525, 1.65, 1.775, 1.9], + [1.55, 1.675, 1.8, 1.925, 2.05], + [1.7, 1.825, 1.95, 2.075, 2.2], + [1.85, 1.975, 2.1, 2.225, 2.35], + ], + ] + ) + + thta = np.array( + [ + [ + [330.0, 350.0, 370.0, 390.0, 410.0], + [340.0, 360.0, 380.0, 400.0, 420.0], + [350.0, 370.0, 390.0, 410.0, 430.0], + [360.0, 380.0, 400.0, 420.0, 440.0], + [370.0, 390.0, 410.0, 430.0, 450.0], + ], + [ + [320.0, 340.0, 360.0, 380.0, 400.0], + [330.0, 350.0, 370.0, 390.0, 410.0], + [340.0, 360.0, 380.0, 400.0, 420.0], + [350.0, 370.0, 390.0, 410.0, 430.0], + [360.0, 380.0, 400.0, 420.0, 440.0], + ], + [ + [310.0, 330.0, 350.0, 370.0, 390.0], + [320.0, 340.0, 360.0, 380.0, 400.0], + [330.0, 350.0, 370.0, 390.0, 410.0], + [340.0, 360.0, 380.0, 400.0, 420.0], + [350.0, 370.0, 390.0, 410.0, 430.0], + ], + ] + ) dt_thta = interpolate_to_isosurface(pv, thta, 2, bottom_up_search=False) - truth = np.array([[330., 350., 370., 385., 400.], - [340., 359., 374., 389., 404.], - [348., 363., 378., 393., 410.], - [352., 367., 382., 400., 420.], - [356., 371., 390., 410., 430.]]) + truth = np.array( + [ + [330.0, 350.0, 370.0, 385.0, 400.0], + [340.0, 359.0, 374.0, 389.0, 404.0], + [348.0, 363.0, 378.0, 393.0, 410.0], + [352.0, 367.0, 382.0, 400.0, 420.0], + [356.0, 371.0, 390.0, 410.0, 430.0], + ] + ) assert_array_almost_equal(truth, dt_thta) diff --git a/tests/interpolate/test_interpolate_tools.py b/tests/interpolate/test_interpolate_tools.py index 51a97e7617e..dbe160ddc2a 100644 --- a/tests/interpolate/test_interpolate_tools.py +++ b/tests/interpolate/test_interpolate_tools.py @@ -9,9 +9,12 @@ import pandas as pd import pytest -from metpy.interpolate import (interpolate_to_grid, remove_nan_observations, - remove_observations_below_value, - remove_repeat_coordinates) +from metpy.interpolate import ( + interpolate_to_grid, + remove_nan_observations, + remove_observations_below_value, + remove_repeat_coordinates, +) from metpy.interpolate.tools import barnes_weights, calc_kappa, cressman_weights @@ -91,14 +94,16 @@ def test_barnes_weights(): gamma = 0.5 - dist = np.array([1000, 2000, 3000, 4000])**2 + dist = np.array([1000, 2000, 3000, 4000]) ** 2 weights = barnes_weights(dist, kappa, gamma) * 10000000 - truth = [1353352.832366126918939, - 3354.626279025118388, - .152299797447126, - .000000126641655] + truth = [ + 1353352.832366126918939, + 3354.626279025118388, + 0.152299797447126, + 0.000000126641655, + ] assert_array_almost_equal(truth, weights) @@ -107,25 +112,25 @@ def test_cressman_weights(): r"""Test Cressman weights function.""" r = 5000 - dist = np.array([1000, 2000, 3000, 4000])**2 + dist = np.array([1000, 2000, 3000, 4000]) ** 2 weights = cressman_weights(dist, r) - truth = [0.923076923076923, - 0.724137931034482, - 0.470588235294117, - 0.219512195121951] + truth = [0.923076923076923, 0.724137931034482, 0.470588235294117, 0.219512195121951] assert_array_almost_equal(truth, weights) def test_interpolate_to_grid_pandas(): r"""Test whether this accepts a `pd.Series` without crashing.""" - df = pd.DataFrame({ - 'lat': [38, 39, 31, 30, 41, 35], - 'lon': [-106, -105, -86, -96, -74, -70], - 'tmp': [-10, -16, 13, 16, 0, 3.5] - }, index=[1, 2, 3, 4, 5, 6]) + df = pd.DataFrame( + { + "lat": [38, 39, 31, 30, 41, 35], + "lon": [-106, -105, -86, -96, -74, -70], + "tmp": [-10, -16, 13, 16, 0, 3.5], + }, + index=[1, 2, 3, 4, 5, 6], + ) interpolate_to_grid( - df['lon'], df['lat'], df['tmp'], - interp_type='natural_neighbor', hres=0.5) + df["lon"], df["lat"], df["tmp"], interp_type="natural_neighbor", hres=0.5 + ) diff --git a/tests/interpolate/test_one_dimension.py b/tests/interpolate/test_one_dimension.py index 2bb9e71e18f..aada4e99d47 100644 --- a/tests/interpolate/test_one_dimension.py +++ b/tests/interpolate/test_one_dimension.py @@ -28,7 +28,7 @@ def test_interpolate_nans_1d_log(): nan_indexes = [1, 5, 11, 12] y_with_nan = y.copy() y_with_nan[nan_indexes] = np.nan - assert_array_almost_equal(y, interpolate_nans_1d(x, y_with_nan, kind='log'), 2) + assert_array_almost_equal(y, interpolate_nans_1d(x, y_with_nan, kind="log"), 2) def test_interpolate_nans_1d_invalid(): @@ -36,7 +36,7 @@ def test_interpolate_nans_1d_invalid(): x = np.logspace(1, 5, 15) y = 5 * np.log(x) + 3 with pytest.raises(ValueError): - interpolate_nans_1d(x, y, kind='loog') + interpolate_nans_1d(x, y, kind="loog") def test_log_interpolate_1d(): @@ -134,7 +134,7 @@ def test_log_interpolate_set_nan_below(): def test_interpolate_2args(): """Test interpolation with 2 arguments.""" - x = np.array([1., 2., 3., 4.]) + x = np.array([1.0, 2.0, 3.0, 4.0]) y = x y2 = x x_interp = np.array([2.5000000, 3.5000000]) @@ -146,7 +146,7 @@ def test_interpolate_2args(): def test_interpolate_decrease(): """Test interpolation with decreasing interpolation points.""" - x = np.array([1., 2., 3., 4.]) + x = np.array([1.0, 2.0, 3.0, 4.0]) y = x x_interp = np.array([3.5000000, 2.5000000]) y_interp_truth = np.array([3.5000000, 2.5000000]) @@ -156,7 +156,7 @@ def test_interpolate_decrease(): def test_interpolate_decrease_xp(): """Test interpolation with decreasing order.""" - x = np.array([4., 3., 2., 1.]) + x = np.array([4.0, 3.0, 2.0, 1.0]) y = x x_interp = np.array([3.5000000, 2.5000000]) y_interp_truth = np.array([3.5000000, 2.5000000]) @@ -166,7 +166,7 @@ def test_interpolate_decrease_xp(): def test_interpolate_end_point(): """Test interpolation with point at data endpoints.""" - x = np.array([1., 2., 3., 4.]) + x = np.array([1.0, 2.0, 3.0, 4.0]) y = x x_interp = np.array([1.0, 4.0]) y_interp_truth = np.array([1.0, 4.0]) @@ -176,9 +176,9 @@ def test_interpolate_end_point(): def test_interpolate_masked_units(): """Test interpolating with masked arrays with units.""" - x = units.Quantity(np.ma.array([1., 2., 3., 4.]), units.m) - y = units.Quantity(np.ma.array([50., 60., 70., 80.]), units.degC) - x_interp = np.array([250., 350.]) * units.cm - y_interp_truth = np.array([65., 75.]) * units.degC + x = units.Quantity(np.ma.array([1.0, 2.0, 3.0, 4.0]), units.m) + y = units.Quantity(np.ma.array([50.0, 60.0, 70.0, 80.0]), units.degC) + x_interp = np.array([250.0, 350.0]) * units.cm + y_interp_truth = np.array([65.0, 75.0]) * units.degC y_interp = interpolate_1d(x_interp, x, y) assert_array_almost_equal(y_interp, y_interp_truth, 7) diff --git a/tests/interpolate/test_points.py b/tests/interpolate/test_points.py index 9e2c6ee1c65..3076d428339 100644 --- a/tests/interpolate/test_points.py +++ b/tests/interpolate/test_points.py @@ -10,16 +10,18 @@ from numpy.testing import assert_almost_equal, assert_array_almost_equal import pytest import scipy -from scipy.spatial import cKDTree, Delaunay +from scipy.spatial import Delaunay, cKDTree from metpy.cbook import get_test_data -from metpy.interpolate import (interpolate_to_points, inverse_distance_to_points, - natural_neighbor_to_points) +from metpy.interpolate import ( + interpolate_to_points, + inverse_distance_to_points, + natural_neighbor_to_points, +) from metpy.interpolate.geometry import dist_2, find_natural_neighbors -from metpy.interpolate.points import (barnes_point, cressman_point, - natural_neighbor_point) +from metpy.interpolate.points import barnes_point, cressman_point, natural_neighbor_point -logging.getLogger('metpy.interpolate.points').setLevel(logging.ERROR) +logging.getLogger("metpy.interpolate.points").setLevel(logging.ERROR) @pytest.fixture() @@ -27,8 +29,9 @@ def test_data(): r"""Return data used for tests in this file.""" x = np.array([8, 67, 79, 10, 52, 53, 98, 34, 15, 58], dtype=float) y = np.array([24, 87, 48, 94, 98, 66, 14, 24, 60, 16], dtype=float) - z = np.array([0.064, 4.489, 6.241, 0.1, 2.704, 2.809, 9.604, 1.156, - 0.225, 3.364], dtype=float) + z = np.array( + [0.064, 4.489, 6.241, 0.1, 2.704, 2.809, 9.604, 1.156, 0.225, 3.364], dtype=float + ) return x, y, z @@ -36,9 +39,9 @@ def test_data(): @pytest.fixture() def test_points(): r"""Return point locations used for tests in this file.""" - with get_test_data('interpolation_test_grid.npz') as fobj: + with get_test_data("interpolation_test_grid.npz") as fobj: data = np.load(fobj) - return np.stack([data['xg'].reshape(-1), data['yg'].reshape(-1)], axis=1) + return np.stack([data["xg"].reshape(-1), data["yg"].reshape(-1)], axis=1) def test_nn_point(test_data): @@ -50,11 +53,11 @@ def test_nn_point(test_data): sim_gridx = [30] sim_gridy = [30] - members, tri_info = find_natural_neighbors(tri, - list(zip(sim_gridx, sim_gridy))) + members, tri_info = find_natural_neighbors(tri, list(zip(sim_gridx, sim_gridy))) - val = natural_neighbor_point(xp, yp, z, [sim_gridx[0], sim_gridy[0]], - tri, members[0], tri_info) + val = natural_neighbor_point( + xp, yp, z, [sim_gridx[0], sim_gridy[0]], tri, members[0], tri_info + ) truth = 1.009 @@ -104,80 +107,88 @@ def test_natural_neighbor_to_points(test_data, test_points): img = natural_neighbor_to_points(obs_points, z, test_points) - with get_test_data('nn_bbox0to100.npz') as fobj: - truth = np.load(fobj)['img'].reshape(-1) + with get_test_data("nn_bbox0to100.npz") as fobj: + truth = np.load(fobj)["img"].reshape(-1) assert_array_almost_equal(truth, img) -interp_methods = ['cressman', 'barnes', 'shouldraise'] +interp_methods = ["cressman", "barnes", "shouldraise"] -@pytest.mark.parametrize('method', interp_methods) +@pytest.mark.parametrize("method", interp_methods) def test_inverse_distance_to_points(method, test_data, test_points): r"""Test inverse distance interpolation to grid function.""" xp, yp, z = test_data obs_points = np.vstack([xp, yp]).transpose() extra_kw = {} - if method == 'cressman': - extra_kw['r'] = 20 - extra_kw['min_neighbors'] = 1 - test_file = 'cressman_r20_mn1.npz' - elif method == 'barnes': - extra_kw['r'] = 40 - extra_kw['kappa'] = 100 - test_file = 'barnes_r40_k100.npz' - elif method == 'shouldraise': - extra_kw['r'] = 40 + if method == "cressman": + extra_kw["r"] = 20 + extra_kw["min_neighbors"] = 1 + test_file = "cressman_r20_mn1.npz" + elif method == "barnes": + extra_kw["r"] = 40 + extra_kw["kappa"] = 100 + test_file = "barnes_r40_k100.npz" + elif method == "shouldraise": + extra_kw["r"] = 40 with pytest.raises(ValueError): - inverse_distance_to_points( - obs_points, z, test_points, kind=method, **extra_kw) + inverse_distance_to_points(obs_points, z, test_points, kind=method, **extra_kw) return img = inverse_distance_to_points(obs_points, z, test_points, kind=method, **extra_kw) with get_test_data(test_file) as fobj: - truth = np.load(fobj)['img'].reshape(-1) + truth = np.load(fobj)["img"].reshape(-1) assert_array_almost_equal(truth, img) # SciPy 1.2.0 fixed a bug in cubic interpolation, so we skip on older versions -interp_methods = ['natural_neighbor', 'cressman', 'barnes', - 'linear', 'nearest', 'rbf', 'shouldraise', - pytest.param('cubic', - marks=pytest.mark.skipif( - scipy.__version__ < '1.2.0', - reason='Need Scipy >=1.2 for fixed cubic interpolation.'))] - - -@pytest.mark.parametrize('method', interp_methods) +interp_methods = [ + "natural_neighbor", + "cressman", + "barnes", + "linear", + "nearest", + "rbf", + "shouldraise", + pytest.param( + "cubic", + marks=pytest.mark.skipif( + scipy.__version__ < "1.2.0", + reason="Need Scipy >=1.2 for fixed cubic interpolation.", + ), + ), +] + + +@pytest.mark.parametrize("method", interp_methods) def test_interpolate_to_points(method, test_data): r"""Test main grid interpolation function.""" xp, yp, z = test_data obs_points = np.vstack([xp, yp]).transpose() * 10 - with get_test_data('interpolation_test_points.npz') as fobj: - test_points = np.load(fobj)['points'] + with get_test_data("interpolation_test_points.npz") as fobj: + test_points = np.load(fobj)["points"] extra_kw = {} - if method == 'cressman': - extra_kw['search_radius'] = 200 - extra_kw['minimum_neighbors'] = 1 - elif method == 'barnes': - extra_kw['search_radius'] = 400 - extra_kw['minimum_neighbors'] = 1 - extra_kw['gamma'] = 1 - elif method == 'shouldraise': + if method == "cressman": + extra_kw["search_radius"] = 200 + extra_kw["minimum_neighbors"] = 1 + elif method == "barnes": + extra_kw["search_radius"] = 400 + extra_kw["minimum_neighbors"] = 1 + extra_kw["gamma"] = 1 + elif method == "shouldraise": with pytest.raises(ValueError): - interpolate_to_points( - obs_points, z, test_points, interp_type=method, **extra_kw) + interpolate_to_points(obs_points, z, test_points, interp_type=method, **extra_kw) return img = interpolate_to_points(obs_points, z, test_points, interp_type=method, **extra_kw) - with get_test_data(f'{method}_test.npz') as fobj: - truth = np.load(fobj)['img'].reshape(-1) + with get_test_data(f"{method}_test.npz") as fobj: + truth = np.load(fobj)["img"].reshape(-1) assert_array_almost_equal(truth, img) diff --git a/tests/interpolate/test_slices.py b/tests/interpolate/test_slices.py index 5f51a9216a1..28cf9d81bfa 100644 --- a/tests/interpolate/test_slices.py +++ b/tests/interpolate/test_slices.py @@ -19,29 +19,29 @@ def test_ds_lonlat(): data_rh = np.linspace(0, 1, 5 * 6 * 7).reshape((5, 6, 7)) * units.dimensionless ds = xr.Dataset( { - 'temperature': (['isobaric', 'lat', 'lon'], data_temp), - 'relative_humidity': (['isobaric', 'lat', 'lon'], data_rh) + "temperature": (["isobaric", "lat", "lon"], data_temp), + "relative_humidity": (["isobaric", "lat", "lon"], data_rh), }, coords={ - 'isobaric': xr.DataArray( + "isobaric": xr.DataArray( np.linspace(1000, 500, 5), - name='isobaric', - dims=['isobaric'], - attrs={'units': 'hPa'} + name="isobaric", + dims=["isobaric"], + attrs={"units": "hPa"}, ), - 'lat': xr.DataArray( + "lat": xr.DataArray( np.linspace(30, 45, 6), - name='lat', - dims=['lat'], - attrs={'units': 'degrees_north'} + name="lat", + dims=["lat"], + attrs={"units": "degrees_north"}, ), - 'lon': xr.DataArray( + "lon": xr.DataArray( np.linspace(255, 275, 7), - name='lon', - dims=['lon'], - attrs={'units': 'degrees_east'} - ) - } + name="lon", + dims=["lon"], + attrs={"units": "degrees_east"}, + ), + }, ) return ds.metpy.parse_cf() @@ -52,57 +52,45 @@ def test_ds_xy(): data_temperature = np.linspace(250, 300, 5 * 6 * 7).reshape((1, 5, 6, 7)) * units.kelvin ds = xr.Dataset( { - 'temperature': (['time', 'isobaric', 'y', 'x'], data_temperature), - 'lambert_conformal': ([], '') + "temperature": (["time", "isobaric", "y", "x"], data_temperature), + "lambert_conformal": ([], ""), }, coords={ - 'time': xr.DataArray( - np.array([np.datetime64('2018-07-01T00:00')]), - name='time', - dims=['time'] + "time": xr.DataArray( + np.array([np.datetime64("2018-07-01T00:00")]), name="time", dims=["time"] ), - 'isobaric': xr.DataArray( + "isobaric": xr.DataArray( np.linspace(1000, 500, 5), - name='isobaric', - dims=['isobaric'], - attrs={'units': 'hPa'} + name="isobaric", + dims=["isobaric"], + attrs={"units": "hPa"}, + ), + "y": xr.DataArray( + np.linspace(-1500, 0, 6), name="y", dims=["y"], attrs={"units": "km"} ), - 'y': xr.DataArray( - np.linspace(-1500, 0, 6), - name='y', - dims=['y'], - attrs={'units': 'km'} + "x": xr.DataArray( + np.linspace(-500, 3000, 7), name="x", dims=["x"], attrs={"units": "km"} ), - 'x': xr.DataArray( - np.linspace(-500, 3000, 7), - name='x', - dims=['x'], - attrs={'units': 'km'} - ) - } + }, ) - ds['temperature'].attrs = { - 'grid_mapping': 'lambert_conformal' - } - ds['lambert_conformal'].attrs = { - 'grid_mapping_name': 'lambert_conformal_conic', - 'standard_parallel': 50.0, - 'longitude_of_central_meridian': -107.0, - 'latitude_of_projection_origin': 50.0, - 'earth_shape': 'spherical', - 'earth_radius': 6367470.21484375 + ds["temperature"].attrs = {"grid_mapping": "lambert_conformal"} + ds["lambert_conformal"].attrs = { + "grid_mapping_name": "lambert_conformal_conic", + "standard_parallel": 50.0, + "longitude_of_central_meridian": -107.0, + "latitude_of_projection_origin": 50.0, + "earth_shape": "spherical", + "earth_radius": 6367470.21484375, } return ds.metpy.parse_cf() def test_interpolate_to_slice_against_selection(test_ds_lonlat): """Test interpolate_to_slice on a simple operation.""" - data = test_ds_lonlat['temperature'] - path = np.array([[265.0, 30.], - [265.0, 36.], - [265.0, 42.]]) + data = test_ds_lonlat["temperature"] + path = np.array([[265.0, 30.0], [265.0, 36.0], [265.0, 42.0]]) test_slice = interpolate_to_slice(data, path) - true_slice = data.sel({'lat': [30., 36., 42.], 'lon': 265.0}) + true_slice = data.sel({"lat": [30.0, 36.0, 42.0], "lon": 265.0}) # Coordinates differ, so just compare the data assert_array_almost_equal(true_slice.metpy.unit_array, test_slice.metpy.unit_array, 5) @@ -110,76 +98,137 @@ def test_interpolate_to_slice_against_selection(test_ds_lonlat): @needs_cartopy def test_geodesic(test_ds_xy): """Test the geodesic construction.""" - crs = test_ds_xy['temperature'].metpy.cartopy_crs + crs = test_ds_xy["temperature"].metpy.cartopy_crs path = geodesic(crs, (36.46, -112.45), (42.95, -68.74), 7) - truth = np.array([[-4.99495719e+05, -1.49986599e+06], - [9.84044354e+04, -1.26871737e+06], - [6.88589099e+05, -1.02913966e+06], - [1.27269045e+06, -7.82037603e+05], - [1.85200974e+06, -5.28093957e+05], - [2.42752546e+06, -2.67710326e+05], - [2.99993290e+06, -9.39107692e+02]]) + truth = np.array( + [ + [-4.99495719e05, -1.49986599e06], + [9.84044354e04, -1.26871737e06], + [6.88589099e05, -1.02913966e06], + [1.27269045e06, -7.82037603e05], + [1.85200974e06, -5.28093957e05], + [2.42752546e06, -2.67710326e05], + [2.99993290e06, -9.39107692e02], + ] + ) assert_array_almost_equal(path, truth, 0) @needs_cartopy def test_cross_section_dataarray_and_linear_interp(test_ds_xy): """Test the cross_section function with a data array and linear interpolation.""" - data = test_ds_xy['temperature'] + data = test_ds_xy["temperature"] start, end = ((36.46, -112.45), (42.95, -68.74)) data_cross = cross_section(data, start, end, steps=7) - truth_values = np.array([[[250.00095489, 251.53646673, 253.11586664, 254.73477364, - 256.38991013, 258.0794356, 259.80334269], - [260.04880178, 261.58431362, 263.16371353, 264.78262053, - 266.43775702, 268.12728249, 269.85118958], - [270.09664867, 271.63216051, 273.21156042, 274.83046742, - 276.48560391, 278.17512938, 279.89903647], - [280.14449556, 281.6800074, 283.25940731, 284.87831431, - 286.5334508, 288.22297627, 289.94688336], - [290.19234245, 291.72785429, 293.3072542, 294.9261612, - 296.58129769, 298.27082316, 299.99473025]]]) - truth_values_x = np.array([-499495.71907062, 98404.43537514, 688589.09865512, - 1272690.44926197, 1852009.73516881, 2427525.45740665, - 2999932.89862589]) - truth_values_y = np.array([-1499865.98780602, -1268717.36799267, -1029139.66048478, - -782037.60343652, -528093.95678826, -267710.32566917, - -939.10769171]) - index = xr.DataArray(range(7), name='index', dims=['index']) + truth_values = np.array( + [ + [ + [ + 250.00095489, + 251.53646673, + 253.11586664, + 254.73477364, + 256.38991013, + 258.0794356, + 259.80334269, + ], + [ + 260.04880178, + 261.58431362, + 263.16371353, + 264.78262053, + 266.43775702, + 268.12728249, + 269.85118958, + ], + [ + 270.09664867, + 271.63216051, + 273.21156042, + 274.83046742, + 276.48560391, + 278.17512938, + 279.89903647, + ], + [ + 280.14449556, + 281.6800074, + 283.25940731, + 284.87831431, + 286.5334508, + 288.22297627, + 289.94688336, + ], + [ + 290.19234245, + 291.72785429, + 293.3072542, + 294.9261612, + 296.58129769, + 298.27082316, + 299.99473025, + ], + ] + ] + ) + truth_values_x = np.array( + [ + -499495.71907062, + 98404.43537514, + 688589.09865512, + 1272690.44926197, + 1852009.73516881, + 2427525.45740665, + 2999932.89862589, + ] + ) + truth_values_y = np.array( + [ + -1499865.98780602, + -1268717.36799267, + -1029139.66048478, + -782037.60343652, + -528093.95678826, + -267710.32566917, + -939.10769171, + ] + ) + index = xr.DataArray(range(7), name="index", dims=["index"]) data_truth_x = xr.DataArray( truth_values_x, - name='x', + name="x", coords={ - 'crs': data['crs'], - 'y': (['index'], truth_values_y), - 'x': (['index'], truth_values_x), - 'index': index, + "crs": data["crs"], + "y": (["index"], truth_values_y), + "x": (["index"], truth_values_x), + "index": index, }, - dims=['index'] + dims=["index"], ) data_truth_y = xr.DataArray( truth_values_y, - name='y', + name="y", coords={ - 'crs': data['crs'], - 'y': (['index'], truth_values_y), - 'x': (['index'], truth_values_x), - 'index': index, + "crs": data["crs"], + "y": (["index"], truth_values_y), + "x": (["index"], truth_values_x), + "index": index, }, - dims=['index'] + dims=["index"], ) data_truth = xr.DataArray( truth_values * units.kelvin, - name='temperature', + name="temperature", coords={ - 'time': data['time'], - 'isobaric': data['isobaric'], - 'index': index, - 'crs': data['crs'], - 'y': data_truth_y, - 'x': data_truth_x + "time": data["time"], + "isobaric": data["isobaric"], + "index": index, + "crs": data["crs"], + "y": data_truth_y, + "x": data_truth_x, }, - dims=['time', 'isobaric', 'index'] + dims=["time", "isobaric", "index"], ) xr.testing.assert_allclose(data_truth, data_cross) @@ -187,7 +236,7 @@ def test_cross_section_dataarray_and_linear_interp(test_ds_xy): def test_cross_section_dataarray_projection_noop(test_ds_xy): """Test the cross_section function with a projection dataarray.""" - data = test_ds_xy['lambert_conformal'] + data = test_ds_xy["lambert_conformal"] start, end = ((36.46, -112.45), (42.95, -68.74)) data_cross = cross_section(data, start, end, steps=7) xr.testing.assert_identical(data, data_cross) @@ -197,28 +246,32 @@ def test_cross_section_dataarray_projection_noop(test_ds_xy): def test_cross_section_dataset_and_nearest_interp(test_ds_lonlat): """Test the cross_section function with a dataset and nearest interpolation.""" start, end = (30.5, 255.5), (44.5, 274.5) - data_cross = cross_section(test_ds_lonlat, start, end, steps=7, interp_type='nearest') - nearest_values = test_ds_lonlat.isel(lat=xr.DataArray([0, 1, 2, 3, 3, 4, 5], dims='index'), - lon=xr.DataArray(range(7), dims='index')) - truth_temp = nearest_values['temperature'].metpy.unit_array - truth_rh = nearest_values['relative_humidity'].metpy.unit_array - truth_values_lon = np.array([255.5, 258.20305939, 261.06299342, 264.10041516, - 267.3372208, 270.7961498, 274.5]) - truth_values_lat = np.array([30.5, 33.02800969, 35.49306226, 37.88512911, 40.19271688, - 42.40267088, 44.5]) - index = xr.DataArray(range(7), name='index', dims=['index']) + data_cross = cross_section(test_ds_lonlat, start, end, steps=7, interp_type="nearest") + nearest_values = test_ds_lonlat.isel( + lat=xr.DataArray([0, 1, 2, 3, 3, 4, 5], dims="index"), + lon=xr.DataArray(range(7), dims="index"), + ) + truth_temp = nearest_values["temperature"].metpy.unit_array + truth_rh = nearest_values["relative_humidity"].metpy.unit_array + truth_values_lon = np.array( + [255.5, 258.20305939, 261.06299342, 264.10041516, 267.3372208, 270.7961498, 274.5] + ) + truth_values_lat = np.array( + [30.5, 33.02800969, 35.49306226, 37.88512911, 40.19271688, 42.40267088, 44.5] + ) + index = xr.DataArray(range(7), name="index", dims=["index"]) data_truth = xr.Dataset( { - 'temperature': (['isobaric', 'index'], truth_temp), - 'relative_humidity': (['isobaric', 'index'], truth_rh) + "temperature": (["isobaric", "index"], truth_temp), + "relative_humidity": (["isobaric", "index"], truth_rh), }, coords={ - 'isobaric': test_ds_lonlat['isobaric'], - 'index': index, - 'crs': test_ds_lonlat['crs'], - 'lat': (['index'], truth_values_lat), - 'lon': (['index'], truth_values_lon) + "isobaric": test_ds_lonlat["isobaric"], + "index": index, + "crs": test_ds_lonlat["crs"], + "lat": (["index"], truth_values_lat), + "lon": (["index"], truth_values_lon), }, ) @@ -228,11 +281,9 @@ def test_cross_section_dataset_and_nearest_interp(test_ds_lonlat): def test_interpolate_to_slice_error_on_missing_coordinate(test_ds_lonlat): """Test that the proper error is raised with missing coordinate.""" # Use a variable with a coordinate removed - data_bad = test_ds_lonlat['temperature'].copy() - del data_bad['lat'] - path = np.array([[265.0, 30.], - [265.0, 36.], - [265.0, 42.]]) + data_bad = test_ds_lonlat["temperature"].copy() + del data_bad["lat"] + path = np.array([[265.0, 30.0], [265.0, 36.0], [265.0, 42.0]]) with pytest.raises(ValueError): interpolate_to_slice(data_bad, path) @@ -241,8 +292,8 @@ def test_interpolate_to_slice_error_on_missing_coordinate(test_ds_lonlat): def test_cross_section_error_on_missing_coordinate(test_ds_lonlat): """Test that the proper error is raised with missing coordinate.""" # Use a variable with no crs coordinate - data_bad = test_ds_lonlat['temperature'].copy() - del data_bad['crs'] + data_bad = test_ds_lonlat["temperature"].copy() + del data_bad["crs"] start, end = (30.5, 255.5), (44.5, 274.5) with pytest.raises(ValueError): diff --git a/tests/io/test_gini.py b/tests/io/test_gini.py index 34631853989..dfb7516ab8c 100644 --- a/tests/io/test_gini.py +++ b/tests/io/test_gini.py @@ -16,46 +16,103 @@ from metpy.io.gini import GiniProjection from metpy.testing import needs_pyproj -logging.getLogger('metpy.io.gini').setLevel(logging.ERROR) +logging.getLogger("metpy.io.gini").setLevel(logging.ERROR) # Reference contents of the named tuples from each file make_pdb = GiniFile.prod_desc_fmt.make_tuple make_pdb2 = GiniFile.prod_desc2_fmt.make_tuple -raw_gini_info = [('WEST-CONUS_4km_WV_20151208_2200.gini', - make_pdb(source=1, creating_entity='GOES-15', sector_id='West CONUS', - channel='WV (6.5/6.7 micron)', num_records=1280, record_len=1100, - datetime=datetime(2015, 12, 8, 22, 0, 19), - projection=GiniProjection.lambert_conformal, nx=1100, ny=1280, - la1=12.19, lo1=-133.4588), - make_pdb2(scanning_mode=[False, False, False], lat_in=25.0, resolution=4, - compression=0, version=1, pdb_size=512, nav_cal=0), - GiniFile.lc_ps_fmt.make_tuple(reserved=0, lov=-95.0, dx=4.0635, dy=4.0635, - proj_center=0)), - ('AK-REGIONAL_8km_3.9_20160408_1445.gini', - make_pdb(source=1, creating_entity='GOES-15', sector_id='Alaska Regional', - channel='IR (3.9 micron)', num_records=408, record_len=576, - datetime=datetime(2016, 4, 8, 14, 45, 20), - projection=GiniProjection.polar_stereographic, nx=576, ny=408, - la1=42.0846, lo1=-175.641), - make_pdb2(scanning_mode=[False, False, False], lat_in=0.0, resolution=8, - compression=0, version=1, pdb_size=512, nav_cal=0), - GiniFile.lc_ps_fmt.make_tuple(reserved=0, lov=210.0, dx=7.9375, dy=7.9375, - proj_center=0)), - ('HI-REGIONAL_4km_3.9_20160616_1715.gini', - make_pdb(source=1, creating_entity='GOES-15', sector_id='Hawaii Regional', - channel='IR (3.9 micron)', num_records=520, record_len=560, - datetime=datetime(2016, 6, 16, 17, 15, 18), - projection=GiniProjection.mercator, nx=560, ny=520, - la1=9.343, lo1=-167.315), - make_pdb2(scanning_mode=[False, False, False], lat_in=20.0, resolution=4, - compression=0, version=1, pdb_size=512, nav_cal=0), - GiniFile.mercator_fmt.make_tuple(resolution=0, la2=28.0922, lo2=-145.878, - di=0, dj=0)) - ] - - -@pytest.mark.parametrize('filename,pdb,pdb2,proj_info', raw_gini_info, - ids=['LCC', 'Stereographic', 'Mercator']) +raw_gini_info = [ + ( + "WEST-CONUS_4km_WV_20151208_2200.gini", + make_pdb( + source=1, + creating_entity="GOES-15", + sector_id="West CONUS", + channel="WV (6.5/6.7 micron)", + num_records=1280, + record_len=1100, + datetime=datetime(2015, 12, 8, 22, 0, 19), + projection=GiniProjection.lambert_conformal, + nx=1100, + ny=1280, + la1=12.19, + lo1=-133.4588, + ), + make_pdb2( + scanning_mode=[False, False, False], + lat_in=25.0, + resolution=4, + compression=0, + version=1, + pdb_size=512, + nav_cal=0, + ), + GiniFile.lc_ps_fmt.make_tuple( + reserved=0, lov=-95.0, dx=4.0635, dy=4.0635, proj_center=0 + ), + ), + ( + "AK-REGIONAL_8km_3.9_20160408_1445.gini", + make_pdb( + source=1, + creating_entity="GOES-15", + sector_id="Alaska Regional", + channel="IR (3.9 micron)", + num_records=408, + record_len=576, + datetime=datetime(2016, 4, 8, 14, 45, 20), + projection=GiniProjection.polar_stereographic, + nx=576, + ny=408, + la1=42.0846, + lo1=-175.641, + ), + make_pdb2( + scanning_mode=[False, False, False], + lat_in=0.0, + resolution=8, + compression=0, + version=1, + pdb_size=512, + nav_cal=0, + ), + GiniFile.lc_ps_fmt.make_tuple( + reserved=0, lov=210.0, dx=7.9375, dy=7.9375, proj_center=0 + ), + ), + ( + "HI-REGIONAL_4km_3.9_20160616_1715.gini", + make_pdb( + source=1, + creating_entity="GOES-15", + sector_id="Hawaii Regional", + channel="IR (3.9 micron)", + num_records=520, + record_len=560, + datetime=datetime(2016, 6, 16, 17, 15, 18), + projection=GiniProjection.mercator, + nx=560, + ny=520, + la1=9.343, + lo1=-167.315, + ), + make_pdb2( + scanning_mode=[False, False, False], + lat_in=20.0, + resolution=4, + compression=0, + version=1, + pdb_size=512, + nav_cal=0, + ), + GiniFile.mercator_fmt.make_tuple(resolution=0, la2=28.0922, lo2=-145.878, di=0, dj=0), + ), +] + + +@pytest.mark.parametrize( + "filename,pdb,pdb2,proj_info", raw_gini_info, ids=["LCC", "Stereographic", "Mercator"] +) def test_raw_gini(filename, pdb, pdb2, proj_info): """Test raw GINI parsing.""" f = GiniFile(get_test_data(filename)) @@ -67,37 +124,64 @@ def test_raw_gini(filename, pdb, pdb2, proj_info): def test_gini_bad_size(): """Test reading a GINI file that reports a bad header size.""" - f = GiniFile(get_test_data('NHEM-MULTICOMP_1km_IR_20151208_2100.gini')) + f = GiniFile(get_test_data("NHEM-MULTICOMP_1km_IR_20151208_2100.gini")) pdb2 = f.prod_desc2 assert pdb2.pdb_size == 512 # Catching bad size # Reference information coming out of the dataset interface, coordinate calculations, # inclusion of correct projection metadata, etc. -gini_dataset_info = [('WEST-CONUS_4km_WV_20151208_2200.gini', - (-4226066.37649, 239720.12351, -832700.70519, 4364515.79481), 'WV', - {'grid_mapping_name': 'lambert_conformal_conic', - 'standard_parallel': 25.0, 'earth_radius': 6371200., - 'latitude_of_projection_origin': 25.0, - 'longitude_of_central_meridian': -95.0}, (150, 150, 184), - datetime(2015, 12, 8, 22, 0, 19)), - ('AK-REGIONAL_8km_3.9_20160408_1445.gini', - (-2286001.13195, 2278061.36805, -4762503.5992, -1531941.0992), 'IR', - {'grid_mapping_name': 'polar_stereographic', 'standard_parallel': 60.0, - 'earth_radius': 6371200., 'latitude_of_projection_origin': 90., - 'straight_vertical_longitude_from_pole': 210.0}, (150, 150, 143), - datetime(2016, 4, 8, 14, 45, 20)), - ('HI-REGIONAL_4km_3.9_20160616_1715.gini', - (0.0, 2236000.0, 980627.44738, 3056627.44738), 'IR', - {'grid_mapping_name': 'mercator', 'standard_parallel': 20.0, - 'earth_radius': 6371200., 'latitude_of_projection_origin': 9.343, - 'longitude_of_projection_origin': -167.315}, (150, 150, 111), - datetime(2016, 6, 16, 17, 15, 18)) - ] - - -@pytest.mark.parametrize('filename,bounds,data_var,proj_attrs,image,dt', gini_dataset_info, - ids=['LCC', 'Stereographic', 'Mercator']) +gini_dataset_info = [ + ( + "WEST-CONUS_4km_WV_20151208_2200.gini", + (-4226066.37649, 239720.12351, -832700.70519, 4364515.79481), + "WV", + { + "grid_mapping_name": "lambert_conformal_conic", + "standard_parallel": 25.0, + "earth_radius": 6371200.0, + "latitude_of_projection_origin": 25.0, + "longitude_of_central_meridian": -95.0, + }, + (150, 150, 184), + datetime(2015, 12, 8, 22, 0, 19), + ), + ( + "AK-REGIONAL_8km_3.9_20160408_1445.gini", + (-2286001.13195, 2278061.36805, -4762503.5992, -1531941.0992), + "IR", + { + "grid_mapping_name": "polar_stereographic", + "standard_parallel": 60.0, + "earth_radius": 6371200.0, + "latitude_of_projection_origin": 90.0, + "straight_vertical_longitude_from_pole": 210.0, + }, + (150, 150, 143), + datetime(2016, 4, 8, 14, 45, 20), + ), + ( + "HI-REGIONAL_4km_3.9_20160616_1715.gini", + (0.0, 2236000.0, 980627.44738, 3056627.44738), + "IR", + { + "grid_mapping_name": "mercator", + "standard_parallel": 20.0, + "earth_radius": 6371200.0, + "latitude_of_projection_origin": 9.343, + "longitude_of_projection_origin": -167.315, + }, + (150, 150, 111), + datetime(2016, 6, 16, 17, 15, 18), + ), +] + + +@pytest.mark.parametrize( + "filename,bounds,data_var,proj_attrs,image,dt", + gini_dataset_info, + ids=["LCC", "Stereographic", "Mercator"], +) @needs_pyproj def test_gini_xarray(filename, bounds, data_var, proj_attrs, image, dt): """Test that GINIFile can be passed to XArray as a datastore.""" @@ -106,41 +190,41 @@ def test_gini_xarray(filename, bounds, data_var, proj_attrs, image, dt): # Check our calculated x and y arrays x0, x1, y0, y1 = bounds - x = ds.variables['x'] + x = ds.variables["x"] assert_almost_equal(x[0], x0, 4) assert_almost_equal(x[-1], x1, 4) # Because the actual data raster has the top row first, the maximum y value is y[0], # while the minimum y value is y[-1] - y = ds.variables['y'] + y = ds.variables["y"] assert_almost_equal(y[-1], y0, 4) assert_almost_equal(y[0], y1, 4) # Check the projection metadata - proj_name = ds.variables[data_var].attrs['grid_mapping'] + proj_name = ds.variables[data_var].attrs["grid_mapping"] proj_var = ds.variables[proj_name] for attr, val in proj_attrs.items(): - assert proj_var.attrs[attr] == val, 'Values mismatch for ' + attr + assert proj_var.attrs[attr] == val, "Values mismatch for " + attr # Check the lower left lon/lat corner - assert_almost_equal(ds.variables['lon'][-1, 0], f.prod_desc.lo1, 4) - assert_almost_equal(ds.variables['lat'][-1, 0], f.prod_desc.la1, 4) + assert_almost_equal(ds.variables["lon"][-1, 0], f.prod_desc.lo1, 4) + assert_almost_equal(ds.variables["lat"][-1, 0], f.prod_desc.la1, 4) # Check a pixel of the image to make sure we're decoding correctly x_ind, y_ind, val = image assert val == ds.variables[data_var][x_ind, y_ind] # Check time decoding - assert np.asarray(dt, dtype='datetime64[ms]') == ds.variables['time'] + assert np.asarray(dt, dtype="datetime64[ms]") == ds.variables["time"] @needs_pyproj def test_gini_mercator_upper_corner(): """Test that the upper corner of the Mercator coordinates is correct.""" - f = GiniFile(get_test_data('HI-REGIONAL_4km_3.9_20160616_1715.gini')) + f = GiniFile(get_test_data("HI-REGIONAL_4km_3.9_20160616_1715.gini")) ds = xr.open_dataset(f) - lat = ds.variables['lat'] - lon = ds.variables['lon'] + lat = ds.variables["lat"] + lon = ds.variables["lon"] # 2nd corner lat/lon are at the "upper right" corner of the pixel, so need to add one # more grid point @@ -150,25 +234,28 @@ def test_gini_mercator_upper_corner(): def test_gini_str(): """Test the str representation of GiniFile.""" - f = GiniFile(get_test_data('WEST-CONUS_4km_WV_20151208_2200.gini')) - truth = ('GiniFile: GOES-15 West CONUS WV (6.5/6.7 micron)\n' - '\tTime: 2015-12-08 22:00:19\n\tSize: 1280x1100\n' - '\tProjection: lambert_conformal\n' - '\tLower Left Corner (Lon, Lat): (-133.4588, 12.19)\n\tResolution: 4km') + f = GiniFile(get_test_data("WEST-CONUS_4km_WV_20151208_2200.gini")) + truth = ( + "GiniFile: GOES-15 West CONUS WV (6.5/6.7 micron)\n" + "\tTime: 2015-12-08 22:00:19\n\tSize: 1280x1100\n" + "\tProjection: lambert_conformal\n" + "\tLower Left Corner (Lon, Lat): (-133.4588, 12.19)\n\tResolution: 4km" + ) assert str(f) == truth def test_gini_pathlib(): """Test that GiniFile works with `pathlib.Path` instances.""" from pathlib import Path - src = Path(get_test_data('WEST-CONUS_4km_WV_20151208_2200.gini', as_file_obj=False)) + + src = Path(get_test_data("WEST-CONUS_4km_WV_20151208_2200.gini", as_file_obj=False)) f = GiniFile(src) - assert f.prod_desc.sector_id == 'West CONUS' + assert f.prod_desc.sector_id == "West CONUS" def test_unidata_composite(): """Test reading radar composites in GINI format made by Unidata.""" - f = GiniFile(get_test_data('Level3_Composite_dhr_1km_20180309_2225.gini')) + f = GiniFile(get_test_data("Level3_Composite_dhr_1km_20180309_2225.gini")) # Check the time stamp assert datetime(2018, 3, 9, 22, 25) == f.prod_desc.datetime @@ -179,6 +266,6 @@ def test_unidata_composite(): def test_percent_normal(): """Test reading PCT products properly.""" - f = GiniFile(get_test_data('PR-NATIONAL_1km_PCT_20200320_0446.gini')) + f = GiniFile(get_test_data("PR-NATIONAL_1km_PCT_20200320_0446.gini")) - assert f.prod_desc.channel == 'Percent Normal TPW' + assert f.prod_desc.channel == "Percent Normal TPW" diff --git a/tests/io/test_metar.py b/tests/io/test_metar.py index e29a0f8c3ab..5b2fcc0b505 100644 --- a/tests/io/test_metar.py +++ b/tests/io/test_metar.py @@ -14,9 +14,10 @@ def test_station_id_not_in_dictionary(): """Test for when the METAR does not correspond to a station in the dictionary.""" - df = parse_metar_to_dataframe('METAR KLBG 261155Z AUTO 00000KT 10SM CLR 05/00 A3001 RMK ' - 'AO2=') - assert df.station_id.values == 'KLBG' + df = parse_metar_to_dataframe( + "METAR KLBG 261155Z AUTO 00000KT 10SM CLR 05/00 A3001 RMK " "AO2=" + ) + assert df.station_id.values == "KLBG" assert_almost_equal(df.latitude.values, np.nan) assert_almost_equal(df.longitude.values, np.nan) assert_almost_equal(df.elevation.values, np.nan) @@ -26,17 +27,19 @@ def test_station_id_not_in_dictionary(): def test_broken_clouds(): """Test for skycover when there are broken clouds.""" - df = parse_metar_to_dataframe('METAR KLOT 261155Z AUTO 00000KT 10SM BKN100 05/00 A3001 ' - 'RMK AO2=') - assert df.low_cloud_type.values == 'BKN' + df = parse_metar_to_dataframe( + "METAR KLOT 261155Z AUTO 00000KT 10SM BKN100 05/00 A3001 " "RMK AO2=" + ) + assert df.low_cloud_type.values == "BKN" assert df.cloud_coverage.values == 6 def test_few_clouds_(): """Test for skycover when there are few clouds.""" - df = parse_metar_to_dataframe('METAR KMKE 266155Z AUTO /////KT 10SM FEW100 05/00 A3001 ' - 'RMK AO2=') - assert df.low_cloud_type.values == 'FEW' + df = parse_metar_to_dataframe( + "METAR KMKE 266155Z AUTO /////KT 10SM FEW100 05/00 A3001 " "RMK AO2=" + ) + assert df.low_cloud_type.values == "FEW" assert df.cloud_coverage.values == 2 assert_almost_equal(df.wind_direction.values, np.nan) assert_almost_equal(df.wind_speed.values, np.nan) @@ -45,24 +48,26 @@ def test_few_clouds_(): def test_all_weather_given(): """Test when all possible weather slots are given.""" - df = parse_metar_to_dataframe('METAR RJOI 261155Z 00000KT 4000 -SHRA BR VCSH BKN009 ' - 'BKN015 OVC030 OVC040 22/21 A2987 RMK SHRAB35E44 SLP114 ' - 'VCSH S-NW P0000 60021 70021 T02220206 10256 20211 55000=') - assert df.station_id.values == 'RJOI' + df = parse_metar_to_dataframe( + "METAR RJOI 261155Z 00000KT 4000 -SHRA BR VCSH BKN009 " + "BKN015 OVC030 OVC040 22/21 A2987 RMK SHRAB35E44 SLP114 " + "VCSH S-NW P0000 60021 70021 T02220206 10256 20211 55000=" + ) + assert df.station_id.values == "RJOI" assert_almost_equal(df.latitude.values, 34.14, decimal=2) assert_almost_equal(df.longitude.values, 132.22, decimal=2) - assert df.current_wx1.values == '-SHRA' - assert df.current_wx2.values == 'BR' - assert df.current_wx3.values == 'VCSH' - assert df.low_cloud_type.values == 'BKN' + assert df.current_wx1.values == "-SHRA" + assert df.current_wx2.values == "BR" + assert df.current_wx3.values == "VCSH" + assert df.low_cloud_type.values == "BKN" assert df.low_cloud_level.values == 900 - assert df.high_cloud_type.values == 'OVC' + assert df.high_cloud_type.values == "OVC" assert df.high_cloud_level.values == 3000 def test_missing_temp_dewp(): """Test when missing both temperature and dewpoint.""" - df = parse_metar_to_dataframe('KIOW 011152Z AUTO A3006 RMK AO2 SLPNO 70020 51013 PWINO=') + df = parse_metar_to_dataframe("KIOW 011152Z AUTO A3006 RMK AO2 SLPNO 70020 51013 PWINO=") assert_almost_equal(df.air_temperature.values, np.nan) assert_almost_equal(df.dew_point_temperature.values, np.nan) assert_almost_equal(df.cloud_coverage.values, 10) @@ -70,8 +75,9 @@ def test_missing_temp_dewp(): def test_missing_values(): """Test for missing values from nearly every field.""" - df = parse_metar_to_dataframe('METAR KBOU 011152Z AUTO 02006KT //// // ////// 42/02 ' - 'Q1004=') + df = parse_metar_to_dataframe( + "METAR KBOU 011152Z AUTO 02006KT //// // ////// 42/02 " "Q1004=" + ) assert_almost_equal(df.current_wx1.values, np.nan) assert_almost_equal(df.current_wx2.values, np.nan) assert_almost_equal(df.current_wx3.values, np.nan) @@ -85,16 +91,21 @@ def test_missing_values(): def test_vertical_vis(): """Test for when vertical visibility is given.""" - df = parse_metar_to_dataframe('KSLK 011151Z AUTO 21005KT 1/4SM FG VV002 14/13 A1013 RMK ' - 'AO2 SLP151 70043 T01390133 10139 20094 53002=') - assert df.low_cloud_type.values == 'VV' + df = parse_metar_to_dataframe( + "KSLK 011151Z AUTO 21005KT 1/4SM FG VV002 14/13 A1013 RMK " + "AO2 SLP151 70043 T01390133 10139 20094 53002=" + ) + assert df.low_cloud_type.values == "VV" def test_date_time_given(): """Test for when date_time is given.""" - df = parse_metar_to_dataframe('K6B0 261200Z AUTO 00000KT 10SM CLR 20/M17 A3002 RMK AO2 ' - 'T01990165=', year=2019, month=6) - assert_equal(df['date_time'][0], datetime(2019, 6, 26, 12)) + df = parse_metar_to_dataframe( + "K6B0 261200Z AUTO 00000KT 10SM CLR 20/M17 A3002 RMK AO2 " "T01990165=", + year=2019, + month=6, + ) + assert_equal(df["date_time"][0], datetime(2019, 6, 26, 12)) assert df.eastward_wind.values == 0 assert df.northward_wind.values == 0 @@ -102,16 +113,19 @@ def test_date_time_given(): def test_parse_metar_df_positional_datetime_failure(): """Test that positional year, month arguments fail for parse_metar_to_dataframe.""" # pylint: disable=too-many-function-args - with pytest.raises(TypeError, match='takes 1 positional argument but 3 were given'): - parse_metar_to_dataframe('K6B0 261200Z AUTO 00000KT 10SM CLR 20/M17' - 'A3002 RMK AO2 T01990165=', 2019, 6) + with pytest.raises(TypeError, match="takes 1 positional argument but 3 were given"): + parse_metar_to_dataframe( + "K6B0 261200Z AUTO 00000KT 10SM CLR 20/M17" "A3002 RMK AO2 T01990165=", 2019, 6 + ) def test_named_tuple_test1(): """Test the named tuple parsing function.""" - df = parse_metar_to_dataframe('KDEN 012153Z 09010KT 10SM FEW060 BKN110 BKN220 27/13 ' - 'A3010 RMK AO2 LTG DSNT SW AND W SLP114 OCNL LTGICCG ' - 'DSNT SW CB DSNT SW MOV E T02670128') + df = parse_metar_to_dataframe( + "KDEN 012153Z 09010KT 10SM FEW060 BKN110 BKN220 27/13 " + "A3010 RMK AO2 LTG DSNT SW AND W SLP114 OCNL LTGICCG " + "DSNT SW CB DSNT SW MOV E T02670128" + ) assert df.wind_direction.values == 90 assert df.wind_speed.values == 10 assert df.air_temperature.values == 27 @@ -120,9 +134,9 @@ def test_named_tuple_test1(): def test_parse_file(): """Test the parser on an entire file.""" - input_file = get_test_data('metar_20190701_1200.txt', as_file_obj=False) + input_file = get_test_data("metar_20190701_1200.txt", as_file_obj=False) df = parse_metar_file(input_file) - test = df[df.station_id == 'KVPZ'] + test = df[df.station_id == "KVPZ"] assert test.air_temperature.values == 23 assert test.air_pressure_at_sea_level.values == 1016.76 @@ -130,25 +144,25 @@ def test_parse_file(): def test_parse_file_positional_datetime_failure(): """Test that positional year, month arguments fail for parse_metar_file.""" # pylint: disable=too-many-function-args - input_file = get_test_data('metar_20190701_1200.txt', as_file_obj=False) - with pytest.raises(TypeError, match='takes 1 positional argument but 3 were given'): + input_file = get_test_data("metar_20190701_1200.txt", as_file_obj=False) + with pytest.raises(TypeError, match="takes 1 positional argument but 3 were given"): parse_metar_file(input_file, 2016, 12) def test_parse_file_bad_encoding(): """Test the parser on an entire file that has at least one bad utf-8 encoding.""" - input_file = get_test_data('2020010600_sao.wmo', as_file_obj=False) + input_file = get_test_data("2020010600_sao.wmo", as_file_obj=False) df = parse_metar_file(input_file) - test = df[df.station_id == 'KDEN'] + test = df[df.station_id == "KDEN"] assert test.air_temperature.values == 2 assert test.air_pressure_at_sea_level.values == 1024.71 def test_parse_file_object(): """Test the parser reading from a file-like object.""" - input_file = get_test_data('metar_20190701_1200.txt', mode='rt') + input_file = get_test_data("metar_20190701_1200.txt", mode="rt") df = parse_metar_file(input_file) - test = df[df.station_id == 'KOKC'] + test = df[df.station_id == "KOKC"] assert test.air_temperature.values == 21 assert test.dew_point_temperature.values == 21 assert test.altimeter.values == 30.03 diff --git a/tests/io/test_nexrad.py b/tests/io/test_nexrad.py index dc8bb3e34d7..f7b19e83161 100644 --- a/tests/io/test_nexrad.py +++ b/tests/io/test_nexrad.py @@ -11,11 +11,11 @@ import numpy as np import pytest -from metpy.cbook import get_test_data, POOCH -from metpy.io import is_precip_mode, Level2File, Level3File +from metpy.cbook import POOCH, get_test_data +from metpy.io import Level2File, Level3File, is_precip_mode # Turn off the warnings for tests -logging.getLogger('metpy.io.nexrad').setLevel(logging.CRITICAL) +logging.getLogger("metpy.io.nexrad").setLevel(logging.CRITICAL) # # NEXRAD Level 2 Tests @@ -24,20 +24,23 @@ # 1999 file tests old message 1 # KFTG tests bzip compression and newer format for a part of message 31 # KTLX 2015 has missing segments for message 18, which was causing exception -level2_files = [('KTLX20130520_201643_V06.gz', datetime(2013, 5, 20, 20, 16, 46), 17, 4, 6), - ('KTLX19990503_235621.gz', datetime(1999, 5, 3, 23, 56, 21), 16, 1, 3), - ('Level2_KFTG_20150430_1419.ar2v', datetime(2015, 4, 30, 14, 19, 11), - 12, 4, 6), - ('KTLX20150530_000802_V06.bz2', datetime(2015, 5, 30, 0, 8, 3), 14, 4, 6), - ('KICX_20170712_1458', datetime(2017, 7, 12, 14, 58, 5), 14, 4, 6), - ('TDAL20191021021543V08.raw.gz', datetime(2019, 10, 21, 2, 15, 43), 10, 1, 3), - ('Level2_FOP1_20191223_003655.ar2v', datetime(2019, 12, 23, 0, 36, 55, 649000), - 16, 5, 7)] +level2_files = [ + ("KTLX20130520_201643_V06.gz", datetime(2013, 5, 20, 20, 16, 46), 17, 4, 6), + ("KTLX19990503_235621.gz", datetime(1999, 5, 3, 23, 56, 21), 16, 1, 3), + ("Level2_KFTG_20150430_1419.ar2v", datetime(2015, 4, 30, 14, 19, 11), 12, 4, 6), + ("KTLX20150530_000802_V06.bz2", datetime(2015, 5, 30, 0, 8, 3), 14, 4, 6), + ("KICX_20170712_1458", datetime(2017, 7, 12, 14, 58, 5), 14, 4, 6), + ("TDAL20191021021543V08.raw.gz", datetime(2019, 10, 21, 2, 15, 43), 10, 1, 3), + ("Level2_FOP1_20191223_003655.ar2v", datetime(2019, 12, 23, 0, 36, 55, 649000), 16, 5, 7), +] # ids here fixes how things are presented in pycharm -@pytest.mark.parametrize('fname, voltime, num_sweeps, mom_first, mom_last', level2_files, - ids=[i[0].replace('.', '_') for i in level2_files]) +@pytest.mark.parametrize( + "fname, voltime, num_sweeps, mom_first, mom_last", + level2_files, + ids=[i[0].replace(".", "_") for i in level2_files], +) def test_level2(fname, voltime, num_sweeps, mom_first, mom_last): """Test reading NEXRAD level 2 files from the filename.""" f = Level2File(get_test_data(fname, as_file_obj=False)) @@ -47,14 +50,20 @@ def test_level2(fname, voltime, num_sweeps, mom_first, mom_last): assert len(f.sweeps[-1][0][-1]) == mom_last -@pytest.mark.parametrize('filename', ['Level2_KFTG_20150430_1419.ar2v', - 'TDAL20191021021543V08.raw.gz', - 'KTLX20150530_000802_V06.bz2']) -@pytest.mark.parametrize('use_seek', [True, False]) +@pytest.mark.parametrize( + "filename", + [ + "Level2_KFTG_20150430_1419.ar2v", + "TDAL20191021021543V08.raw.gz", + "KTLX20150530_000802_V06.bz2", + ], +) +@pytest.mark.parametrize("use_seek", [True, False]) def test_level2_fobj(filename, use_seek): """Test reading NEXRAD level2 data from a file object.""" f = get_test_data(filename) if not use_seek: + class SeeklessReader: """Simulate file-like object access without seek.""" @@ -71,46 +80,51 @@ def read(self, n=None): def test_doubled_file(): """Test for #489 where doubled-up files didn't parse at all.""" - data = get_test_data('Level2_KFTG_20150430_1419.ar2v').read() + data = get_test_data("Level2_KFTG_20150430_1419.ar2v").read() fobj = BytesIO(data + data) f = Level2File(fobj) assert len(f.sweeps) == 12 -@pytest.mark.parametrize('fname, has_v2', [('KTLX20130520_201643_V06.gz', False), - ('Level2_KFTG_20150430_1419.ar2v', True), - ('TDAL20191021021543V08.raw.gz', False)]) +@pytest.mark.parametrize( + "fname, has_v2", + [ + ("KTLX20130520_201643_V06.gz", False), + ("Level2_KFTG_20150430_1419.ar2v", True), + ("TDAL20191021021543V08.raw.gz", False), + ], +) def test_conditional_radconst(fname, has_v2): """Test whether we're using the right volume constants.""" f = Level2File(get_test_data(fname, as_file_obj=False)) - assert hasattr(f.sweeps[0][0][3], 'calib_dbz0_v') == has_v2 + assert hasattr(f.sweeps[0][0][3], "calib_dbz0_v") == has_v2 def test_msg15(): """Check proper decoding of message type 15.""" - f = Level2File(get_test_data('KTLX20130520_201643_V06.gz', as_file_obj=False)) - data = f.clutter_filter_map['data'] + f = Level2File(get_test_data("KTLX20130520_201643_V06.gz", as_file_obj=False)) + data = f.clutter_filter_map["data"] assert isinstance(data[0][0], list) - assert f.clutter_filter_map['datetime'] == datetime(2013, 5, 19, 0, 0, 0, 315000) + assert f.clutter_filter_map["datetime"] == datetime(2013, 5, 19, 0, 0, 0, 315000) def test_single_chunk(caplog): """Check that Level2File copes with reading a file containing a single chunk.""" # Need to override the test level set above - caplog.set_level(logging.WARNING, 'metpy.io.nexrad') - f = Level2File(get_test_data('Level2_KLBB_single_chunk')) + caplog.set_level(logging.WARNING, "metpy.io.nexrad") + f = Level2File(get_test_data("Level2_KLBB_single_chunk")) assert len(f.sweeps) == 1 - assert 'Unable to read volume header' in caplog.text + assert "Unable to read volume header" in caplog.text # Make sure the warning is not present if we pass the right kwarg. caplog.clear() - Level2File(get_test_data('Level2_KLBB_single_chunk'), has_volume_header=False) - assert 'Unable to read volume header' not in caplog.text + Level2File(get_test_data("Level2_KLBB_single_chunk"), has_volume_header=False) + assert "Unable to read volume header" not in caplog.text def test_build19_level2_additions(): """Test handling of new additions in Build 19 level2 data.""" - f = Level2File(get_test_data('Level2_KDDC_20200823_204121.ar2v')) + f = Level2File(get_test_data("Level2_KDDC_20200823_204121.ar2v")) assert f.vcp_info.vcp_version == 1 assert f.sweeps[0][0].header.az_spacing == 0.5 @@ -118,31 +132,36 @@ def test_build19_level2_additions(): # # NIDS/Level 3 Tests # -nexrad_nids_files = [get_test_data(fname, as_file_obj=False) - for fname in POOCH.registry if fname.startswith('nids/K')] +nexrad_nids_files = [ + get_test_data(fname, as_file_obj=False) + for fname in POOCH.registry + if fname.startswith("nids/K") +] -@pytest.mark.parametrize('fname', nexrad_nids_files) +@pytest.mark.parametrize("fname", nexrad_nids_files) def test_level3_files(fname): """Test opening a NEXRAD NIDS file.""" f = Level3File(fname) # If we have some raster data in the symbology block, feed it into the mapper to make # sure it's working properly (Checks for #253) - if hasattr(f, 'sym_block'): + if hasattr(f, "sym_block"): block = f.sym_block[0][0] - if 'data' in block: - f.map_data(block['data']) + if "data" in block: + f.map_data(block["data"]) assert f.filename == fname -tdwr_nids_files = [get_test_data(fname, as_file_obj=False) - for fname in POOCH.registry if (fname.startswith('nids/Level3_MCI_') - or fname.startswith('nids/Level3_DEN_'))] +tdwr_nids_files = [ + get_test_data(fname, as_file_obj=False) + for fname in POOCH.registry + if (fname.startswith("nids/Level3_MCI_") or fname.startswith("nids/Level3_DEN_")) +] -@pytest.mark.parametrize('fname', tdwr_nids_files) +@pytest.mark.parametrize("fname", tdwr_nids_files) def test_tdwr_nids(fname): """Test opening a TDWR NIDS file.""" Level3File(fname) @@ -150,12 +169,13 @@ def test_tdwr_nids(fname): def test_basic(): """Test reading one specific NEXRAD NIDS file based on the filename.""" - f = Level3File(get_test_data('nids/Level3_FFC_N0Q_20140407_1805.nids', as_file_obj=False)) - assert f.metadata['prod_time'].replace(second=0) == datetime(2014, 4, 7, 18, 5) - assert f.metadata['vol_time'].replace(second=0) == datetime(2014, 4, 7, 18, 5) - assert f.metadata['msg_time'].replace(second=0) == datetime(2014, 4, 7, 18, 6) - assert f.filename == get_test_data('nids/Level3_FFC_N0Q_20140407_1805.nids', - as_file_obj=False) + f = Level3File(get_test_data("nids/Level3_FFC_N0Q_20140407_1805.nids", as_file_obj=False)) + assert f.metadata["prod_time"].replace(second=0) == datetime(2014, 4, 7, 18, 5) + assert f.metadata["vol_time"].replace(second=0) == datetime(2014, 4, 7, 18, 5) + assert f.metadata["msg_time"].replace(second=0) == datetime(2014, 4, 7, 18, 6) + assert f.filename == get_test_data( + "nids/Level3_FFC_N0Q_20140407_1805.nids", as_file_obj=False + ) # At this point, really just want to make sure that __str__ is able to run and produce # something not empty, the format is still up for grabs. @@ -164,9 +184,9 @@ def test_basic(): def test_new_gsm(): """Test parsing recent mods to the GSM product.""" - f = Level3File(get_test_data('nids/KDDC-gsm.nids')) + f = Level3File(get_test_data("nids/KDDC-gsm.nids")) - assert f.gsm_additional.vcp_supplemental == ['AVSET', 'SAILS', 'RxR Noise', 'CBT'] + assert f.gsm_additional.vcp_supplemental == ["AVSET", "SAILS", "RxR Noise", "CBT"] assert f.gsm_additional.supplemental_cut_count == 2 truth = [False] * 16 truth[2] = True @@ -180,110 +200,114 @@ def test_new_gsm(): def test_bad_length(caplog): """Test reading a product with too many bytes produces a log message.""" - fname = get_test_data('nids/KOUN_SDUS84_DAATLX_201305202016', as_file_obj=False) - with open(fname, 'rb') as inf: + fname = get_test_data("nids/KOUN_SDUS84_DAATLX_201305202016", as_file_obj=False) + with open(fname, "rb") as inf: data = inf.read() fobj = BytesIO(data + data) - with caplog.at_level(logging.WARNING, 'metpy.io.nexrad'): + with caplog.at_level(logging.WARNING, "metpy.io.nexrad"): Level3File(fobj) assert len(caplog.records) == 1 - assert 'This product may not parse correctly' in caplog.records[0].message + assert "This product may not parse correctly" in caplog.records[0].message def test_tdwr(): """Test reading a specific TDWR file.""" - f = Level3File(get_test_data('nids/Level3_SLC_TV0_20160516_2359.nids')) + f = Level3File(get_test_data("nids/Level3_SLC_TV0_20160516_2359.nids")) assert f.prod_desc.prod_code == 182 def test_dhr(): """Test reading a time field for DHR product.""" - f = Level3File(get_test_data('nids/KOUN_SDUS54_DHRTLX_201305202016')) - assert f.metadata['avg_time'] == datetime(2013, 5, 20, 20, 18) + f = Level3File(get_test_data("nids/KOUN_SDUS54_DHRTLX_201305202016")) + assert f.metadata["avg_time"] == datetime(2013, 5, 20, 20, 18) def test_nwstg(): """Test reading a nids file pulled from the NWSTG.""" - Level3File(get_test_data('nids/sn.last', as_file_obj=False)) + Level3File(get_test_data("nids/sn.last", as_file_obj=False)) def test_fobj(): """Test reading a specific NEXRAD NIDS files from a file object.""" - Level3File(get_test_data('nids/Level3_FFC_N0Q_20140407_1805.nids')) + Level3File(get_test_data("nids/Level3_FFC_N0Q_20140407_1805.nids")) def test_level3_pathlib(): """Test that reading with Level3File properly sets the filename from a Path.""" - fname = Path(get_test_data('nids/Level3_FFC_N0Q_20140407_1805.nids', as_file_obj=False)) + fname = Path(get_test_data("nids/Level3_FFC_N0Q_20140407_1805.nids", as_file_obj=False)) f = Level3File(fname) assert f.filename == str(fname) def test_nids_super_res_width(): """Test decoding a super resolution spectrum width product.""" - f = Level3File(get_test_data('nids/KLZK_H0W_20200812_1305')) - width = f.map_data(f.sym_block[0][0]['data']) + f = Level3File(get_test_data("nids/KLZK_H0W_20200812_1305")) + width = f.map_data(f.sym_block[0][0]["data"]) assert np.nanmax(width) == 15 def test_power_removed_control(): """Test decoding new PRC product.""" - f = Level3File(get_test_data('nids/KGJX_NXF_20200817_0600.nids')) + f = Level3File(get_test_data("nids/KGJX_NXF_20200817_0600.nids")) assert f.prod_desc.prod_code == 113 - assert f.metadata['rpg_cut_num'] == 1 - assert f.metadata['cmd_generated'] == 0 - assert f.metadata['el_angle'] == -0.2 - assert f.metadata['clutter_filter_map_dt'] == datetime(2020, 8, 17, 4, 16) - assert f.metadata['compression'] == 1 + assert f.metadata["rpg_cut_num"] == 1 + assert f.metadata["cmd_generated"] == 0 + assert f.metadata["el_angle"] == -0.2 + assert f.metadata["clutter_filter_map_dt"] == datetime(2020, 8, 17, 4, 16) + assert f.metadata["compression"] == 1 assert f.sym_block[0][0] def test21_precip(): """Test checking whether VCP 21 is precipitation mode.""" - assert is_precip_mode(21), 'VCP 21 is precip' + assert is_precip_mode(21), "VCP 21 is precip" def test11_precip(): """Test checking whether VCP 11 is precipitation mode.""" - assert is_precip_mode(11), 'VCP 11 is precip' + assert is_precip_mode(11), "VCP 11 is precip" def test31_clear_air(): """Test checking whether VCP 31 is clear air mode.""" - assert not is_precip_mode(31), 'VCP 31 is not precip' + assert not is_precip_mode(31), "VCP 31 is not precip" def test_tracks(): """Check that tracks are properly decoded.""" - f = Level3File(get_test_data('nids/KOUN_SDUS34_NSTTLX_201305202016')) + f = Level3File(get_test_data("nids/KOUN_SDUS34_NSTTLX_201305202016")) for data in f.sym_block[0]: - if 'track' in data: - x, y = np.array(data['track']).T + if "track" in data: + x, y = np.array(data["track"]).T assert len(x) assert len(y) def test_vector_packet(): """Check that vector packets are properly decoded.""" - f = Level3File(get_test_data('nids/KOUN_SDUS64_NHITLX_201305202016')) + f = Level3File(get_test_data("nids/KOUN_SDUS64_NHITLX_201305202016")) for page in f.graph_pages: for item in page: - if 'vectors' in item: - x1, x2, y1, y2 = np.array(item['vectors']).T + if "vectors" in item: + x1, x2, y1, y2 = np.array(item["vectors"]).T assert len(x1) assert len(x2) assert len(y1) assert len(y2) -@pytest.mark.parametrize('fname,truth', - [('nids/KEAX_N0Q_20200817_0401.nids', (0, 'MRLE scan')), - ('nids/KEAX_N0Q_20200817_0405.nids', (0, 'Non-supplemental scan')), - ('nids/KDDC_N0Q_20200817_0501.nids', (16, 'Non-supplemental scan')), - ('nids/KDDC_N0Q_20200817_0503.nids', (143, 'SAILS scan'))]) +@pytest.mark.parametrize( + "fname,truth", + [ + ("nids/KEAX_N0Q_20200817_0401.nids", (0, "MRLE scan")), + ("nids/KEAX_N0Q_20200817_0405.nids", (0, "Non-supplemental scan")), + ("nids/KDDC_N0Q_20200817_0501.nids", (16, "Non-supplemental scan")), + ("nids/KDDC_N0Q_20200817_0503.nids", (143, "SAILS scan")), + ], +) def test_nids_supplemental(fname, truth): """Checks decoding of supplemental scan fields for some nids products.""" f = Level3File(get_test_data(fname)) - assert f.metadata['delta_time'] == truth[0] - assert f.metadata['supplemental_scan'] == truth[1] + assert f.metadata["delta_time"] == truth[0] + assert f.metadata["supplemental_scan"] == truth[1] diff --git a/tests/io/test_station_data.py b/tests/io/test_station_data.py index c08327a312c..bc53fc43e5f 100644 --- a/tests/io/test_station_data.py +++ b/tests/io/test_station_data.py @@ -11,14 +11,14 @@ def test_add_lat_lon_station_data(): """Test for when the METAR does not correspond to a station in the dictionary.""" - df = pd.DataFrame({'station': ['KOUN', 'KVPZ', 'KDEN', 'PAAA']}) + df = pd.DataFrame({"station": ["KOUN", "KVPZ", "KDEN", "PAAA"]}) - df = add_station_lat_lon(df, 'station') - assert_almost_equal(df.loc[df.station == 'KOUN'].latitude.values[0], 35.25) - assert_almost_equal(df.loc[df.station == 'KOUN'].longitude.values[0], -97.47) - assert_almost_equal(df.loc[df.station == 'KVPZ'].latitude.values[0], 41.45) - assert_almost_equal(df.loc[df.station == 'KVPZ'].longitude.values[0], -87) - assert_almost_equal(df.loc[df.station == 'KDEN'].latitude.values[0], 39.85) - assert_almost_equal(df.loc[df.station == 'KDEN'].longitude.values[0], -104.65) - assert_almost_equal(df.loc[df.station == 'PAAA'].latitude.values[0], np.nan) - assert_almost_equal(df.loc[df.station == 'PAAA'].longitude.values[0], np.nan) + df = add_station_lat_lon(df, "station") + assert_almost_equal(df.loc[df.station == "KOUN"].latitude.values[0], 35.25) + assert_almost_equal(df.loc[df.station == "KOUN"].longitude.values[0], -97.47) + assert_almost_equal(df.loc[df.station == "KVPZ"].latitude.values[0], 41.45) + assert_almost_equal(df.loc[df.station == "KVPZ"].longitude.values[0], -87) + assert_almost_equal(df.loc[df.station == "KDEN"].latitude.values[0], 39.85) + assert_almost_equal(df.loc[df.station == "KDEN"].longitude.values[0], -104.65) + assert_almost_equal(df.loc[df.station == "PAAA"].latitude.values[0], np.nan) + assert_almost_equal(df.loc[df.station == "PAAA"].longitude.values[0], np.nan) diff --git a/tests/io/test_tools.py b/tests/io/test_tools.py index 5ae7a619e6a..73f4ec95fac 100644 --- a/tests/io/test_tools.py +++ b/tests/io/test_tools.py @@ -8,16 +8,16 @@ def test_unpack(): """Test unpacking a NamedStruct from bytes.""" - struct = NamedStruct([('field1', 'i'), ('field2', 'h')], '>') + struct = NamedStruct([("field1", "i"), ("field2", "h")], ">") - s = struct.unpack(b'\x00\x01\x00\x01\x00\x02') + s = struct.unpack(b"\x00\x01\x00\x01\x00\x02") assert s.field1 == 65537 assert s.field2 == 2 def test_pack(): """Test packing a NamedStruct into bytes.""" - struct = NamedStruct([('field1', 'i'), ('field2', 'h')], '>') + struct = NamedStruct([("field1", "i"), ("field2", "h")], ">") b = struct.pack(field1=8, field2=3) - assert b == b'\x00\x00\x00\x08\x00\x03' + assert b == b"\x00\x00\x00\x08\x00\x03" diff --git a/tests/plots/test_cartopy_utils.py b/tests/plots/test_cartopy_utils.py index 598361b9acd..0cdabb01b84 100644 --- a/tests/plots/test_cartopy_utils.py +++ b/tests/plots/test_cartopy_utils.py @@ -12,14 +12,16 @@ except ImportError: pass # No CartoPy from metpy.plots.cartopy_utils import import_cartopy + # Fixture to make sure we have the right backend from metpy.testing import set_agg_backend # noqa: F401, I202 MPL_VERSION = matplotlib.__version__[:3] -@pytest.mark.mpl_image_compare(tolerance={'2.1': 0.161}.get(MPL_VERSION, 0.053), - remove_text=True) +@pytest.mark.mpl_image_compare( + tolerance={"2.1": 0.161}.get(MPL_VERSION, 0.053), remove_text=True +) def test_us_county_defaults(ccrs): """Test the default US county plotting.""" proj = ccrs.LambertConformal(central_longitude=-85.0, central_latitude=45.0) @@ -31,8 +33,9 @@ def test_us_county_defaults(ccrs): return fig -@pytest.mark.mpl_image_compare(tolerance={'2.1': 0.1994}.get(MPL_VERSION, 0.092), - remove_text=True) +@pytest.mark.mpl_image_compare( + tolerance={"2.1": 0.1994}.get(MPL_VERSION, 0.092), remove_text=True +) def test_us_county_scales(ccrs): """Test US county plotting with all scales.""" proj = ccrs.LambertConformal(central_longitude=-85.0, central_latitude=45.0) @@ -42,7 +45,7 @@ def test_us_county_scales(ccrs): ax2 = fig.add_subplot(1, 3, 2, projection=proj) ax3 = fig.add_subplot(1, 3, 3, projection=proj) - for scale, axis in zip(['20m', '5m', '500k'], [ax1, ax2, ax3]): + for scale, axis in zip(["20m", "5m", "500k"], [ax1, ax2, ax3]): axis.set_extent([270.25, 270.9, 38.15, 38.75], ccrs.Geodetic()) axis.add_feature(USCOUNTIES.with_scale(scale)) return fig @@ -60,8 +63,9 @@ def test_us_states_defaults(ccrs): return fig -@pytest.mark.mpl_image_compare(tolerance={'2.1': 0.991}.get(MPL_VERSION, 0.092), - remove_text=True) +@pytest.mark.mpl_image_compare( + tolerance={"2.1": 0.991}.get(MPL_VERSION, 0.092), remove_text=True +) def test_us_states_scales(ccrs): """Test the default US States plotting with all scales.""" proj = ccrs.LambertConformal(central_longitude=-85.0, central_latitude=45.0) @@ -71,7 +75,7 @@ def test_us_states_scales(ccrs): ax2 = fig.add_subplot(1, 3, 2, projection=proj) ax3 = fig.add_subplot(1, 3, 3, projection=proj) - for scale, axis in zip(['20m', '5m', '500k'], [ax1, ax2, ax3]): + for scale, axis in zip(["20m", "5m", "500k"], [ax1, ax2, ax3]): axis.set_extent([270, 280, 28, 39], ccrs.Geodetic()) axis.add_feature(USSTATES.with_scale(scale)) return fig @@ -80,9 +84,10 @@ def test_us_states_scales(ccrs): def test_cartopy_stub(monkeypatch): """Test that the CartoPy stub will issue an error if CartoPy is not present.""" import sys + # This makes sure that cartopy is not found - monkeypatch.setitem(sys.modules, 'cartopy.crs', None) + monkeypatch.setitem(sys.modules, "cartopy.crs", None) ccrs = import_cartopy() - with pytest.raises(RuntimeError, match='CartoPy is required'): + with pytest.raises(RuntimeError, match="CartoPy is required"): ccrs.PlateCarree() diff --git a/tests/plots/test_ctables.py b/tests/plots/test_ctables.py index 54a63c66609..5beb0ffc707 100644 --- a/tests/plots/test_ctables.py +++ b/tests/plots/test_ctables.py @@ -21,21 +21,21 @@ def registry(): def test_package_resource(registry): """Test registry scanning package resource.""" - registry.scan_resource('metpy.plots', 'nexrad_tables') - assert 'cc_table' in registry + registry.scan_resource("metpy.plots", "nexrad_tables") + assert "cc_table" in registry def test_scan_dir(registry): """Test registry scanning a directory and ignoring files it can't handle .""" try: - kwargs = {'mode': 'w', 'dir': '.', 'suffix': '.tbl', 'delete': False, 'buffering': 1} + kwargs = {"mode": "w", "dir": ".", "suffix": ".tbl", "delete": False, "buffering": 1} with tempfile.NamedTemporaryFile(**kwargs) as fobj: fobj.write('"red"\n"lime"\n"blue"\n') fname = fobj.name # Unrelated table file that *should not* impact the scan with tempfile.NamedTemporaryFile(**kwargs) as fobj: - fobj.write('PADK 704540 ADAK NAS\n') + fobj.write("PADK 704540 ADAK NAS\n") bad_file = fobj.name # Needs to be outside with so it's closed on windows @@ -53,73 +53,76 @@ def test_read_file(registry): """Test reading a colortable from a file.""" fobj = StringIO('(0., 0., 1.0)\n"red"\n"#0000FF" #Blue') - registry.add_colortable(fobj, 'test_table') + registry.add_colortable(fobj, "test_table") - assert 'test_table' in registry - assert registry['test_table'] == [(0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (0.0, 0.0, 1.0)] + assert "test_table" in registry + assert registry["test_table"] == [(0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (0.0, 0.0, 1.0)] def test_read_bad_file(registry): """Test what error results when reading a malformed file.""" with pytest.raises(RuntimeError): - fobj = StringIO('PADK 704540 ADAK NAS ' - 'AK US 5188 -17665 4 0') - registry.add_colortable(fobj, 'sfstns') + fobj = StringIO( + "PADK 704540 ADAK NAS " "AK US 5188 -17665 4 0" + ) + registry.add_colortable(fobj, "sfstns") def test_get_colortable(registry): """Test getting a colortable from the registry.""" true_colors = [(0.0, 0.0, 1.0), (1.0, 0.0, 0.0)] - registry['table'] = true_colors + registry["table"] = true_colors - table = registry.get_colortable('table') + table = registry.get_colortable("table") assert table.N == 2 assert table.colors == true_colors def test_get_steps(registry): """Test getting a colortable and norm with appropriate steps.""" - registry['table'] = [(0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)] - norm, cmap = registry.get_with_steps('table', 5., 10.) - assert cmap(norm(np.array([6.]))).tolist() == [[0.0, 0.0, 1.0, 1.0]] + registry["table"] = [(0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)] + norm, cmap = registry.get_with_steps("table", 5.0, 10.0) + assert cmap(norm(np.array([6.0]))).tolist() == [[0.0, 0.0, 1.0, 1.0]] assert cmap(norm(np.array([14.9]))).tolist() == [[0.0, 0.0, 1.0, 1.0]] assert cmap(norm(np.array([15.1]))).tolist() == [[1.0, 0.0, 0.0, 1.0]] - assert cmap(norm(np.array([26.]))).tolist() == [[0.0, 1.0, 0.0, 1.0]] + assert cmap(norm(np.array([26.0]))).tolist() == [[0.0, 1.0, 0.0, 1.0]] def test_get_steps_negative_start(registry): """Test bad start for get with steps (issue #81).""" - registry['table'] = [(0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)] - norm, _ = registry.get_with_steps('table', -10, 5) + registry["table"] = [(0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)] + norm, _ = registry.get_with_steps("table", -10, 5) assert norm.vmin == -10 assert norm.vmax == 5 def test_get_range(registry): """Test getting a colortable and norm with appropriate range.""" - registry['table'] = [(0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)] - norm, cmap = registry.get_with_range('table', 5., 35.) - assert cmap(norm(np.array([6.]))).tolist() == [[0.0, 0.0, 1.0, 1.0]] + registry["table"] = [(0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)] + norm, cmap = registry.get_with_range("table", 5.0, 35.0) + assert cmap(norm(np.array([6.0]))).tolist() == [[0.0, 0.0, 1.0, 1.0]] assert cmap(norm(np.array([14.9]))).tolist() == [[0.0, 0.0, 1.0, 1.0]] assert cmap(norm(np.array([15.1]))).tolist() == [[1.0, 0.0, 0.0, 1.0]] - assert cmap(norm(np.array([26.]))).tolist() == [[0.0, 1.0, 0.0, 1.0]] + assert cmap(norm(np.array([26.0]))).tolist() == [[0.0, 1.0, 0.0, 1.0]] def test_get_boundaries(registry): """Test getting a colortable with explicit boundaries.""" - registry['table'] = [(0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)] - norm, cmap = registry.get_with_boundaries('table', [0., 8., 10., 20.]) - assert cmap(norm(np.array([7.]))).tolist() == [[0.0, 0.0, 1.0, 1.0]] - assert cmap(norm(np.array([9.]))).tolist() == [[1.0, 0.0, 0.0, 1.0]] + registry["table"] = [(0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)] + norm, cmap = registry.get_with_boundaries("table", [0.0, 8.0, 10.0, 20.0]) + assert cmap(norm(np.array([7.0]))).tolist() == [[0.0, 0.0, 1.0, 1.0]] + assert cmap(norm(np.array([9.0]))).tolist() == [[1.0, 0.0, 0.0, 1.0]] assert cmap(norm(np.array([10.1]))).tolist() == [[0.0, 1.0, 0.0, 1.0]] def test_gempak(): """Test GEMPAK colortable conversion.""" - infile = StringIO("""! wvcolor.tbl + infile = StringIO( + """! wvcolor.tbl 0 0 0 255 255 255 - """) + """ + ) outfile = StringIO() # Do the conversion @@ -129,4 +132,4 @@ def test_gempak(): outfile.seek(0) result = outfile.read() - assert result == '(0.000000, 0.000000, 0.000000)\n(1.000000, 1.000000, 1.000000)\n' + assert result == "(0.000000, 0.000000, 0.000000)\n(1.000000, 1.000000, 1.000000)\n" diff --git a/tests/plots/test_declarative.py b/tests/plots/test_declarative.py index 3956ebf583a..be01077a1d8 100644 --- a/tests/plots/test_declarative.py +++ b/tests/plots/test_declarative.py @@ -17,13 +17,20 @@ from metpy.cbook import get_test_data from metpy.io import GiniFile from metpy.io.metar import parse_metar_file -from metpy.plots import (BarbPlot, ContourPlot, FilledContourPlot, ImagePlot, MapPanel, - PanelContainer, PlotObs) +from metpy.plots import ( + BarbPlot, + ContourPlot, + FilledContourPlot, + ImagePlot, + MapPanel, + PanelContainer, + PlotObs, +) + # Fixtures to make sure we have the right backend from metpy.testing import needs_cartopy, needs_pyproj, set_agg_backend # noqa: F401, I202 from metpy.units import units - MPL_VERSION = matplotlib.__version__[:3] @@ -32,44 +39,45 @@ @needs_cartopy def test_declarative_image(): """Test making an image plot.""" - data = xr.open_dataset(GiniFile(get_test_data('NHEM-MULTICOMP_1km_IR_20151208_2100.gini'))) + data = xr.open_dataset(GiniFile(get_test_data("NHEM-MULTICOMP_1km_IR_20151208_2100.gini"))) img = ImagePlot() - img.data = data.metpy.parse_cf('IR') - img.colormap = 'Greys_r' + img.data = data.metpy.parse_cf("IR") + img.colormap = "Greys_r" panel = MapPanel() - panel.title = 'Test' + panel.title = "Test" panel.plots = [img] pc = PanelContainer() pc.panel = panel pc.draw() - assert panel.ax.get_title() == 'Test' + assert panel.ax.get_title() == "Test" return pc.figure -@pytest.mark.mpl_image_compare(remove_text=True, - tolerance={'2.1': 0.256}.get(MPL_VERSION, 0.022)) +@pytest.mark.mpl_image_compare( + remove_text=True, tolerance={"2.1": 0.256}.get(MPL_VERSION, 0.022) +) @needs_cartopy def test_declarative_contour(): """Test making a contour plot.""" - data = xr.open_dataset(get_test_data('narr_example.nc', as_file_obj=False)) + data = xr.open_dataset(get_test_data("narr_example.nc", as_file_obj=False)) contour = ContourPlot() contour.data = data - contour.field = 'Temperature' + contour.field = "Temperature" contour.level = 700 * units.hPa contour.contours = 30 contour.linewidth = 1 - contour.linecolor = 'red' + contour.linecolor = "red" panel = MapPanel() - panel.area = 'us' - panel.proj = 'lcc' - panel.layers = ['coastline', 'borders', 'usstates'] + panel.area = "us" + panel.proj = "lcc" + panel.layers = ["coastline", "borders", "usstates"] panel.plots = [contour] pc = PanelContainer() @@ -89,32 +97,36 @@ def fix_is_closed_polygon(monkeypatch): results for macOS vs. Linux/Windows. """ - monkeypatch.setattr(matplotlib.contour, '_is_closed_polygon', - lambda X: np.allclose(X[0], X[-1], rtol=1e-10, atol=1e-13), - raising=False) + monkeypatch.setattr( + matplotlib.contour, + "_is_closed_polygon", + lambda X: np.allclose(X[0], X[-1], rtol=1e-10, atol=1e-13), + raising=False, + ) -@pytest.mark.mpl_image_compare(remove_text=True, - tolerance={'2.1': 5.477}.get(MPL_VERSION, 0.035)) +@pytest.mark.mpl_image_compare( + remove_text=True, tolerance={"2.1": 5.477}.get(MPL_VERSION, 0.035) +) @needs_cartopy def test_declarative_contour_options(fix_is_closed_polygon): """Test making a contour plot.""" - data = xr.open_dataset(get_test_data('narr_example.nc', as_file_obj=False)) + data = xr.open_dataset(get_test_data("narr_example.nc", as_file_obj=False)) contour = ContourPlot() contour.data = data - contour.field = 'Temperature' + contour.field = "Temperature" contour.level = 700 * units.hPa contour.contours = 30 contour.linewidth = 1 - contour.linecolor = 'red' - contour.linestyle = 'dashed' + contour.linecolor = "red" + contour.linestyle = "dashed" contour.clabels = True panel = MapPanel() - panel.area = 'us' - panel.proj = 'lcc' - panel.layers = ['coastline', 'borders', 'usstates'] + panel.area = "us" + panel.proj = "lcc" + panel.layers = ["coastline", "borders", "usstates"] panel.plots = [contour] pc = PanelContainer() @@ -125,28 +137,29 @@ def test_declarative_contour_options(fix_is_closed_polygon): return pc.figure -@pytest.mark.mpl_image_compare(remove_text=True, - tolerance={'2.1': 2.007}.get(MPL_VERSION, 0.035)) +@pytest.mark.mpl_image_compare( + remove_text=True, tolerance={"2.1": 2.007}.get(MPL_VERSION, 0.035) +) @needs_cartopy def test_declarative_contour_convert_units(fix_is_closed_polygon): """Test making a contour plot.""" - data = xr.open_dataset(get_test_data('narr_example.nc', as_file_obj=False)) + data = xr.open_dataset(get_test_data("narr_example.nc", as_file_obj=False)) contour = ContourPlot() contour.data = data - contour.field = 'Temperature' + contour.field = "Temperature" contour.level = 700 * units.hPa contour.contours = 30 contour.linewidth = 1 - contour.linecolor = 'red' - contour.linestyle = 'dashed' + contour.linecolor = "red" + contour.linestyle = "dashed" contour.clabels = True - contour.plot_units = 'degC' + contour.plot_units = "degC" panel = MapPanel() - panel.area = 'us' - panel.proj = 'lcc' - panel.layers = ['coastline', 'borders', 'usstates'] + panel.area = "us" + panel.proj = "lcc" + panel.layers = ["coastline", "borders", "usstates"] panel.plots = [contour] pc = PanelContainer() @@ -161,27 +174,27 @@ def test_declarative_contour_convert_units(fix_is_closed_polygon): @needs_cartopy def test_declarative_events(): """Test that resetting traitlets properly propagates.""" - data = xr.open_dataset(get_test_data('narr_example.nc', as_file_obj=False)) + data = xr.open_dataset(get_test_data("narr_example.nc", as_file_obj=False)) contour = ContourPlot() contour.data = data - contour.field = 'Temperature' + contour.field = "Temperature" contour.level = 850 * units.hPa contour.contours = 30 contour.linewidth = 1 - contour.linecolor = 'red' + contour.linecolor = "red" img = ImagePlot() img.data = data - img.field = 'v_wind' + img.field = "v_wind" img.level = 700 * units.hPa - img.colormap = 'hot' + img.colormap = "hot" img.image_range = (3000, 5000) panel = MapPanel() - panel.area = 'us' - panel.proj = 'lcc' - panel.layers = ['coastline', 'borders', 'states'] + panel.area = "us" + panel.proj = "lcc" + panel.layers = ["coastline", "borders", "states"] panel.plots = [contour, img] pc = PanelContainer() @@ -191,19 +204,19 @@ def test_declarative_events(): # Update some properties to make sure it regenerates the figure contour.linewidth = 2 - contour.linecolor = 'green' + contour.linecolor = "green" contour.level = 700 * units.hPa - contour.field = 'Specific_humidity' - img.field = 'Geopotential_height' - img.colormap = 'plasma' - img.colorbar = 'horizontal' + contour.field = "Specific_humidity" + img.field = "Geopotential_height" + img.colormap = "plasma" + img.colorbar = "horizontal" return pc.figure def test_no_field_error(): """Make sure we get a useful error when the field is not set.""" - data = xr.open_dataset(get_test_data('narr_example.nc', as_file_obj=False)) + data = xr.open_dataset(get_test_data("narr_example.nc", as_file_obj=False)) contour = ContourPlot() contour.data = data @@ -215,7 +228,7 @@ def test_no_field_error(): def test_no_field_error_barbs(): """Make sure we get a useful error when the field is not set.""" - data = xr.open_dataset(get_test_data('narr_example.nc', as_file_obj=False)) + data = xr.open_dataset(get_test_data("narr_example.nc", as_file_obj=False)) barbs = BarbPlot() barbs.data = data @@ -227,12 +240,12 @@ def test_no_field_error_barbs(): def test_projection_object(ccrs, cfeature): """Test that we can pass a custom map projection.""" - data = xr.open_dataset(get_test_data('narr_example.nc', as_file_obj=False)) + data = xr.open_dataset(get_test_data("narr_example.nc", as_file_obj=False)) contour = ContourPlot() contour.data = data contour.level = 700 * units.hPa - contour.field = 'Temperature' + contour.field = "Temperature" panel = MapPanel() panel.area = (-110, -60, 25, 55) @@ -250,14 +263,14 @@ def test_projection_object(ccrs, cfeature): @pytest.mark.mpl_image_compare(remove_text=True, tolerance=0.016) def test_colorfill(cfeature): """Test that we can use ContourFillPlot.""" - data = xr.open_dataset(get_test_data('narr_example.nc', as_file_obj=False)) + data = xr.open_dataset(get_test_data("narr_example.nc", as_file_obj=False)) contour = FilledContourPlot() contour.data = data contour.level = 700 * units.hPa - contour.field = 'Temperature' - contour.colormap = 'coolwarm' - contour.colorbar = 'vertical' + contour.field = "Temperature" + contour.colormap = "coolwarm" + contour.colorbar = "vertical" panel = MapPanel() panel.area = (-110, -60, 25, 55) @@ -275,14 +288,14 @@ def test_colorfill(cfeature): @pytest.mark.mpl_image_compare(remove_text=True, tolerance=0.016) def test_colorfill_horiz_colorbar(cfeature): """Test that we can use ContourFillPlot.""" - data = xr.open_dataset(get_test_data('narr_example.nc', as_file_obj=False)) + data = xr.open_dataset(get_test_data("narr_example.nc", as_file_obj=False)) contour = FilledContourPlot() contour.data = data contour.level = 700 * units.hPa - contour.field = 'Temperature' - contour.colormap = 'coolwarm' - contour.colorbar = 'horizontal' + contour.field = "Temperature" + contour.colormap = "coolwarm" + contour.colorbar = "horizontal" panel = MapPanel() panel.area = (-110, -60, 25, 55) @@ -297,17 +310,18 @@ def test_colorfill_horiz_colorbar(cfeature): return pc.figure -@pytest.mark.mpl_image_compare(remove_text=True, - tolerance={'2.1': 0.355}.get(MPL_VERSION, 0.016)) +@pytest.mark.mpl_image_compare( + remove_text=True, tolerance={"2.1": 0.355}.get(MPL_VERSION, 0.016) +) def test_colorfill_no_colorbar(cfeature): """Test that we can use ContourFillPlot.""" - data = xr.open_dataset(get_test_data('narr_example.nc', as_file_obj=False)) + data = xr.open_dataset(get_test_data("narr_example.nc", as_file_obj=False)) contour = FilledContourPlot() contour.data = data contour.level = 700 * units.hPa - contour.field = 'Temperature' - contour.colormap = 'coolwarm' + contour.field = "Temperature" + contour.colormap = "coolwarm" contour.colorbar = None panel = MapPanel() @@ -328,15 +342,15 @@ def test_colorfill_no_colorbar(cfeature): @needs_cartopy def test_global(): """Test that we can set global extent.""" - data = xr.open_dataset(GiniFile(get_test_data('NHEM-MULTICOMP_1km_IR_20151208_2100.gini'))) + data = xr.open_dataset(GiniFile(get_test_data("NHEM-MULTICOMP_1km_IR_20151208_2100.gini"))) img = ImagePlot() img.data = data - img.field = 'IR' + img.field = "IR" img.colorbar = None panel = MapPanel() - panel.area = 'global' + panel.area = "global" panel.plots = [img] pc = PanelContainer() @@ -350,24 +364,24 @@ def test_global(): @needs_cartopy def test_latlon(): """Test our handling of lat/lon information.""" - data = xr.open_dataset(get_test_data('irma_gfs_example.nc', as_file_obj=False)) + data = xr.open_dataset(get_test_data("irma_gfs_example.nc", as_file_obj=False)) img = ImagePlot() img.data = data - img.field = 'Temperature_isobaric' + img.field = "Temperature_isobaric" img.level = 500 * units.hPa img.time = datetime(2017, 9, 5, 15, 0, 0) img.colorbar = None contour = ContourPlot() contour.data = data - contour.field = 'Geopotential_height_isobaric' + contour.field = "Geopotential_height_isobaric" contour.level = img.level contour.time = img.time panel = MapPanel() - panel.projection = 'lcc' - panel.area = 'us' + panel.projection = "lcc" + panel.area = "us" panel.plots = [img, contour] pc = PanelContainer() @@ -377,26 +391,27 @@ def test_latlon(): return pc.figure -@pytest.mark.mpl_image_compare(remove_text=True, - tolerance={'2.1': 0.418}.get(MPL_VERSION, 0.37)) +@pytest.mark.mpl_image_compare( + remove_text=True, tolerance={"2.1": 0.418}.get(MPL_VERSION, 0.37) +) @needs_cartopy def test_declarative_barb_options(): """Test making a contour plot.""" - data = xr.open_dataset(get_test_data('narr_example.nc', as_file_obj=False)) + data = xr.open_dataset(get_test_data("narr_example.nc", as_file_obj=False)) barb = BarbPlot() barb.data = data barb.level = 300 * units.hPa - barb.field = ['u_wind', 'v_wind'] + barb.field = ["u_wind", "v_wind"] barb.skip = (10, 10) - barb.color = 'blue' - barb.pivot = 'tip' + barb.color = "blue" + barb.pivot = "tip" barb.barblength = 6.5 panel = MapPanel() - panel.area = 'us' - panel.projection = 'data' - panel.layers = ['coastline', 'borders', 'usstates'] + panel.area = "us" + panel.projection = "data" + panel.layers = ["coastline", "borders", "usstates"] panel.plots = [barb] pc = PanelContainer() @@ -407,20 +422,22 @@ def test_declarative_barb_options(): return pc.figure -@pytest.mark.mpl_image_compare(remove_text=True, - tolerance={'2.1': 0.819}.get(MPL_VERSION, 0.612)) +@pytest.mark.mpl_image_compare( + remove_text=True, tolerance={"2.1": 0.819}.get(MPL_VERSION, 0.612) +) @needs_cartopy def test_declarative_barb_earth_relative(): """Test making a contour plot.""" import numpy as np - data = xr.open_dataset(get_test_data('NAM_test.nc', as_file_obj=False)) + + data = xr.open_dataset(get_test_data("NAM_test.nc", as_file_obj=False)) contour = ContourPlot() contour.data = data - contour.field = 'Geopotential_height_isobaric' + contour.field = "Geopotential_height_isobaric" contour.level = 300 * units.hPa - contour.linecolor = 'red' - contour.linestyle = '-' + contour.linecolor = "red" + contour.linestyle = "-" contour.linewidth = 2 contour.contours = np.arange(0, 20000, 120).tolist() @@ -428,16 +445,16 @@ def test_declarative_barb_earth_relative(): barb.data = data barb.level = 300 * units.hPa barb.time = datetime(2016, 10, 31, 12) - barb.field = ['u-component_of_wind_isobaric', 'v-component_of_wind_isobaric'] + barb.field = ["u-component_of_wind_isobaric", "v-component_of_wind_isobaric"] barb.skip = (5, 5) - barb.color = 'black' + barb.color = "black" barb.barblength = 6.5 barb.earth_relative = False panel = MapPanel() panel.area = (-124, -72, 20, 53) - panel.projection = 'lcc' - panel.layers = ['coastline', 'borders', 'usstates'] + panel.projection = "lcc" + panel.layers = ["coastline", "borders", "usstates"] panel.plots = [contour, barb] pc = PanelContainer() @@ -453,11 +470,12 @@ def test_declarative_barb_earth_relative(): def test_declarative_gridded_scale(): """Test making a contour plot.""" import numpy as np - data = xr.open_dataset(get_test_data('NAM_test.nc', as_file_obj=False)) + + data = xr.open_dataset(get_test_data("NAM_test.nc", as_file_obj=False)) contour = ContourPlot() contour.data = data - contour.field = 'Geopotential_height_isobaric' + contour.field = "Geopotential_height_isobaric" contour.level = 300 * units.hPa contour.linewidth = 2 contour.contours = np.arange(0, 2000, 12).tolist() @@ -466,8 +484,8 @@ def test_declarative_gridded_scale(): panel = MapPanel() panel.area = (-124, -72, 20, 53) - panel.projection = 'lcc' - panel.layers = ['coastline', 'borders', 'usstates'] + panel.projection = "lcc" + panel.layers = ["coastline", "borders", "usstates"] panel.plots = [contour] pc = PanelContainer() @@ -482,19 +500,19 @@ def test_declarative_gridded_scale(): @needs_cartopy def test_declarative_barb_gfs(): """Test making a contour plot.""" - data = xr.open_dataset(get_test_data('GFS_test.nc', as_file_obj=False)) + data = xr.open_dataset(get_test_data("GFS_test.nc", as_file_obj=False)) barb = BarbPlot() barb.data = data barb.level = 300 * units.hPa - barb.field = ['u-component_of_wind_isobaric', 'v-component_of_wind_isobaric'] + barb.field = ["u-component_of_wind_isobaric", "v-component_of_wind_isobaric"] barb.skip = (2, 2) barb.earth_relative = False panel = MapPanel() - panel.area = 'us' - panel.projection = 'data' - panel.layers = ['coastline', 'borders', 'usstates'] + panel.area = "us" + panel.projection = "data" + panel.layers = ["coastline", "borders", "usstates"] panel.plots = [barb] pc = PanelContainer() @@ -511,20 +529,20 @@ def test_declarative_barb_gfs(): @needs_cartopy def test_declarative_barb_gfs_knots(): """Test making a contour plot.""" - data = xr.open_dataset(get_test_data('GFS_test.nc', as_file_obj=False)) + data = xr.open_dataset(get_test_data("GFS_test.nc", as_file_obj=False)) barb = BarbPlot() barb.data = data barb.level = 300 * units.hPa - barb.field = ['u-component_of_wind_isobaric', 'v-component_of_wind_isobaric'] + barb.field = ["u-component_of_wind_isobaric", "v-component_of_wind_isobaric"] barb.skip = (3, 3) barb.earth_relative = False - barb.plot_units = 'knot' + barb.plot_units = "knot" panel = MapPanel() - panel.area = 'us' - panel.projection = 'data' - panel.layers = ['coastline', 'borders', 'usstates'] + panel.area = "us" + panel.projection = "data" + panel.layers = ["coastline", "borders", "usstates"] panel.plots = [barb] pc = PanelContainer() @@ -538,15 +556,19 @@ def test_declarative_barb_gfs_knots(): @pytest.fixture() def sample_obs(): """Generate sample observational data for testing.""" - return pd.DataFrame([('2020-08-05 12:00', 'KDEN', 1000, 1, 9), - ('2020-08-05 12:01', 'KOKC', 1000, 2, 10), - ('2020-08-05 12:00', 'KDEN', 500, 3, 11), - ('2020-08-05 12:01', 'KOKC', 500, 4, 12), - ('2020-08-06 13:00', 'KDEN', 1000, 5, 13), - ('2020-08-06 12:59', 'KOKC', 1000, 6, 14), - ('2020-08-06 13:00', 'KDEN', 500, 7, 15), - ('2020-08-06 12:59', 'KOKC', 500, 8, 16)], - columns=['time', 'stid', 'pressure', 'temperature', 'dewpoint']) + return pd.DataFrame( + [ + ("2020-08-05 12:00", "KDEN", 1000, 1, 9), + ("2020-08-05 12:01", "KOKC", 1000, 2, 10), + ("2020-08-05 12:00", "KDEN", 500, 3, 11), + ("2020-08-05 12:01", "KOKC", 500, 4, 12), + ("2020-08-06 13:00", "KDEN", 1000, 5, 13), + ("2020-08-06 12:59", "KOKC", 1000, 6, 14), + ("2020-08-06 13:00", "KDEN", 500, 7, 15), + ("2020-08-06 12:59", "KOKC", 500, 8, 16), + ], + columns=["time", "stid", "pressure", "temperature", "dewpoint"], + ) def test_plotobs_subset_default_nolevel(sample_obs): @@ -554,10 +576,11 @@ def test_plotobs_subset_default_nolevel(sample_obs): obs = PlotObs() obs.data = sample_obs - truth = pd.DataFrame([('2020-08-06 13:00', 'KDEN', 500, 7, 15), - ('2020-08-06 12:59', 'KOKC', 500, 8, 16)], - columns=['time', 'stid', 'pressure', 'temperature', 'dewpoint'], - index=[6, 7]) + truth = pd.DataFrame( + [("2020-08-06 13:00", "KDEN", 500, 7, 15), ("2020-08-06 12:59", "KOKC", 500, 8, 16)], + columns=["time", "stid", "pressure", "temperature", "dewpoint"], + index=[6, 7], + ) pd.testing.assert_frame_equal(obs.obsdata, truth) @@ -567,10 +590,11 @@ def test_plotobs_subset_level(sample_obs): obs.data = sample_obs obs.level = 1000 * units.hPa - truth = pd.DataFrame([('2020-08-06 13:00', 'KDEN', 1000, 5, 13), - ('2020-08-06 12:59', 'KOKC', 1000, 6, 14)], - columns=['time', 'stid', 'pressure', 'temperature', 'dewpoint'], - index=[4, 5]) + truth = pd.DataFrame( + [("2020-08-06 13:00", "KDEN", 1000, 5, 13), ("2020-08-06 12:59", "KOKC", 1000, 6, 14)], + columns=["time", "stid", "pressure", "temperature", "dewpoint"], + index=[4, 5], + ) pd.testing.assert_frame_equal(obs.obsdata, truth) @@ -580,10 +604,11 @@ def test_plotobs_subset_level_no_units(sample_obs): obs.data = sample_obs obs.level = 1000 - truth = pd.DataFrame([('2020-08-06 13:00', 'KDEN', 1000, 5, 13), - ('2020-08-06 12:59', 'KOKC', 1000, 6, 14)], - columns=['time', 'stid', 'pressure', 'temperature', 'dewpoint'], - index=[4, 5]) + truth = pd.DataFrame( + [("2020-08-06 13:00", "KDEN", 1000, 5, 13), ("2020-08-06 12:59", "KOKC", 1000, 6, 14)], + columns=["time", "stid", "pressure", "temperature", "dewpoint"], + index=[4, 5], + ) pd.testing.assert_frame_equal(obs.obsdata, truth) @@ -594,17 +619,19 @@ def test_plotobs_subset_time(sample_obs): obs.level = None obs.time = datetime(2020, 8, 6, 13) - truth = pd.DataFrame([('2020-08-06 13:00', 'KDEN', 500, 7, 15)], - columns=['time', 'stid', 'pressure', 'temperature', 'dewpoint']) - truth = truth.set_index(pd.to_datetime(truth['time'])) + truth = pd.DataFrame( + [("2020-08-06 13:00", "KDEN", 500, 7, 15)], + columns=["time", "stid", "pressure", "temperature", "dewpoint"], + ) + truth = truth.set_index(pd.to_datetime(truth["time"])) pd.testing.assert_frame_equal(obs.obsdata, truth) def test_plotobs_subset_time_window(sample_obs): """Test PlotObs subsetting for a particular time with a window.""" # Test also using an existing index - sample_obs['time'] = pd.to_datetime(sample_obs['time']) - sample_obs.set_index('time') + sample_obs["time"] = pd.to_datetime(sample_obs["time"]) + sample_obs.set_index("time") obs = PlotObs() obs.data = sample_obs @@ -612,18 +639,22 @@ def test_plotobs_subset_time_window(sample_obs): obs.time = datetime(2020, 8, 5, 12) obs.time_window = timedelta(minutes=30) - truth = pd.DataFrame([(datetime(2020, 8, 5, 12), 'KDEN', 500, 3, 11), - (datetime(2020, 8, 5, 12, 1), 'KOKC', 500, 4, 12)], - columns=['time', 'stid', 'pressure', 'temperature', 'dewpoint']) - truth = truth.set_index('time') + truth = pd.DataFrame( + [ + (datetime(2020, 8, 5, 12), "KDEN", 500, 3, 11), + (datetime(2020, 8, 5, 12, 1), "KOKC", 500, 4, 12), + ], + columns=["time", "stid", "pressure", "temperature", "dewpoint"], + ) + truth = truth.set_index("time") pd.testing.assert_frame_equal(obs.obsdata, truth) def test_plotobs_subset_time_window_level(sample_obs): """Test PlotObs subsetting for a particular time with a window and a level.""" # Test also using an existing index - sample_obs['time'] = pd.to_datetime(sample_obs['time']) - sample_obs.set_index('time') + sample_obs["time"] = pd.to_datetime(sample_obs["time"]) + sample_obs.set_index("time") obs = PlotObs() obs.data = sample_obs @@ -631,34 +662,42 @@ def test_plotobs_subset_time_window_level(sample_obs): obs.time = datetime(2020, 8, 5, 12) obs.time_window = timedelta(minutes=30) - truth = pd.DataFrame([(datetime(2020, 8, 5, 12), 'KDEN', 1000, 1, 9), - (datetime(2020, 8, 5, 12, 1), 'KOKC', 1000, 2, 10)], - columns=['time', 'stid', 'pressure', 'temperature', 'dewpoint']) - truth = truth.set_index('time') + truth = pd.DataFrame( + [ + (datetime(2020, 8, 5, 12), "KDEN", 1000, 1, 9), + (datetime(2020, 8, 5, 12, 1), "KOKC", 1000, 2, 10), + ], + columns=["time", "stid", "pressure", "temperature", "dewpoint"], + ) + truth = truth.set_index("time") pd.testing.assert_frame_equal(obs.obsdata, truth) -@pytest.mark.mpl_image_compare(remove_text=True, - tolerance={'2.1': 0.407}.get(MPL_VERSION, 0.022)) +@pytest.mark.mpl_image_compare( + remove_text=True, tolerance={"2.1": 0.407}.get(MPL_VERSION, 0.022) +) def test_declarative_sfc_obs(ccrs): """Test making a surface observation plot.""" - data = pd.read_csv(get_test_data('SFC_obs.csv', as_file_obj=False), - infer_datetime_format=True, parse_dates=['valid']) + data = pd.read_csv( + get_test_data("SFC_obs.csv", as_file_obj=False), + infer_datetime_format=True, + parse_dates=["valid"], + ) obs = PlotObs() obs.data = data obs.time = datetime(1993, 3, 12, 12) obs.time_window = timedelta(minutes=15) obs.level = None - obs.fields = ['tmpf'] - obs.colors = ['black'] + obs.fields = ["tmpf"] + obs.colors = ["black"] # Panel for plot with Map features panel = MapPanel() panel.layout = (1, 1, 1) panel.projection = ccrs.PlateCarree() - panel.area = 'in' - panel.layers = ['states'] + panel.area = "in" + panel.layers = ["states"] panel.plots = [obs] # Bringing it all together @@ -671,29 +710,33 @@ def test_declarative_sfc_obs(ccrs): return pc.figure -@pytest.mark.mpl_image_compare(remove_text=True, - tolerance={'2.1': 8.09}.get(MPL_VERSION, 0.022)) +@pytest.mark.mpl_image_compare( + remove_text=True, tolerance={"2.1": 8.09}.get(MPL_VERSION, 0.022) +) @needs_cartopy def test_declarative_sfc_text(): """Test making a surface observation plot with text.""" - data = pd.read_csv(get_test_data('SFC_obs.csv', as_file_obj=False), - infer_datetime_format=True, parse_dates=['valid']) + data = pd.read_csv( + get_test_data("SFC_obs.csv", as_file_obj=False), + infer_datetime_format=True, + parse_dates=["valid"], + ) obs = PlotObs() obs.data = data obs.time = datetime(1993, 3, 12, 12) obs.time_window = timedelta(minutes=15) obs.level = None - obs.fields = ['station'] - obs.colors = ['black'] - obs.formats = ['text'] + obs.fields = ["station"] + obs.colors = ["black"] + obs.formats = ["text"] # Panel for plot with Map features panel = MapPanel() panel.layout = (1, 1, 1) - panel.projection = 'lcc' - panel.area = 'in' - panel.layers = ['states'] + panel.projection = "lcc" + panel.area = "in" + panel.layers = ["states"] panel.plots = [obs] # Bringing it all together @@ -706,29 +749,33 @@ def test_declarative_sfc_text(): return pc.figure -@pytest.mark.mpl_image_compare(remove_text=True, - tolerance={'2.1': 0.407}.get(MPL_VERSION, 0.022)) +@pytest.mark.mpl_image_compare( + remove_text=True, tolerance={"2.1": 0.407}.get(MPL_VERSION, 0.022) +) def test_declarative_sfc_obs_changes(ccrs): """Test making a surface observation plot, changing the field.""" - data = pd.read_csv(get_test_data('SFC_obs.csv', as_file_obj=False), - infer_datetime_format=True, parse_dates=['valid']) + data = pd.read_csv( + get_test_data("SFC_obs.csv", as_file_obj=False), + infer_datetime_format=True, + parse_dates=["valid"], + ) obs = PlotObs() obs.data = data obs.time = datetime(1993, 3, 12, 12) obs.level = None - obs.fields = ['tmpf'] - obs.colors = ['black'] + obs.fields = ["tmpf"] + obs.colors = ["black"] obs.time_window = timedelta(minutes=15) # Panel for plot with Map features panel = MapPanel() panel.layout = (1, 1, 1) panel.projection = ccrs.PlateCarree() - panel.area = 'in' - panel.layers = ['states'] + panel.area = "in" + panel.layers = ["states"] panel.plots = [obs] - panel.title = f'Surface Observations for {obs.time}' + panel.title = f"Surface Observations for {obs.time}" # Bringing it all together pc = PanelContainer() @@ -737,33 +784,37 @@ def test_declarative_sfc_obs_changes(ccrs): pc.draw() - obs.fields = ['dwpf'] - obs.colors = ['green'] + obs.fields = ["dwpf"] + obs.colors = ["green"] return pc.figure -@pytest.mark.mpl_image_compare(remove_text=True, - tolerance={'2.1': 0.378}.get(MPL_VERSION, 0.00586)) +@pytest.mark.mpl_image_compare( + remove_text=True, tolerance={"2.1": 0.378}.get(MPL_VERSION, 0.00586) +) def test_declarative_colored_barbs(ccrs): """Test making a surface plot with a colored barb (gh-1274).""" - data = pd.read_csv(get_test_data('SFC_obs.csv', as_file_obj=False), - infer_datetime_format=True, parse_dates=['valid']) + data = pd.read_csv( + get_test_data("SFC_obs.csv", as_file_obj=False), + infer_datetime_format=True, + parse_dates=["valid"], + ) obs = PlotObs() obs.data = data obs.time = datetime(1993, 3, 12, 13) obs.level = None - obs.vector_field = ('uwind', 'vwind') - obs.vector_field_color = 'red' - obs.reduce_points = .5 + obs.vector_field = ("uwind", "vwind") + obs.vector_field_color = "red" + obs.reduce_points = 0.5 # Panel for plot with Map features panel = MapPanel() panel.layout = (1, 1, 1) panel.projection = ccrs.PlateCarree() - panel.area = 'NE' - panel.layers = ['states'] + panel.area = "NE" + panel.layers = ["states"] panel.plots = [obs] # Bringing it all together @@ -776,34 +827,42 @@ def test_declarative_colored_barbs(ccrs): return pc.figure -@pytest.mark.mpl_image_compare(remove_text=True, - tolerance={'3.1': 9.771, - '2.1': 9.785}.get(MPL_VERSION, 0.00651)) +@pytest.mark.mpl_image_compare( + remove_text=True, tolerance={"3.1": 9.771, "2.1": 9.785}.get(MPL_VERSION, 0.00651) +) def test_declarative_sfc_obs_full(ccrs): """Test making a full surface observation plot.""" - data = pd.read_csv(get_test_data('SFC_obs.csv', as_file_obj=False), - infer_datetime_format=True, parse_dates=['valid']) + data = pd.read_csv( + get_test_data("SFC_obs.csv", as_file_obj=False), + infer_datetime_format=True, + parse_dates=["valid"], + ) obs = PlotObs() obs.data = data obs.time = datetime(1993, 3, 12, 13) obs.time_window = timedelta(minutes=15) obs.level = None - obs.fields = ['tmpf', 'dwpf', 'emsl', 'cloud_cover', 'wxsym'] - obs.locations = ['NW', 'SW', 'NE', 'C', 'W'] - obs.colors = ['red', 'green', 'black', 'black', 'blue'] - obs.formats = [None, None, lambda v: format(10 * v, '.0f')[-3:], 'sky_cover', - 'current_weather'] - obs.vector_field = ('uwind', 'vwind') + obs.fields = ["tmpf", "dwpf", "emsl", "cloud_cover", "wxsym"] + obs.locations = ["NW", "SW", "NE", "C", "W"] + obs.colors = ["red", "green", "black", "black", "blue"] + obs.formats = [ + None, + None, + lambda v: format(10 * v, ".0f")[-3:], + "sky_cover", + "current_weather", + ] + obs.vector_field = ("uwind", "vwind") obs.reduce_points = 1 # Panel for plot with Map features panel = MapPanel() panel.layout = (1, 1, 1) panel.area = (-124, -72, 20, 53) - panel.area = 'il' + panel.area = "il" panel.projection = ccrs.PlateCarree() - panel.layers = ['coastline', 'borders', 'states'] + panel.layers = ["coastline", "borders", "states"] panel.plots = [obs] # Bringing it all together @@ -820,16 +879,16 @@ def test_declarative_sfc_obs_full(ccrs): @needs_cartopy def test_declarative_upa_obs(): """Test making a full upperair observation plot.""" - data = pd.read_csv(get_test_data('UPA_obs.csv', as_file_obj=False)) + data = pd.read_csv(get_test_data("UPA_obs.csv", as_file_obj=False)) obs = PlotObs() obs.data = data obs.time = datetime(1993, 3, 14, 0) obs.level = 500 * units.hPa - obs.fields = ['temperature', 'dewpoint', 'height'] - obs.locations = ['NW', 'SW', 'NE'] - obs.formats = [None, None, lambda v: format(v, '.0f')[:3]] - obs.vector_field = ('u_wind', 'v_wind') + obs.fields = ["temperature", "dewpoint", "height"] + obs.locations = ["NW", "SW", "NE"] + obs.formats = [None, None, lambda v: format(v, ".0f")[:3]] + obs.vector_field = ("u_wind", "v_wind") obs.vector_field_length = 7 obs.reduce_points = 0 @@ -837,8 +896,8 @@ def test_declarative_upa_obs(): panel = MapPanel() panel.layout = (1, 1, 1) panel.area = (-124, -72, 20, 53) - panel.projection = 'lcc' - panel.layers = ['coastline', 'borders', 'states', 'land'] + panel.projection = "lcc" + panel.layers = ["coastline", "borders", "states", "land"] panel.plots = [obs] # Bringing it all together @@ -857,31 +916,41 @@ def test_declarative_upa_obs(): @needs_cartopy def test_declarative_upa_obs_convert_barb_units(): """Test making a full upperair observation plot.""" - data = pd.read_csv(get_test_data('UPA_obs.csv', as_file_obj=False)) - data.units = '' - data.units = {'pressure': 'hPa', 'height': 'meters', 'temperature': 'degC', - 'dewpoint': 'degC', 'direction': 'degrees', 'speed': 'knots', - 'station': None, 'time': None, 'u_wind': 'knots', 'v_wind': 'knots', - 'latitude': 'degrees', 'longitude': 'degrees'} + data = pd.read_csv(get_test_data("UPA_obs.csv", as_file_obj=False)) + data.units = "" + data.units = { + "pressure": "hPa", + "height": "meters", + "temperature": "degC", + "dewpoint": "degC", + "direction": "degrees", + "speed": "knots", + "station": None, + "time": None, + "u_wind": "knots", + "v_wind": "knots", + "latitude": "degrees", + "longitude": "degrees", + } obs = PlotObs() obs.data = data obs.time = datetime(1993, 3, 14, 0) obs.level = 500 * units.hPa - obs.fields = ['temperature', 'dewpoint', 'height'] - obs.locations = ['NW', 'SW', 'NE'] - obs.formats = [None, None, lambda v: format(v, '.0f')[:3]] - obs.vector_field = ('u_wind', 'v_wind') + obs.fields = ["temperature", "dewpoint", "height"] + obs.locations = ["NW", "SW", "NE"] + obs.formats = [None, None, lambda v: format(v, ".0f")[:3]] + obs.vector_field = ("u_wind", "v_wind") obs.vector_field_length = 7 - obs.vector_plot_units = 'm/s' + obs.vector_plot_units = "m/s" obs.reduce_points = 0 # Panel for plot with Map features panel = MapPanel() panel.layout = (1, 1, 1) panel.area = (-124, -72, 20, 53) - panel.projection = 'lcc' - panel.layers = ['coastline', 'borders', 'states', 'land'] + panel.projection = "lcc" + panel.layers = ["coastline", "borders", "states", "land"] panel.plots = [obs] # Bringing it all together @@ -898,25 +967,28 @@ def test_declarative_upa_obs_convert_barb_units(): def test_attribute_error_time(ccrs): """Make sure we get a useful error when the time variable is not found.""" - data = pd.read_csv(get_test_data('SFC_obs.csv', as_file_obj=False), - infer_datetime_format=True, parse_dates=['valid']) - data.rename(columns={'valid': 'vtime'}, inplace=True) + data = pd.read_csv( + get_test_data("SFC_obs.csv", as_file_obj=False), + infer_datetime_format=True, + parse_dates=["valid"], + ) + data.rename(columns={"valid": "vtime"}, inplace=True) obs = PlotObs() obs.data = data obs.time = datetime(1993, 3, 12, 12) obs.level = None - obs.fields = ['tmpf'] + obs.fields = ["tmpf"] obs.time_window = timedelta(minutes=15) # Panel for plot with Map features panel = MapPanel() panel.layout = (1, 1, 1) panel.projection = ccrs.PlateCarree() - panel.area = 'in' - panel.layers = ['states'] + panel.area = "in" + panel.layers = ["states"] panel.plots = [obs] - panel.title = f'Surface Observations for {obs.time}' + panel.title = f"Surface Observations for {obs.time}" # Bringing it all together pc = PanelContainer() @@ -929,25 +1001,28 @@ def test_attribute_error_time(ccrs): def test_attribute_error_station(ccrs): """Make sure we get a useful error when the station variable is not found.""" - data = pd.read_csv(get_test_data('SFC_obs.csv', as_file_obj=False), - infer_datetime_format=True, parse_dates=['valid']) - data.rename(columns={'station': 'location'}, inplace=True) + data = pd.read_csv( + get_test_data("SFC_obs.csv", as_file_obj=False), + infer_datetime_format=True, + parse_dates=["valid"], + ) + data.rename(columns={"station": "location"}, inplace=True) obs = PlotObs() obs.data = data obs.time = datetime(1993, 3, 12, 12) obs.level = None - obs.fields = ['tmpf'] + obs.fields = ["tmpf"] obs.time_window = timedelta(minutes=15) # Panel for plot with Map features panel = MapPanel() panel.layout = (1, 1, 1) panel.projection = ccrs.PlateCarree() - panel.area = 'in' - panel.layers = ['states'] + panel.area = "in" + panel.layers = ["states"] panel.plots = [obs] - panel.title = f'Surface Observations for {obs.time}' + panel.title = f"Surface Observations for {obs.time}" # Bringing it all together pc = PanelContainer() @@ -958,28 +1033,30 @@ def test_attribute_error_station(ccrs): pc.draw() -@pytest.mark.mpl_image_compare(remove_text=True, - tolerance={'2.1': 0.407}.get(MPL_VERSION, 0.022)) +@pytest.mark.mpl_image_compare( + remove_text=True, tolerance={"2.1": 0.407}.get(MPL_VERSION, 0.022) +) def test_declarative_sfc_obs_change_units(ccrs): """Test making a surface observation plot.""" - data = parse_metar_file(get_test_data('metar_20190701_1200.txt', as_file_obj=False), - year=2019, month=7) + data = parse_metar_file( + get_test_data("metar_20190701_1200.txt", as_file_obj=False), year=2019, month=7 + ) obs = PlotObs() obs.data = data obs.time = datetime(2019, 7, 1, 12) obs.time_window = timedelta(minutes=15) obs.level = None - obs.fields = ['air_temperature'] - obs.color = ['black'] - obs.plot_units = ['degF'] + obs.fields = ["air_temperature"] + obs.color = ["black"] + obs.plot_units = ["degF"] # Panel for plot with Map features panel = MapPanel() panel.layout = (1, 1, 1) panel.projection = ccrs.PlateCarree() - panel.area = 'in' - panel.layers = ['states'] + panel.area = "in" + panel.layers = ["states"] panel.plots = [obs] # Bringing it all together @@ -992,30 +1069,32 @@ def test_declarative_sfc_obs_change_units(ccrs): return pc.figure -@pytest.mark.mpl_image_compare(remove_text=True, - tolerance={'2.1': 0.09}.get(MPL_VERSION, 0.022)) +@pytest.mark.mpl_image_compare( + remove_text=True, tolerance={"2.1": 0.09}.get(MPL_VERSION, 0.022) +) def test_declarative_multiple_sfc_obs_change_units(ccrs): """Test making a surface observation plot.""" - data = parse_metar_file(get_test_data('metar_20190701_1200.txt', as_file_obj=False), - year=2019, month=7) + data = parse_metar_file( + get_test_data("metar_20190701_1200.txt", as_file_obj=False), year=2019, month=7 + ) obs = PlotObs() obs.data = data obs.time = datetime(2019, 7, 1, 12) obs.time_window = timedelta(minutes=15) obs.level = None - obs.fields = ['air_temperature', 'dew_point_temperature', 'air_pressure_at_sea_level'] - obs.locations = ['NW', 'W', 'NE'] - obs.colors = ['red', 'green', 'black'] + obs.fields = ["air_temperature", "dew_point_temperature", "air_pressure_at_sea_level"] + obs.locations = ["NW", "W", "NE"] + obs.colors = ["red", "green", "black"] obs.reduce_points = 0.75 - obs.plot_units = ['degF', 'degF', None] + obs.plot_units = ["degF", "degF", None] # Panel for plot with Map features panel = MapPanel() panel.layout = (1, 1, 1) panel.projection = ccrs.PlateCarree() - panel.area = 'in' - panel.layers = ['states'] + panel.area = "in" + panel.layers = ["states"] panel.plots = [obs] # Bringing it all together @@ -1032,7 +1111,7 @@ def test_save(): """Test that our saving function works.""" pc = PanelContainer() fobj = BytesIO() - pc.save(fobj, format='png') + pc.save(fobj, format="png") fobj.seek(0) @@ -1046,7 +1125,7 @@ def test_show(): # Matplotlib warns when using show with Agg with warnings.catch_warnings(): - warnings.simplefilter('ignore', UserWarning) + warnings.simplefilter("ignore", UserWarning) pc.show() diff --git a/tests/plots/test_mapping.py b/tests/plots/test_mapping.py index d9b868b2e69..1eec0eda861 100644 --- a/tests/plots/test_mapping.py +++ b/tests/plots/test_mapping.py @@ -5,135 +5,152 @@ import pytest -ccrs = pytest.importorskip('cartopy.crs') +ccrs = pytest.importorskip("cartopy.crs") from metpy.plots.mapping import CFProjection # noqa: E402 def test_cfprojection_arg_mapping(): """Test the projection mapping arguments.""" - source = {'source': 'a', 'longitude_of_projection_origin': -100} + source = {"source": "a", "longitude_of_projection_origin": -100} # 'dest' should be the argument in the output, with the value from source - mapping = [('dest', 'source')] + mapping = [("dest", "source")] kwargs = CFProjection.build_projection_kwargs(source, mapping) - assert kwargs == {'dest': 'a', 'central_longitude': -100} + assert kwargs == {"dest": "a", "central_longitude": -100} def test_cfprojection_api(): """Test the basic API of the projection interface.""" - attrs = {'grid_mapping_name': 'lambert_conformal_conic', 'earth_radius': 6367000} + attrs = {"grid_mapping_name": "lambert_conformal_conic", "earth_radius": 6367000} proj = CFProjection(attrs) - assert proj['earth_radius'] == 6367000 + assert proj["earth_radius"] == 6367000 assert proj.to_dict() == attrs - assert str(proj) == 'Projection: lambert_conformal_conic' + assert str(proj) == "Projection: lambert_conformal_conic" def test_bad_projection_raises(): """Test behavior when given an unknown projection.""" - attrs = {'grid_mapping_name': 'unknown'} + attrs = {"grid_mapping_name": "unknown"} with pytest.raises(ValueError) as exc: CFProjection(attrs).to_cartopy() - assert 'Unhandled projection' in str(exc.value) + assert "Unhandled projection" in str(exc.value) def test_globe(): """Test handling building a cartopy globe.""" - attrs = {'grid_mapping_name': 'lambert_conformal_conic', 'earth_radius': 6367000, - 'standard_parallel': 25} + attrs = { + "grid_mapping_name": "lambert_conformal_conic", + "earth_radius": 6367000, + "standard_parallel": 25, + } proj = CFProjection(attrs) crs = proj.to_cartopy() globe_params = crs.globe.to_proj4_params() - assert globe_params['ellps'] == 'sphere' - assert globe_params['a'] == 6367000 - assert globe_params['b'] == 6367000 + assert globe_params["ellps"] == "sphere" + assert globe_params["a"] == 6367000 + assert globe_params["b"] == 6367000 def test_globe_spheroid(): """Test handling building a cartopy globe that is not spherical.""" - attrs = {'grid_mapping_name': 'lambert_conformal_conic', 'semi_major_axis': 6367000, - 'semi_minor_axis': 6360000} + attrs = { + "grid_mapping_name": "lambert_conformal_conic", + "semi_major_axis": 6367000, + "semi_minor_axis": 6360000, + } proj = CFProjection(attrs) crs = proj.to_cartopy() globe_params = crs.globe.to_proj4_params() - assert 'ellps' not in globe_params - assert globe_params['a'] == 6367000 - assert globe_params['b'] == 6360000 + assert "ellps" not in globe_params + assert globe_params["a"] == 6367000 + assert globe_params["b"] == 6360000 def test_aea(): """Test handling albers equal area projection.""" - attrs = {'grid_mapping_name': 'albers_conical_equal_area', 'earth_radius': 6367000, - 'standard_parallel': [20, 50]} + attrs = { + "grid_mapping_name": "albers_conical_equal_area", + "earth_radius": 6367000, + "standard_parallel": [20, 50], + } proj = CFProjection(attrs) crs = proj.to_cartopy() assert isinstance(crs, ccrs.AlbersEqualArea) - assert crs.proj4_params['lat_1'] == 20 - assert crs.proj4_params['lat_2'] == 50 - assert crs.globe.to_proj4_params()['ellps'] == 'sphere' + assert crs.proj4_params["lat_1"] == 20 + assert crs.proj4_params["lat_2"] == 50 + assert crs.globe.to_proj4_params()["ellps"] == "sphere" def test_aea_minimal(): """Test handling albers equal area projection with minimal attributes.""" - attrs = {'grid_mapping_name': 'albers_conical_equal_area'} + attrs = {"grid_mapping_name": "albers_conical_equal_area"} crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.AlbersEqualArea) def test_aea_single_std_parallel(): """Test albers equal area with one standard parallel.""" - attrs = {'grid_mapping_name': 'albers_conical_equal_area', 'standard_parallel': 20} + attrs = {"grid_mapping_name": "albers_conical_equal_area", "standard_parallel": 20} crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.AlbersEqualArea) - assert crs.proj4_params['lat_1'] == 20 + assert crs.proj4_params["lat_1"] == 20 def test_lcc(): """Test handling lambert conformal conic projection.""" - attrs = {'grid_mapping_name': 'lambert_conformal_conic', 'earth_radius': 6367000, - 'standard_parallel': [25, 30]} + attrs = { + "grid_mapping_name": "lambert_conformal_conic", + "earth_radius": 6367000, + "standard_parallel": [25, 30], + } proj = CFProjection(attrs) crs = proj.to_cartopy() assert isinstance(crs, ccrs.LambertConformal) - assert crs.proj4_params['lat_1'] == 25 - assert crs.proj4_params['lat_2'] == 30 - assert crs.globe.to_proj4_params()['ellps'] == 'sphere' + assert crs.proj4_params["lat_1"] == 25 + assert crs.proj4_params["lat_2"] == 30 + assert crs.globe.to_proj4_params()["ellps"] == "sphere" def test_lcc_minimal(): """Test handling lambert conformal conic projection with minimal attributes.""" - attrs = {'grid_mapping_name': 'lambert_conformal_conic'} + attrs = {"grid_mapping_name": "lambert_conformal_conic"} crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.LambertConformal) def test_lcc_single_std_parallel(): """Test lambert conformal projection with one standard parallel.""" - attrs = {'grid_mapping_name': 'lambert_conformal_conic', 'standard_parallel': 25} + attrs = {"grid_mapping_name": "lambert_conformal_conic", "standard_parallel": 25} crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.LambertConformal) - assert crs.proj4_params['lat_1'] == 25 + assert crs.proj4_params["lat_1"] == 25 def test_mercator(): """Test handling a mercator projection.""" - attrs = {'grid_mapping_name': 'mercator', 'standard_parallel': 25, - 'longitude_of_projection_origin': -100, 'false_easting': 0, 'false_westing': 0, - 'central_latitude': 0} + attrs = { + "grid_mapping_name": "mercator", + "standard_parallel": 25, + "longitude_of_projection_origin": -100, + "false_easting": 0, + "false_westing": 0, + "central_latitude": 0, + } crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.Mercator) - assert crs.proj4_params['lat_ts'] == 25 - assert crs.proj4_params['lon_0'] == -100 + assert crs.proj4_params["lat_ts"] == 25 + assert crs.proj4_params["lon_0"] == -100 # This won't work until at least CartoPy > 0.16.0 @@ -148,72 +165,86 @@ def test_mercator(): def test_geostationary(): """Test handling a geostationary projection.""" - attrs = {'grid_mapping_name': 'geostationary', 'perspective_point_height': 35000000, - 'longitude_of_projection_origin': -100, 'sweep_angle_axis': 'x', - 'latitude_of_projection_origin': 0} + attrs = { + "grid_mapping_name": "geostationary", + "perspective_point_height": 35000000, + "longitude_of_projection_origin": -100, + "sweep_angle_axis": "x", + "latitude_of_projection_origin": 0, + } crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.Geostationary) - assert crs.proj4_params['h'] == 35000000 - assert crs.proj4_params['lon_0'] == -100 - assert crs.proj4_params['sweep'] == 'x' + assert crs.proj4_params["h"] == 35000000 + assert crs.proj4_params["lon_0"] == -100 + assert crs.proj4_params["sweep"] == "x" def test_geostationary_fixed_angle(): """Test handling geostationary information that gives fixed angle instead of sweep.""" - attrs = {'grid_mapping_name': 'geostationary', 'fixed_angle_axis': 'y'} + attrs = {"grid_mapping_name": "geostationary", "fixed_angle_axis": "y"} crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.Geostationary) - assert crs.proj4_params['sweep'] == 'x' + assert crs.proj4_params["sweep"] == "x" def test_stereographic(): """Test handling a stereographic projection.""" - attrs = {'grid_mapping_name': 'stereographic', 'scale_factor_at_projection_origin': 0.9, - 'longitude_of_projection_origin': -100, 'latitude_of_projection_origin': 60} + attrs = { + "grid_mapping_name": "stereographic", + "scale_factor_at_projection_origin": 0.9, + "longitude_of_projection_origin": -100, + "latitude_of_projection_origin": 60, + } crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.Stereographic) - assert crs.proj4_params['lon_0'] == -100 - assert crs.proj4_params['lat_0'] == 60 - assert crs.proj4_params['k_0'] == 0.9 + assert crs.proj4_params["lon_0"] == -100 + assert crs.proj4_params["lat_0"] == 60 + assert crs.proj4_params["k_0"] == 0.9 def test_polar_stereographic(): """Test handling a polar stereographic projection.""" - attrs = {'grid_mapping_name': 'polar_stereographic', 'latitude_of_projection_origin': 90, - 'scale_factor_at_projection_origin': 0.9, - 'straight_vertical_longitude_from_pole': -100, } + attrs = { + "grid_mapping_name": "polar_stereographic", + "latitude_of_projection_origin": 90, + "scale_factor_at_projection_origin": 0.9, + "straight_vertical_longitude_from_pole": -100, + } crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.Stereographic) - assert crs.proj4_params['lon_0'] == -100 - assert crs.proj4_params['lat_0'] == 90 - assert crs.proj4_params['k_0'] == 0.9 + assert crs.proj4_params["lon_0"] == -100 + assert crs.proj4_params["lat_0"] == 90 + assert crs.proj4_params["k_0"] == 0.9 def test_polar_stereographic_std_parallel(): """Test handling a polar stereographic projection that gives a standard parallel.""" - attrs = {'grid_mapping_name': 'polar_stereographic', 'latitude_of_projection_origin': -90, - 'standard_parallel': 60} + attrs = { + "grid_mapping_name": "polar_stereographic", + "latitude_of_projection_origin": -90, + "standard_parallel": 60, + } crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.Stereographic) - assert crs.proj4_params['lat_0'] == -90 - assert crs.proj4_params['lat_ts'] == 60 + assert crs.proj4_params["lat_0"] == -90 + assert crs.proj4_params["lat_ts"] == 60 def test_lat_lon(): """Test handling basic lat/lon projection.""" - attrs = {'grid_mapping_name': 'latitude_longitude'} + attrs = {"grid_mapping_name": "latitude_longitude"} crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.PlateCarree) def test_eq(): """Test that two CFProjection instances are equal given that they have the same attrs.""" - attrs = {'grid_mapping_name': 'latitude_longitude'} + attrs = {"grid_mapping_name": "latitude_longitude"} cf_proj_1 = CFProjection(attrs) cf_proj_2 = CFProjection(attrs.copy()) assert cf_proj_1 == cf_proj_2 @@ -221,6 +252,6 @@ def test_eq(): def test_ne(): """Test that two CFProjection instances are not equal when attrs differs.""" - cf_proj_1 = CFProjection({'grid_mapping_name': 'latitude_longitude'}) - cf_proj_2 = CFProjection({'grid_mapping_name': 'lambert_conformal_conic'}) + cf_proj_1 = CFProjection({"grid_mapping_name": "latitude_longitude"}) + cf_proj_2 = CFProjection({"grid_mapping_name": "lambert_conformal_conic"}) assert cf_proj_1 != cf_proj_2 diff --git a/tests/plots/test_mpl.py b/tests/plots/test_mpl.py index 601b7822f96..4ae2aecb1b6 100644 --- a/tests/plots/test_mpl.py +++ b/tests/plots/test_mpl.py @@ -11,6 +11,7 @@ # Needed to trigger scattertext monkey-patching import metpy.plots # noqa: F401, I202 + # Fixture to make sure we have the right backend from metpy.testing import set_agg_backend # noqa: F401, I202 @@ -19,13 +20,18 @@ # to handle robustly def test_scattertext_patheffect_empty(): """Test scattertext with empty strings and PathEffects (Issue #245).""" - strings = ['abc', '', 'def'] + strings = ["abc", "", "def"] x, y = np.arange(6).reshape(2, 3) fig = plt.figure() ax = fig.add_subplot(1, 1, 1) - ax.scattertext(x, y, strings, color='white', - path_effects=[mpatheffects.withStroke(linewidth=1, foreground='black')]) + ax.scattertext( + x, + y, + strings, + color="white", + path_effects=[mpatheffects.withStroke(linewidth=1, foreground="black")], + ) # Need to trigger a render - with TemporaryFile('wb') as fobj: + with TemporaryFile("wb") as fobj: fig.savefig(fobj) diff --git a/tests/plots/test_skewt.py b/tests/plots/test_skewt.py index 3afb26219eb..ed1eab0c577 100644 --- a/tests/plots/test_skewt.py +++ b/tests/plots/test_skewt.py @@ -10,24 +10,25 @@ import pytest from metpy.plots import Hodograph, SkewT + # Fixtures to make sure we have the right backend and consistent round from metpy.testing import set_agg_backend # noqa: F401, I202 from metpy.units import units -@pytest.mark.mpl_image_compare(tolerance=.0202, remove_text=True, style='default') +@pytest.mark.mpl_image_compare(tolerance=0.0202, remove_text=True, style="default") def test_skewt_api(): """Test the SkewT API.""" - with matplotlib.rc_context({'axes.autolimit_mode': 'data'}): + with matplotlib.rc_context({"axes.autolimit_mode": "data"}): fig = plt.figure(figsize=(9, 9)) - skew = SkewT(fig, aspect='auto') + skew = SkewT(fig, aspect="auto") # Plot the data using normal plotting functions, in this case using # log scaling in Y, as dictated by the typical meteorological plot p = np.linspace(1000, 100, 10) t = np.linspace(20, -20, 10) u = np.linspace(-10, 10, 10) - skew.plot(p, t, 'r') + skew.plot(p, t, "r") skew.plot_barbs(p, u, u) skew.ax.set_xlim(-20, 30) @@ -46,18 +47,21 @@ def test_skewt_api(): return fig -@pytest.mark.mpl_image_compare(tolerance=.0272 if matplotlib.__version__ < '3.2' else 34.4, - remove_text=True, style='default') +@pytest.mark.mpl_image_compare( + tolerance=0.0272 if matplotlib.__version__ < "3.2" else 34.4, + remove_text=True, + style="default", +) def test_skewt_api_units(): """#Test the SkewT API when units are provided.""" - with matplotlib.rc_context({'axes.autolimit_mode': 'data'}): + with matplotlib.rc_context({"axes.autolimit_mode": "data"}): fig = plt.figure(figsize=(9, 9)) skew = SkewT(fig) p = (np.linspace(950, 100, 10) * units.hPa).to(units.Pa) t = (np.linspace(18, -20, 10) * units.degC).to(units.kelvin) u = np.linspace(-20, 20, 10) * units.knots - skew.plot(p, t, 'r') + skew.plot(p, t, "r") skew.plot_barbs(p, u, u) # Add the relevant special lines @@ -66,13 +70,16 @@ def test_skewt_api_units(): skew.plot_mixing_lines() # This works around the fact that newer pint versions default to degrees_Celsius - skew.ax.set_xlabel('degC') + skew.ax.set_xlabel("degC") return fig -@pytest.mark.mpl_image_compare(tolerance=0. if matplotlib.__version__ >= '3.2' else 30., - remove_text=True, style='default') +@pytest.mark.mpl_image_compare( + tolerance=0.0 if matplotlib.__version__ >= "3.2" else 30.0, + remove_text=True, + style="default", +) def test_skewt_default_aspect_empty(): """Test SkewT with default aspect and no plots, only special lines.""" # With this rotation and the default aspect, this matches exactly the NWS SkewT PDF @@ -84,10 +91,13 @@ def test_skewt_default_aspect_empty(): return fig -@pytest.mark.skipif(matplotlib.__version__ < '3.2', - reason='Matplotlib versions generate different image sizes.') -@pytest.mark.mpl_image_compare(tolerance=0., remove_text=False, style='default', - savefig_kwargs={'bbox_inches': 'tight'}) +@pytest.mark.skipif( + matplotlib.__version__ < "3.2", + reason="Matplotlib versions generate different image sizes.", +) +@pytest.mark.mpl_image_compare( + tolerance=0.0, remove_text=False, style="default", savefig_kwargs={"bbox_inches": "tight"} +) def test_skewt_tight_bbox(): """Test SkewT when saved with `savefig(..., bbox_inches='tight')`.""" fig = plt.figure(figsize=(12, 9)) @@ -95,35 +105,35 @@ def test_skewt_tight_bbox(): return fig -@pytest.mark.mpl_image_compare(tolerance=0.811, remove_text=True, style='default') +@pytest.mark.mpl_image_compare(tolerance=0.811, remove_text=True, style="default") def test_skewt_subplot(): """Test using SkewT on a sub-plot.""" fig = plt.figure(figsize=(9, 9)) - SkewT(fig, subplot=(2, 2, 1), aspect='auto') + SkewT(fig, subplot=(2, 2, 1), aspect="auto") return fig -@pytest.mark.mpl_image_compare(tolerance=0, remove_text=True, style='default') +@pytest.mark.mpl_image_compare(tolerance=0, remove_text=True, style="default") def test_skewt_gridspec(): """Test using SkewT on a GridSpec sub-plot.""" fig = plt.figure(figsize=(9, 9)) gs = GridSpec(1, 2) - SkewT(fig, subplot=gs[0, 1], aspect='auto') + SkewT(fig, subplot=gs[0, 1], aspect="auto") return fig def test_skewt_with_grid_enabled(): """Test using SkewT when gridlines are already enabled (#271).""" - with plt.rc_context(rc={'axes.grid': True}): + with plt.rc_context(rc={"axes.grid": True}): # Also tests when we don't pass in Figure - SkewT(aspect='auto') + SkewT(aspect="auto") -@pytest.mark.mpl_image_compare(tolerance=0., remove_text=True, style='default') +@pytest.mark.mpl_image_compare(tolerance=0.0, remove_text=True, style="default") def test_skewt_arbitrary_rect(): """Test placing the SkewT in an arbitrary rectangle.""" fig = plt.figure(figsize=(9, 9)) - SkewT(fig, rect=(0.15, 0.35, 0.8, 0.3), aspect='auto') + SkewT(fig, rect=(0.15, 0.35, 0.8, 0.3), aspect="auto") return fig @@ -133,107 +143,263 @@ def test_skewt_subplot_rect_conflict(): SkewT(rect=(0.15, 0.35, 0.8, 0.3), subplot=(1, 1, 1)) -@pytest.mark.mpl_image_compare(tolerance=0., remove_text=True, style='default') +@pytest.mark.mpl_image_compare(tolerance=0.0, remove_text=True, style="default") def test_skewt_units(): """Test that plotting with SkewT works with units properly.""" fig = plt.figure(figsize=(9, 9)) - skew = SkewT(fig, aspect='auto') + skew = SkewT(fig, aspect="auto") - skew.ax.axvline(np.array([273]) * units.kelvin, color='purple') - skew.ax.axhline(np.array([50000]) * units.Pa, color='red') - skew.ax.axvline(np.array([-20]) * units.degC, color='darkred') - skew.ax.axvline(-10, color='orange') + skew.ax.axvline(np.array([273]) * units.kelvin, color="purple") + skew.ax.axhline(np.array([50000]) * units.Pa, color="red") + skew.ax.axvline(np.array([-20]) * units.degC, color="darkred") + skew.ax.axvline(-10, color="orange") return fig @pytest.fixture() def test_profile(): """Return data for a test profile.""" - pressure = np.array([966., 937.2, 925., 904.6, 872.6, 853., 850., 836., 821., 811.6, 782.3, - 754.2, 726.9, 700., 648.9, 624.6, 601.1, 595., 587., 576., 555.7, - 534.2, 524., 500., 473.3, 400., 384.5, 358., 343., 308.3, 300., 276., - 273., 268.5, 250., 244.2, 233., 200.]) * units.mbar - temperature = np.array([18.2, 16.8, 16.2, 15.1, 13.3, 12.2, 12.4, 14., 14.4, - 13.7, 11.4, 9.1, 6.8, 4.4, -1.4, -4.4, -7.3, -8.1, - -7.9, -7.7, -8.7, -9.8, -10.3, -13.5, -17.1, -28.1, -30.7, - -35.3, -37.1, -43.5, -45.1, -49.9, -50.4, -51.1, -54.1, -55., - -56.7, -57.5]) * units.degC - dewpoint = np.array([16.9, 15.9, 15.5, 14.2, 12.1, 10.8, 8.6, 0., -3.6, -4.4, - -6.9, -9.5, -12., -14.6, -15.8, -16.4, -16.9, -17.1, -27.9, -42.7, - -44.1, -45.6, -46.3, -45.5, -47.1, -52.1, -50.4, -47.3, -57.1, - -57.9, -58.1, -60.9, -61.4, -62.1, -65.1, -65.6, - -66.7, -70.5]) * units.degC - profile = np. array([18.2, 16.18287437, 15.68644745, 14.8369451, - 13.45220646, 12.57020365, 12.43280242, 11.78283506, - 11.0698586, 10.61393901, 9.14490966, 7.66233636, - 6.1454231, 4.56888673, 1.31644072, -0.36678427, - -2.09120703, -2.55566745, -3.17594616, -4.05032505, - -5.73356001, -7.62361933, -8.56236581, -10.88846868, - -13.69095789, -22.82604468, -25.08463516, -29.26014016, - -31.81335912, -38.29612829, -39.97374452, -45.11966793, - -45.79482793, -46.82129892, -51.21936594, -52.65924319, - -55.52598916, -64.68843697]) * units.degC + pressure = ( + np.array( + [ + 966.0, + 937.2, + 925.0, + 904.6, + 872.6, + 853.0, + 850.0, + 836.0, + 821.0, + 811.6, + 782.3, + 754.2, + 726.9, + 700.0, + 648.9, + 624.6, + 601.1, + 595.0, + 587.0, + 576.0, + 555.7, + 534.2, + 524.0, + 500.0, + 473.3, + 400.0, + 384.5, + 358.0, + 343.0, + 308.3, + 300.0, + 276.0, + 273.0, + 268.5, + 250.0, + 244.2, + 233.0, + 200.0, + ] + ) + * units.mbar + ) + temperature = ( + np.array( + [ + 18.2, + 16.8, + 16.2, + 15.1, + 13.3, + 12.2, + 12.4, + 14.0, + 14.4, + 13.7, + 11.4, + 9.1, + 6.8, + 4.4, + -1.4, + -4.4, + -7.3, + -8.1, + -7.9, + -7.7, + -8.7, + -9.8, + -10.3, + -13.5, + -17.1, + -28.1, + -30.7, + -35.3, + -37.1, + -43.5, + -45.1, + -49.9, + -50.4, + -51.1, + -54.1, + -55.0, + -56.7, + -57.5, + ] + ) + * units.degC + ) + dewpoint = ( + np.array( + [ + 16.9, + 15.9, + 15.5, + 14.2, + 12.1, + 10.8, + 8.6, + 0.0, + -3.6, + -4.4, + -6.9, + -9.5, + -12.0, + -14.6, + -15.8, + -16.4, + -16.9, + -17.1, + -27.9, + -42.7, + -44.1, + -45.6, + -46.3, + -45.5, + -47.1, + -52.1, + -50.4, + -47.3, + -57.1, + -57.9, + -58.1, + -60.9, + -61.4, + -62.1, + -65.1, + -65.6, + -66.7, + -70.5, + ] + ) + * units.degC + ) + profile = ( + np.array( + [ + 18.2, + 16.18287437, + 15.68644745, + 14.8369451, + 13.45220646, + 12.57020365, + 12.43280242, + 11.78283506, + 11.0698586, + 10.61393901, + 9.14490966, + 7.66233636, + 6.1454231, + 4.56888673, + 1.31644072, + -0.36678427, + -2.09120703, + -2.55566745, + -3.17594616, + -4.05032505, + -5.73356001, + -7.62361933, + -8.56236581, + -10.88846868, + -13.69095789, + -22.82604468, + -25.08463516, + -29.26014016, + -31.81335912, + -38.29612829, + -39.97374452, + -45.11966793, + -45.79482793, + -46.82129892, + -51.21936594, + -52.65924319, + -55.52598916, + -64.68843697, + ] + ) + * units.degC + ) return pressure, temperature, dewpoint, profile -@pytest.mark.mpl_image_compare(tolerance=.033, remove_text=True, style='default') +@pytest.mark.mpl_image_compare(tolerance=0.033, remove_text=True, style="default") def test_skewt_shade_cape_cin(test_profile): """Test shading CAPE and CIN on a SkewT plot.""" p, t, td, tp = test_profile - with matplotlib.rc_context({'axes.autolimit_mode': 'data'}): + with matplotlib.rc_context({"axes.autolimit_mode": "data"}): fig = plt.figure(figsize=(9, 9)) - skew = SkewT(fig, aspect='auto') - skew.plot(p, t, 'r') - skew.plot(p, tp, 'k') + skew = SkewT(fig, aspect="auto") + skew.plot(p, t, "r") + skew.plot(p, tp, "k") skew.shade_cape(p, t, tp) skew.shade_cin(p, t, tp, td) skew.ax.set_xlim(-50, 50) skew.ax.set_ylim(1000, 100) # This works around the fact that newer pint versions default to degrees_Celsius - skew.ax.set_xlabel('degC') + skew.ax.set_xlabel("degC") return fig -@pytest.mark.mpl_image_compare(tolerance=0.033, remove_text=True, style='default') +@pytest.mark.mpl_image_compare(tolerance=0.033, remove_text=True, style="default") def test_skewt_shade_cape_cin_no_limit(test_profile): """Test shading CIN without limits.""" p, t, _, tp = test_profile - with matplotlib.rc_context({'axes.autolimit_mode': 'data'}): + with matplotlib.rc_context({"axes.autolimit_mode": "data"}): fig = plt.figure(figsize=(9, 9)) - skew = SkewT(fig, aspect='auto') - skew.plot(p, t, 'r') - skew.plot(p, tp, 'k') + skew = SkewT(fig, aspect="auto") + skew.plot(p, t, "r") + skew.plot(p, tp, "k") skew.shade_cape(p, t, tp) skew.shade_cin(p, t, tp) skew.ax.set_xlim(-50, 50) skew.ax.set_ylim(1000, 100) # This works around the fact that newer pint versions default to degrees_Celsius - skew.ax.set_xlabel('degC') + skew.ax.set_xlabel("degC") return fig -@pytest.mark.mpl_image_compare(tolerance=0.033, remove_text=True, style='default') +@pytest.mark.mpl_image_compare(tolerance=0.033, remove_text=True, style="default") def test_skewt_shade_area(test_profile): """Test shading areas on a SkewT plot.""" p, t, _, tp = test_profile - with matplotlib.rc_context({'axes.autolimit_mode': 'data'}): + with matplotlib.rc_context({"axes.autolimit_mode": "data"}): fig = plt.figure(figsize=(9, 9)) - skew = SkewT(fig, aspect='auto') - skew.plot(p, t, 'r') - skew.plot(p, tp, 'k') + skew = SkewT(fig, aspect="auto") + skew.plot(p, t, "r") + skew.plot(p, tp, "k") skew.shade_area(p, t, tp) skew.ax.set_xlim(-50, 50) skew.ax.set_ylim(1000, 100) # This works around the fact that newer pint versions default to degrees_Celsius - skew.ax.set_xlabel('degC') + skew.ax.set_xlabel("degC") return fig @@ -242,47 +408,47 @@ def test_skewt_shade_area_invalid(test_profile): """Test shading areas on a SkewT plot.""" p, t, _, tp = test_profile fig = plt.figure(figsize=(9, 9)) - skew = SkewT(fig, aspect='auto') - skew.plot(p, t, 'r') - skew.plot(p, tp, 'k') + skew = SkewT(fig, aspect="auto") + skew.plot(p, t, "r") + skew.plot(p, tp, "k") with pytest.raises(ValueError): - skew.shade_area(p, t, tp, which='positve') + skew.shade_area(p, t, tp, which="positve") -@pytest.mark.mpl_image_compare(tolerance=0.033, remove_text=True, style='default') +@pytest.mark.mpl_image_compare(tolerance=0.033, remove_text=True, style="default") def test_skewt_shade_area_kwargs(test_profile): """Test shading areas on a SkewT plot with kwargs.""" p, t, _, tp = test_profile - with matplotlib.rc_context({'axes.autolimit_mode': 'data'}): + with matplotlib.rc_context({"axes.autolimit_mode": "data"}): fig = plt.figure(figsize=(9, 9)) - skew = SkewT(fig, aspect='auto') - skew.plot(p, t, 'r') - skew.plot(p, tp, 'k') - skew.shade_area(p, t, tp, facecolor='m') + skew = SkewT(fig, aspect="auto") + skew.plot(p, t, "r") + skew.plot(p, tp, "k") + skew.shade_area(p, t, tp, facecolor="m") skew.ax.set_xlim(-50, 50) skew.ax.set_ylim(1000, 100) # This works around the fact that newer pint versions default to degrees_Celsius - skew.ax.set_xlabel('degC') + skew.ax.set_xlabel("degC") return fig -@pytest.mark.mpl_image_compare(tolerance=0.039, remove_text=True, style='default') +@pytest.mark.mpl_image_compare(tolerance=0.039, remove_text=True, style="default") def test_skewt_wide_aspect_ratio(test_profile): """Test plotting a skewT with a wide aspect ratio.""" p, t, _, tp = test_profile fig = plt.figure(figsize=(12.5, 3)) - skew = SkewT(fig, aspect='auto') - skew.plot(p, t, 'r') - skew.plot(p, tp, 'k') + skew = SkewT(fig, aspect="auto") + skew.plot(p, t, "r") + skew.plot(p, tp, "k") skew.ax.set_xlim(-30, 50) skew.ax.set_ylim(1050, 700) # This works around the fact that newer pint versions default to degrees_Celsius - skew.ax.set_xlabel('degC') + skew.ax.set_xlabel("degC") return fig @@ -292,10 +458,14 @@ def test_hodograph_api(): fig = plt.figure(figsize=(9, 9)) ax = fig.add_subplot(1, 1, 1) hodo = Hodograph(ax, component_range=60) - hodo.add_grid(increment=5, color='k') - hodo.plot([1, 10], [1, 10], color='red') - hodo.plot_colormapped(np.array([1, 3, 5, 10]), np.array([2, 4, 6, 11]), - np.array([0.1, 0.3, 0.5, 0.9]), cmap='Greys') + hodo.add_grid(increment=5, color="k") + hodo.plot([1, 10], [1, 10], color="red") + hodo.plot_colormapped( + np.array([1, 3, 5, 10]), + np.array([2, 4, 6, 11]), + np.array([0.1, 0.3, 0.5, 0.9]), + cmap="Greys", + ) return fig @@ -308,9 +478,9 @@ def test_hodograph_units(): u = np.arange(10) * units.kt v = np.arange(10) * units.kt hodo.plot(u, v) - hodo.plot_colormapped(u, v, np.sqrt(u * u + v * v), cmap='Greys') - ax.set_xlabel('') - ax.set_ylabel('') + hodo.plot_colormapped(u, v, np.sqrt(u * u + v * v), cmap="Greys") + ax.set_xlabel("") + ax.set_ylabel("") return fig @@ -322,24 +492,24 @@ def test_hodograph_alone(): @pytest.mark.mpl_image_compare(tolerance=0, remove_text=True) def test_hodograph_plot_colormapped(): """Test hodograph colored line with NaN values.""" - u = np.arange(5., 65., 5) - v = np.arange(-5., -65., -5) + u = np.arange(5.0, 65.0, 5) + v = np.arange(-5.0, -65.0, -5) u[3] = np.nan v[6] = np.nan fig = plt.figure(figsize=(9, 9)) ax = fig.add_subplot(1, 1, 1) hodo = Hodograph(ax, component_range=80) - hodo.add_grid(increment=20, color='k') - hodo.plot_colormapped(u, v, np.hypot(u, v), cmap='Greys') + hodo.add_grid(increment=20, color="k") + hodo.plot_colormapped(u, v, np.hypot(u, v), cmap="Greys") return fig -@pytest.mark.mpl_image_compare(tolerance=0, remove_text=True, style='default') +@pytest.mark.mpl_image_compare(tolerance=0, remove_text=True, style="default") def test_skewt_barb_color(): """Test plotting colored wind barbs on the Skew-T.""" fig = plt.figure(figsize=(9, 9)) - skew = SkewT(fig, aspect='auto') + skew = SkewT(fig, aspect="auto") p = np.linspace(1000, 100, 10) u = np.linspace(-10, 10, 10) @@ -348,17 +518,17 @@ def test_skewt_barb_color(): return fig -@pytest.mark.mpl_image_compare(tolerance=0.02, remove_text=True, style='default') +@pytest.mark.mpl_image_compare(tolerance=0.02, remove_text=True, style="default") def test_skewt_barb_unit_conversion(): """Test that barbs units can be converted at plot time (#737).""" - u_wind = np.array([3.63767155210412]) * units('m/s') - v_wind = np.array([3.63767155210412]) * units('m/s') + u_wind = np.array([3.63767155210412]) * units("m/s") + v_wind = np.array([3.63767155210412]) * units("m/s") p_wind = np.array([500]) * units.hPa fig = plt.figure(figsize=(9, 9)) - skew = SkewT(fig, aspect='auto') - skew.ax.set_ylabel('') # remove_text doesn't do this as of pytest 0.9 - skew.plot_barbs(p_wind, u_wind, v_wind, plot_units='knots') + skew = SkewT(fig, aspect="auto") + skew.ax.set_ylabel("") # remove_text doesn't do this as of pytest 0.9 + skew.plot_barbs(p_wind, u_wind, v_wind, plot_units="knots") skew.ax.set_ylim(1000, 500) skew.ax.set_yticks([1000, 750, 500]) skew.ax.set_xlim(-20, 20) @@ -366,16 +536,16 @@ def test_skewt_barb_unit_conversion(): return fig -@pytest.mark.mpl_image_compare(tolerance=0.02, remove_text=True, style='default') +@pytest.mark.mpl_image_compare(tolerance=0.02, remove_text=True, style="default") def test_skewt_barb_no_default_unit_conversion(): """Test that barbs units are left alone by default (#737).""" - u_wind = np.array([3.63767155210412]) * units('m/s') - v_wind = np.array([3.63767155210412]) * units('m/s') + u_wind = np.array([3.63767155210412]) * units("m/s") + v_wind = np.array([3.63767155210412]) * units("m/s") p_wind = np.array([500]) * units.hPa fig = plt.figure(figsize=(9, 9)) - skew = SkewT(fig, aspect='auto') - skew.ax.set_ylabel('') # remove_text doesn't do this as of pytest 0.9 + skew = SkewT(fig, aspect="auto") + skew.ax.set_ylabel("") # remove_text doesn't do this as of pytest 0.9 skew.plot_barbs(p_wind, u_wind, v_wind) skew.ax.set_ylim(1000, 500) skew.ax.set_yticks([1000, 750, 500]) @@ -384,16 +554,21 @@ def test_skewt_barb_no_default_unit_conversion(): return fig -@pytest.mark.parametrize('u,v', [(np.array([3]) * units('m/s'), np.array([3])), - (np.array([3]), np.array([3]) * units('m/s'))]) +@pytest.mark.parametrize( + "u,v", + [ + (np.array([3]) * units("m/s"), np.array([3])), + (np.array([3]), np.array([3]) * units("m/s")), + ], +) def test_skewt_barb_unit_conversion_exception(u, v): """Test that errors are raise if unit conversion is requested on un-united data.""" p_wind = np.array([500]) * units.hPa fig = plt.figure(figsize=(9, 9)) - skew = SkewT(fig, aspect='auto') + skew = SkewT(fig, aspect="auto") with pytest.raises(ValueError): - skew.plot_barbs(p_wind, u, v, plot_units='knots') + skew.plot_barbs(p_wind, u, v, plot_units="knots") @pytest.mark.mpl_image_compare(tolerance=0, remove_text=True) @@ -403,7 +578,7 @@ def test_hodograph_plot_layers(): v = np.array([0, 10, 20, 30, 40, 50]) * units.knots heights = np.array([0, 1000, 2000, 3000, 4000, 5000]) * units.m intervals = np.array([500, 1500, 2500, 3500, 4500]) * units.m - colors = ['r', 'g', 'b', 'r'] + colors = ["r", "g", "b", "r"] fig = plt.figure(figsize=(7, 7)) ax1 = fig.add_subplot(1, 1, 1) h = Hodograph(ax1) @@ -422,7 +597,7 @@ def test_hodograph_plot_layers_different_units(): v = np.array([0, 10, 20, 30, 40, 50]) * units.knots heights = np.array([0, 1, 2, 3, 4, 5]) * units.km intervals = np.array([500, 1500, 2500, 3500, 4500]) * units.m - colors = ['r', 'g', 'b', 'r'] + colors = ["r", "g", "b", "r"] fig = plt.figure(figsize=(7, 7)) ax1 = fig.add_subplot(1, 1, 1) h = Hodograph(ax1) @@ -440,7 +615,7 @@ def test_hodograph_plot_layers_bound_units(): v = np.array([0, 10, 20, 30, 40, 50]) * units.knots heights = np.array([0, 1000, 2000, 3000, 4000, 5000]) * units.m intervals = np.array([0.5, 1.5, 2.5, 3.5, 4.5]) * units.km - colors = ['r', 'g', 'b', 'r'] + colors = ["r", "g", "b", "r"] fig = plt.figure(figsize=(7, 7)) ax1 = fig.add_subplot(1, 1, 1) h = Hodograph(ax1) @@ -454,15 +629,15 @@ def test_hodograph_plot_layers_bound_units(): @pytest.mark.mpl_image_compare(tolerance=0, remove_text=True) def test_hodograph_plot_arbitrary_layer(): """Test hodograph colored layers for arbitrary variables without interpolation.""" - u = np.arange(5, 65, 5) * units('knot') - v = np.arange(-5, -65, -5) * units('knot') + u = np.arange(5, 65, 5) * units("knot") + v = np.arange(-5, -65, -5) * units("knot") speed = np.sqrt(u ** 2 + v ** 2) - colors = ['red', 'green', 'blue'] - levels = [0, 10, 20, 30] * units('knot') + colors = ["red", "green", "blue"] + levels = [0, 10, 20, 30] * units("knot") fig = plt.figure(figsize=(9, 9)) ax = fig.add_subplot(1, 1, 1) hodo = Hodograph(ax, component_range=80) - hodo.add_grid(increment=20, color='k') + hodo.add_grid(increment=20, color="k") hodo.plot_colormapped(u, v, speed, intervals=levels, colors=colors) return fig @@ -481,10 +656,12 @@ def test_hodograph_wind_vectors(): return fig -@pytest.mark.skipif(matplotlib.__version__ < '3.0.1', - reason="Earlier Matplotlib versions don't have a required fix.") +@pytest.mark.skipif( + matplotlib.__version__ < "3.0.1", + reason="Earlier Matplotlib versions don't have a required fix.", +) def test_united_hodograph_range(): """Tests making a hodograph with a united ranged.""" fig = plt.figure(figsize=(6, 6)) ax = fig.add_subplot(1, 1, 1) - Hodograph(ax, component_range=60. * units.knots) + Hodograph(ax, component_range=60.0 * units.knots) diff --git a/tests/plots/test_station_plot.py b/tests/plots/test_station_plot.py index f84088eb6fc..669610a5287 100644 --- a/tests/plots/test_station_plot.py +++ b/tests/plots/test_station_plot.py @@ -9,17 +9,24 @@ import pandas as pd import pytest -from metpy.plots import (current_weather, high_clouds, nws_layout, simple_layout, - sky_cover, StationPlot, StationPlotLayout) +from metpy.plots import ( + StationPlot, + StationPlotLayout, + current_weather, + high_clouds, + nws_layout, + simple_layout, + sky_cover, +) + # Fixtures to make sure we have the right backend and consistent round from metpy.testing import set_agg_backend # noqa: F401, I202 from metpy.units import units - MPL_VERSION = matplotlib.__version__[:3] -@pytest.mark.mpl_image_compare(tolerance=2.444, savefig_kwargs={'dpi': 300}, remove_text=True) +@pytest.mark.mpl_image_compare(tolerance=2.444, savefig_kwargs={"dpi": 300}, remove_text=True) def test_stationplot_api(): """Test the StationPlot API.""" fig = plt.figure(figsize=(9, 9)) @@ -31,9 +38,9 @@ def test_stationplot_api(): # Make the plot sp = StationPlot(fig.add_subplot(1, 1, 1), x, y, fontsize=16) sp.plot_barb([20, 0], [0, -50]) - sp.plot_text('E', ['KOKC', 'ICT'], color='blue') - sp.plot_parameter('NW', [10.5, 15] * units.degC, color='red') - sp.plot_symbol('S', [5, 7], high_clouds, color='green') + sp.plot_text("E", ["KOKC", "ICT"], color="blue") + sp.plot_parameter("NW", [10.5, 15] * units.degC, color="red") + sp.plot_symbol("S", [5, 7], high_clouds, color="green") sp.ax.set_xlim(0, 6) sp.ax.set_ylim(0, 6) @@ -41,7 +48,7 @@ def test_stationplot_api(): return fig -@pytest.mark.mpl_image_compare(tolerance=1.976, savefig_kwargs={'dpi': 300}, remove_text=True) +@pytest.mark.mpl_image_compare(tolerance=1.976, savefig_kwargs={"dpi": 300}, remove_text=True) def test_stationplot_clipping(): """Test the that clipping can be enabled as a default parameter.""" fig = plt.figure(figsize=(9, 9)) @@ -53,9 +60,9 @@ def test_stationplot_clipping(): # Make the plot sp = StationPlot(fig.add_subplot(1, 1, 1), x, y, fontsize=16, clip_on=True) sp.plot_barb([20, 0], [0, -50]) - sp.plot_text('E', ['KOKC', 'ICT'], color='blue') - sp.plot_parameter('NW', [10.5, 15] * units.degC, color='red') - sp.plot_symbol('S', [5, 7], high_clouds, color='green') + sp.plot_text("E", ["KOKC", "ICT"], color="blue") + sp.plot_parameter("NW", [10.5, 15] * units.degC, color="red") + sp.plot_symbol("S", [5, 7], high_clouds, color="green") sp.ax.set_xlim(1, 5) sp.ax.set_ylim(1.75, 4.25) @@ -63,7 +70,7 @@ def test_stationplot_clipping(): return fig -@pytest.mark.mpl_image_compare(tolerance=0.25, savefig_kwargs={'dpi': 300}, remove_text=True) +@pytest.mark.mpl_image_compare(tolerance=0.25, savefig_kwargs={"dpi": 300}, remove_text=True) def test_station_plot_replace(): """Test that locations are properly replaced.""" fig = plt.figure(figsize=(3, 3)) @@ -76,8 +83,8 @@ def test_station_plot_replace(): sp = StationPlot(fig.add_subplot(1, 1, 1), x, y, fontsize=16) sp.plot_barb([20], [0]) sp.plot_barb([5], [0]) - sp.plot_parameter('NW', [10.5], color='red') - sp.plot_parameter('NW', [20], color='blue') + sp.plot_parameter("NW", [10.5], color="red") + sp.plot_parameter("NW", [20], color="blue") sp.ax.set_xlim(-3, 3) sp.ax.set_ylim(-3, 3) @@ -85,13 +92,34 @@ def test_station_plot_replace(): return fig -@pytest.mark.mpl_image_compare(tolerance=0.25, savefig_kwargs={'dpi': 300}, remove_text=True) +@pytest.mark.mpl_image_compare(tolerance=0.25, savefig_kwargs={"dpi": 300}, remove_text=True) def test_station_plot_locations(): """Test that locations are properly replaced.""" fig = plt.figure(figsize=(3, 3)) - locations = ['C', 'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N2', 'NNE', 'ENE', 'E2', - 'ESE', 'SSE', 'S2', 'SSW', 'WSW', 'W2', 'WNW', 'NNW'] + locations = [ + "C", + "N", + "NE", + "E", + "SE", + "S", + "SW", + "W", + "NW", + "N2", + "NNE", + "ENE", + "E2", + "ESE", + "SSE", + "S2", + "SSW", + "WSW", + "W2", + "WNW", + "NNW", + ] x_pos = np.array([0]) y_pos = np.array([0]) @@ -106,8 +134,9 @@ def test_station_plot_locations(): return fig -@pytest.mark.mpl_image_compare(tolerance=0.00413, savefig_kwargs={'dpi': 300}, - remove_text=True) +@pytest.mark.mpl_image_compare( + tolerance=0.00413, savefig_kwargs={"dpi": 300}, remove_text=True +) def test_stationlayout_api(): """Test the StationPlot API.""" fig = plt.figure(figsize=(9, 9)) @@ -115,16 +144,21 @@ def test_stationlayout_api(): # testing data x = np.array([1, 5]) y = np.array([2, 4]) - data = {'temp': np.array([33., 212.]) * units.degF, 'u': np.array([2, 0]) * units.knots, - 'v': np.array([0, 5]) * units.knots, 'stid': ['KDEN', 'KSHV'], 'cover': [3, 8]} + data = { + "temp": np.array([33.0, 212.0]) * units.degF, + "u": np.array([2, 0]) * units.knots, + "v": np.array([0, 5]) * units.knots, + "stid": ["KDEN", "KSHV"], + "cover": [3, 8], + } # Set up the layout layout = StationPlotLayout() - layout.add_barb('u', 'v', units='knots') - layout.add_value('NW', 'temp', fmt='0.1f', units=units.degC, color='darkred') - layout.add_symbol('C', 'cover', sky_cover, color='magenta') - layout.add_text((0, 2), 'stid', color='darkgrey') - layout.add_value('NE', 'dewpt', color='green') # This should be ignored + layout.add_barb("u", "v", units="knots") + layout.add_value("NW", "temp", fmt="0.1f", units=units.degC, color="darkred") + layout.add_symbol("C", "cover", sky_cover, color="magenta") + layout.add_text((0, 2), "stid", color="darkgrey") + layout.add_value("NE", "dewpt", color="green") # This should be ignored # Make the plot sp = StationPlot(fig.add_subplot(1, 1, 1), x, y, fontsize=12) @@ -142,11 +176,11 @@ def test_station_layout_odd_data(): # Set up test layout layout = StationPlotLayout() - layout.add_barb('u', 'v') - layout.add_value('W', 'temperature', units='degF') + layout.add_barb("u", "v") + layout.add_value("W", "temperature", units="degF") # Now only use data without wind and no units - data = {'temperature': [25.]} + data = {"temperature": [25.0]} # Make the plot sp = StationPlot(fig.add_subplot(1, 1, 1), [1], [2], fontsize=12) @@ -157,24 +191,24 @@ def test_station_layout_odd_data(): def test_station_layout_replace(): """Test that layout locations are replaced.""" layout = StationPlotLayout() - layout.add_text('E', 'temperature') - layout.add_value('E', 'dewpoint') - assert 'E' in layout - assert layout['E'][0] is StationPlotLayout.PlotTypes.value - assert layout['E'][1] == 'dewpoint' + layout.add_text("E", "temperature") + layout.add_value("E", "dewpoint") + assert "E" in layout + assert layout["E"][0] is StationPlotLayout.PlotTypes.value + assert layout["E"][1] == "dewpoint" def test_station_layout_names(): """Test getting station layout names.""" layout = StationPlotLayout() - layout.add_barb('u', 'v') - layout.add_text('E', 'stid') - layout.add_value('W', 'temp') - layout.add_symbol('C', 'cover', lambda x: x) - assert sorted(layout.names()) == ['cover', 'stid', 'temp', 'u', 'v'] + layout.add_barb("u", "v") + layout.add_text("E", "stid") + layout.add_value("W", "temp") + layout.add_symbol("C", "cover", lambda x: x) + assert sorted(layout.names()) == ["cover", "stid", "temp", "u", "v"] -@pytest.mark.mpl_image_compare(tolerance=0, savefig_kwargs={'dpi': 300}, remove_text=True) +@pytest.mark.mpl_image_compare(tolerance=0, savefig_kwargs={"dpi": 300}, remove_text=True) def test_simple_layout(): """Test metpy's simple layout for station plots.""" fig = plt.figure(figsize=(9, 9)) @@ -182,12 +216,16 @@ def test_simple_layout(): # testing data x = np.array([1, 5]) y = np.array([2, 4]) - data = {'air_temperature': np.array([33., 212.]) * units.degF, - 'dew_point_temperature': np.array([28., 80.]) * units.degF, - 'air_pressure_at_sea_level': np.array([29.92, 28.00]) * units.inHg, - 'eastward_wind': np.array([2, 0]) * units.knots, - 'northward_wind': np.array([0, 5]) * units.knots, 'cloud_coverage': [3, 8], - 'present_weather': [65, 75], 'unused': [1, 2]} + data = { + "air_temperature": np.array([33.0, 212.0]) * units.degF, + "dew_point_temperature": np.array([28.0, 80.0]) * units.degF, + "air_pressure_at_sea_level": np.array([29.92, 28.00]) * units.inHg, + "eastward_wind": np.array([2, 0]) * units.knots, + "northward_wind": np.array([0, 5]) * units.knots, + "cloud_coverage": [3, 8], + "present_weather": [65, 75], + "unused": [1, 2], + } # Make the plot sp = StationPlot(fig.add_subplot(1, 1, 1), x, y, fontsize=12) @@ -199,7 +237,7 @@ def test_simple_layout(): return fig -@pytest.mark.mpl_image_compare(tolerance=0.1848, savefig_kwargs={'dpi': 300}, remove_text=True) +@pytest.mark.mpl_image_compare(tolerance=0.1848, savefig_kwargs={"dpi": 300}, remove_text=True) def test_nws_layout(): """Test metpy's NWS layout for station plots.""" fig = plt.figure(figsize=(3, 3)) @@ -207,15 +245,21 @@ def test_nws_layout(): # testing data x = np.array([1]) y = np.array([2]) - data = {'air_temperature': np.array([77]) * units.degF, - 'dew_point_temperature': np.array([71]) * units.degF, - 'air_pressure_at_sea_level': np.array([999.8]) * units('mbar'), - 'eastward_wind': np.array([15.]) * units.knots, - 'northward_wind': np.array([15.]) * units.knots, 'cloud_coverage': [7], - 'present_weather': [80], 'high_cloud_type': [1], 'medium_cloud_type': [3], - 'low_cloud_type': [2], 'visibility_in_air': np.array([5.]) * units.mile, - 'tendency_of_air_pressure': np.array([-0.3]) * units('mbar'), - 'tendency_of_air_pressure_symbol': [8]} + data = { + "air_temperature": np.array([77]) * units.degF, + "dew_point_temperature": np.array([71]) * units.degF, + "air_pressure_at_sea_level": np.array([999.8]) * units("mbar"), + "eastward_wind": np.array([15.0]) * units.knots, + "northward_wind": np.array([15.0]) * units.knots, + "cloud_coverage": [7], + "present_weather": [80], + "high_cloud_type": [1], + "medium_cloud_type": [3], + "low_cloud_type": [2], + "visibility_in_air": np.array([5.0]) * units.mile, + "tendency_of_air_pressure": np.array([-0.3]) * units("mbar"), + "tendency_of_air_pressure_symbol": [8], + } # Make the plot sp = StationPlot(fig.add_subplot(1, 1, 1), x, y, fontsize=12, spacing=16) @@ -239,8 +283,8 @@ def test_plot_text_fontsize(): # Make the plot sp = StationPlot(ax, x, y, fontsize=36) - sp.plot_text('NW', ['72'], fontsize=24) - sp.plot_text('SW', ['60'], fontsize=4) + sp.plot_text("NW", ["72"], fontsize=24) + sp.plot_text("SW", ["60"], fontsize=4) sp.ax.set_xlim(0, 3) sp.ax.set_ylim(0, 3) @@ -255,8 +299,8 @@ def test_plot_symbol_fontsize(): ax = plt.subplot(1, 1, 1) sp = StationPlot(ax, [0], [0], fontsize=8, spacing=32) - sp.plot_symbol('E', [92], current_weather) - sp.plot_symbol('W', [96], current_weather, fontsize=100) + sp.plot_symbol("E", [92], current_weather) + sp.plot_symbol("W", [96], current_weather, fontsize=100) return fig @@ -264,12 +308,14 @@ def test_plot_symbol_fontsize(): def test_layout_str(): """Test layout string representation.""" layout = StationPlotLayout() - layout.add_barb('u', 'v') - layout.add_text('E', 'stid') - layout.add_value('W', 'temp') - layout.add_symbol('C', 'cover', lambda x: x) - assert str(layout) == ('{C: (symbol, cover, ...), E: (text, stid, ...), ' - "W: (value, temp, ...), barb: (barb, ('u', 'v'), ...)}") + layout.add_barb("u", "v") + layout.add_text("E", "stid") + layout.add_value("W", "temp") + layout.add_symbol("C", "cover", lambda x: x) + assert str(layout) == ( + "{C: (symbol, cover, ...), E: (text, stid, ...), " + "W: (value, temp, ...), barb: (barb, ('u', 'v'), ...)}" + ) @pytest.fixture @@ -281,8 +327,9 @@ def wind_plot(): return u, v, x, y -@pytest.mark.mpl_image_compare(tolerance={'2.1': 0.0423}.get(MPL_VERSION, 0.00434), - remove_text=True) +@pytest.mark.mpl_image_compare( + tolerance={"2.1": 0.0423}.get(MPL_VERSION, 0.00434), remove_text=True +) def test_barb_projection(wind_plot, ccrs): """Test that barbs are properly projected (#598).""" u, v, x, y = wind_plot @@ -297,8 +344,9 @@ def test_barb_projection(wind_plot, ccrs): return fig -@pytest.mark.mpl_image_compare(tolerance={'2.1': 0.0693}.get(MPL_VERSION, 0.00382), - remove_text=True) +@pytest.mark.mpl_image_compare( + tolerance={"2.1": 0.0693}.get(MPL_VERSION, 0.00382), remove_text=True +) def test_arrow_projection(wind_plot, ccrs): """Test that arrows are properly projected.""" u, v, x, y = wind_plot @@ -319,8 +367,8 @@ def wind_projection_list(): """Create wind lists for testing.""" lat = [38.22, 38.18, 38.25] lon = [-85.76, -85.86, -85.77] - u = [1.89778964, -3.83776523, 3.64147732] * units('m/s') - v = [1.93480072, 1.31000184, 1.36075552] * units('m/s') + u = [1.89778964, -3.83776523, 3.64147732] * units("m/s") + v = [1.93480072, 1.31000184, 1.36075552] * units("m/s") return lat, lon, u, v @@ -351,8 +399,8 @@ def barbs_units(): """Create barbs with units for testing.""" x_pos = np.array([0]) y_pos = np.array([0]) - u_wind = np.array([3.63767155210412]) * units('m/s') - v_wind = np.array([3.63767155210412]) * units('m/s') + u_wind = np.array([3.63767155210412]) * units("m/s") + v_wind = np.array([3.63767155210412]) * units("m/s") return x_pos, y_pos, u_wind, v_wind @@ -364,7 +412,7 @@ def test_barb_unit_conversion(barbs_units): fig = plt.figure() ax = fig.add_subplot(1, 1, 1) stnplot = StationPlot(ax, x_pos, y_pos) - stnplot.plot_barb(u_wind, v_wind, plot_units='knots') + stnplot.plot_barb(u_wind, v_wind, plot_units="knots") ax.set_xlim(-5, 5) ax.set_ylim(-5, 5) @@ -379,7 +427,7 @@ def test_arrow_unit_conversion(barbs_units): fig = plt.figure() ax = fig.add_subplot(1, 1, 1) stnplot = StationPlot(ax, x_pos, y_pos) - stnplot.plot_arrow(u_wind, v_wind, plot_units='knots') + stnplot.plot_arrow(u_wind, v_wind, plot_units="knots") ax.set_xlim(-5, 5) ax.set_ylim(-5, 5) @@ -391,8 +439,8 @@ def test_barb_no_default_unit_conversion(): """Test that barbs units are left alone by default (#737).""" x_pos = np.array([0]) y_pos = np.array([0]) - u_wind = np.array([3.63767155210412]) * units('m/s') - v_wind = np.array([3.63767155210412]) * units('m/s') + u_wind = np.array([3.63767155210412]) * units("m/s") + v_wind = np.array([3.63767155210412]) * units("m/s") fig = plt.figure() ax = fig.add_subplot(1, 1, 1) @@ -404,8 +452,13 @@ def test_barb_no_default_unit_conversion(): return fig -@pytest.mark.parametrize('u,v', [(np.array([3]) * units('m/s'), np.array([3])), - (np.array([3]), np.array([3]) * units('m/s'))]) +@pytest.mark.parametrize( + "u,v", + [ + (np.array([3]) * units("m/s"), np.array([3])), + (np.array([3]), np.array([3]) * units("m/s")), + ], +) def test_barb_unit_conversion_exception(u, v): """Test that errors are raise if unit conversion is requested on un-united data.""" x_pos = np.array([0]) @@ -415,27 +468,27 @@ def test_barb_unit_conversion_exception(u, v): ax = fig.add_subplot(1, 1, 1) stnplot = StationPlot(ax, x_pos, y_pos) with pytest.raises(ValueError): - stnplot.plot_barb(u, v, plot_units='knots') + stnplot.plot_barb(u, v, plot_units="knots") -@pytest.mark.mpl_image_compare(tolerance=0.021, savefig_kwargs={'dpi': 300}, remove_text=True) +@pytest.mark.mpl_image_compare(tolerance=0.021, savefig_kwargs={"dpi": 300}, remove_text=True) def test_symbol_pandas_timeseries(): """Test the usage of Pandas DatetimeIndex as a valid `x` input into StationPlot.""" pd.plotting.register_matplotlib_converters() - rng = pd.date_range('12/1/2017', periods=5, freq='D') + rng = pd.date_range("12/1/2017", periods=5, freq="D") sc = [1, 2, 3, 4, 5] ts = pd.Series(sc, index=rng) fig, ax = plt.subplots() y = np.ones(len(ts.index)) stationplot = StationPlot(ax, ts.index, y, fontsize=12) - stationplot.plot_symbol('C', ts, sky_cover) + stationplot.plot_symbol("C", ts, sky_cover) ax.xaxis.set_major_locator(matplotlib.dates.DayLocator()) - ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter('%-d')) + ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter("%-d")) return fig -@pytest.mark.mpl_image_compare(tolerance=2.444, savefig_kwargs={'dpi': 300}, remove_text=True) +@pytest.mark.mpl_image_compare(tolerance=2.444, savefig_kwargs={"dpi": 300}, remove_text=True) def test_stationplot_unit_conversion(): """Test the StationPlot API.""" fig = plt.figure(figsize=(9, 9)) @@ -447,9 +500,9 @@ def test_stationplot_unit_conversion(): # Make the plot sp = StationPlot(fig.add_subplot(1, 1, 1), x, y, fontsize=16) sp.plot_barb([20, 0], [0, -50]) - sp.plot_text('E', ['KOKC', 'ICT'], color='blue') - sp.plot_parameter('NW', [10.5, 15] * units.degC, plot_units='degF', color='red') - sp.plot_symbol('S', [5, 7], high_clouds, color='green') + sp.plot_text("E", ["KOKC", "ICT"], color="blue") + sp.plot_parameter("NW", [10.5, 15] * units.degC, plot_units="degF", color="red") + sp.plot_symbol("S", [5, 7], high_clouds, color="green") sp.ax.set_xlim(0, 6) sp.ax.set_ylim(0, 6) @@ -467,4 +520,4 @@ def test_scalar_unit_conversion_exception(): ax = fig.add_subplot(1, 1, 1) stnplot = StationPlot(ax, x_pos, y_pos) with pytest.raises(ValueError): - stnplot.plot_parameter('C', T, plot_units='degC') + stnplot.plot_parameter("C", T, plot_units="degC") diff --git a/tests/plots/test_util.py b/tests/plots/test_util.py index 4cfd172dca3..a2fa5c5be50 100644 --- a/tests/plots/test_util.py +++ b/tests/plots/test_util.py @@ -11,6 +11,7 @@ import pytest from metpy.plots import add_metpy_logo, add_timestamp, add_unidata_logo, convert_gempak_color + # Fixture to make sure we have the right backend from metpy.testing import set_agg_backend # noqa: F401, I202 @@ -31,7 +32,7 @@ def test_add_timestamp_custom_format(): """Test adding a timestamp to an axes object with custom time formatting.""" fig = plt.figure(figsize=(9, 9)) ax = plt.subplot(1, 1, 1) - add_timestamp(ax, time=datetime(2017, 1, 1), time_format='%H:%M:%S %Y/%m/%d') + add_timestamp(ax, time=datetime(2017, 1, 1), time_format="%H:%M:%S %Y/%m/%d") return fig @@ -40,7 +41,7 @@ def test_add_timestamp_pretext(): """Test adding a timestamp to an axes object with custom pre-text.""" fig = plt.figure(figsize=(9, 9)) ax = plt.subplot(1, 1, 1) - add_timestamp(ax, time=datetime(2017, 1, 1), pretext='Valid: ') + add_timestamp(ax, time=datetime(2017, 1, 1), pretext="Valid: ") return fig @@ -65,7 +66,7 @@ def test_add_metpy_logo_small(): def test_add_metpy_logo_large(): """Test adding a large MetPy logo to a figure.""" fig = plt.figure(figsize=(9, 9)) - add_metpy_logo(fig, size='large') + add_metpy_logo(fig, size="large") return fig @@ -81,7 +82,7 @@ def test_add_logo_invalid_size(): """Test adding a logo to a figure with an invalid size specification.""" fig = plt.figure(figsize=(9, 9)) with pytest.raises(ValueError): - add_metpy_logo(fig, size='jumbo') + add_metpy_logo(fig, size="jumbo") @pytest.mark.mpl_image_compare(tolerance=0.01, remove_text=True) @@ -93,8 +94,8 @@ def test_gempak_color_image_compare(): delta = 0.025 x = y = np.arange(-3.0, 3.01, delta) xx, yy = np.meshgrid(x, y) - z1 = np.exp(-xx**2 - yy**2) - z2 = np.exp(-(xx - 1)**2 - (yy - 1)**2) + z1 = np.exp(-(xx ** 2) - yy ** 2) + z2 = np.exp(-((xx - 1) ** 2) - (yy - 1) ** 2) z = (z1 - z2) * 2 fig = plt.figure(figsize=(9, 9)) @@ -107,13 +108,13 @@ def test_gempak_color_image_compare(): def test_gempak_color_xw_image_compare(): """Test creating a plot with all the GEMPAK colors using xw style.""" c = range(32) - mplc = convert_gempak_color(c, style='xw') + mplc = convert_gempak_color(c, style="xw") delta = 0.025 x = y = np.arange(-3.0, 3.01, delta) xx, yy = np.meshgrid(x, y) - z1 = np.exp(-xx**2 - yy**2) - z2 = np.exp(-(xx - 1)**2 - (yy - 1)**2) + z1 = np.exp(-(xx ** 2) - yy ** 2) + z2 = np.exp(-((xx - 1) ** 2) - (yy - 1) ** 2) z = (z1 - z2) * 2 fig = plt.figure(figsize=(9, 9)) @@ -126,19 +127,19 @@ def test_gempak_color_invalid_style(): """Test converting a GEMPAK color with an invalid style parameter.""" c = range(32) with pytest.raises(ValueError): - convert_gempak_color(c, style='plt') + convert_gempak_color(c, style="plt") def test_gempak_color_quirks(): """Test converting some unusual GEMPAK colors.""" c = [-5, 95, 101] mplc = convert_gempak_color(c) - truth = ['white', 'bisque', 'white'] + truth = ["white", "bisque", "white"] assert mplc == truth def test_gempak_color_scalar(): """Test converting a single GEMPAK color.""" mplc = convert_gempak_color(6) - truth = 'cyan' + truth = "cyan" assert mplc == truth diff --git a/tests/plots/test_wx_symbols.py b/tests/plots/test_wx_symbols.py index 4a90cef9149..bfa3c782399 100644 --- a/tests/plots/test_wx_symbols.py +++ b/tests/plots/test_wx_symbols.py @@ -10,16 +10,16 @@ def test_mapper(): """Test for symbol mapping functionality.""" - assert current_weather(0) == '' - assert current_weather(4) == '\ue9a2' - assert current_weather(7) == '\ue9a5' - assert current_weather(65) == '\ue9e1' + assert current_weather(0) == "" + assert current_weather(4) == "\ue9a2" + assert current_weather(7) == "\ue9a5" + assert current_weather(65) == "\ue9e1" def test_alt_char(): """Test alternate character functionality for mapper.""" - assert current_weather.alt_char(7, 1) == '\ue9a6' - assert current_weather.alt_char(7, 2) == '\ue9a7' + assert current_weather.alt_char(7, 1) == "\ue9a6" + assert current_weather.alt_char(7, 2) == "\ue9a7" def test_mapper_len(): @@ -29,17 +29,149 @@ def test_mapper_len(): def test_wx_code_to_numeric(): """Test getting numeric weather codes from METAR.""" - data = ['SN', '-RA', '-SHSN', '-SHRA', 'DZ', 'RA', 'SHSN', 'TSRA', '-FZRA', '-SN', '-TSRA', - '-RASN', '+SN', 'FG', '-SHRASN', '-DZ', 'SHRA', '-FZRASN', 'TSSN', 'MIBCFG', - '-RAPL', 'RAPL', 'TSSNPL', '-SNPL', '+RA', '-RASNPL', '-BLSN', '-SHSNIC', '+TSRA', - 'TS', 'PL', 'SNPL', '-SHRAPL', '-SNSG', '-TSSN', 'SG', 'IC', 'FU', '+SNPL', - 'TSSNPLGR', '-TSSNPLGR', '-SHSNSG', 'SHRAPL', '-TSRASN', 'FZRA', '-TSRAPL', - '-FZDZSN', '+TSSN', '-TSRASNPL', 'TSRAPL', 'RASN', '-SNIC', 'FZRAPL', '-FZRASNPL', - '+RAPL', '-RASGPL', '-TSSNPL', 'FZRASN', '+TSSNGR', 'TSPLGR', '', 'RA BR', '-TSSG', - '-TS', '-NA', 'NANA', '+NANA', 'NANANA', 'NANANANA'] - true_codes = np.array([73, 61, 85, 80, 53, 63, 86, 95, 66, 71, 95, 68, 75, 45, 83, 51, 81, - 66, 95, 0, 79, 79, 95, 79, 65, 79, 36, 85, 97, 17, 79, 79, 80, 77, - 95, 77, 78, 4, 79, 95, 95, 85, 81, 95, 67, 95, 56, 97, 95, 95, 69, - 71, 79, 66, 79, 61, 95, 67, 97, 95, 0, 63, 17, 17, 0, 0, 0, 0, 0]) + data = [ + "SN", + "-RA", + "-SHSN", + "-SHRA", + "DZ", + "RA", + "SHSN", + "TSRA", + "-FZRA", + "-SN", + "-TSRA", + "-RASN", + "+SN", + "FG", + "-SHRASN", + "-DZ", + "SHRA", + "-FZRASN", + "TSSN", + "MIBCFG", + "-RAPL", + "RAPL", + "TSSNPL", + "-SNPL", + "+RA", + "-RASNPL", + "-BLSN", + "-SHSNIC", + "+TSRA", + "TS", + "PL", + "SNPL", + "-SHRAPL", + "-SNSG", + "-TSSN", + "SG", + "IC", + "FU", + "+SNPL", + "TSSNPLGR", + "-TSSNPLGR", + "-SHSNSG", + "SHRAPL", + "-TSRASN", + "FZRA", + "-TSRAPL", + "-FZDZSN", + "+TSSN", + "-TSRASNPL", + "TSRAPL", + "RASN", + "-SNIC", + "FZRAPL", + "-FZRASNPL", + "+RAPL", + "-RASGPL", + "-TSSNPL", + "FZRASN", + "+TSSNGR", + "TSPLGR", + "", + "RA BR", + "-TSSG", + "-TS", + "-NA", + "NANA", + "+NANA", + "NANANA", + "NANANANA", + ] + true_codes = np.array( + [ + 73, + 61, + 85, + 80, + 53, + 63, + 86, + 95, + 66, + 71, + 95, + 68, + 75, + 45, + 83, + 51, + 81, + 66, + 95, + 0, + 79, + 79, + 95, + 79, + 65, + 79, + 36, + 85, + 97, + 17, + 79, + 79, + 80, + 77, + 95, + 77, + 78, + 4, + 79, + 95, + 95, + 85, + 81, + 95, + 67, + 95, + 56, + 97, + 95, + 95, + 69, + 71, + 79, + 66, + 79, + 61, + 95, + 67, + 97, + 95, + 0, + 63, + 17, + 17, + 0, + 0, + 0, + 0, + 0, + ] + ) wx_codes = wx_code_to_numeric(data) assert_array_equal(wx_codes, true_codes) diff --git a/tests/test_cbook.py b/tests/test_cbook.py index c94713a4526..3f2773bb13a 100644 --- a/tests/test_cbook.py +++ b/tests/test_cbook.py @@ -10,7 +10,7 @@ def test_registry(): """Test that the registry properly registers things.""" reg = Registry() - a = 'foo' - reg.register('mine')(a) + a = "foo" + reg.register("mine")(a) - assert reg['mine'] is a + assert reg["mine"] is a diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index d5aa1f3b119..63ff681ae14 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -14,11 +14,11 @@ class FakeyMcFakeface: @classmethod def dontuse(cls): """Don't use.""" - deprecation.warn_deprecated('0.0.1', pending=True) + deprecation.warn_deprecated("0.0.1", pending=True) return False @classmethod - @deprecation.deprecated('0.0.1') + @deprecation.deprecated("0.0.1") def really_dontuse(cls): """Really, don't use.""" return False diff --git a/tests/test_testing.py b/tests/test_testing.py index 9efd83ab50f..c5770b512e2 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -9,16 +9,20 @@ import xarray as xr from metpy.deprecation import MetpyDeprecationWarning -from metpy.testing import (assert_array_almost_equal, check_and_drop_units, - check_and_silence_deprecation) +from metpy.testing import ( + assert_array_almost_equal, + check_and_drop_units, + check_and_silence_deprecation, +) # Test #1183: numpy.testing.assert_array* ignores any masked value, so work-around def test_masked_arrays(): """Test that we catch masked arrays with different masks.""" with pytest.raises(AssertionError): - assert_array_almost_equal(np.array([10, 20]), - np.ma.array([10, np.nan], mask=[False, True]), 2) + assert_array_almost_equal( + np.array([10, 20]), np.ma.array([10, np.nan], mask=[False, True]), 2 + ) def test_masked_and_no_mask(): @@ -31,13 +35,13 @@ def test_masked_and_no_mask(): @check_and_silence_deprecation def test_deprecation_decorator(): """Make sure the deprecation checker works.""" - warnings.warn('Testing warning.', MetpyDeprecationWarning) + warnings.warn("Testing warning.", MetpyDeprecationWarning) def test_check_and_drop_units_with_dataarray(): """Make sure check_and_drop_units functions properly with both arguments as DataArrays.""" - var_0 = xr.DataArray([[1, 2], [3, 4]], attrs={'units': 'cm'}) - var_1 = xr.DataArray([[0.01, 0.02], [0.03, 0.04]], attrs={'units': 'm'}) + var_0 = xr.DataArray([[1, 2], [3, 4]], attrs={"units": "cm"}) + var_1 = xr.DataArray([[0.01, 0.02], [0.03, 0.04]], attrs={"units": "m"}) actual, desired = check_and_drop_units(var_0, var_1) assert isinstance(actual, np.ndarray) assert isinstance(desired, np.ndarray) diff --git a/tests/test_xarray.py b/tests/test_xarray.py index 6fa1537364d..1ffb316d062 100644 --- a/tests/test_xarray.py +++ b/tests/test_xarray.py @@ -9,8 +9,13 @@ import xarray as xr from metpy.plots.mapping import CFProjection -from metpy.testing import (assert_almost_equal, assert_array_almost_equal, assert_array_equal, - get_test_data, needs_cartopy) +from metpy.testing import ( + assert_almost_equal, + assert_array_almost_equal, + assert_array_equal, + get_test_data, + needs_cartopy, +) from metpy.units import DimensionalityError, units from metpy.xarray import ( add_grid_arguments_from_xarray, @@ -18,52 +23,59 @@ check_axis, check_matching_coordinates, grid_deltas_from_dataarray, - preprocess_and_wrap + preprocess_and_wrap, ) @pytest.fixture def test_ds(): """Provide an xarray dataset for testing.""" - return xr.open_dataset(get_test_data('narr_example.nc', as_file_obj=False)) + return xr.open_dataset(get_test_data("narr_example.nc", as_file_obj=False)) @pytest.fixture def test_ds_generic(): """Provide a generic-coordinate dataset for testing.""" - return xr.DataArray(np.zeros((1, 3, 3, 5, 5)), - coords={ - 'a': xr.DataArray(np.arange(1), dims='a'), - 'b': xr.DataArray(np.arange(3), dims='b'), - 'c': xr.DataArray(np.arange(3), dims='c'), - 'd': xr.DataArray(np.arange(5), dims='d'), - 'e': xr.DataArray(np.arange(5), dims='e') - }, dims=['a', 'b', 'c', 'd', 'e'], name='test').to_dataset() + return xr.DataArray( + np.zeros((1, 3, 3, 5, 5)), + coords={ + "a": xr.DataArray(np.arange(1), dims="a"), + "b": xr.DataArray(np.arange(3), dims="b"), + "c": xr.DataArray(np.arange(3), dims="c"), + "d": xr.DataArray(np.arange(5), dims="d"), + "e": xr.DataArray(np.arange(5), dims="e"), + }, + dims=["a", "b", "c", "d", "e"], + name="test", + ).to_dataset() @pytest.fixture def test_var(test_ds): """Provide a standard, parsed, variable for testing.""" - return test_ds.metpy.parse_cf('Temperature') + return test_ds.metpy.parse_cf("Temperature") @pytest.fixture def test_var_multidim_full(test_ds): """Provide a variable with x/y coords and multidimensional lat/lon auxiliary coords.""" - return (test_ds[{'isobaric': [6, 12], 'y': [95, 96], 'x': [122, 123]}] - .squeeze().set_coords(['lat', 'lon'])['Temperature']) + return ( + test_ds[{"isobaric": [6, 12], "y": [95, 96], "x": [122, 123]}] + .squeeze() + .set_coords(["lat", "lon"])["Temperature"] + ) @pytest.fixture def test_var_multidim_no_xy(test_var_multidim_full): """Provide a variable with multidimensional lat/lon coords but without x/y coords.""" - return test_var_multidim_full.drop_vars(['y', 'x']) + return test_var_multidim_full.drop_vars(["y", "x"]) def test_projection(test_var, ccrs): """Test getting the proper projection out of the variable.""" crs = test_var.metpy.crs - assert crs['grid_mapping_name'] == 'lambert_conformal_conic' + assert crs["grid_mapping_name"] == "lambert_conformal_conic" assert isinstance(test_var.metpy.cartopy_crs, ccrs.LambertConformal) @@ -74,16 +86,16 @@ def test_no_projection(test_ds): with pytest.raises(AttributeError) as exc: var.metpy.crs - assert 'not available' in str(exc.value) + assert "not available" in str(exc.value) def test_globe(test_var, ccrs): """Test getting the globe belonging to the projection.""" globe = test_var.metpy.cartopy_globe - assert globe.to_proj4_params() == OrderedDict([('ellps', 'sphere'), - ('a', 6367470.21484375), - ('b', 6367470.21484375)]) + assert globe.to_proj4_params() == OrderedDict( + [("ellps", "sphere"), ("a", 6367470.21484375), ("b", 6367470.21484375)] + ) assert isinstance(globe, ccrs.Globe) @@ -109,8 +121,8 @@ def test_units(test_var): def test_units_percent(): """Test that '%' is handled as 'percent'.""" test_var_percent = xr.open_dataset( - get_test_data('irma_gfs_example.nc', - as_file_obj=False))['Relative_humidity_isobaric'] + get_test_data("irma_gfs_example.nc", as_file_obj=False) + )["Relative_humidity_isobaric"] assert test_var_percent.metpy.units == units.percent @@ -122,16 +134,15 @@ def test_magnitude_with_quantity(test_var): def test_magnitude_without_quantity(test_ds_generic): """Test magnitude property on accessor when data is not a quantity.""" - assert isinstance(test_ds_generic['test'].data, np.ndarray) + assert isinstance(test_ds_generic["test"].data, np.ndarray) np.testing.assert_array_equal( - test_ds_generic['test'].metpy.magnitude, - np.asarray(test_ds_generic['test'].values) + test_ds_generic["test"].metpy.magnitude, np.asarray(test_ds_generic["test"].values) ) def test_convert_units(test_var): """Test conversion of units.""" - result = test_var.metpy.convert_units('degC') + result = test_var.metpy.convert_units("degC") # Check that units are updated without modifying original assert result.metpy.units == units.degC @@ -143,345 +154,361 @@ def test_convert_units(test_var): def test_convert_coordinate_units(test_ds_generic): """Test conversion of coordinate units.""" - result = test_ds_generic['test'].metpy.convert_coordinate_units('b', 'percent') - assert result['b'].data[1] == 100. - assert result['b'].metpy.units == units.percent + result = test_ds_generic["test"].metpy.convert_coordinate_units("b", "percent") + assert result["b"].data[1] == 100.0 + assert result["b"].metpy.units == units.percent def test_quantify(test_ds_generic): """Test quantify method for converting data to Quantity.""" - original = test_ds_generic['test'].values - result = test_ds_generic['test'].metpy.quantify() + original = test_ds_generic["test"].values + result = test_ds_generic["test"].metpy.quantify() assert isinstance(result.data, units.Quantity) assert result.data.units == units.dimensionless - assert 'units' not in result.attrs + assert "units" not in result.attrs np.testing.assert_array_almost_equal(result.data, units.Quantity(original)) def test_dequantify(): """Test dequantify method for converting data away from Quantity.""" - original = xr.DataArray(units.Quantity([280, 290, 300], 'K')) + original = xr.DataArray(units.Quantity([280, 290, 300], "K")) result = original.metpy.dequantify() assert isinstance(result.data, np.ndarray) - assert result.attrs['units'] == 'kelvin' + assert result.attrs["units"] == "kelvin" np.testing.assert_array_almost_equal(result.data, original.data.magnitude) def test_dataset_quantify(test_ds_generic): """Test quantify method for converting data to Quantity on Datasets.""" result = test_ds_generic.metpy.quantify() - assert isinstance(result['test'].data, units.Quantity) - assert result['test'].data.units == units.dimensionless - assert 'units' not in result['test'].attrs + assert isinstance(result["test"].data, units.Quantity) + assert result["test"].data.units == units.dimensionless + assert "units" not in result["test"].attrs np.testing.assert_array_almost_equal( - result['test'].data, - units.Quantity(test_ds_generic['test'].data) + result["test"].data, units.Quantity(test_ds_generic["test"].data) ) def test_dataset_dequantify(): """Test dequantify method for converting data away from Quantity on Datasets.""" - original = xr.Dataset({ - 'test': ('x', units.Quantity([280, 290, 300], 'K')), - 'x': np.arange(3) - }) + original = xr.Dataset( + {"test": ("x", units.Quantity([280, 290, 300], "K")), "x": np.arange(3)} + ) result = original.metpy.dequantify() - assert isinstance(result['test'].data, np.ndarray) - assert result['test'].attrs['units'] == 'kelvin' - np.testing.assert_array_almost_equal(result['test'].data, original['test'].data.magnitude) + assert isinstance(result["test"].data, np.ndarray) + assert result["test"].attrs["units"] == "kelvin" + np.testing.assert_array_almost_equal(result["test"].data, original["test"].data.magnitude) def test_radian_projection_coords(): """Test fallback code for radian units in projection coordinate variables.""" - proj = xr.DataArray(0, attrs={'grid_mapping_name': 'geostationary', - 'perspective_point_height': 3}) - x = xr.DataArray(np.arange(3), - attrs={'standard_name': 'projection_x_coordinate', 'units': 'radians'}) - y = xr.DataArray(np.arange(2), - attrs={'standard_name': 'projection_y_coordinate', 'units': 'radians'}) - data = xr.DataArray(np.arange(6).reshape(2, 3), coords=(y, x), dims=('y', 'x'), - attrs={'grid_mapping': 'fixedgrid_projection'}) - ds = xr.Dataset({'data': data, 'fixedgrid_projection': proj}) + proj = xr.DataArray( + 0, attrs={"grid_mapping_name": "geostationary", "perspective_point_height": 3} + ) + x = xr.DataArray( + np.arange(3), attrs={"standard_name": "projection_x_coordinate", "units": "radians"} + ) + y = xr.DataArray( + np.arange(2), attrs={"standard_name": "projection_y_coordinate", "units": "radians"} + ) + data = xr.DataArray( + np.arange(6).reshape(2, 3), + coords=(y, x), + dims=("y", "x"), + attrs={"grid_mapping": "fixedgrid_projection"}, + ) + ds = xr.Dataset({"data": data, "fixedgrid_projection": proj}) # Check that the coordinates in this case are properly converted - data_var = ds.metpy.parse_cf('data') - assert data_var.coords['x'].metpy.unit_array[1] == 3 * units.meter - assert data_var.coords['y'].metpy.unit_array[1] == 3 * units.meter + data_var = ds.metpy.parse_cf("data") + assert data_var.coords["x"].metpy.unit_array[1] == 3 * units.meter + assert data_var.coords["y"].metpy.unit_array[1] == 3 * units.meter def test_missing_grid_mapping(): """Test falling back to implicit lat/lon projection.""" - lon = xr.DataArray(-np.arange(3), - attrs={'standard_name': 'longitude', 'units': 'degrees_east'}) - lat = xr.DataArray(np.arange(2), - attrs={'standard_name': 'latitude', 'units': 'degrees_north'}) - data = xr.DataArray(np.arange(6).reshape(2, 3), coords=(lat, lon), dims=('y', 'x')) - ds = xr.Dataset({'data': data}) + lon = xr.DataArray( + -np.arange(3), attrs={"standard_name": "longitude", "units": "degrees_east"} + ) + lat = xr.DataArray( + np.arange(2), attrs={"standard_name": "latitude", "units": "degrees_north"} + ) + data = xr.DataArray(np.arange(6).reshape(2, 3), coords=(lat, lon), dims=("y", "x")) + ds = xr.Dataset({"data": data}) - data_var = ds.metpy.parse_cf('data') - assert 'crs' in data_var.coords + data_var = ds.metpy.parse_cf("data") + assert "crs" in data_var.coords def test_missing_grid_mapping_var(caplog): """Test behavior when we can't find the variable pointed to by grid_mapping.""" - x = xr.DataArray(np.arange(3), - attrs={'standard_name': 'projection_x_coordinate', 'units': 'radians'}) - y = xr.DataArray(np.arange(2), - attrs={'standard_name': 'projection_y_coordinate', 'units': 'radians'}) - data = xr.DataArray(np.arange(6).reshape(2, 3), coords=(y, x), dims=('y', 'x'), - attrs={'grid_mapping': 'fixedgrid_projection'}) - ds = xr.Dataset({'data': data}) + x = xr.DataArray( + np.arange(3), attrs={"standard_name": "projection_x_coordinate", "units": "radians"} + ) + y = xr.DataArray( + np.arange(2), attrs={"standard_name": "projection_y_coordinate", "units": "radians"} + ) + data = xr.DataArray( + np.arange(6).reshape(2, 3), + coords=(y, x), + dims=("y", "x"), + attrs={"grid_mapping": "fixedgrid_projection"}, + ) + ds = xr.Dataset({"data": data}) - ds.metpy.parse_cf('data') # Should log a warning + ds.metpy.parse_cf("data") # Should log a warning for record in caplog.records: - assert record.levelname == 'WARNING' - assert 'Could not find' in caplog.text + assert record.levelname == "WARNING" + assert "Could not find" in caplog.text def test_preprocess_and_wrap_only_preprocessing(): """Test xarray preprocessing and wrapping decorator for only preprocessing.""" - data = xr.DataArray(np.ones(3), attrs={'units': 'km'}) - data2 = xr.DataArray(np.ones(3), attrs={'units': 'm'}) + data = xr.DataArray(np.ones(3), attrs={"units": "km"}) + data2 = xr.DataArray(np.ones(3), attrs={"units": "m"}) @preprocess_and_wrap() def func(a, b): - return a.to('m') + b + return a.to("m") + b assert_array_equal(func(data, b=data2), np.array([1001, 1001, 1001]) * units.m) def test_coordinates_basic_by_method(test_var): """Test that NARR example coordinates are like we expect using coordinates method.""" - x, y, vertical, time = test_var.metpy.coordinates('x', 'y', 'vertical', 'time') + x, y, vertical, time = test_var.metpy.coordinates("x", "y", "vertical", "time") - assert test_var['x'].identical(x) - assert test_var['y'].identical(y) - assert test_var['isobaric'].identical(vertical) - assert test_var['time'].identical(time) + assert test_var["x"].identical(x) + assert test_var["y"].identical(y) + assert test_var["isobaric"].identical(vertical) + assert test_var["time"].identical(time) def test_coordinates_basic_by_property(test_var): """Test that NARR example coordinates are like we expect using properties.""" - assert test_var['x'].identical(test_var.metpy.x) - assert test_var['y'].identical(test_var.metpy.y) - assert test_var['isobaric'].identical(test_var.metpy.vertical) - assert test_var['time'].identical(test_var.metpy.time) + assert test_var["x"].identical(test_var.metpy.x) + assert test_var["y"].identical(test_var.metpy.y) + assert test_var["isobaric"].identical(test_var.metpy.vertical) + assert test_var["time"].identical(test_var.metpy.time) def test_coordinates_specified_by_name_with_dataset(test_ds_generic): """Test that we can manually specify the coordinates by name.""" - data = test_ds_generic.metpy.parse_cf(coordinates={'time': 'a', 'vertical': 'b', 'y': 'c', - 'x': 'd'}) - x, y, vertical, time = data['test'].metpy.coordinates('x', 'y', 'vertical', 'time') + data = test_ds_generic.metpy.parse_cf( + coordinates={"time": "a", "vertical": "b", "y": "c", "x": "d"} + ) + x, y, vertical, time = data["test"].metpy.coordinates("x", "y", "vertical", "time") - assert data['test']['d'].identical(x) - assert data['test']['c'].identical(y) - assert data['test']['b'].identical(vertical) - assert data['test']['a'].identical(time) + assert data["test"]["d"].identical(x) + assert data["test"]["c"].identical(y) + assert data["test"]["b"].identical(vertical) + assert data["test"]["a"].identical(time) def test_coordinates_specified_by_dataarray_with_dataset(test_ds_generic): """Test that we can manually specify the coordinates by DataArray.""" - data = test_ds_generic.metpy.parse_cf(coordinates={ - 'time': test_ds_generic['d'], - 'vertical': test_ds_generic['a'], - 'y': test_ds_generic['b'], - 'x': test_ds_generic['c'] - }) - x, y, vertical, time = data['test'].metpy.coordinates('x', 'y', 'vertical', 'time') + data = test_ds_generic.metpy.parse_cf( + coordinates={ + "time": test_ds_generic["d"], + "vertical": test_ds_generic["a"], + "y": test_ds_generic["b"], + "x": test_ds_generic["c"], + } + ) + x, y, vertical, time = data["test"].metpy.coordinates("x", "y", "vertical", "time") - assert data['test']['c'].identical(x) - assert data['test']['b'].identical(y) - assert data['test']['a'].identical(vertical) - assert data['test']['d'].identical(time) + assert data["test"]["c"].identical(x) + assert data["test"]["b"].identical(y) + assert data["test"]["a"].identical(vertical) + assert data["test"]["d"].identical(time) def test_missing_coordinate_type(test_ds_generic): """Test that an AttributeError is raised when an axis/coordinate type is unavailable.""" - data = test_ds_generic.metpy.parse_cf('test', coordinates={'vertical': 'e'}) + data = test_ds_generic.metpy.parse_cf("test", coordinates={"vertical": "e"}) with pytest.raises(AttributeError) as exc: data.metpy.time - assert 'not available' in str(exc.value) + assert "not available" in str(exc.value) def test_assign_coordinates_not_overwrite(test_ds_generic): """Test that assign_coordinates does not overwrite past axis attributes.""" data = test_ds_generic.copy() - data['c'].attrs['axis'] = 'X' - data['test'] = data['test'].metpy.assign_coordinates({'y': data['c']}) - assert data['c'].identical(data['test'].metpy.y) - assert data['c'].attrs['axis'] == 'X' + data["c"].attrs["axis"] = "X" + data["test"] = data["test"].metpy.assign_coordinates({"y": data["c"]}) + assert data["c"].identical(data["test"].metpy.y) + assert data["c"].attrs["axis"] == "X" def test_resolve_axis_conflict_lonlat_and_xy(test_ds_generic): """Test _resolve_axis_conflict with both lon/lat and x/y coordinates.""" - test_ds_generic['b'].attrs['_CoordinateAxisType'] = 'GeoX' - test_ds_generic['c'].attrs['_CoordinateAxisType'] = 'Lon' - test_ds_generic['d'].attrs['_CoordinateAxisType'] = 'GeoY' - test_ds_generic['e'].attrs['_CoordinateAxisType'] = 'Lat' + test_ds_generic["b"].attrs["_CoordinateAxisType"] = "GeoX" + test_ds_generic["c"].attrs["_CoordinateAxisType"] = "Lon" + test_ds_generic["d"].attrs["_CoordinateAxisType"] = "GeoY" + test_ds_generic["e"].attrs["_CoordinateAxisType"] = "Lat" - assert test_ds_generic['test'].metpy.x.name == 'b' - assert test_ds_generic['test'].metpy.y.name == 'd' + assert test_ds_generic["test"].metpy.x.name == "b" + assert test_ds_generic["test"].metpy.y.name == "d" def test_resolve_axis_conflict_double_lonlat(test_ds_generic): """Test _resolve_axis_conflict with double lon/lat coordinates.""" - test_ds_generic['b'].attrs['_CoordinateAxisType'] = 'Lat' - test_ds_generic['c'].attrs['_CoordinateAxisType'] = 'Lon' - test_ds_generic['d'].attrs['_CoordinateAxisType'] = 'Lat' - test_ds_generic['e'].attrs['_CoordinateAxisType'] = 'Lon' + test_ds_generic["b"].attrs["_CoordinateAxisType"] = "Lat" + test_ds_generic["c"].attrs["_CoordinateAxisType"] = "Lon" + test_ds_generic["d"].attrs["_CoordinateAxisType"] = "Lat" + test_ds_generic["e"].attrs["_CoordinateAxisType"] = "Lon" - with pytest.warns(UserWarning, match='More than one x coordinate'): + with pytest.warns(UserWarning, match="More than one x coordinate"): with pytest.raises(AttributeError): - test_ds_generic['test'].metpy.x - with pytest.warns(UserWarning, match='More than one y coordinate'): + test_ds_generic["test"].metpy.x + with pytest.warns(UserWarning, match="More than one y coordinate"): with pytest.raises(AttributeError): - test_ds_generic['test'].metpy.y + test_ds_generic["test"].metpy.y def test_resolve_axis_conflict_double_xy(test_ds_generic): """Test _resolve_axis_conflict with double x/y coordinates.""" - test_ds_generic['b'].attrs['standard_name'] = 'projection_x_coordinate' - test_ds_generic['c'].attrs['standard_name'] = 'projection_y_coordinate' - test_ds_generic['d'].attrs['standard_name'] = 'projection_x_coordinate' - test_ds_generic['e'].attrs['standard_name'] = 'projection_y_coordinate' + test_ds_generic["b"].attrs["standard_name"] = "projection_x_coordinate" + test_ds_generic["c"].attrs["standard_name"] = "projection_y_coordinate" + test_ds_generic["d"].attrs["standard_name"] = "projection_x_coordinate" + test_ds_generic["e"].attrs["standard_name"] = "projection_y_coordinate" - with pytest.warns(UserWarning, match='More than one x coordinate'): + with pytest.warns(UserWarning, match="More than one x coordinate"): with pytest.raises(AttributeError): - test_ds_generic['test'].metpy.x - with pytest.warns(UserWarning, match='More than one y coordinate'): + test_ds_generic["test"].metpy.x + with pytest.warns(UserWarning, match="More than one y coordinate"): with pytest.raises(AttributeError): - test_ds_generic['test'].metpy.y + test_ds_generic["test"].metpy.y def test_resolve_axis_conflict_double_x_with_single_dim(test_ds_generic): """Test _resolve_axis_conflict with double x coordinate, but only one being a dim.""" - test_ds_generic['e'].attrs['standard_name'] = 'projection_x_coordinate' - test_ds_generic.coords['f'] = ('e', np.linspace(0, 1, 5)) - test_ds_generic['f'].attrs['standard_name'] = 'projection_x_coordinate' + test_ds_generic["e"].attrs["standard_name"] = "projection_x_coordinate" + test_ds_generic.coords["f"] = ("e", np.linspace(0, 1, 5)) + test_ds_generic["f"].attrs["standard_name"] = "projection_x_coordinate" - assert test_ds_generic['test'].metpy.x.name == 'e' + assert test_ds_generic["test"].metpy.x.name == "e" def test_resolve_axis_conflict_double_vertical(test_ds_generic): """Test _resolve_axis_conflict with double vertical coordinates.""" - test_ds_generic['b'].attrs['units'] = 'hPa' - test_ds_generic['c'].attrs['units'] = 'Pa' + test_ds_generic["b"].attrs["units"] = "hPa" + test_ds_generic["c"].attrs["units"] = "Pa" - with pytest.warns(UserWarning, match='More than one vertical coordinate'): + with pytest.warns(UserWarning, match="More than one vertical coordinate"): with pytest.raises(AttributeError): - test_ds_generic['test'].metpy.vertical + test_ds_generic["test"].metpy.vertical criterion_matches = [ - ('standard_name', 'time', 'time'), - ('standard_name', 'model_level_number', 'vertical'), - ('standard_name', 'atmosphere_hybrid_sigma_pressure_coordinate', 'vertical'), - ('standard_name', 'geopotential_height', 'vertical'), - ('standard_name', 'height_above_geopotential_datum', 'vertical'), - ('standard_name', 'altitude', 'vertical'), - ('standard_name', 'atmosphere_sigma_coordinate', 'vertical'), - ('standard_name', 'height_above_reference_ellipsoid', 'vertical'), - ('standard_name', 'height', 'vertical'), - ('standard_name', 'atmosphere_sleve_coordinate', 'vertical'), - ('standard_name', 'height_above_mean_sea_level', 'vertical'), - ('standard_name', 'atmosphere_hybrid_height_coordinate', 'vertical'), - ('standard_name', 'atmosphere_ln_pressure_coordinate', 'vertical'), - ('standard_name', 'air_pressure', 'vertical'), - ('standard_name', 'projection_y_coordinate', 'y'), - ('standard_name', 'latitude', 'latitude'), - ('standard_name', 'projection_x_coordinate', 'x'), - ('standard_name', 'longitude', 'longitude'), - ('_CoordinateAxisType', 'Time', 'time'), - ('_CoordinateAxisType', 'Pressure', 'vertical'), - ('_CoordinateAxisType', 'GeoZ', 'vertical'), - ('_CoordinateAxisType', 'Height', 'vertical'), - ('_CoordinateAxisType', 'GeoY', 'y'), - ('_CoordinateAxisType', 'Lat', 'latitude'), - ('_CoordinateAxisType', 'GeoX', 'x'), - ('_CoordinateAxisType', 'Lon', 'longitude'), - ('axis', 'T', 'time'), - ('axis', 'Z', 'vertical'), - ('axis', 'Y', 'y'), - ('axis', 'X', 'x'), - ('positive', 'up', 'vertical'), - ('positive', 'down', 'vertical') + ("standard_name", "time", "time"), + ("standard_name", "model_level_number", "vertical"), + ("standard_name", "atmosphere_hybrid_sigma_pressure_coordinate", "vertical"), + ("standard_name", "geopotential_height", "vertical"), + ("standard_name", "height_above_geopotential_datum", "vertical"), + ("standard_name", "altitude", "vertical"), + ("standard_name", "atmosphere_sigma_coordinate", "vertical"), + ("standard_name", "height_above_reference_ellipsoid", "vertical"), + ("standard_name", "height", "vertical"), + ("standard_name", "atmosphere_sleve_coordinate", "vertical"), + ("standard_name", "height_above_mean_sea_level", "vertical"), + ("standard_name", "atmosphere_hybrid_height_coordinate", "vertical"), + ("standard_name", "atmosphere_ln_pressure_coordinate", "vertical"), + ("standard_name", "air_pressure", "vertical"), + ("standard_name", "projection_y_coordinate", "y"), + ("standard_name", "latitude", "latitude"), + ("standard_name", "projection_x_coordinate", "x"), + ("standard_name", "longitude", "longitude"), + ("_CoordinateAxisType", "Time", "time"), + ("_CoordinateAxisType", "Pressure", "vertical"), + ("_CoordinateAxisType", "GeoZ", "vertical"), + ("_CoordinateAxisType", "Height", "vertical"), + ("_CoordinateAxisType", "GeoY", "y"), + ("_CoordinateAxisType", "Lat", "latitude"), + ("_CoordinateAxisType", "GeoX", "x"), + ("_CoordinateAxisType", "Lon", "longitude"), + ("axis", "T", "time"), + ("axis", "Z", "vertical"), + ("axis", "Y", "y"), + ("axis", "X", "x"), + ("positive", "up", "vertical"), + ("positive", "down", "vertical"), ] -@pytest.mark.parametrize('test_tuple', criterion_matches) +@pytest.mark.parametrize("test_tuple", criterion_matches) def test_check_axis_criterion_match(test_ds_generic, test_tuple): """Test the variety of possibilities for check_axis in the criterion match.""" - test_ds_generic['e'].attrs[test_tuple[0]] = test_tuple[1] - assert check_axis(test_ds_generic['e'], test_tuple[2]) + test_ds_generic["e"].attrs[test_tuple[0]] = test_tuple[1] + assert check_axis(test_ds_generic["e"], test_tuple[2]) unit_matches = [ - ('Pa', 'vertical'), - ('hPa', 'vertical'), - ('mbar', 'vertical'), - ('degreeN', 'latitude'), - ('degreesN', 'latitude'), - ('degree_north', 'latitude'), - ('degree_N', 'latitude'), - ('degrees_north', 'latitude'), - ('degrees_N', 'latitude'), - ('degreeE', 'longitude'), - ('degrees_east', 'longitude'), - ('degree_east', 'longitude'), - ('degreesE', 'longitude'), - ('degree_E', 'longitude'), - ('degrees_E', 'longitude') + ("Pa", "vertical"), + ("hPa", "vertical"), + ("mbar", "vertical"), + ("degreeN", "latitude"), + ("degreesN", "latitude"), + ("degree_north", "latitude"), + ("degree_N", "latitude"), + ("degrees_north", "latitude"), + ("degrees_N", "latitude"), + ("degreeE", "longitude"), + ("degrees_east", "longitude"), + ("degree_east", "longitude"), + ("degreesE", "longitude"), + ("degree_E", "longitude"), + ("degrees_E", "longitude"), ] -@pytest.mark.parametrize('test_tuple', unit_matches) +@pytest.mark.parametrize("test_tuple", unit_matches) def test_check_axis_unit_match(test_ds_generic, test_tuple): """Test the variety of possibilities for check_axis in the unit match.""" - test_ds_generic['e'].attrs['units'] = test_tuple[0] - assert check_axis(test_ds_generic['e'], test_tuple[1]) + test_ds_generic["e"].attrs["units"] = test_tuple[0] + assert check_axis(test_ds_generic["e"], test_tuple[1]) regex_matches = [ - ('time', 'time'), - ('time1', 'time'), - ('time42', 'time'), - ('Time', 'time'), - ('TIME', 'time'), - ('bottom_top', 'vertical'), - ('sigma', 'vertical'), - ('HGHT', 'vertical'), - ('height', 'vertical'), - ('Altitude', 'vertical'), - ('depth', 'vertical'), - ('isobaric', 'vertical'), - ('isobaric1', 'vertical'), - ('isobaric42', 'vertical'), - ('lv_HTGL5', 'vertical'), - ('PRES', 'vertical'), - ('pressure', 'vertical'), - ('pressure_difference_layer', 'vertical'), - ('isothermal', 'vertical'), - ('y', 'y'), - ('Y', 'y'), - ('lat', 'latitude'), - ('latitude', 'latitude'), - ('Latitude', 'latitude'), - ('XLAT', 'latitude'), - ('x', 'x'), - ('X', 'x'), - ('lon', 'longitude'), - ('longitude', 'longitude'), - ('Longitude', 'longitude'), - ('XLONG', 'longitude') + ("time", "time"), + ("time1", "time"), + ("time42", "time"), + ("Time", "time"), + ("TIME", "time"), + ("bottom_top", "vertical"), + ("sigma", "vertical"), + ("HGHT", "vertical"), + ("height", "vertical"), + ("Altitude", "vertical"), + ("depth", "vertical"), + ("isobaric", "vertical"), + ("isobaric1", "vertical"), + ("isobaric42", "vertical"), + ("lv_HTGL5", "vertical"), + ("PRES", "vertical"), + ("pressure", "vertical"), + ("pressure_difference_layer", "vertical"), + ("isothermal", "vertical"), + ("y", "y"), + ("Y", "y"), + ("lat", "latitude"), + ("latitude", "latitude"), + ("Latitude", "latitude"), + ("XLAT", "latitude"), + ("x", "x"), + ("X", "x"), + ("lon", "longitude"), + ("longitude", "longitude"), + ("Longitude", "longitude"), + ("XLONG", "longitude"), ] -@pytest.mark.parametrize('test_tuple', regex_matches) +@pytest.mark.parametrize("test_tuple", regex_matches) def test_check_axis_regular_expression_match(test_ds_generic, test_tuple): """Test the variety of possibilities for check_axis in the regular expression match.""" - data = test_ds_generic.rename({'e': test_tuple[0]}) + data = test_ds_generic.rename({"e": test_tuple[0]}) assert check_axis(data[test_tuple[0]], test_tuple[1]) @@ -492,72 +519,73 @@ def test_narr_example_variable_without_grid_mapping(test_ds): # scaling based on whether that variable has the grid_mapping attribute. This would # otherwise double the coordinates's shapes since xarray tries to combine the coordinates # with different scaling from differing units. - assert test_ds['x'].shape == data['lon'].metpy.x.shape - assert test_ds['y'].shape == data['lon'].metpy.y.shape - assert data['lon'].metpy.x.identical(data['Temperature'].metpy.x) - assert data['lon'].metpy.y.identical(data['Temperature'].metpy.y) + assert test_ds["x"].shape == data["lon"].metpy.x.shape + assert test_ds["y"].shape == data["lon"].metpy.y.shape + assert data["lon"].metpy.x.identical(data["Temperature"].metpy.x) + assert data["lon"].metpy.y.identical(data["Temperature"].metpy.y) def test_coordinates_identical_true(test_ds_generic): """Test coordinates identical method when true.""" - assert test_ds_generic['test'].metpy.coordinates_identical(test_ds_generic['test']) + assert test_ds_generic["test"].metpy.coordinates_identical(test_ds_generic["test"]) def test_coordinates_identical_false_number_of_coords(test_ds_generic): """Test coordinates identical method when false due to number of coordinates.""" - other_ds = test_ds_generic.drop_vars('e') - assert not test_ds_generic['test'].metpy.coordinates_identical(other_ds['test']) + other_ds = test_ds_generic.drop_vars("e") + assert not test_ds_generic["test"].metpy.coordinates_identical(other_ds["test"]) def test_coordinates_identical_false_coords_mismatch(test_ds_generic): """Test coordinates identical method when false due to coordinates not matching.""" other_ds = test_ds_generic.copy() - other_ds['e'].attrs['units'] = 'meters' - assert not test_ds_generic['test'].metpy.coordinates_identical(other_ds['test']) + other_ds["e"].attrs["units"] = "meters" + assert not test_ds_generic["test"].metpy.coordinates_identical(other_ds["test"]) def test_check_matching_coordinates(test_ds_generic): """Test xarray coordinate checking decorator.""" - other = test_ds_generic['test'].rename({'a': 'time'}) + other = test_ds_generic["test"].rename({"a": "time"}) @check_matching_coordinates def add(a, b): return a + b - xr.testing.assert_identical(add(test_ds_generic['test'], test_ds_generic['test']), - test_ds_generic['test'] * 2) + xr.testing.assert_identical( + add(test_ds_generic["test"], test_ds_generic["test"]), test_ds_generic["test"] * 2 + ) with pytest.raises(ValueError): - add(test_ds_generic['test'], other) + add(test_ds_generic["test"], other) def test_time_deltas(): """Test the time_deltas attribute.""" - ds = xr.open_dataset(get_test_data('irma_gfs_example.nc', as_file_obj=False)) - time = ds['time1'] + ds = xr.open_dataset(get_test_data("irma_gfs_example.nc", as_file_obj=False)) + time = ds["time1"] truth = 3 * np.ones(8) * units.hr assert_array_almost_equal(time.metpy.time_deltas, truth) def test_find_axis_name_integer(test_var): """Test getting axis name using the axis number identifier.""" - assert test_var.metpy.find_axis_name(2) == 'y' + assert test_var.metpy.find_axis_name(2) == "y" def test_find_axis_name_axis_type(test_var): """Test getting axis name using the axis type identifier.""" - assert test_var.metpy.find_axis_name('vertical') == 'isobaric' + assert test_var.metpy.find_axis_name("vertical") == "isobaric" def test_find_axis_name_dim_coord_name(test_var): """Test getting axis name using the dimension coordinate name identifier.""" - assert test_var.metpy.find_axis_name('isobaric') == 'isobaric' + assert test_var.metpy.find_axis_name("isobaric") == "isobaric" def test_find_axis_name_bad_identifier(test_var): """Test getting axis name using the axis type identifier.""" with pytest.raises(ValueError) as exc: - test_var.metpy.find_axis_name('ens') - assert 'axis is not valid' in str(exc.value) + test_var.metpy.find_axis_name("ens") + assert "axis is not valid" in str(exc.value) def test_find_axis_number_integer(test_var): @@ -567,29 +595,29 @@ def test_find_axis_number_integer(test_var): def test_find_axis_number_axis_type(test_var): """Test getting axis number using the axis type identifier.""" - assert test_var.metpy.find_axis_number('vertical') == 1 + assert test_var.metpy.find_axis_number("vertical") == 1 def test_find_axis_number_dim_coord_number(test_var): """Test getting axis number using the dimension coordinate name identifier.""" - assert test_var.metpy.find_axis_number('isobaric') == 1 + assert test_var.metpy.find_axis_number("isobaric") == 1 def test_find_axis_number_bad_identifier(test_var): """Test getting axis number using the axis type identifier.""" with pytest.raises(ValueError) as exc: - test_var.metpy.find_axis_number('ens') - assert 'axis is not valid' in str(exc.value) + test_var.metpy.find_axis_number("ens") + assert "axis is not valid" in str(exc.value) def test_cf_parse_with_grid_mapping(test_var): """Test cf_parse dont delete grid_mapping attribute.""" - assert test_var.grid_mapping == 'Lambert_Conformal' + assert test_var.grid_mapping == "Lambert_Conformal" def test_data_array_loc_get_with_units(test_var): """Test the .loc indexer on the metpy accessor.""" - truth = test_var.loc[:, 850.] + truth = test_var.loc[:, 850.0] assert truth.identical(test_var.metpy.loc[:, 8.5e4 * units.Pa]) @@ -597,8 +625,8 @@ def test_data_array_loc_set_with_units(test_var): """Test the .loc indexer on the metpy accessor for setting.""" temperature = test_var.copy() temperature.metpy.loc[:, 8.5e4 * units.Pa] = np.nan - assert np.isnan(temperature.loc[:, 850.]).all() - assert not np.isnan(temperature.loc[:, 700.]).any() + assert np.isnan(temperature.loc[:, 850.0]).all() + assert not np.isnan(temperature.loc[:, 700.0]).any() def test_data_array_loc_with_ellipsis(test_var): @@ -610,7 +638,7 @@ def test_data_array_loc_with_ellipsis(test_var): def test_data_array_loc_non_tuple(test_var): """Test the .loc indexer with a non-tuple indexer.""" truth = test_var[-1] - assert truth.identical(test_var.metpy.loc['1987-04-04T18:00']) + assert truth.identical(test_var.metpy.loc["1987-04-04T18:00"]) def test_data_array_loc_too_many_indices(test_var): @@ -621,51 +649,57 @@ def test_data_array_loc_too_many_indices(test_var): def test_data_array_sel_dict_with_units(test_var): """Test .sel on the metpy accessor with dictionary.""" - truth = test_var.squeeze().loc[500.] - assert truth.identical(test_var.metpy.sel({'time': '1987-04-04T18:00:00', - 'isobaric': 5e4 * units.Pa})) + truth = test_var.squeeze().loc[500.0] + assert truth.identical( + test_var.metpy.sel({"time": "1987-04-04T18:00:00", "isobaric": 5e4 * units.Pa}) + ) def test_data_array_sel_kwargs_with_units(test_var): """Test .sel on the metpy accessor with kwargs and axis type.""" - truth = test_var.loc[:, 500.][..., 122] - selection = ( - test_var.metpy - .sel(vertical=5e4 * units.Pa, x=-16.569 * units.km, tolerance=1., method='nearest') - .metpy - .assign_coordinates(None) - ) + truth = test_var.loc[:, 500.0][..., 122] + selection = test_var.metpy.sel( + vertical=5e4 * units.Pa, x=-16.569 * units.km, tolerance=1.0, method="nearest" + ).metpy.assign_coordinates(None) assert truth.identical(selection) def test_dataset_loc_with_units(test_ds): """Test .loc on the metpy accessor for Datasets using slices.""" - truth = test_ds[{'isobaric': slice(6, 17)}] - assert truth.identical(test_ds.metpy.loc[{'isobaric': slice(8.5e4 * units.Pa, - 5e4 * units.Pa)}]) + truth = test_ds[{"isobaric": slice(6, 17)}] + assert truth.identical( + test_ds.metpy.loc[{"isobaric": slice(8.5e4 * units.Pa, 5e4 * units.Pa)}] + ) def test_dataset_sel_kwargs_with_units(test_ds): """Test .sel on the metpy accessor for Datasets with kwargs.""" - truth = test_ds[{'time': 0, 'y': 50, 'x': 122}] - assert truth.identical(test_ds.metpy.sel(time='1987-04-04T18:00:00', y=-1.464e6 * units.m, - x=-17. * units.km, tolerance=1., - method='nearest')) + truth = test_ds[{"time": 0, "y": 50, "x": 122}] + assert truth.identical( + test_ds.metpy.sel( + time="1987-04-04T18:00:00", + y=-1.464e6 * units.m, + x=-17.0 * units.km, + tolerance=1.0, + method="nearest", + ) + ) def test_dataset_sel_non_dict_pos_arg(test_ds): """Test that .sel errors when first positional argument is not a dict.""" with pytest.raises(ValueError) as exc: - test_ds.metpy.sel('1987-04-04T18:00:00') - assert 'must be a dictionary' in str(exc.value) + test_ds.metpy.sel("1987-04-04T18:00:00") + assert "must be a dictionary" in str(exc.value) def test_dataset_sel_mixed_dict_and_kwarg(test_ds): """Test that .sel errors when dict positional argument and kwargs are mixed.""" with pytest.raises(ValueError) as exc: - test_ds.metpy.sel({'isobaric': slice(8.5e4 * units.Pa, 5e4 * units.Pa)}, - time='1987-04-04T18:00:00') - assert 'cannot specify both keyword and positional arguments' in str(exc.value) + test_ds.metpy.sel( + {"isobaric": slice(8.5e4 * units.Pa, 5e4 * units.Pa)}, time="1987-04-04T18:00:00" + ) + assert "cannot specify both keyword and positional arguments" in str(exc.value) def test_dataset_loc_without_dict(test_ds): @@ -684,17 +718,17 @@ def test_dataset_parse_cf_keep_attrs(test_ds): def test_check_axis_with_bad_unit(test_ds_generic): """Test that check_axis does not raise an exception when provided a bad unit.""" - var = test_ds_generic['e'] - var.attrs['units'] = 'nondimensional' - assert not check_axis(var, 'x', 'y', 'vertical', 'time') + var = test_ds_generic["e"] + var.attrs["units"] = "nondimensional" + assert not check_axis(var, "x", "y", "vertical", "time") def test_dataset_parse_cf_varname_list(test_ds): """Test that .parse_cf() returns correct subset of dataset when given list of vars.""" full_ds = test_ds.copy().metpy.parse_cf() - partial_ds = test_ds.metpy.parse_cf(['u_wind', 'v_wind']) + partial_ds = test_ds.metpy.parse_cf(["u_wind", "v_wind"]) - assert full_ds[['u_wind', 'v_wind']].identical(partial_ds) + assert full_ds[["u_wind", "v_wind"]].identical(partial_ds) def test_coordinate_identification_shared_but_not_equal_coords(): @@ -703,59 +737,74 @@ def test_coordinate_identification_shared_but_not_equal_coords(): See GH Issue #1124. """ # Create minimal dataset - temperature = xr.DataArray([[8, 4], [13, 12]], name='temperature', - dims=('isobaric1', 'x'), - coords={ - 'isobaric1': xr.DataArray([700, 850], - name='isobaric1', - dims='isobaric1', - attrs={'units': 'hPa', - 'axis': 'Z'}), - 'x': xr.DataArray([0, 450], name='x', dims='x', - attrs={'units': 'km', 'axis': 'X'})}, - attrs={'units': 'degC'}) - u = xr.DataArray([[30, 20], [10, 10]], name='u', dims=('isobaric2', 'x'), - coords={ - 'isobaric2': xr.DataArray([500, 850], name='isobaric2', - dims='isobaric2', - attrs={'units': 'hPa', 'axis': 'Z'}), - 'x': xr.DataArray([0, 450], name='x', dims='x', - attrs={'units': 'km', 'axis': 'X'})}, - attrs={'units': 'kts'}) - ds = xr.Dataset({'temperature': temperature, 'u': u}) + temperature = xr.DataArray( + [[8, 4], [13, 12]], + name="temperature", + dims=("isobaric1", "x"), + coords={ + "isobaric1": xr.DataArray( + [700, 850], + name="isobaric1", + dims="isobaric1", + attrs={"units": "hPa", "axis": "Z"}, + ), + "x": xr.DataArray( + [0, 450], name="x", dims="x", attrs={"units": "km", "axis": "X"} + ), + }, + attrs={"units": "degC"}, + ) + u = xr.DataArray( + [[30, 20], [10, 10]], + name="u", + dims=("isobaric2", "x"), + coords={ + "isobaric2": xr.DataArray( + [500, 850], + name="isobaric2", + dims="isobaric2", + attrs={"units": "hPa", "axis": "Z"}, + ), + "x": xr.DataArray( + [0, 450], name="x", dims="x", attrs={"units": "km", "axis": "X"} + ), + }, + attrs={"units": "kts"}, + ) + ds = xr.Dataset({"temperature": temperature, "u": u}) # Check coordinates on temperature - assert ds['isobaric1'].identical(ds['temperature'].metpy.vertical) - assert ds['x'].identical(ds['temperature'].metpy.x) + assert ds["isobaric1"].identical(ds["temperature"].metpy.vertical) + assert ds["x"].identical(ds["temperature"].metpy.x) # Check vertical coordinate on u # Fails prior to resolution of Issue #1124 - assert ds['isobaric2'].identical(ds['u'].metpy.vertical) + assert ds["isobaric2"].identical(ds["u"].metpy.vertical) def test_one_dimensional_lat_lon(test_ds_generic): """Test that 1D lat/lon coords are recognized as both x/y and longitude/latitude.""" - test_ds_generic['d'].attrs['units'] = 'degrees_north' - test_ds_generic['e'].attrs['units'] = 'degrees_east' - var = test_ds_generic.metpy.parse_cf('test') - assert var['d'].identical(var.metpy.y) - assert var['d'].identical(var.metpy.latitude) - assert var['e'].identical(var.metpy.x) - assert var['e'].identical(var.metpy.longitude) + test_ds_generic["d"].attrs["units"] = "degrees_north" + test_ds_generic["e"].attrs["units"] = "degrees_east" + var = test_ds_generic.metpy.parse_cf("test") + assert var["d"].identical(var.metpy.y) + assert var["d"].identical(var.metpy.latitude) + assert var["e"].identical(var.metpy.x) + assert var["e"].identical(var.metpy.longitude) def test_auxilary_lat_lon_with_xy(test_var_multidim_full): """Test that auxiliary lat/lon coord identification works with other x/y coords present.""" - assert test_var_multidim_full['y'].identical(test_var_multidim_full.metpy.y) - assert test_var_multidim_full['lat'].identical(test_var_multidim_full.metpy.latitude) - assert test_var_multidim_full['x'].identical(test_var_multidim_full.metpy.x) - assert test_var_multidim_full['lon'].identical(test_var_multidim_full.metpy.longitude) + assert test_var_multidim_full["y"].identical(test_var_multidim_full.metpy.y) + assert test_var_multidim_full["lat"].identical(test_var_multidim_full.metpy.latitude) + assert test_var_multidim_full["x"].identical(test_var_multidim_full.metpy.x) + assert test_var_multidim_full["lon"].identical(test_var_multidim_full.metpy.longitude) def test_auxilary_lat_lon_without_xy(test_var_multidim_no_xy): """Test that multidimensional lat/lon are recognized in absence of x/y coords.""" - assert test_var_multidim_no_xy['lat'].identical(test_var_multidim_no_xy.metpy.latitude) - assert test_var_multidim_no_xy['lon'].identical(test_var_multidim_no_xy.metpy.longitude) + assert test_var_multidim_no_xy["lat"].identical(test_var_multidim_no_xy.metpy.latitude) + assert test_var_multidim_no_xy["lon"].identical(test_var_multidim_no_xy.metpy.longitude) def test_auxilary_lat_lon_without_xy_as_xy(test_var_multidim_no_xy): @@ -769,86 +818,90 @@ def test_auxilary_lat_lon_without_xy_as_xy(test_var_multidim_no_xy): # Declare a sample projection with CF attributes sample_cf_attrs = { - 'grid_mapping_name': 'lambert_conformal_conic', - 'earth_radius': 6370000, - 'standard_parallel': [30., 40.], - 'longitude_of_central_meridian': 260., - 'latitude_of_projection_origin': 35. + "grid_mapping_name": "lambert_conformal_conic", + "earth_radius": 6370000, + "standard_parallel": [30.0, 40.0], + "longitude_of_central_meridian": 260.0, + "latitude_of_projection_origin": 35.0, } def test_assign_crs_dataarray_by_argument(test_ds_generic, ccrs): """Test assigning CRS to DataArray by projection dict.""" - da = test_ds_generic['test'] + da = test_ds_generic["test"] new_da = da.metpy.assign_crs(sample_cf_attrs) assert isinstance(new_da.metpy.cartopy_crs, ccrs.LambertConformal) - assert new_da['crs'] == CFProjection(sample_cf_attrs) + assert new_da["crs"] == CFProjection(sample_cf_attrs) def test_assign_crs_dataarray_by_kwargs(test_ds_generic, ccrs): """Test assigning CRS to DataArray by projection kwargs.""" - da = test_ds_generic['test'] + da = test_ds_generic["test"] new_da = da.metpy.assign_crs(**sample_cf_attrs) assert isinstance(new_da.metpy.cartopy_crs, ccrs.LambertConformal) - assert new_da['crs'] == CFProjection(sample_cf_attrs) + assert new_da["crs"] == CFProjection(sample_cf_attrs) def test_assign_crs_dataset_by_argument(test_ds_generic, ccrs): """Test assigning CRS to Dataset by projection dict.""" new_ds = test_ds_generic.metpy.assign_crs(sample_cf_attrs) - assert isinstance(new_ds['test'].metpy.cartopy_crs, ccrs.LambertConformal) - assert new_ds['crs'] == CFProjection(sample_cf_attrs) + assert isinstance(new_ds["test"].metpy.cartopy_crs, ccrs.LambertConformal) + assert new_ds["crs"] == CFProjection(sample_cf_attrs) def test_assign_crs_dataset_by_kwargs(test_ds_generic, ccrs): """Test assigning CRS to Dataset by projection kwargs.""" new_ds = test_ds_generic.metpy.assign_crs(**sample_cf_attrs) - assert isinstance(new_ds['test'].metpy.cartopy_crs, ccrs.LambertConformal) - assert new_ds['crs'] == CFProjection(sample_cf_attrs) + assert isinstance(new_ds["test"].metpy.cartopy_crs, ccrs.LambertConformal) + assert new_ds["crs"] == CFProjection(sample_cf_attrs) def test_assign_crs_error_with_both_attrs(test_ds_generic): """Test ValueError is raised when both dictionary and kwargs given.""" with pytest.raises(ValueError) as exc: test_ds_generic.metpy.assign_crs(sample_cf_attrs, **sample_cf_attrs) - assert 'Cannot specify both' in str(exc) + assert "Cannot specify both" in str(exc) def test_assign_crs_error_with_neither_attrs(test_ds_generic): """Test ValueError is raised when neither dictionary and kwargs given.""" with pytest.raises(ValueError) as exc: test_ds_generic.metpy.assign_crs() - assert 'Must specify either' in str(exc) + assert "Must specify either" in str(exc) def test_assign_latitude_longitude_no_horizontal(test_ds_generic): """Test that assign_latitude_longitude only warns when no horizontal coordinates.""" with pytest.warns(UserWarning): - xr.testing.assert_identical(test_ds_generic, - test_ds_generic.metpy.assign_latitude_longitude()) + xr.testing.assert_identical( + test_ds_generic, test_ds_generic.metpy.assign_latitude_longitude() + ) def test_assign_y_x_no_horizontal(test_ds_generic): """Test that assign_y_x only warns when no horizontal coordinates.""" with pytest.warns(UserWarning): - xr.testing.assert_identical(test_ds_generic, - test_ds_generic.metpy.assign_y_x()) + xr.testing.assert_identical(test_ds_generic, test_ds_generic.metpy.assign_y_x()) @pytest.fixture def test_coord_helper_da_yx(): """Provide a DataArray with y/x coords for coord helpers.""" - return xr.DataArray(np.arange(9).reshape((3, 3)), - dims=('y', 'x'), - coords={'y': np.linspace(0, 1e5, 3), - 'x': np.linspace(-1e5, 0, 3), - 'crs': CFProjection(sample_cf_attrs)}) + return xr.DataArray( + np.arange(9).reshape((3, 3)), + dims=("y", "x"), + coords={ + "y": np.linspace(0, 1e5, 3), + "x": np.linspace(-1e5, 0, 3), + "crs": CFProjection(sample_cf_attrs), + }, + ) @pytest.fixture def test_coord_helper_da_dummy_latlon(test_coord_helper_da_yx): """Provide DataArray with bad dummy lat/lon coords to be overwritten.""" - return test_coord_helper_da_yx.assign_coords(latitude=0., longitude=0.) + return test_coord_helper_da_yx.assign_coords(latitude=0.0, longitude=0.0) @pytest.fixture @@ -856,26 +909,30 @@ def test_coord_helper_da_latlon(): """Provide a DataArray with lat/lon coords for coord helpers.""" return xr.DataArray( np.arange(9).reshape((3, 3)), - dims=('y', 'x'), + dims=("y", "x"), coords={ - 'latitude': xr.DataArray( + "latitude": xr.DataArray( np.array( - [[34.99501239, 34.99875307, 35.], - [35.44643155, 35.45019292, 35.45144675], - [35.89782579, 35.90160784, 35.90286857]] + [ + [34.99501239, 34.99875307, 35.0], + [35.44643155, 35.45019292, 35.45144675], + [35.89782579, 35.90160784, 35.90286857], + ] ), - dims=('y', 'x') + dims=("y", "x"), ), - 'longitude': xr.DataArray( + "longitude": xr.DataArray( np.array( - [[-101.10219213, -100.55111288, -100.], - [-101.10831414, -100.55417417, -100.], - [-101.11450453, -100.55726965, -100.]] + [ + [-101.10219213, -100.55111288, -100.0], + [-101.10831414, -100.55417417, -100.0], + [-101.11450453, -100.55726965, -100.0], + ] ), - dims=('y', 'x') + dims=("y", "x"), ), - 'crs': CFProjection(sample_cf_attrs) - } + "crs": CFProjection(sample_cf_attrs), + }, ) @@ -886,296 +943,297 @@ def test_coord_helper_da_dummy_yx(test_coord_helper_da_latlon): @needs_cartopy -def test_assign_latitude_longitude_basic_dataarray(test_coord_helper_da_yx, - test_coord_helper_da_latlon): +def test_assign_latitude_longitude_basic_dataarray( + test_coord_helper_da_yx, test_coord_helper_da_latlon +): """Test assign_latitude_longitude in basic usage on DataArray.""" new_da = test_coord_helper_da_yx.metpy.assign_latitude_longitude() - lat, lon = new_da.metpy.coordinates('latitude', 'longitude') - np.testing.assert_array_almost_equal(test_coord_helper_da_latlon['latitude'].values, - lat.values, 3) - np.testing.assert_array_almost_equal(test_coord_helper_da_latlon['longitude'].values, - lon.values, 3) + lat, lon = new_da.metpy.coordinates("latitude", "longitude") + np.testing.assert_array_almost_equal( + test_coord_helper_da_latlon["latitude"].values, lat.values, 3 + ) + np.testing.assert_array_almost_equal( + test_coord_helper_da_latlon["longitude"].values, lon.values, 3 + ) -def test_assign_latitude_longitude_error_existing_dataarray( - test_coord_helper_da_dummy_latlon): +def test_assign_latitude_longitude_error_existing_dataarray(test_coord_helper_da_dummy_latlon): """Test assign_latitude_longitude failure with existing coordinates.""" with pytest.raises(RuntimeError) as exc: test_coord_helper_da_dummy_latlon.metpy.assign_latitude_longitude() - assert 'Latitude/longitude coordinate(s) are present' in str(exc) + assert "Latitude/longitude coordinate(s) are present" in str(exc) @needs_cartopy def test_assign_latitude_longitude_force_existing_dataarray( - test_coord_helper_da_dummy_latlon, test_coord_helper_da_latlon): + test_coord_helper_da_dummy_latlon, test_coord_helper_da_latlon +): """Test assign_latitude_longitude with existing coordinates forcing new.""" new_da = test_coord_helper_da_dummy_latlon.metpy.assign_latitude_longitude(True) - lat, lon = new_da.metpy.coordinates('latitude', 'longitude') - np.testing.assert_array_almost_equal(test_coord_helper_da_latlon['latitude'].values, - lat.values, 3) - np.testing.assert_array_almost_equal(test_coord_helper_da_latlon['longitude'].values, - lon.values, 3) + lat, lon = new_da.metpy.coordinates("latitude", "longitude") + np.testing.assert_array_almost_equal( + test_coord_helper_da_latlon["latitude"].values, lat.values, 3 + ) + np.testing.assert_array_almost_equal( + test_coord_helper_da_latlon["longitude"].values, lon.values, 3 + ) @needs_cartopy -def test_assign_latitude_longitude_basic_dataset(test_coord_helper_da_yx, - test_coord_helper_da_latlon): +def test_assign_latitude_longitude_basic_dataset( + test_coord_helper_da_yx, test_coord_helper_da_latlon +): """Test assign_latitude_longitude in basic usage on Dataset.""" - ds = test_coord_helper_da_yx.to_dataset(name='test').metpy.assign_latitude_longitude() - lat, lon = ds['test'].metpy.coordinates('latitude', 'longitude') - np.testing.assert_array_almost_equal(test_coord_helper_da_latlon['latitude'].values, - lat.values, 3) - np.testing.assert_array_almost_equal(test_coord_helper_da_latlon['longitude'].values, - lon.values, 3) + ds = test_coord_helper_da_yx.to_dataset(name="test").metpy.assign_latitude_longitude() + lat, lon = ds["test"].metpy.coordinates("latitude", "longitude") + np.testing.assert_array_almost_equal( + test_coord_helper_da_latlon["latitude"].values, lat.values, 3 + ) + np.testing.assert_array_almost_equal( + test_coord_helper_da_latlon["longitude"].values, lon.values, 3 + ) @needs_cartopy def test_assign_y_x_basic_dataarray(test_coord_helper_da_yx, test_coord_helper_da_latlon): """Test assign_y_x in basic usage on DataArray.""" new_da = test_coord_helper_da_latlon.metpy.assign_y_x() - y, x = new_da.metpy.coordinates('y', 'x') - np.testing.assert_array_almost_equal(test_coord_helper_da_yx['y'].values, y.values, 3) - np.testing.assert_array_almost_equal(test_coord_helper_da_yx['x'].values, x.values, 3) + y, x = new_da.metpy.coordinates("y", "x") + np.testing.assert_array_almost_equal(test_coord_helper_da_yx["y"].values, y.values, 3) + np.testing.assert_array_almost_equal(test_coord_helper_da_yx["x"].values, x.values, 3) -def test_assign_y_x_error_existing_dataarray( - test_coord_helper_da_dummy_yx): +def test_assign_y_x_error_existing_dataarray(test_coord_helper_da_dummy_yx): """Test assign_y_x failure with existing coordinates.""" with pytest.raises(RuntimeError) as exc: test_coord_helper_da_dummy_yx.metpy.assign_y_x() - assert 'y/x coordinate(s) are present' in str(exc) + assert "y/x coordinate(s) are present" in str(exc) @needs_cartopy def test_assign_y_x_force_existing_dataarray( - test_coord_helper_da_dummy_yx, test_coord_helper_da_yx): + test_coord_helper_da_dummy_yx, test_coord_helper_da_yx +): """Test assign_y_x with existing coordinates forcing new.""" new_da = test_coord_helper_da_dummy_yx.metpy.assign_y_x(True) - y, x = new_da.metpy.coordinates('y', 'x') - np.testing.assert_array_almost_equal(test_coord_helper_da_yx['y'].values, y.values, 3) - np.testing.assert_array_almost_equal(test_coord_helper_da_yx['x'].values, x.values, 3) + y, x = new_da.metpy.coordinates("y", "x") + np.testing.assert_array_almost_equal(test_coord_helper_da_yx["y"].values, y.values, 3) + np.testing.assert_array_almost_equal(test_coord_helper_da_yx["x"].values, x.values, 3) @needs_cartopy def test_assign_y_x_dataarray_outside_tolerance(test_coord_helper_da_latlon): """Test assign_y_x raises ValueError when tolerance is exceeded on DataArray.""" with pytest.raises(ValueError) as exc: - test_coord_helper_da_latlon.metpy.assign_y_x(tolerance=1 * units('um')) - assert 'cannot be collapsed to 1D within tolerance' in str(exc) + test_coord_helper_da_latlon.metpy.assign_y_x(tolerance=1 * units("um")) + assert "cannot be collapsed to 1D within tolerance" in str(exc) @needs_cartopy def test_assign_y_x_dataarray_transposed(test_coord_helper_da_yx, test_coord_helper_da_latlon): """Test assign_y_x on DataArray with transposed order.""" new_da = test_coord_helper_da_latlon.transpose(transpose_coords=True).metpy.assign_y_x() - y, x = new_da.metpy.coordinates('y', 'x') - np.testing.assert_array_almost_equal(test_coord_helper_da_yx['y'].values, y.values, 3) - np.testing.assert_array_almost_equal(test_coord_helper_da_yx['x'].values, x.values, 3) + y, x = new_da.metpy.coordinates("y", "x") + np.testing.assert_array_almost_equal(test_coord_helper_da_yx["y"].values, y.values, 3) + np.testing.assert_array_almost_equal(test_coord_helper_da_yx["x"].values, x.values, 3) @needs_cartopy -def test_assign_y_x_dataset_assumed_order(test_coord_helper_da_yx, - test_coord_helper_da_latlon): +def test_assign_y_x_dataset_assumed_order( + test_coord_helper_da_yx, test_coord_helper_da_latlon +): """Test assign_y_x on Dataset where order must be assumed.""" with pytest.warns(UserWarning): - new_ds = test_coord_helper_da_latlon.to_dataset(name='test').rename_dims( - {'y': 'b', 'x': 'a'}).metpy.assign_y_x() - y, x = new_ds['test'].metpy.coordinates('y', 'x') - np.testing.assert_array_almost_equal(test_coord_helper_da_yx['y'].values, y.values, 3) - np.testing.assert_array_almost_equal(test_coord_helper_da_yx['x'].values, x.values, 3) - assert y.name == 'b' - assert x.name == 'a' + new_ds = ( + test_coord_helper_da_latlon.to_dataset(name="test") + .rename_dims({"y": "b", "x": "a"}) + .metpy.assign_y_x() + ) + y, x = new_ds["test"].metpy.coordinates("y", "x") + np.testing.assert_array_almost_equal(test_coord_helper_da_yx["y"].values, y.values, 3) + np.testing.assert_array_almost_equal(test_coord_helper_da_yx["x"].values, x.values, 3) + assert y.name == "b" + assert x.name == "a" -def test_assign_y_x_error_existing_dataset( - test_coord_helper_da_dummy_yx): +def test_assign_y_x_error_existing_dataset(test_coord_helper_da_dummy_yx): """Test assign_y_x failure with existing coordinates for Dataset.""" with pytest.raises(RuntimeError) as exc: - test_coord_helper_da_dummy_yx.to_dataset(name='test').metpy.assign_y_x() - assert 'y/x coordinate(s) are present' in str(exc) + test_coord_helper_da_dummy_yx.to_dataset(name="test").metpy.assign_y_x() + assert "y/x coordinate(s) are present" in str(exc) def test_update_attribute_dictionary(test_ds_generic): """Test update_attribute using dictionary.""" - descriptions = { - 'test': 'Filler data', - 'c': 'The third coordinate' - } - result = test_ds_generic.metpy.update_attribute('description', descriptions) + descriptions = {"test": "Filler data", "c": "The third coordinate"} + result = test_ds_generic.metpy.update_attribute("description", descriptions) # Test attribute updates - assert 'description' not in result['a'].attrs - assert 'description' not in result['b'].attrs - assert result['c'].attrs['description'] == 'The third coordinate' - assert 'description' not in result['d'].attrs - assert 'description' not in result['e'].attrs - assert result['test'].attrs['description'] == 'Filler data' + assert "description" not in result["a"].attrs + assert "description" not in result["b"].attrs + assert result["c"].attrs["description"] == "The third coordinate" + assert "description" not in result["d"].attrs + assert "description" not in result["e"].attrs + assert result["test"].attrs["description"] == "Filler data" # Test for no side effects - assert 'description' not in test_ds_generic['c'].attrs - assert 'description' not in test_ds_generic['test'].attrs + assert "description" not in test_ds_generic["c"].attrs + assert "description" not in test_ds_generic["test"].attrs def test_update_attribute_callable(test_ds_generic): """Test update_attribute using callable.""" + def even_ascii(varname, **kwargs): if ord(varname[0]) % 2 == 0: - return 'yes' - result = test_ds_generic.metpy.update_attribute('even', even_ascii) + return "yes" + + result = test_ds_generic.metpy.update_attribute("even", even_ascii) # Test attribute updates - assert 'even' not in result['a'].attrs - assert result['b'].attrs['even'] == 'yes' - assert 'even' not in result['c'].attrs - assert result['d'].attrs['even'] == 'yes' - assert 'even' not in result['e'].attrs - assert result['test'].attrs['even'] == 'yes' + assert "even" not in result["a"].attrs + assert result["b"].attrs["even"] == "yes" + assert "even" not in result["c"].attrs + assert result["d"].attrs["even"] == "yes" + assert "even" not in result["e"].attrs + assert result["test"].attrs["even"] == "yes" # Test for no side effects - assert 'even' not in test_ds_generic['b'].attrs - assert 'even' not in test_ds_generic['d'].attrs - assert 'even' not in test_ds_generic['test'].attrs - test_ds_generic.metpy.update_attribute('even', even_ascii) - - -@pytest.mark.parametrize('test, other, match_unit, expected', [ - (np.arange(4), np.arange(4), False, np.arange(4)), - (np.arange(4), np.arange(4), True, np.arange(4) * units('dimensionless')), - (np.arange(4), [0] * units.m, False, np.arange(4) * units('dimensionless')), - (np.arange(4), [0] * units.m, True, np.arange(4) * units.m), - ( - np.arange(4), - xr.DataArray( - np.zeros(4) * units.meter, - dims=('x',), - coords={'x': np.linspace(0, 1, 4)}, - attrs={'description': 'Just some zeros'} - ), - False, - xr.DataArray( - np.arange(4) * units.dimensionless, - dims=('x',), - coords={'x': np.linspace(0, 1, 4)} - ) - ), - ( - np.arange(4), - xr.DataArray( - np.zeros(4) * units.meter, - dims=('x',), - coords={'x': np.linspace(0, 1, 4)}, - attrs={'description': 'Just some zeros'} - ), - True, - xr.DataArray( - np.arange(4) * units.meter, - dims=('x',), - coords={'x': np.linspace(0, 1, 4)} - ) - ), - ([2, 4, 8] * units.kg, [0] * units.m, False, [2, 4, 8] * units.kg), - ([2, 4, 8] * units.kg, [0] * units.g, True, [2000, 4000, 8000] * units.g), - ( - [2, 4, 8] * units.kg, - xr.DataArray( - np.zeros(3) * units.meter, - dims=('x',), - coords={'x': np.linspace(0, 1, 3)} + assert "even" not in test_ds_generic["b"].attrs + assert "even" not in test_ds_generic["d"].attrs + assert "even" not in test_ds_generic["test"].attrs + test_ds_generic.metpy.update_attribute("even", even_ascii) + + +@pytest.mark.parametrize( + "test, other, match_unit, expected", + [ + (np.arange(4), np.arange(4), False, np.arange(4)), + (np.arange(4), np.arange(4), True, np.arange(4) * units("dimensionless")), + (np.arange(4), [0] * units.m, False, np.arange(4) * units("dimensionless")), + (np.arange(4), [0] * units.m, True, np.arange(4) * units.m), + ( + np.arange(4), + xr.DataArray( + np.zeros(4) * units.meter, + dims=("x",), + coords={"x": np.linspace(0, 1, 4)}, + attrs={"description": "Just some zeros"}, + ), + False, + xr.DataArray( + np.arange(4) * units.dimensionless, + dims=("x",), + coords={"x": np.linspace(0, 1, 4)}, + ), ), - False, - xr.DataArray( - [2, 4, 8] * units.kilogram, - dims=('x',), - coords={'x': np.linspace(0, 1, 3)} - ) - ), - ( - [2, 4, 8] * units.kg, - xr.DataArray( - np.zeros(3) * units.gram, - dims=('x',), - coords={'x': np.linspace(0, 1, 3)} + ( + np.arange(4), + xr.DataArray( + np.zeros(4) * units.meter, + dims=("x",), + coords={"x": np.linspace(0, 1, 4)}, + attrs={"description": "Just some zeros"}, + ), + True, + xr.DataArray( + np.arange(4) * units.meter, dims=("x",), coords={"x": np.linspace(0, 1, 4)} + ), ), - True, - xr.DataArray( - [2000, 4000, 8000] * units.gram, - dims=('x',), - coords={'x': np.linspace(0, 1, 3)} - ) - ), - ( - xr.DataArray( - np.linspace(0, 1, 5) * units.meter, - attrs={'description': 'A range of values'} + ([2, 4, 8] * units.kg, [0] * units.m, False, [2, 4, 8] * units.kg), + ([2, 4, 8] * units.kg, [0] * units.g, True, [2000, 4000, 8000] * units.g), + ( + [2, 4, 8] * units.kg, + xr.DataArray( + np.zeros(3) * units.meter, dims=("x",), coords={"x": np.linspace(0, 1, 3)} + ), + False, + xr.DataArray( + [2, 4, 8] * units.kilogram, dims=("x",), coords={"x": np.linspace(0, 1, 3)} + ), ), - np.arange(4, dtype=np.float64), - False, - units.Quantity(np.linspace(0, 1, 5), 'meter') - ), - ( - xr.DataArray( - np.linspace(0, 1, 5) * units.meter, - attrs={'description': 'A range of values'} + ( + [2, 4, 8] * units.kg, + xr.DataArray( + np.zeros(3) * units.gram, dims=("x",), coords={"x": np.linspace(0, 1, 3)} + ), + True, + xr.DataArray( + [2000, 4000, 8000] * units.gram, + dims=("x",), + coords={"x": np.linspace(0, 1, 3)}, + ), ), - [0] * units.kg, - False, - np.linspace(0, 1, 5) * units.m - ), - ( - xr.DataArray( - np.linspace(0, 1, 5) * units.meter, - attrs={'description': 'A range of values'} + ( + xr.DataArray( + np.linspace(0, 1, 5) * units.meter, attrs={"description": "A range of values"} + ), + np.arange(4, dtype=np.float64), + False, + units.Quantity(np.linspace(0, 1, 5), "meter"), ), - [0] * units.cm, - True, - np.linspace(0, 100, 5) * units.cm - ), - ( - xr.DataArray( - np.linspace(0, 1, 5) * units.meter, - attrs={'description': 'A range of values'} + ( + xr.DataArray( + np.linspace(0, 1, 5) * units.meter, attrs={"description": "A range of values"} + ), + [0] * units.kg, + False, + np.linspace(0, 1, 5) * units.m, ), - xr.DataArray( - np.zeros(5) * units.kilogram, - dims=('x',), - coords={'x': np.linspace(0, 1, 5)}, - attrs={'description': 'Alternative data'} + ( + xr.DataArray( + np.linspace(0, 1, 5) * units.meter, attrs={"description": "A range of values"} + ), + [0] * units.cm, + True, + np.linspace(0, 100, 5) * units.cm, ), - False, - xr.DataArray( - np.linspace(0, 1, 5) * units.meter, - dims=('x',), - coords={'x': np.linspace(0, 1, 5)} - ) - ), - ( - xr.DataArray( - np.linspace(0, 1, 5) * units.meter, - attrs={'description': 'A range of values'} + ( + xr.DataArray( + np.linspace(0, 1, 5) * units.meter, attrs={"description": "A range of values"} + ), + xr.DataArray( + np.zeros(5) * units.kilogram, + dims=("x",), + coords={"x": np.linspace(0, 1, 5)}, + attrs={"description": "Alternative data"}, + ), + False, + xr.DataArray( + np.linspace(0, 1, 5) * units.meter, + dims=("x",), + coords={"x": np.linspace(0, 1, 5)}, + ), ), - xr.DataArray( - np.zeros(5) * units.centimeter, - dims=('x',), - coords={'x': np.linspace(0, 1, 5)}, - attrs={'description': 'Alternative data'} + ( + xr.DataArray( + np.linspace(0, 1, 5) * units.meter, attrs={"description": "A range of values"} + ), + xr.DataArray( + np.zeros(5) * units.centimeter, + dims=("x",), + coords={"x": np.linspace(0, 1, 5)}, + attrs={"description": "Alternative data"}, + ), + True, + xr.DataArray( + np.linspace(0, 100, 5) * units.centimeter, + dims=("x",), + coords={"x": np.linspace(0, 1, 5)}, + ), ), - True, - xr.DataArray( - np.linspace(0, 100, 5) * units.centimeter, - dims=('x',), - coords={'x': np.linspace(0, 1, 5)} - ) - ), -]) + ], +) def test_wrap_with_wrap_like_kwarg(test, other, match_unit, expected): """Test the preprocess and wrap decorator when using wrap_like.""" + @preprocess_and_wrap(wrap_like=other, match_unit=match_unit) def almost_identity(arg): return arg result = almost_identity(test) - if hasattr(expected, 'units'): + if hasattr(expected, "units"): assert expected.units == result.units if isinstance(expected, xr.DataArray): xr.testing.assert_identical(result, expected) @@ -1183,42 +1241,34 @@ def almost_identity(arg): assert_array_equal(result, expected) -@pytest.mark.parametrize('test, other', [ - ([2, 4, 8] * units.kg, [0] * units.m), - ( - [2, 4, 8] * units.kg, - xr.DataArray( - np.zeros(3) * units.meter, - dims=('x',), - coords={'x': np.linspace(0, 1, 3)} - ) - ), - ( - xr.DataArray( - np.linspace(0, 1, 5) * units.meter +@pytest.mark.parametrize( + "test, other", + [ + ([2, 4, 8] * units.kg, [0] * units.m), + ( + [2, 4, 8] * units.kg, + xr.DataArray( + np.zeros(3) * units.meter, dims=("x",), coords={"x": np.linspace(0, 1, 3)} + ), ), - [0] * units.kg - ), - ( - xr.DataArray( - np.linspace(0, 1, 5) * units.meter + (xr.DataArray(np.linspace(0, 1, 5) * units.meter), [0] * units.kg), + ( + xr.DataArray(np.linspace(0, 1, 5) * units.meter), + xr.DataArray( + np.zeros(5) * units.kg, dims=("x",), coords={"x": np.linspace(0, 1, 5)} + ), ), - xr.DataArray( - np.zeros(5) * units.kg, - dims=('x',), - coords={'x': np.linspace(0, 1, 5)} - ) - ), - ( - xr.DataArray( - np.linspace(0, 1, 5) * units.meter, - attrs={'description': 'A range of values'} + ( + xr.DataArray( + np.linspace(0, 1, 5) * units.meter, attrs={"description": "A range of values"} + ), + np.arange(4, dtype=np.float64), ), - np.arange(4, dtype=np.float64) - ) -]) + ], +) def test_wrap_with_wrap_like_kwarg_raising_dimensionality_error(test, other): """Test the preprocess and wrap decorator when a dimensionality error is raised.""" + @preprocess_and_wrap(wrap_like=other, match_unit=True) def almost_identity(arg): return arg @@ -1229,7 +1279,8 @@ def almost_identity(arg): def test_wrap_with_argument_kwarg(): """Test the preprocess and wrap decorator with signature recognition.""" - @preprocess_and_wrap(wrap_like='a') + + @preprocess_and_wrap(wrap_like="a") def double(a): return units.Quantity(2) * a @@ -1242,15 +1293,15 @@ def double(a): def test_preprocess_and_wrap_with_broadcasting(): """Test preprocessing and wrapping decorator with arguments to broadcast specified.""" # Not quantified - data = xr.DataArray(np.arange(9).reshape((3, 3)), dims=('y', 'x'), attrs={'units': 'N'}) + data = xr.DataArray(np.arange(9).reshape((3, 3)), dims=("y", "x"), attrs={"units": "N"}) # Quantified - data2 = xr.DataArray([1, 0, 0] * units.m, dims=('y')) + data2 = xr.DataArray([1, 0, 0] * units.m, dims=("y")) - @preprocess_and_wrap(broadcast=('a', 'b')) + @preprocess_and_wrap(broadcast=("a", "b")) def func(a, b): return a * b - assert_array_equal(func(data, data2), [[0, 1, 2], [0, 0, 0], [0, 0, 0]] * units('N m')) + assert_array_equal(func(data, data2), [[0, 1, 2], [0, 0, 0], [0, 0, 0]] * units("N m")) def test_preprocess_and_wrap_with_to_magnitude(): @@ -1258,7 +1309,7 @@ def test_preprocess_and_wrap_with_to_magnitude(): data = xr.DataArray([1, 0, 1] * units.m) data2 = [0, 1, 1] * units.cm - @preprocess_and_wrap(wrap_like='a', to_magnitude=True) + @preprocess_and_wrap(wrap_like="a", to_magnitude=True) def func(a, b): return a * b @@ -1267,21 +1318,21 @@ def func(a, b): def test_preprocess_and_wrap_with_variable(): """Test preprocess and wrapping decorator when using an xarray Variable.""" - data1 = xr.DataArray([1, 0, 1], dims=('x',), attrs={'units': 'meter'}) - data2 = xr.Variable(data=[0, 1, 1], dims=('x',), attrs={'units': 'meter'}) + data1 = xr.DataArray([1, 0, 1], dims=("x",), attrs={"units": "meter"}) + data2 = xr.Variable(data=[0, 1, 1], dims=("x",), attrs={"units": "meter"}) - @preprocess_and_wrap(wrap_like='a') + @preprocess_and_wrap(wrap_like="a") def func(a, b): return a * b # Note, expected units are meter, not meter**2, since attributes are stripped from # Variables - expected_12 = xr.DataArray([0, 0, 1] * units.m, dims=('x',)) + expected_12 = xr.DataArray([0, 0, 1] * units.m, dims=("x",)) expected_21 = [0, 0, 1] * units.m - with pytest.warns(UserWarning, match='Argument b given as xarray Variable'): + with pytest.warns(UserWarning, match="Argument b given as xarray Variable"): result_12 = func(data1, data2) - with pytest.warns(UserWarning, match='Argument a given as xarray Variable'): + with pytest.warns(UserWarning, match="Argument a given as xarray Variable"): result_21 = func(data2, data1) assert isinstance(result_12, xr.DataArray) @@ -1293,14 +1344,31 @@ def func(a, b): def test_grid_deltas_from_dataarray_lonlat(test_da_lonlat): """Test grid_deltas_from_dataarray with a lonlat grid.""" dx, dy = grid_deltas_from_dataarray(test_da_lonlat) - true_dx = np.array([[[321609.59212064, 321609.59212065, 321609.59212064], - [310320.85961483, 310320.85961483, 310320.85961483], - [297980.72966733, 297980.72966733, 297980.72966733], - [284629.6008561, 284629.6008561, 284629.6008561]]]) * units.m - true_dy = np.array([[[369603.78775948, 369603.78775948, 369603.78775948, 369603.78775948], - [369802.28173967, 369802.28173967, 369802.28173967, 369802.28173967], - [370009.56291098, 370009.56291098, 370009.56291098, - 370009.56291098]]]) * units.m + true_dx = ( + np.array( + [ + [ + [321609.59212064, 321609.59212065, 321609.59212064], + [310320.85961483, 310320.85961483, 310320.85961483], + [297980.72966733, 297980.72966733, 297980.72966733], + [284629.6008561, 284629.6008561, 284629.6008561], + ] + ] + ) + * units.m + ) + true_dy = ( + np.array( + [ + [ + [369603.78775948, 369603.78775948, 369603.78775948, 369603.78775948], + [369802.28173967, 369802.28173967, 369802.28173967, 369802.28173967], + [370009.56291098, 370009.56291098, 370009.56291098, 370009.56291098], + ] + ] + ) + * units.m + ) assert_array_almost_equal(dx, true_dx, 5) assert_array_almost_equal(dy, true_dy, 5) @@ -1308,8 +1376,8 @@ def test_grid_deltas_from_dataarray_lonlat(test_da_lonlat): def test_grid_deltas_from_dataarray_xy(test_da_xy): """Test grid_deltas_from_dataarray with a xy grid.""" dx, dy = grid_deltas_from_dataarray(test_da_xy) - true_dx = np.array([[[[500] * 3]]]) * units('km') - true_dy = np.array([[[[500]] * 3]]) * units('km') + true_dx = np.array([[[[500] * 3]]]) * units("km") + true_dy = np.array([[[[500]] * 3]]) * units("km") assert_array_almost_equal(dx, true_dx, 5) assert_array_almost_equal(dy, true_dy, 5) @@ -1317,31 +1385,45 @@ def test_grid_deltas_from_dataarray_xy(test_da_xy): def test_grid_deltas_from_dataarray_actual_xy(test_da_xy, ccrs): """Test grid_deltas_from_dataarray with a xy grid and kind='actual'.""" # Construct lon/lat coordinates - y, x = xr.broadcast(*test_da_xy.metpy.coordinates('y', 'x')) - lonlat = (ccrs.Geodetic(test_da_xy.metpy.cartopy_globe) - .transform_points(test_da_xy.metpy.cartopy_crs, x.values, y.values)) + y, x = xr.broadcast(*test_da_xy.metpy.coordinates("y", "x")) + lonlat = ccrs.Geodetic(test_da_xy.metpy.cartopy_globe).transform_points( + test_da_xy.metpy.cartopy_crs, x.values, y.values + ) lon = lonlat[..., 0] lat = lonlat[..., 1] test_da_xy = test_da_xy.assign_coords( - longitude=xr.DataArray(lon, dims=('y', 'x'), attrs={'units': 'degrees_east'}), - latitude=xr.DataArray(lat, dims=('y', 'x'), attrs={'units': 'degrees_north'})) + longitude=xr.DataArray(lon, dims=("y", "x"), attrs={"units": "degrees_east"}), + latitude=xr.DataArray(lat, dims=("y", "x"), attrs={"units": "degrees_north"}), + ) # Actually test calculation - dx, dy = grid_deltas_from_dataarray(test_da_xy, kind='actual') - true_dx = [[[[494426.3249766, 493977.6028005, 493044.0656467], - [498740.2046073, 498474.9771064, 497891.6588559], - [500276.2649627, 500256.3440237, 500139.9484845], - [498740.6956936, 499045.0391707, 499542.7244501]]]] * units.m - true_dy = [[[[496862.4106337, 496685.4729999, 496132.0732114, 495137.8882404], - [499774.9676486, 499706.3354977, 499467.5546773, 498965.2587818], - [499750.8962991, 499826.2263137, 500004.4977747, 500150.9897759]]]] * units.m + dx, dy = grid_deltas_from_dataarray(test_da_xy, kind="actual") + true_dx = [ + [ + [ + [494426.3249766, 493977.6028005, 493044.0656467], + [498740.2046073, 498474.9771064, 497891.6588559], + [500276.2649627, 500256.3440237, 500139.9484845], + [498740.6956936, 499045.0391707, 499542.7244501], + ] + ] + ] * units.m + true_dy = [ + [ + [ + [496862.4106337, 496685.4729999, 496132.0732114, 495137.8882404], + [499774.9676486, 499706.3354977, 499467.5546773, 498965.2587818], + [499750.8962991, 499826.2263137, 500004.4977747, 500150.9897759], + ] + ] + ] * units.m assert_array_almost_equal(dx, true_dx, 3) assert_array_almost_equal(dy, true_dy, 3) def test_grid_deltas_from_dataarray_nominal_lonlat(test_da_lonlat): """Test grid_deltas_from_dataarray with a lonlat grid and kind='nominal'.""" - dx, dy = grid_deltas_from_dataarray(test_da_lonlat, kind='nominal') + dx, dy = grid_deltas_from_dataarray(test_da_lonlat, kind="nominal") true_dx = [[[3.333333] * 3]] * units.degrees true_dy = [[[3.333333]] * 3] * units.degrees assert_array_almost_equal(dx, true_dx, 5) @@ -1352,28 +1434,40 @@ def test_grid_deltas_from_dataarray_nominal_lonlat(test_da_lonlat): def test_grid_deltas_from_dataarray_lonlat_assumed_order(): """Test grid_deltas_from_dataarray when dim order must be assumed.""" # Create test dataarray - lat, lon = np.meshgrid(np.array([38., 40., 42]), np.array([263., 265., 267.])) - test_da = xr.DataArray( - np.linspace(300, 250, 3 * 3).reshape((3, 3)), - name='temperature', - dims=('dim_0', 'dim_1'), - coords={ - 'lat': xr.DataArray(lat, dims=('dim_0', 'dim_1'), - attrs={'units': 'degrees_north'}), - 'lon': xr.DataArray(lon, dims=('dim_0', 'dim_1'), attrs={'units': 'degrees_east'}) - }, - attrs={'units': 'K'}).to_dataset().metpy.parse_cf('temperature') + lat, lon = np.meshgrid(np.array([38.0, 40.0, 42]), np.array([263.0, 265.0, 267.0])) + test_da = ( + xr.DataArray( + np.linspace(300, 250, 3 * 3).reshape((3, 3)), + name="temperature", + dims=("dim_0", "dim_1"), + coords={ + "lat": xr.DataArray( + lat, dims=("dim_0", "dim_1"), attrs={"units": "degrees_north"} + ), + "lon": xr.DataArray( + lon, dims=("dim_0", "dim_1"), attrs={"units": "degrees_east"} + ), + }, + attrs={"units": "K"}, + ) + .to_dataset() + .metpy.parse_cf("temperature") + ) # Run and check for warning - with pytest.warns(UserWarning, match=r'y and x dimensions unable to be identified.*'): + with pytest.warns(UserWarning, match=r"y and x dimensions unable to be identified.*"): dx, dy = grid_deltas_from_dataarray(test_da) # Check results - true_dx = [[222031.0111961, 222107.8492205], - [222031.0111961, 222107.8492205], - [222031.0111961, 222107.8492205]] * units.m - true_dy = [[175661.5413976, 170784.1311091, 165697.7563223], - [175661.5413976, 170784.1311091, 165697.7563223]] * units.m + true_dx = [ + [222031.0111961, 222107.8492205], + [222031.0111961, 222107.8492205], + [222031.0111961, 222107.8492205], + ] * units.m + true_dy = [ + [175661.5413976, 170784.1311091, 165697.7563223], + [175661.5413976, 170784.1311091, 165697.7563223], + ] * units.m assert_array_almost_equal(dx, true_dx, 5) assert_array_almost_equal(dy, true_dy, 5) @@ -1381,63 +1475,62 @@ def test_grid_deltas_from_dataarray_lonlat_assumed_order(): def test_grid_deltas_from_dataarray_invalid_kind(test_da_xy): """Test grid_deltas_from_dataarray when kind is invalid.""" with pytest.raises(ValueError): - grid_deltas_from_dataarray(test_da_xy, kind='invalid') + grid_deltas_from_dataarray(test_da_xy, kind="invalid") @needs_cartopy def test_add_grid_arguments_from_dataarray(): """Test the grid argument decorator for adding in arguments from xarray.""" + @add_grid_arguments_from_xarray def return_the_kwargs( - da, - dz=None, - dy=None, - dx=None, - vertical_dim=None, - y_dim=None, - x_dim=None, - latitude=None + da, dz=None, dy=None, dx=None, vertical_dim=None, y_dim=None, x_dim=None, latitude=None ): return { - 'dz': dz, - 'dy': dy, - 'dx': dx, - 'vertical_dim': vertical_dim, - 'y_dim': y_dim, - 'x_dim': x_dim, - 'latitude': latitude + "dz": dz, + "dy": dy, + "dx": dx, + "vertical_dim": vertical_dim, + "y_dim": y_dim, + "x_dim": x_dim, + "latitude": latitude, } - data = xr.DataArray( - np.zeros((1, 2, 2, 2)), - dims=('time', 'isobaric', 'lat', 'lon'), - coords={ - 'time': ['2020-01-01T00:00Z'], - 'isobaric': (('isobaric',), [850., 700.], {'units': 'hPa'}), - 'lat': (('lat',), [30., 40.], {'units': 'degrees_north'}), - 'lon': (('lon',), [-100., -90.], {'units': 'degrees_east'}) - } - ).to_dataset(name='zeros').metpy.parse_cf('zeros') + data = ( + xr.DataArray( + np.zeros((1, 2, 2, 2)), + dims=("time", "isobaric", "lat", "lon"), + coords={ + "time": ["2020-01-01T00:00Z"], + "isobaric": (("isobaric",), [850.0, 700.0], {"units": "hPa"}), + "lat": (("lat",), [30.0, 40.0], {"units": "degrees_north"}), + "lon": (("lon",), [-100.0, -90.0], {"units": "degrees_east"}), + }, + ) + .to_dataset(name="zeros") + .metpy.parse_cf("zeros") + ) result = return_the_kwargs(data) - assert_array_almost_equal(result['dz'], [-150.] * units.hPa) - assert_array_almost_equal(result['dy'], [[[[1109415.632] * 2]]] * units.meter, 2) - assert_array_almost_equal(result['dx'], [[[[964555.967], [853490.014]]]] * units.meter, 2) - assert result['vertical_dim'] == 1 - assert result['y_dim'] == 2 - assert result['x_dim'] == 3 + assert_array_almost_equal(result["dz"], [-150.0] * units.hPa) + assert_array_almost_equal(result["dy"], [[[[1109415.632] * 2]]] * units.meter, 2) + assert_array_almost_equal(result["dx"], [[[[964555.967], [853490.014]]]] * units.meter, 2) + assert result["vertical_dim"] == 1 + assert result["y_dim"] == 2 + assert result["x_dim"] == 3 assert_array_almost_equal( - result['latitude'].metpy.unit_array, - [30., 40.] * units.degrees_north + result["latitude"].metpy.unit_array, [30.0, 40.0] * units.degrees_north ) # Verify latitude is xarray so can be broadcast, # see https://github.com/Unidata/MetPy/pull/1490#discussion_r483198245 - assert isinstance(result['latitude'], xr.DataArray) + assert isinstance(result["latitude"], xr.DataArray) def test_add_vertical_dim_from_xarray(): """Test decorator for automatically determining the vertical dimension number.""" + @add_vertical_dim_from_xarray def return_vertical_dim(data, vertical_dim=None): return vertical_dim - test_da = xr.DataArray(np.zeros((2, 2, 2, 2)), dims=('time', 'isobaric', 'y', 'x')) + + test_da = xr.DataArray(np.zeros((2, 2, 2, 2)), dims=("time", "isobaric", "y", "x")) assert return_vertical_dim(test_da) == 1 diff --git a/tests/units/test_units.py b/tests/units/test_units.py index 3a7035c3bb0..4183478b995 100644 --- a/tests/units/test_units.py +++ b/tests/units/test_units.py @@ -8,8 +8,12 @@ import pandas as pd import pytest -from metpy.testing import assert_array_almost_equal, assert_array_equal -from metpy.testing import assert_nan, set_agg_backend # noqa: F401 +from metpy.testing import ( # noqa: F401 + assert_array_almost_equal, + assert_array_equal, + assert_nan, + set_agg_backend, +) from metpy.units import check_units, concatenate, pandas_dataframe_to_unit_arrays, units @@ -22,13 +26,13 @@ def test_concatenate(): def test_concatenate_masked(): """Test concatenate preserves masks.""" - d1 = units.Quantity(np.ma.array([1, 2, 3], mask=[False, True, False]), 'degC') + d1 = units.Quantity(np.ma.array([1, 2, 3], mask=[False, True, False]), "degC") result = concatenate((d1, 32 * units.degF)) truth = np.ma.array([1, np.inf, 3, 0]) truth[1] = np.ma.masked - assert_array_almost_equal(result, units.Quantity(truth, 'degC'), 6) + assert_array_almost_equal(result, units.Quantity(truth, "degC"), 6) assert_array_equal(result.mask, np.array([False, True, False, False])) @@ -36,9 +40,9 @@ def test_concatenate_masked(): def test_axhline(): r"""Ensure that passing a quantity to axhline does not error.""" fig, ax = plt.subplots() - ax.axhline(930 * units('mbar')) + ax.axhline(930 * units("mbar")) ax.set_ylim(900, 950) - ax.set_ylabel('') + ax.set_ylabel("") return fig @@ -46,9 +50,9 @@ def test_axhline(): def test_axvline(): r"""Ensure that passing a quantity to axvline does not error.""" fig, ax = plt.subplots() - ax.axvline(0 * units('degC')) + ax.axvline(0 * units("degC")) ax.set_xlim(-1, 1) - ax.set_xlabel('') + ax.set_xlabel("") return fig @@ -63,35 +67,60 @@ def unit_calc(temp, press, dens, mixing, unitless_const): test_funcs = [ - check_units('[temperature]', '[pressure]', dens='[mass]/[volume]', - mixing='[dimensionless]')(unit_calc), - check_units(temp='[temperature]', press='[pressure]', dens='[mass]/[volume]', - mixing='[dimensionless]')(unit_calc), - check_units('[temperature]', '[pressure]', '[mass]/[volume]', - '[dimensionless]')(unit_calc)] - - -@pytest.mark.parametrize('func', test_funcs, ids=['some kwargs', 'all kwargs', 'all pos']) + check_units( + "[temperature]", "[pressure]", dens="[mass]/[volume]", mixing="[dimensionless]" + )(unit_calc), + check_units( + temp="[temperature]", + press="[pressure]", + dens="[mass]/[volume]", + mixing="[dimensionless]", + )(unit_calc), + check_units("[temperature]", "[pressure]", "[mass]/[volume]", "[dimensionless]")( + unit_calc + ), +] + + +@pytest.mark.parametrize("func", test_funcs, ids=["some kwargs", "all kwargs", "all pos"]) def test_good_units(func): r"""Test that unit checking passes good units regardless.""" - func(30 * units.degC, 1000 * units.mbar, 1.0 * units('kg/m^3'), 1, 5.) - - -test_params = [((30 * units.degC, 1000 * units.mb, 1 * units('kg/m^3'), 1, 5 * units('J/kg')), - {}, [('press', '[pressure]', 'millibarn')]), - ((30, 1000, 1.0, 1, 5.), {}, [('press', '[pressure]', 'none'), - ('temp', '[temperature]', 'none'), - ('dens', '[mass]/[volume]', 'none')]), - ((30, 1000 * units.mbar), - {'dens': 1.0 * units('kg / m'), 'mixing': 5 * units.m, 'unitless_const': 2}, - [('temp', '[temperature]', 'none'), - ('dens', '[mass]/[volume]', 'kilogram / meter'), - ('mixing', '[dimensionless]', 'meter')])] - - -@pytest.mark.parametrize('func', test_funcs, ids=['some kwargs', 'all kwargs', 'all pos']) -@pytest.mark.parametrize('args,kwargs,bad_parts', test_params, - ids=['one bad arg', 'all args no units', 'mixed args']) + func(30 * units.degC, 1000 * units.mbar, 1.0 * units("kg/m^3"), 1, 5.0) + + +test_params = [ + ( + (30 * units.degC, 1000 * units.mb, 1 * units("kg/m^3"), 1, 5 * units("J/kg")), + {}, + [("press", "[pressure]", "millibarn")], + ), + ( + (30, 1000, 1.0, 1, 5.0), + {}, + [ + ("press", "[pressure]", "none"), + ("temp", "[temperature]", "none"), + ("dens", "[mass]/[volume]", "none"), + ], + ), + ( + (30, 1000 * units.mbar), + {"dens": 1.0 * units("kg / m"), "mixing": 5 * units.m, "unitless_const": 2}, + [ + ("temp", "[temperature]", "none"), + ("dens", "[mass]/[volume]", "kilogram / meter"), + ("mixing", "[dimensionless]", "meter"), + ], + ), +] + + +@pytest.mark.parametrize("func", test_funcs, ids=["some kwargs", "all kwargs", "all pos"]) +@pytest.mark.parametrize( + "args,kwargs,bad_parts", + test_params, + ids=["one bad arg", "all args no units", "mixed args"], +) def test_bad(func, args, kwargs, bad_parts): r"""Test that unit checking flags appropriate arguments.""" with pytest.raises(ValueError) as exc: @@ -103,47 +132,47 @@ def test_bad(func, args, kwargs, bad_parts): assert '`{}` requires "{}" but given "{}"'.format(*param) in message # Should never complain about the const argument - assert 'unitless_const' not in message + assert "unitless_const" not in message def test_pandas_units_simple(): """Simple unit attachment to two columns.""" - df = pd.DataFrame(data=[[1, 4], [2, 5], [3, 6]], columns=['cola', 'colb']) - df_units = {'cola': 'kilometers', 'colb': 'degC'} + df = pd.DataFrame(data=[[1, 4], [2, 5], [3, 6]], columns=["cola", "colb"]) + df_units = {"cola": "kilometers", "colb": "degC"} res = pandas_dataframe_to_unit_arrays(df, column_units=df_units) cola_truth = np.array([1, 2, 3]) * units.km colb_truth = np.array([4, 5, 6]) * units.degC - assert_array_equal(res['cola'], cola_truth) - assert_array_equal(res['colb'], colb_truth) + assert_array_equal(res["cola"], cola_truth) + assert_array_equal(res["colb"], colb_truth) @pytest.mark.filterwarnings("ignore:Pandas doesn't allow columns to be created") def test_pandas_units_on_dataframe(): """Unit attachment based on a units attribute to a dataframe.""" - df = pd.DataFrame(data=[[1, 4], [2, 5], [3, 6]], columns=['cola', 'colb']) - df.units = {'cola': 'kilometers', 'colb': 'degC'} + df = pd.DataFrame(data=[[1, 4], [2, 5], [3, 6]], columns=["cola", "colb"]) + df.units = {"cola": "kilometers", "colb": "degC"} res = pandas_dataframe_to_unit_arrays(df) cola_truth = np.array([1, 2, 3]) * units.km colb_truth = np.array([4, 5, 6]) * units.degC - assert_array_equal(res['cola'], cola_truth) - assert_array_equal(res['colb'], colb_truth) + assert_array_equal(res["cola"], cola_truth) + assert_array_equal(res["colb"], colb_truth) @pytest.mark.filterwarnings("ignore:Pandas doesn't allow columns to be created") def test_pandas_units_on_dataframe_not_all_united(): """Unit attachment with units attribute with a column with no units.""" - df = pd.DataFrame(data=[[1, 4], [2, 5], [3, 6]], columns=['cola', 'colb']) - df.units = {'cola': 'kilometers'} + df = pd.DataFrame(data=[[1, 4], [2, 5], [3, 6]], columns=["cola", "colb"]) + df.units = {"cola": "kilometers"} res = pandas_dataframe_to_unit_arrays(df) cola_truth = np.array([1, 2, 3]) * units.km colb_truth = np.array([4, 5, 6]) - assert_array_equal(res['cola'], cola_truth) - assert_array_equal(res['colb'], colb_truth) + assert_array_equal(res["cola"], cola_truth) + assert_array_equal(res["colb"], colb_truth) def test_pandas_units_no_units_given(): """Ensure unit attachment fails if no unit information is given.""" - df = pd.DataFrame(data=[[1, 4], [2, 5], [3, 6]], columns=['cola', 'colb']) + df = pd.DataFrame(data=[[1, 4], [2, 5], [3, 6]], columns=["cola", "colb"]) with pytest.raises(ValueError): pandas_dataframe_to_unit_arrays(df) @@ -151,28 +180,28 @@ def test_pandas_units_no_units_given(): def test_added_degrees_units(): """Test that our added degrees units are present in the registry.""" # Test equivalence of abbreviations/aliases to our defined names - assert str(units('degrees_N').units) == 'degrees_north' - assert str(units('degreesN').units) == 'degrees_north' - assert str(units('degree_north').units) == 'degrees_north' - assert str(units('degree_N').units) == 'degrees_north' - assert str(units('degreeN').units) == 'degrees_north' - assert str(units('degrees_E').units) == 'degrees_east' - assert str(units('degreesE').units) == 'degrees_east' - assert str(units('degree_east').units) == 'degrees_east' - assert str(units('degree_E').units) == 'degrees_east' - assert str(units('degreeE').units) == 'degrees_east' + assert str(units("degrees_N").units) == "degrees_north" + assert str(units("degreesN").units) == "degrees_north" + assert str(units("degree_north").units) == "degrees_north" + assert str(units("degree_N").units) == "degrees_north" + assert str(units("degreeN").units) == "degrees_north" + assert str(units("degrees_E").units) == "degrees_east" + assert str(units("degreesE").units) == "degrees_east" + assert str(units("degree_east").units) == "degrees_east" + assert str(units("degree_E").units) == "degrees_east" + assert str(units("degreeE").units) == "degrees_east" # Test equivalence of our defined units to base units - assert units('degrees_north') == units('degrees') - assert units('degrees_north').to_base_units().units == units.radian - assert units('degrees_east') == units('degrees') - assert units('degrees_east').to_base_units().units == units.radian + assert units("degrees_north") == units("degrees") + assert units("degrees_north").to_base_units().units == units.radian + assert units("degrees_east") == units("degrees") + assert units("degrees_east").to_base_units().units == units.radian def test_gpm_unit(): """Test that the gpm unit does alias to meters.""" - x = 1 * units('gpm') - assert str(x.units) == 'meter' + x = 1 * units("gpm") + assert str(x.units) == "meter" def test_assert_nan(): @@ -189,9 +218,9 @@ def test_assert_nan_checks_units(): def test_percent_units(): """Test that percent sign units are properly parsed and interpreted.""" - assert str(units('%').units) == 'percent' + assert str(units("%").units) == "percent" def test_udunits_power_syntax(): """Test that UDUNITS style powers are properly parsed and interpreted.""" - assert units('m2 s-2').units == units.m ** 2 / units.s ** 2 + assert units("m2 s-2").units == units.m ** 2 / units.s ** 2 diff --git a/tools/nexrad_msgs/parse_spec.py b/tools/nexrad_msgs/parse_spec.py index 9fe8e6e4a9b..610178ede87 100644 --- a/tools/nexrad_msgs/parse_spec.py +++ b/tools/nexrad_msgs/parse_spec.py @@ -9,10 +9,12 @@ def register_processor(num): """Register functions to handle particular message numbers.""" + def inner(func): """Perform actual function registration.""" processors[num] = func return func + return inner @@ -22,15 +24,15 @@ def inner(func): @register_processor(3) def process_msg3(fname): """Handle information for message type 3.""" - with open(fname, 'r') as infile: + with open(fname, "r") as infile: info = [] for lineno, line in enumerate(infile): - parts = line.split(' ') + parts = line.split(" ") try: var_name, desc, typ, units = parts[:4] size_hw = parts[-1] - if '-' in size_hw: - start, end = map(int, size_hw.split('-')) + if "-" in size_hw: + start, end = map(int, size_hw.split("-")) size = (end - start + 1) * 2 else: size = 2 @@ -41,13 +43,13 @@ def process_msg3(fname): var_name = fix_var_name(var_name) full_desc = fix_desc(desc, units) - info.append({'name': var_name, 'desc': full_desc, 'fmt': fmt}) + info.append({"name": var_name, "desc": full_desc, "fmt": fmt}) - if ignored_item(info[-1]) and var_name != 'Spare': - warnings.warn('{} has type {}. Setting as Spare'.format(var_name, typ)) + if ignored_item(info[-1]) and var_name != "Spare": + warnings.warn("{} has type {}. Setting as Spare".format(var_name, typ)) except (ValueError, AssertionError): - warnings.warn('{} > {}'.format(lineno + 1, ':'.join(parts))) + warnings.warn("{} > {}".format(lineno + 1, ":".join(parts))) raise return info @@ -55,45 +57,48 @@ def process_msg3(fname): @register_processor(18) def process_msg18(fname): """Handle information for message type 18.""" - with open(fname, 'r') as infile: + with open(fname, "r") as infile: info = [] for lineno, line in enumerate(infile): - parts = line.split(' ') + parts = line.split(" ") try: if len(parts) == 8: parts = parts[:6] + [parts[6] + parts[7]] var_name, desc, typ, units, rng, prec, byte_range = parts - start, end = map(int, byte_range.split('-')) + start, end = map(int, byte_range.split("-")) size = end - start + 1 assert size >= 4 - fmt = fix_type(typ, size, - additional=[('See Note (5)', ('{size}s', 1172))]) + fmt = fix_type(typ, size, additional=[("See Note (5)", ("{size}s", 1172))]) - if ' ' in var_name: - warnings.warn('Space in {}'.format(var_name)) + if " " in var_name: + warnings.warn("Space in {}".format(var_name)) if not desc: - warnings.warn('null description for {}'.format(var_name)) + warnings.warn("null description for {}".format(var_name)) var_name = fix_var_name(var_name) full_desc = fix_desc(desc, units) - info.append({'name': var_name, 'desc': full_desc, 'fmt': fmt}) + info.append({"name": var_name, "desc": full_desc, "fmt": fmt}) - if (ignored_item(info[-1]) and var_name != 'SPARE' - and 'SPARE' not in full_desc): - warnings.warn('{} has type {}. Setting as SPARE'.format(var_name, typ)) + if ignored_item(info[-1]) and var_name != "SPARE" and "SPARE" not in full_desc: + warnings.warn("{} has type {}. Setting as SPARE".format(var_name, typ)) except (ValueError, AssertionError): - warnings.warn('{} > {}'.format(lineno + 1, ':'.join(parts))) + warnings.warn("{} > {}".format(lineno + 1, ":".join(parts))) raise return info -types = [('Real*4', ('f', 4)), ('Integer*4', ('L', 4)), ('SInteger*4', ('l', 4)), - ('Integer*2', ('H', 2)), - ('', lambda s: ('{size}x', s)), ('N/A', lambda s: ('{size}x', s)), - (lambda t: t.startswith('String'), lambda s: ('{size}s', s))] +types = [ + ("Real*4", ("f", 4)), + ("Integer*4", ("L", 4)), + ("SInteger*4", ("l", 4)), + ("Integer*2", ("H", 2)), + ("", lambda s: ("{size}x", s)), + ("N/A", lambda s: ("{size}x", s)), + (lambda t: t.startswith("String"), lambda s: ("{size}s", s)), +] def fix_type(typ, size, additional=None): @@ -114,21 +119,22 @@ def fix_type(typ, size, additional=None): fmt_str, true_size = info(size) else: fmt_str, true_size = info - assert size == true_size, ('{}: Got size {} instead of {}'.format(typ, size, - true_size)) + assert size == true_size, "{}: Got size {} instead of {}".format( + typ, size, true_size + ) return fmt_str.format(size=size) - raise ValueError('No type match! ({})'.format(typ)) + raise ValueError("No type match! ({})".format(typ)) def fix_var_name(var_name): """Clean up and apply standard formatting to variable names.""" name = var_name.strip() - for char in '(). /#,': - name = name.replace(char, '_') - name = name.replace('+', 'pos_') - name = name.replace('-', 'neg_') - if name.endswith('_'): + for char in "(). /#,": + name = name.replace(char, "_") + name = name.replace("+", "pos_") + name = name.replace("-", "neg_") + if name.endswith("_"): name = name[:-1] return name @@ -136,9 +142,9 @@ def fix_var_name(var_name): def fix_desc(desc, units=None): """Clean up description column.""" full_desc = desc.strip() - if units and units != 'N/A': + if units and units != "N/A": if full_desc: - full_desc += ' (' + units + ')' + full_desc += " (" + units + ")" else: full_desc = units return full_desc @@ -146,55 +152,57 @@ def fix_desc(desc, units=None): def ignored_item(item): """Determine whether this item should be ignored.""" - return item['name'].upper() == 'SPARE' or 'x' in item['fmt'] + return item["name"].upper() == "SPARE" or "x" in item["fmt"] def need_desc(item): """Determine whether we need a description for this item.""" - return item['desc'] and not ignored_item(item) + return item["desc"] and not ignored_item(item) def field_name(item): """Return the field name if appropriate.""" - return '"{:s}"'.format(item['name']) if not ignored_item(item) else None + return '"{:s}"'.format(item["name"]) if not ignored_item(item) else None def field_fmt(item): """Return the field format if appropriate.""" - return '"{:s}"'.format(item['fmt']) if '"' not in item['fmt'] else item['fmt'] + return '"{:s}"'.format(item["fmt"]) if '"' not in item["fmt"] else item["fmt"] def write_file(fname, info): """Write out the generated Python code.""" - with open(fname, 'w') as outfile: + with open(fname, "w") as outfile: # File header - outfile.write('# Copyright (c) 2018 MetPy Developers.\n') - outfile.write('# Distributed under the terms of the BSD 3-Clause License.\n') - outfile.write('# SPDX-License-Identifier: BSD-3-Clause\n\n') - outfile.write('# flake8: noqa\n') - outfile.write('# Generated file -- do not modify\n') + outfile.write("# Copyright (c) 2018 MetPy Developers.\n") + outfile.write("# Distributed under the terms of the BSD 3-Clause License.\n") + outfile.write("# SPDX-License-Identifier: BSD-3-Clause\n\n") + outfile.write("# flake8: noqa\n") + outfile.write("# Generated file -- do not modify\n") # Variable descriptions - outfile.write('descriptions = {') - outdata = ',\n '.join('"{name}": "{desc}"'.format( - **i) for i in info if need_desc(i)) + outfile.write("descriptions = {") + outdata = ",\n ".join( + '"{name}": "{desc}"'.format(**i) for i in info if need_desc(i) + ) outfile.write(outdata) - outfile.write('}\n\n') + outfile.write("}\n\n") # Now the struct format - outfile.write('fields = [') - outdata = ',\n '.join('({fname}, "{fmt}")'.format( - fname=field_name(i), **i) for i in info) + outfile.write("fields = [") + outdata = ",\n ".join( + '({fname}, "{fmt}")'.format(fname=field_name(i), **i) for i in info + ) outfile.write(outdata) - outfile.write(']\n') + outfile.write("]\n") -if __name__ == '__main__': +if __name__ == "__main__": import os.path for num in [18, 3]: - fname = 'msg{:d}.spec'.format(num) - print('Processing {}...'.format(fname)) # noqa: T001 + fname = "msg{:d}.spec".format(num) + print("Processing {}...".format(fname)) # noqa: T001 info = processors[num](fname) - fname = os.path.splitext(fname)[0] + '.py' + fname = os.path.splitext(fname)[0] + ".py" write_file(fname, info) diff --git a/tutorials/unit_tutorial.py b/tutorials/unit_tutorial.py index d70dda55ca3..0c85207c82d 100644 --- a/tutorials/unit_tutorial.py +++ b/tutorials/unit_tutorial.py @@ -58,7 +58,7 @@ # Units can be converted using the `to()` method. While you won't see square meters in # the units list, we can parse complex/compound units as strings: -print(area.to('m^2')) +print(area.to("m^2")) ######################################################################### # Temperature @@ -110,8 +110,8 @@ # We can create compound units for things like speed by parsing a string of # units. Abbreviations or full unit names are acceptable. -u = np.random.randint(0, 15, 10) * units('m/s') -v = np.random.randint(0, 15, 10) * units('meters/second') +u = np.random.randint(0, 15, 10) * units("m/s") +v = np.random.randint(0, 15, 10) * units("meters/second") print(u) print(v) diff --git a/tutorials/upperair_soundings.py b/tutorials/upperair_soundings.py index 107ecdda1d8..058be1c285d 100644 --- a/tutorials/upperair_soundings.py +++ b/tutorials/upperair_soundings.py @@ -31,25 +31,30 @@ # Upper air data can be obtained using the siphon package, but for this tutorial we will use # some of MetPy's sample data. This event is the Veterans Day tornado outbreak in 2002. -col_names = ['pressure', 'height', 'temperature', 'dewpoint', 'direction', 'speed'] +col_names = ["pressure", "height", "temperature", "dewpoint", "direction", "speed"] -df = pd.read_fwf(get_test_data('nov11_sounding.txt', as_file_obj=False), - skiprows=5, usecols=[0, 1, 2, 3, 6, 7], names=col_names) +df = pd.read_fwf( + get_test_data("nov11_sounding.txt", as_file_obj=False), + skiprows=5, + usecols=[0, 1, 2, 3, 6, 7], + names=col_names, +) # Drop any rows with all NaN values for T, Td, winds -df = df.dropna(subset=('temperature', 'dewpoint', 'direction', 'speed' - ), how='all').reset_index(drop=True) +df = df.dropna( + subset=("temperature", "dewpoint", "direction", "speed"), how="all" +).reset_index(drop=True) ########################################################################## # We will pull the data out of the example dataset into individual variables and # assign units. -p = df['pressure'].values * units.hPa -T = df['temperature'].values * units.degC -Td = df['dewpoint'].values * units.degC -wind_speed = df['speed'].values * units.knots -wind_dir = df['direction'].values * units.degrees +p = df["pressure"].values * units.hPa +T = df["temperature"].values * units.degC +Td = df["dewpoint"].values * units.degC +wind_speed = df["speed"].values * units.knots +wind_dir = df["direction"].values * units.degrees u, v = mpcalc.wind_components(wind_speed, wind_dir) ########################################################################## @@ -71,7 +76,7 @@ print(lcl_pressure, lcl_temperature) # Calculate the parcel profile. -parcel_prof = mpcalc.parcel_profile(p, T[0], Td[0]).to('degC') +parcel_prof = mpcalc.parcel_profile(p, T[0], Td[0]).to("degC") ########################################################################## # Basic Skew-T Plotting @@ -105,8 +110,8 @@ # Plot the data using normal plotting functions, in this case using # log scaling in Y, as dictated by the typical meteorological plot -skew.plot(p, T, 'r', linewidth=2) -skew.plot(p, Td, 'g', linewidth=2) +skew.plot(p, T, "r", linewidth=2) +skew.plot(p, Td, "g", linewidth=2) skew.plot_barbs(p, u, v) # Show the plot @@ -126,24 +131,24 @@ # Plot the data using normal plotting functions, in this case using # log scaling in Y, as dictated by the typical meteorological plot -skew.plot(p, T, 'r') -skew.plot(p, Td, 'g') +skew.plot(p, T, "r") +skew.plot(p, Td, "g") skew.plot_barbs(p, u, v) skew.ax.set_ylim(1000, 100) skew.ax.set_xlim(-40, 60) # Plot LCL temperature as black dot -skew.plot(lcl_pressure, lcl_temperature, 'ko', markerfacecolor='black') +skew.plot(lcl_pressure, lcl_temperature, "ko", markerfacecolor="black") # Plot the parcel profile as a black line -skew.plot(p, parcel_prof, 'k', linewidth=2) +skew.plot(p, parcel_prof, "k", linewidth=2) # Shade areas of CAPE and CIN skew.shade_cin(p, T, parcel_prof, Td) skew.shade_cape(p, T, parcel_prof) # Plot a zero degree isotherm -skew.ax.axvline(0, color='c', linestyle='--', linewidth=2) +skew.ax.axvline(0, color="c", linestyle="--", linewidth=2) # Add the relevant special lines skew.plot_dry_adiabats() @@ -169,24 +174,24 @@ # Plot the data using normal plotting functions, in this case using # log scaling in Y, as dictated by the typical meteorological plot -skew.plot(p, T, 'r') -skew.plot(p, Td, 'g') +skew.plot(p, T, "r") +skew.plot(p, Td, "g") skew.plot_barbs(p, u, v) skew.ax.set_ylim(1000, 100) skew.ax.set_xlim(-40, 60) # Plot LCL as black dot -skew.plot(lcl_pressure, lcl_temperature, 'ko', markerfacecolor='black') +skew.plot(lcl_pressure, lcl_temperature, "ko", markerfacecolor="black") # Plot the parcel profile as a black line -skew.plot(p, parcel_prof, 'k', linewidth=2) +skew.plot(p, parcel_prof, "k", linewidth=2) # Shade areas of CAPE and CIN skew.shade_cin(p, T, parcel_prof, Td) skew.shade_cape(p, T, parcel_prof) # Plot a zero degree isotherm -skew.ax.axvline(0, color='c', linestyle='--', linewidth=2) +skew.ax.axvline(0, color="c", linestyle="--", linewidth=2) # Add the relevant special lines skew.plot_dry_adiabats() @@ -196,8 +201,8 @@ # Create a hodograph # Create an inset axes object that is 40% width and height of the # figure and put it in the upper right hand corner. -ax_hod = inset_axes(skew.ax, '40%', '40%', loc=1) -h = Hodograph(ax_hod, component_range=80.) +ax_hod = inset_axes(skew.ax, "40%", "40%", loc=1) +h = Hodograph(ax_hod, component_range=80.0) h.add_grid(increment=20) h.plot_colormapped(u, v, wind_speed) # Plot a line colored by wind speed diff --git a/tutorials/xarray_tutorial.py b/tutorials/xarray_tutorial.py index 363eacadc33..1a8abdde36f 100644 --- a/tutorials/xarray_tutorial.py +++ b/tutorials/xarray_tutorial.py @@ -36,7 +36,7 @@ # 2017). # Open the netCDF file as a xarray Dataset -data = xr.open_dataset(get_test_data('irma_gfs_example.nc', False)) +data = xr.open_dataset(get_test_data("irma_gfs_example.nc", False)) # View a summary of the Dataset print(data) @@ -58,21 +58,24 @@ # If we instead want just a single variable, we can pass that variable name to parse_cf and # it will return just that data variable as a DataArray. -data_var = data.metpy.parse_cf('Temperature_isobaric') +data_var = data.metpy.parse_cf("Temperature_isobaric") # If we want only a subset of variables, we can pass a list of variable names as well. -data_subset = data.metpy.parse_cf(['u-component_of_wind_isobaric', - 'v-component_of_wind_isobaric']) +data_subset = data.metpy.parse_cf( + ["u-component_of_wind_isobaric", "v-component_of_wind_isobaric"] +) # To rename variables, supply a dictionary between old and new names to the rename method -data = data.rename({ - 'Vertical_velocity_pressure_isobaric': 'omega', - 'Relative_humidity_isobaric': 'relative_humidity', - 'Temperature_isobaric': 'temperature', - 'u-component_of_wind_isobaric': 'u', - 'v-component_of_wind_isobaric': 'v', - 'Geopotential_height_isobaric': 'height' -}) +data = data.rename( + { + "Vertical_velocity_pressure_isobaric": "omega", + "Relative_humidity_isobaric": "relative_humidity", + "Temperature_isobaric": "temperature", + "u-component_of_wind_isobaric": "u", + "v-component_of_wind_isobaric": "v", + "Geopotential_height_isobaric": "height", + } +) ######################################################################### # Units @@ -83,7 +86,7 @@ # convert the the data from one unit to another (keeping it as a DataArray). For now, we'll # just use ``convert_units`` to convert our temperature to ``degC``. -data['temperature'] = data['temperature'].metpy.convert_units('degC') +data["temperature"] = data["temperature"].metpy.convert_units("degC") ######################################################################### # Coordinates @@ -117,14 +120,14 @@ # will be identical (as will ``y`` and ``latitude``). # Get multiple coordinates (for example, in just the x and y direction) -x, y = data['temperature'].metpy.coordinates('x', 'y') +x, y = data["temperature"].metpy.coordinates("x", "y") # If we want to get just a single coordinate from the coordinates method, we have to use # tuple unpacking because the coordinates method returns a generator -vertical, = data['temperature'].metpy.coordinates('vertical') +(vertical,) = data["temperature"].metpy.coordinates("vertical") # Or, we can just get a coordinate from the property -time = data['temperature'].metpy.time +time = data["temperature"].metpy.time # To verify, we can inspect all their names print([coord.name for coord in (x, y, vertical, time)]) @@ -138,7 +141,7 @@ # mentioned above as aliases for the coordinates. And so, if we wanted 850 hPa heights, # we would take: -print(data['height'].metpy.sel(vertical=850 * units.hPa)) +print(data["height"].metpy.sel(vertical=850 * units.hPa)) ######################################################################### # For full details on xarray indexing/selection, see @@ -151,14 +154,14 @@ # Getting the cartopy coordinate reference system (CRS) of the projection of a DataArray is as # straightforward as using the ``data_var.metpy.cartopy_crs`` property: -data_crs = data['temperature'].metpy.cartopy_crs +data_crs = data["temperature"].metpy.cartopy_crs print(data_crs) ######################################################################### # The cartopy ``Globe`` can similarly be accessed via the ``data_var.metpy.cartopy_globe`` # property: -data_globe = data['temperature'].metpy.cartopy_globe +data_globe = data["temperature"].metpy.cartopy_globe print(data_globe) ######################################################################### @@ -180,7 +183,7 @@ lat, lon = xr.broadcast(y, x) dx, dy = mpcalc.lat_lon_grid_deltas(lon, lat, initstring=data_crs.proj4_init) -heights = data['height'].metpy.loc[{'time': time[0], 'vertical': 500. * units.hPa}] +heights = data["height"].metpy.loc[{"time": time[0], "vertical": 500.0 * units.hPa}] u_geo, v_geo = mpcalc.geostrophic_wind(heights, dx, dy, lat) print(u_geo) print(v_geo) @@ -214,7 +217,7 @@ # takes a ``DataArray`` input, but returns unit arrays for use in other calculations. We could # rewrite the above geostrophic wind example using this helper function as follows: -heights = data['height'].metpy.loc[{'time': time[0], 'vertical': 500. * units.hPa}] +heights = data["height"].metpy.loc[{"time": time[0], "vertical": 500.0 * units.hPa}] lat, lon = xr.broadcast(y, x) dx, dy = grid_deltas_from_dataarray(heights) u_geo, v_geo = mpcalc.geostrophic_wind(heights, dx, dy, lat) @@ -234,15 +237,16 @@ # `_.) # A very simple example example of a plot of 500 hPa heights -data['height'].metpy.loc[{'time': time[0], 'vertical': 500. * units.hPa}].plot() +data["height"].metpy.loc[{"time": time[0], "vertical": 500.0 * units.hPa}].plot() plt.show() ######################################################################### # Let's add a projection and coastlines to it ax = plt.axes(projection=ccrs.LambertConformal()) -data['height'].metpy.loc[{'time': time[0], - 'vertical': 500. * units.hPa}].plot(ax=ax, transform=data_crs) +data["height"].metpy.loc[{"time": time[0], "vertical": 500.0 * units.hPa}].plot( + ax=ax, transform=data_crs +) ax.coastlines() plt.show() @@ -251,41 +255,76 @@ # Or, let's make a full 500 hPa map with heights, temperature, winds, and humidity # Select the data for this time and level -data_level = data.metpy.loc[{time.name: time[0], vertical.name: 500. * units.hPa}] +data_level = data.metpy.loc[{time.name: time[0], vertical.name: 500.0 * units.hPa}] # Create the matplotlib figure and axis -fig, ax = plt.subplots(1, 1, figsize=(12, 8), subplot_kw={'projection': data_crs}) +fig, ax = plt.subplots(1, 1, figsize=(12, 8), subplot_kw={"projection": data_crs}) # Plot RH as filled contours -rh = ax.contourf(x, y, data_level['relative_humidity'], levels=[70, 80, 90, 100], - colors=['#99ff00', '#00ff00', '#00cc00']) +rh = ax.contourf( + x, + y, + data_level["relative_humidity"], + levels=[70, 80, 90, 100], + colors=["#99ff00", "#00ff00", "#00cc00"], +) # Plot wind barbs, but not all of them wind_slice = slice(5, -5, 5) -ax.barbs(x[wind_slice], y[wind_slice], - data_level['u'].metpy.unit_array[wind_slice, wind_slice].to('knots'), - data_level['v'].metpy.unit_array[wind_slice, wind_slice].to('knots'), - length=6) +ax.barbs( + x[wind_slice], + y[wind_slice], + data_level["u"].metpy.unit_array[wind_slice, wind_slice].to("knots"), + data_level["v"].metpy.unit_array[wind_slice, wind_slice].to("knots"), + length=6, +) # Plot heights and temperature as contours -h_contour = ax.contour(x, y, data_level['height'], colors='k', levels=range(5400, 6000, 60)) -h_contour.clabel(fontsize=8, colors='k', inline=1, inline_spacing=8, - fmt='%i', rightside_up=True, use_clabeltext=True) -t_contour = ax.contour(x, y, data_level['temperature'], colors='xkcd:deep blue', - levels=range(-26, 4, 2), alpha=0.8, linestyles='--') -t_contour.clabel(fontsize=8, colors='xkcd:deep blue', inline=1, inline_spacing=8, - fmt='%i', rightside_up=True, use_clabeltext=True) +h_contour = ax.contour(x, y, data_level["height"], colors="k", levels=range(5400, 6000, 60)) +h_contour.clabel( + fontsize=8, + colors="k", + inline=1, + inline_spacing=8, + fmt="%i", + rightside_up=True, + use_clabeltext=True, +) +t_contour = ax.contour( + x, + y, + data_level["temperature"], + colors="xkcd:deep blue", + levels=range(-26, 4, 2), + alpha=0.8, + linestyles="--", +) +t_contour.clabel( + fontsize=8, + colors="xkcd:deep blue", + inline=1, + inline_spacing=8, + fmt="%i", + rightside_up=True, + use_clabeltext=True, +) # Add geographic features -ax.add_feature(cfeature.LAND.with_scale('50m'), facecolor=cfeature.COLORS['land']) -ax.add_feature(cfeature.OCEAN.with_scale('50m'), facecolor=cfeature.COLORS['water']) -ax.add_feature(cfeature.STATES.with_scale('50m'), edgecolor='#c7c783', zorder=0) -ax.add_feature(cfeature.LAKES.with_scale('50m'), facecolor=cfeature.COLORS['water'], - edgecolor='#c7c783', zorder=0) +ax.add_feature(cfeature.LAND.with_scale("50m"), facecolor=cfeature.COLORS["land"]) +ax.add_feature(cfeature.OCEAN.with_scale("50m"), facecolor=cfeature.COLORS["water"]) +ax.add_feature(cfeature.STATES.with_scale("50m"), edgecolor="#c7c783", zorder=0) +ax.add_feature( + cfeature.LAKES.with_scale("50m"), + facecolor=cfeature.COLORS["water"], + edgecolor="#c7c783", + zorder=0, +) # Set a title and show the plot -ax.set_title('500 hPa Heights (m), Temperature (\u00B0C), Humidity (%) at ' - + time[0].dt.strftime('%Y-%m-%d %H:%MZ').item()) +ax.set_title( + "500 hPa Heights (m), Temperature (\u00B0C), Humidity (%) at " + + time[0].dt.strftime("%Y-%m-%d %H:%MZ").item() +) plt.show() #########################################################################