diff --git a/.travis.yml b/.travis.yml index bdbf7815e6..2bb6344156 100644 --- a/.travis.yml +++ b/.travis.yml @@ -61,25 +61,17 @@ matrix: env: TASK="docs" PRE="--pre" - python: nightly env: PRE="--pre" - - python: 3.7 + - python: "3.7-dev" env: - TASK="coverage" - VERSIONS="git+git://github.com/hgrecco/pint@master#egg=pint git+git://github.com/pydata/xarray@master#egg=xarray" - - python: 3.7 + - python: "3.7-dev" env: - TASK="docs" - VERSIONS="git+git://github.com/hgrecco/pint@master#egg=pint git+git://github.com/pydata/xarray@master#egg=xarray" allow_failures: - python: "3.7-dev" - python: nightly - - python: 3.7 - env: - - TASK="coverage" - - VERSIONS="git+git://github.com/hgrecco/pint@master#egg=pint git+git://github.com/pydata/xarray@master#egg=xarray" - - python: 3.7 - env: - - TASK="docs" - - VERSIONS="git+git://github.com/hgrecco/pint@master#egg=pint git+git://github.com/pydata/xarray@master#egg=xarray" before_install: # We hard-code the sphinx_rtd_theme to lock in our build with patch for @@ -117,11 +109,12 @@ before_install: # Cython needs to be installed before even downloading CartoPy - python -m pip install Cython; - python -m pip download -d $WHEELDIR ".[$EXTRA_INSTALLS]" $EXTRA_PACKAGES -f $WHEELHOUSE $PRE $VERSIONS; - - touch $WHEELDIR/download_marker && ls -lrt $WHEELDIR; + - touch $WHEELDIR/download_marker; - travis_wait python -m pip wheel -w $WHEELDIR $EXTRA_PACKAGES -f $WHEELHOUSE $PRE $VERSIONS; - python -m pip install $EXTRA_PACKAGES --upgrade --upgrade-strategy=eager --no-index -f file://$PWD/$WHEELDIR $VERSIONS; - travis_wait 30 python -m pip wheel -w $WHEELDIR ".[$EXTRA_INSTALLS]" $EXTRA_PACKAGES -f $WHEELHOUSE $PRE $VERSIONS; - - rm -f $WHEELDIR/MetPy*.whl; + # Make sure we don't upload MetPy or other development build wheels + - rm -f $WHEELDIR/MetPy*.whl $WHEELDIR/xarray-*+*.whl $WHEELDIR/Pint-*dev*.whl; install: - python -m pip install ".[$EXTRA_INSTALLS]" --upgrade --upgrade-strategy=eager --no-index $PRE -f file://$PWD/$WHEELDIR $VERSIONS; @@ -156,6 +149,8 @@ after_script: python-codacy-coverage -r coverage.xml; codeclimate-test-reporter; fi + - ls -lr --full-time $WHEELDIR; + - echo $(find $WHEELDIR -newer $WHEELDIR/download_marker -name *.whl | tr [:space:] :) before_deploy: # Remove unused, unminified javascript from sphinx @@ -167,14 +162,11 @@ before_deploy: deploy: - provider: pypi - user: dopplershift - password: - secure: VYbxLZZnQ1hR2WZwe6+NXLNVbxceDQzlaVM/G3PW8mYlnyWgIVJBgCcgpH22wT4IsNQqo1r9ow9HiybzwcU1VTZ9KXjYsjre/kCZob0jmuPKlDtujOLaMJFf0XzOw7Y/AFXaMakFA8ZOYJLaMXc0WMLwGT7Hw/oP/e2ztpVLxRA= + username: __token__ distributions: sdist bdist_wheel - upload_docs: no on: repo: Unidata/MetPy - python: 3.6 + python: 3.7 condition: '$TASK != "docs"' tags: true - provider: script @@ -182,7 +174,7 @@ deploy: skip_cleanup: true on: all_branches: true - python: 3.6 + python: 3.7 condition: '$TASK == "docs"' notifications: diff --git a/docs/SUPPORT.md b/docs/SUPPORT.md new file mode 120000 index 0000000000..531fff8f5d --- /dev/null +++ b/docs/SUPPORT.md @@ -0,0 +1 @@ +../SUPPORT.md \ No newline at end of file diff --git a/docs/_templates/autosummary/module.rst b/docs/_templates/autosummary/module.rst index 0d80ebac7a..86e72828eb 100644 --- a/docs/_templates/autosummary/module.rst +++ b/docs/_templates/autosummary/module.rst @@ -1,4 +1,4 @@ -{% if fullname in ['metpy.calc'] %} +{% if fullname in ['metpy.calc', 'metpy.plots.ctables', 'metpy.xarray'] %} {% include 'overrides/' ~ fullname ~ '.rst' with context %} diff --git a/docs/_templates/overrides/metpy.plots.ctables.rst b/docs/_templates/overrides/metpy.plots.ctables.rst new file mode 100644 index 0000000000..76a8f8e749 --- /dev/null +++ b/docs/_templates/overrides/metpy.plots.ctables.rst @@ -0,0 +1,26 @@ + + +ctables +=================== + +.. automodule:: metpy.plots.ctables + + + + .. rubric:: Functions + + .. autosummary:: + :toctree: ./ + + convert_gempak_table + read_colortable + resource_listdir + resource_stream + + .. rubric:: Classes + + .. autosummary:: + :toctree: ./ + + ColortableRegistry + colortables diff --git a/docs/_templates/overrides/metpy.xarray.rst b/docs/_templates/overrides/metpy.xarray.rst new file mode 100644 index 0000000000..e8dd8c868e --- /dev/null +++ b/docs/_templates/overrides/metpy.xarray.rst @@ -0,0 +1,13 @@ +xarray +========== + +.. automodule:: metpy.xarray + +Accessors +--------- + +.. autoclass:: MetPyDataArrayAccessor() + :members: + +.. autoclass:: MetPyDatasetAccessor() + :members: diff --git a/docs/api/index.rst b/docs/api/index.rst index 5d8f9b93b5..f226df920b 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -17,6 +17,7 @@ Reference Guide metpy.plots.ctables metpy.interpolate metpy.gridding + metpy.xarray * :ref:`modindex` * :ref:`genindex` diff --git a/docs/index.rst b/docs/index.rst index eff8d4f5d5..473a122c36 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,12 +11,14 @@ :hidden: installguide + startingguide units examples/index tutorials/index api/index roadmap gempak + SUPPORT CONTRIBUTING infrastructureguide citing @@ -27,7 +29,8 @@ MetPy ===== MetPy is a collection of tools in Python for reading, visualizing, and -performing calculations with weather data. +performing calculations with weather data. If you're new to MetPy, check +out our :doc:`Getting Started ` guide. MetPy follows `semantic versioning `_ in its version number. With our current 0.x version, that implies that MetPy's APIs (application programming interfaces) are @@ -122,11 +125,11 @@ Related Projects ---------------- * netCDF4-python_ is the officially blessed Python API for netCDF_ -* siphon_ is an API for accessing remote data on `THREDDS Data Server`__ -* unidata_python_gallery_ is a collection of meteorological Python scripts +* siphon_ is a Python API for accessing remote data on `THREDDS Data Servers`__ +* The `Unidata Python Gallery`_ is a collection of meteorological Python scripts .. _netCDF4-python: https://unidata.github.io/netcdf4-python/ .. _netCDF: https://www.unidata.ucar.edu/software/netcdf/ .. _siphon: https://unidata.github.io/siphon/ -.. _unidata_python_gallery: https://unidata.github.io/python-gallery/ +.. _Unidata Python Gallery: https://unidata.github.io/python-gallery/ __ https://www.unidata.ucar.edu/software/thredds/current/tds/ diff --git a/docs/override_check.py b/docs/override_check.py index 6f88953053..6469d4642c 100644 --- a/docs/override_check.py +++ b/docs/override_check.py @@ -13,12 +13,18 @@ import sys +modules_to_skip = ['metpy.xarray'] + + failed = False for full_path in glob.glob('_templates/overrides/metpy.*.rst'): filename = os.path.basename(full_path) module = filename.split('.rst')[0] + if module in modules_to_skip: + continue + # Get all functions in the module i = importlib.import_module(module) functions = set(i.__all__) diff --git a/docs/startingguide.rst b/docs/startingguide.rst new file mode 100644 index 0000000000..41028e6465 --- /dev/null +++ b/docs/startingguide.rst @@ -0,0 +1,374 @@ +Getting Started with MetPy +========================== + +Welcome to MetPy! We're glad you're here and we hope that you find this Python library +to be useful for your needs. In order to help get you started with MetPy, we've put together +this guide to introduce you to the basic syntax and functionality of this library. If you're +new to Python, please visit the Unidata `Online Python Training`_ site for in-depth +discussion and examples of the Scientific Python ecosystem. + +.. _`Online Python Training`: https://unidata.github.io/online-python-training/ + +------------ +Installation +------------ + +For installation instructions, please see our :doc:`Installation Guide `. +MetPy Monday videos `#1`_, `#2`_, and `#3`_ demonstrate how to install the conda package +manager and Python packages, and how to work with conda environments. + +.. _#1: https://youtu.be/-fOfyHYpKck +.. _#2: https://youtu.be/G3AF-nhNyDk +.. _#3: https://youtu.be/15DNH25UCi0 + +----- +Units +----- + +For the in-depth explanation of units, associated syntax, and unique features, please see +our :doc:`Units ` page. What follows in this section is a short summary of how MetPy +uses units. + +One of the most significant differences in syntax for MetPy, compared to other Python +libraries, is the frequent requirement of units to be attached to arrays before being +passed to MetPy functions. There are very few exceptions to this, and you'll usually be +safer to always use units whenever applicable to make sure that your analyses are done +correctly. Once you get used to the units syntax, it becomes very handy, as you never have +to worry about unit conversion for any calculation. MetPy does it for you! + +To demonstrate the units syntax briefly here, we can do this: + +.. code-block:: python + + import numpy as np + from metpy.units import units + + distance = np.arange(1, 5) * units.meters + +Another way to attach units is do create the array directly with the :class:`pint.Quantity` +object: + +.. code-block:: python + + time = units.Quantity(np.arange(2, 10, 2), 'sec') + +Unit-aware calculations can then be done with these variables: + +.. code-block:: python + + print(distance / time) + +.. parsed-literal:: + [ 0.5 0.5 0.5 0.5] meter / second + + +In addition to the :doc:`Units ` page, checkout the MetPy Monday blog on +`units `_ +or watch our MetPy Monday video on +`temperature units `_. + +------------- +Functionality +------------- + +MetPy aims to have three primary purposes: read and write meteorological data (I/O), calculate +meteorological quantities with well-documented equations, and create publication-quality plots +of meteorological data. The three subsections that follow will demonstrate just some of this +functionality. For full reference to all of MetPy's API, please see our +:doc:`Reference Guide `. + +++++++++++++ +Input/Output +++++++++++++ + +MetPy has built in support for reading GINI satellite and NEXRAD radar files. If you have one +of these files, opening it with MetPy is as easy as: + +.. code-block:: python + + from metpy.io import Level2File, Level3File, GiniFile + + f = GiniFile(example_filename.gini) + f = Level2File(example_filename.gz) + f = Level3File(example_filename.gz) + +From there, you can pull out the variables you want to analyze and plot. For more information, +see the :doc:`GINI `, +:doc:`NEXRAD Level 2 `, and +:doc:`NEXRAD Level 3 ` examples. MetPy Monday videos +`#29`_ and `#30`_ also show how to plot radar files with MetPy. + +.. _`#29`: https://youtu.be/73fhfV2zOt8 +.. _`#30`: https://youtu.be/fSax8g9EfxM + +The other exciting feature is MetPy's Xarray accessor. Xarray is a Python package that +makes working with multi-dimensional labeled data (i.e. netCDF files) easy. For a thorough +look at Xarray's capabilities, see this `MetPy Monday video `_. +With MetPy's accessor to this package, we can quickly pull out common dimensions, parse +Climate and Forecasting (CF) metadata, and handle projection information. While the +:doc:`Xarray with MetPy ` is the best place to see the full utility +of the MetPy Xarray accessor, let's demonstrate some of the functionality here: + +.. code-block:: python + + import xarray as xr + import metpy + from metpy.cbook import get_test_data + + data = xr.open_dataset(get_test_data('narr_example.nc', as_file_obj = False)) + data = data.metpy.parse_cf() + + # Grab lat/lon values from file as unit arrays + lats = ds.lat.metpy.unit_array + lons = ds.lon.metpy.unit_array + + # Get the valid time + vtime = data.Temperature_isobaric.metpy.time[0] + + # Get the 700-hPa heights without manually identifying the vertical coordinate + hght_700 = data.Geopotential_height_isobaric.metpy.sel(vertical=700 * units.hPa, + time=vtime) + +From here, you could make a map of the 700-hPa geopotential heights. We'll discuss how to +do that in the Plotting section. + +++++++++++++ +Calculations +++++++++++++ + +Meteorology and atmospheric science are fully-dependent on complex equations and formulas. +Rather than figuring out how to write them efficiently in Python yourself, MetPy provides +support for many of the common equations within the field. For the full list, please see the +`Calculations `_ reference guide. If you don't see the equation +you're looking for, consider submitting a feature request to MetPy +`here `_. + +To demonstrate some of the calculations MetPy can do, let's show a simple example: + +.. code-block:: python + + import numpy as np + from metpy.units import units + import metpy.calc as mpcalc + + temperature = [20] * units.degC + rel_humidity = [50] * units.percent + print(dewpoint_rh(temperature, rel_humidity)) + +.. parsed-literal:: + + array([9.27008599]) + +.. code-block:: python + + speed = np.array([5, 10, 15, 20]) * units.knots + direction = np.array([0, 90, 180, 270]) * units.degrees + u, v = mpcalc.wind_components(speed, direction) + print(u, v) + +.. parsed-literal:: + + [0 -10 0 20] knot + [-5 0 15 0] knot + +As discussed above, if you don't provide units to these functions, they will frequently +fail with the following error: + +.. parsed-literal:: + + ValueError: `calculation` given arguments with incorrect units: `variable` requires + "[`type of unit`]" but given "none". Any variable `x` can be assigned a unit as follows: + from metpy.units import units + x = x * units.meter / units.second + +If you see this error in your code, just attach the appropriate units and you'll be good to go! + +++++++++ +Plotting +++++++++ + +MetPy contains two special types of meteorological plots, the Skew-T Log-P and Station plots, +that more general Python plotting packages don't support as readily. Additionally, with the +goal to replace GEMPAK, MetPy's declarative plotting interface is being actively developed, +which will make plotting a simple task with straight-forward syntax, similar to GEMPAK. + +****** +Skew-T +****** + +The Skew-T Log-P diagram is the canonical thermodynamic diagram within meteorology. Using +:mod:`matplotlib`, MetPy is able to readily create a Skew-T for you: + +.. plot:: + :include-source: True + + import matplotlib.pyplot as plt + import numpy as np + import metpy.calc as mpcalc + from metpy.plots import SkewT + from metpy.units import units + + fig = plt.figure(figsize=(9, 9)) + skew = SkewT(fig) + + # Create arrays of pressure, temperature, dewpoint, and wind components + p = [902, 897, 893, 889, 883, 874, 866, 857, 849, 841, 833, 824, 812, 796, 776, 751, + 727, 704, 680, 656, 629, 597, 565, 533, 501, 468, 435, 401, 366, 331, 295, 258, + 220, 182, 144, 106] * units.hPa + t = [-3, -3.7, -4.1, -4.5, -5.1, -5.8, -6.5, -7.2, -7.9, -8.6, -8.9, -7.6, -6, -5.1, + -5.2, -5.6, -5.4, -4.9, -5.2, -6.3, -8.4, -11.5, -14.9, -18.4, -21.9, -25.4, + -28, -32, -37, -43, -49, -54, -56, -57, -58, -60] * units.degC + td = [-22, -22.1, -22.2, -22.3, -22.4, -22.5, -22.6, -22.7, -22.8, -22.9, -22.4, + -21.6, -21.6, -21.9, -23.6, -27.1, -31, -38, -44, -46, -43, -37, -34, -36, + -42, -46, -49, -48, -47, -49, -55, -63, -72, -88, -93, -92] * units.degC + # Calculate parcel profile + prof = mpcalc.parcel_profile(p, t[0], td[0]).to('degC') + u = np.linspace(-10, 10, len(p)) * units.knots + v = np.linspace(-20, 20, len(p)) * units.knots + + skew.plot(p, t, 'r') + skew.plot(p, td, 'g') + skew.plot(p, prof, 'k') # Plot parcel profile + skew.plot_barbs(p[::5], u[::5], v[::5]) + + skew.ax.set_xlim(-50, 15) + skew.ax.set_ylim(1000, 100) + + # Add the relevant special lines + skew.plot_dry_adiabats() + skew.plot_moist_adiabats() + skew.plot_mixing_lines() + + plt.show() + + +For some MetPy Monday videos on Skew-Ts, please watch `#16`_, `#18`_, and `#19`_. Hodographs +can also be created and plotted with a Skew-T (see MetPy Monday video `#38`_). +For more examples on how to do create Skew-Ts and Hodographs, please visit +check out the :doc:`Simple Sounding `, +:doc:`Advanced Sounding `, and +:doc:`Hodograph Inset `. + +.. _`#16`: https://youtu.be/oog6_b-844Q +.. _`#18`: https://youtu.be/quFXzaNbWXM +.. _`#19`: https://youtu.be/7QsBJTwuLvE +.. _`#38`: https://youtu.be/c0Uc7imDNv0 + +************* +Station Plots +************* + +Station plots display surface or upper-air station data in a concise manner. The creation of +these plots is made straightforward with MetPy. MetPy supplies the ability to create each +station plot and place the points on the map. The creation of 2-D cartographic maps, commonly +used in meteorology for observational and model visualization, relies upon the :mod:`CartoPy` +library. This package handles projections and transforms to make sure your data is plotted in +the correct location. + +For examples on how to make a station plot, please see the +:doc:`Station Plot ` and +:doc:`Station Plot Layout ` examples. + +************ +Gridded Data +************ + +While MetPy doesn't provide many new tools for 2-D gridded data maps, we do provide lots of +examples illustrating how to use MetPy for data analysis and CartoPy for visualization. Those +examples can be found in the :doc:`MetPy Gallery ` and the +`Unidata Python Gallery`_. + +One unique tool in MetPy for gridded data is cross-section analysis. A detailed example of how +to create a cross section with your gridded data is available +:doc:`here `. + +.. _`Unidata Python Gallery`: https://unidata.github.io/python-gallery/ + +******************** +Declarative Plotting +******************** + +The declarative plotting interface, which is still under active development, aims to replicate +the simple plotting declarations in GEMPAK to make map creation straightforward, especially +for those less familiar with Python, CartoPy, and matplotlib. To demonstrate the ease of +creating a plot with this interface, let's make a color-filled plot of temperature using +NARR data. + +.. plot:: + :include-source: True + + import xarray as xr + from metpy.cbook import get_test_data + from metpy.plots import ImagePlot, MapPanel, PanelContainer + 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)) + + img = ImagePlot() + img.data = narr + img.field = 'Geopotential_height' + img.level = 850 * units.hPa + + panel = MapPanel() + panel.area = 'us' + panel.layers = ['coastline', 'borders', 'states', 'rivers', 'ocean', 'land'] + panel.title = 'NARR Example' + panel.plots = [img] + + pc = PanelContainer() + pc.size = (10, 8) + pc.panels = [panel] + pc.show() + +Other plot types are available, including contouring to create overlay maps. For an example of +this, check out the :doc:`Combined Plotting ` example. MetPy +Monday videos `#69`_, `#70`_, and `#71`_ also demonstrate the declarative plotting interface. + +.. _`#69`: https://youtu.be/mbxE2ovXx9M +.. _`#70`: https://youtu.be/QgS27jwj8OI +.. _`#71`: https://youtu.be/RBJ8Pm7x4ok + +---------------------- +Other Python Resources +---------------------- + +While MetPy does a lot of things, it doesn't do everything. Here are some other good resources +to use as you start using MetPy and Python for meteorology and atmospheric science: + +**Training and Example Sites** + +* `Online Python Training`_ +* `Unidata Python Gallery`_ +* `Unidata Python Workshop`_ +* `MetPy Monday Playlist`_ + +**Useful Python Packages** + +* `Siphon`_: remote access of meteorological data via THREDDS servers +* `Xarray`_: reading/writing labeled N-dimensional arrays +* `Pandas`_: reading/writing tabular data +* `NumPy`_: numerical computations +* `Matplotlib`_: creation of publication-quality figures +* `CartoPy`_: publication-quality cartographic maps +* `SatPy`_: read and visualize satellite data +* `PyART`_: read and visualize radar data + +.. _Siphon: https://unidata.github.io/siphon/ +.. _`Unidata Python Workshop`: https://unidata.github.io/python-workshop +.. _`MetPy Monday Playlist`: + https://www.youtube.com/playlist?list=PLQut5OXpV-0ir4IdllSt1iEZKTwFBa7kO +.. _`Xarray`: http://xarray.pydata.org/en/stable/ +.. _`Pandas`: https://pandas.pydata.org +.. _`NumPy`: https://numpy.org/devdocs +.. _`Matplotlib`: https://matplotlib.org +.. _`CartoPy`: https://scitools.org.uk/cartopy/docs/latest/ +.. _`SatPy`: https://satpy.readthedocs.io/en/latest/ +.. _`PyART`: https://arm-doe.github.io/pyart/ + +------- +Support +------- + +Get stuck trying to use MetPy with your data? Unidata's Python team is here to help! See our +`support page `_ for more information. diff --git a/examples/formats/NEXRAD_Level_2_File.py b/examples/formats/NEXRAD_Level_2_File.py index ba64caa088..78056172f0 100644 --- a/examples/formats/NEXRAD_Level_2_File.py +++ b/examples/formats/NEXRAD_Level_2_File.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015,2018 MetPy Developers. +# Copyright (c) 2015,2018,2019 MetPy Developers. # Distributed under the terms of the BSD 3-Clause License. # SPDX-License-Identifier: BSD-3-Clause """ @@ -12,7 +12,7 @@ from metpy.cbook import get_test_data from metpy.io import Level2File -from metpy.plots import add_metpy_logo, add_timestamp, colortables +from metpy.plots import add_metpy_logo, add_timestamp ########################################### @@ -53,8 +53,7 @@ ylocs = var_range * np.cos(np.deg2rad(az[:, np.newaxis])) # Plot the data - cmap = colortables.get_colortable('viridis') - ax.pcolormesh(xlocs, ylocs, data, cmap=cmap) + ax.pcolormesh(xlocs, ylocs, data, cmap='viridis') ax.set_aspect('equal', 'datalim') ax.set_xlim(-40, 20) ax.set_ylim(-30, 30) diff --git a/examples/plots/Mesonet_Stationplot.py b/examples/plots/Mesonet_Stationplot.py new file mode 100644 index 0000000000..d505eb05dc --- /dev/null +++ b/examples/plots/Mesonet_Stationplot.py @@ -0,0 +1,107 @@ +# Copyright (c) 2019 MetPy Developers. +# Distributed under the terms of the BSD 3-Clause License. +# SPDX-License-Identifier: BSD-3-Clause +""" +Mesonet Station Plot +==================== + +Make a surface station plot with Oklahoma Mesonet data. + +The station plot itself is pretty straightforward, but there is a bit of code to perform the +data-wrangling. +""" +import cartopy.crs as ccrs +import cartopy.feature as cfeature +import matplotlib.pyplot as plt +import pandas as pd + +import metpy.calc as mpcalc +from metpy.cbook import get_test_data +from metpy.plots import add_metpy_logo, StationPlot +from metpy.units import units + +########################################### +# Read in the data and wrangle it +# ------------------------------- +# +# First read in the data. We use pandas because it simplifies a lot of tasks, like dealing +# with strings. We'll also convert any blank cells to NaNs, and then drop rows with NaNs +# in variables that we want to plot + +# 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=' ') + +# Drop stations with missing values of data we want +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. +# The number of stations plotted will be reduced using `reduce_point_density`. + +# 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) +data = data[mpcalc.reduce_point_density(point_locs, 50 * units.km)] + +########################################### +# Now that we have the data we want, we need to perform some conversions: +# +# - First, assign units to the data, as applicable +# - Convert cardinal wind direction to degrees +# - 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'] + +# 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) + +########################################### +# Create the figure +# ----------------- + +# Create the figure and an axes set to the projection. +fig = plt.figure(figsize=(20, 8)) +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')) + +# 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) + +# 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') + +# 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:]) + +# Add wind barbs +stationplot.plot_barb(u, v) + +# 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 -1 in y. +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.show() diff --git a/metpy/calc/tests/test_calc_tools.py b/metpy/calc/tests/test_calc_tools.py index 53ca9c790a..80a62844f2 100644 --- a/metpy/calc/tests/test_calc_tools.py +++ b/metpy/calc/tests/test_calc_tools.py @@ -7,6 +7,7 @@ import numpy as np import numpy.ma as ma +import pandas as pd import pytest import xarray as xr @@ -652,7 +653,7 @@ def test_laplacian_x_deprecation(deriv_2d_data): def test_parse_angle_abbrieviated(): """Test abbrieviated directional text in degrees.""" expected_angles_degrees = FULL_CIRCLE_DEGREES - output_angles_degrees = list(map(parse_angle, DIR_STRS[:-1])) + output_angles_degrees = parse_angle(DIR_STRS[:-1]) assert_array_almost_equal(output_angles_degrees, expected_angles_degrees) @@ -662,8 +663,8 @@ def test_parse_angle_ext(): '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 = FULL_CIRCLE_DEGREES - output_angles_degrees = list(map(parse_angle, test_dir_strs)) + 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) @@ -713,6 +714,29 @@ def test_parse_angle_mix_multiple_arr(): assert_array_almost_equal(output_angles_degrees, expected_angles_degrees) +def test_parse_angles_array(): + """Test array of angles to parse.""" + 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) + + +def test_parse_angles_series(): + """Test pandas.Series of angles to parse.""" + 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) + + +def test_parse_angles_single(): + """Test single input into `parse_angles`.""" + calculated_angle = parse_angle('SOUTH SOUTH EAST') + expected_angle = 157.5 * units.degree + assert_almost_equal(calculated_angle, expected_angle) + + 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)) diff --git a/metpy/calc/tests/test_thermo.py b/metpy/calc/tests/test_thermo.py index a16c4121b0..59cbb4e9bf 100644 --- a/metpy/calc/tests/test_thermo.py +++ b/metpy/calc/tests/test_thermo.py @@ -1,4 +1,4 @@ -# Copyright (c) 2008,2015,2016,2017,2018 MetPy Developers. +# Copyright (c) 2008,2015,2016,2017,2018,2019 MetPy Developers. # Distributed under the terms of the BSD 3-Clause License. # SPDX-License-Identifier: BSD-3-Clause """Test the `thermo` module.""" @@ -260,6 +260,16 @@ def test_lcl(): assert_almost_equal(lcl_temperature, 17.676 * units.degC, 2) +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) + 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 + + def test_lcl_convergence(): """Test LCL calculation convergence failure.""" with pytest.raises(RuntimeError): @@ -276,6 +286,19 @@ def test_lfc_basic(): assert_almost_equal(lfc_temp, 9.705 * units.celsius, 2) +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 + 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) + assert lfc_temp.units == temperature.units + + def test_lfc_ml(): """Test Mixed-Layer LFC calculation.""" levels = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.mbar @@ -499,6 +522,17 @@ def test_el(): assert_almost_equal(el_temperature, -11.7027 * units.degC, 3) +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 + 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) + assert el_temp.units == temperatures.units + + 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 @@ -535,6 +569,20 @@ def test_no_el_multi_crossing(): assert_nan(el_temperature, temperatures.units) +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 + 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) + assert_nan(lfc_pressure, pressure.units) + assert_nan(lfc_temperature, temperature.units) + assert_nan(el_pressure, pressure.units) + assert_nan(el_temperature, temperature.units) + + 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., diff --git a/metpy/calc/thermo.py b/metpy/calc/thermo.py index 00628858fa..feb6666265 100644 --- a/metpy/calc/thermo.py +++ b/metpy/calc/thermo.py @@ -1,4 +1,4 @@ -# Copyright (c) 2008,2015,2016,2017,2018 MetPy Developers. +# Copyright (c) 2008,2015,2016,2017,2018,2019 MetPy Developers. # Distributed under the terms of the BSD 3-Clause License. # SPDX-License-Identifier: BSD-3-Clause """Contains a collection of thermodynamic calculations.""" @@ -359,7 +359,7 @@ def _lcl_iter(p, p0, w, t): fp = so.fixed_point(_lcl_iter, pressure.m, args=(pressure.m, w, temperature), xtol=eps, maxiter=max_iters) lcl_p = fp * pressure.units - return lcl_p, dewpoint(vapor_pressure(lcl_p, w)) + return lcl_p, dewpoint(vapor_pressure(lcl_p, w)).to(temperature.units) @exporter.export @@ -406,8 +406,7 @@ def lfc(pressure, temperature, dewpt, parcel_temperature_profile=None, dewpt_sta if parcel_temperature_profile is None: new_stuff = parcel_profile_with_lcl(pressure, temperature, dewpt) pressure, temperature, _, parcel_temperature_profile = new_stuff - temperature = temperature.to('degC') - parcel_temperature_profile = parcel_temperature_profile.to('degC') + parcel_temperature_profile = parcel_temperature_profile.to(temperature.units) if dewpt_start is None: dewpt_start = dewpt[0] @@ -436,17 +435,23 @@ def lfc(pressure, temperature, dewpt, parcel_temperature_profile=None, dewpt_sta mask = pressure < this_lcl[0] if np.all(_less_or_close(parcel_temperature_profile[mask], temperature[mask])): # LFC doesn't exist - return np.nan * pressure.units, np.nan * temperature.units + x, y = np.nan * pressure.units, np.nan * temperature.units else: # LFC = LCL x, y = this_lcl - return x, y + return x, y # LFC exists. Make sure it is no lower than the LCL else: idx = x < this_lcl[0] # LFC height < LCL height, so set LFC = LCL if not any(idx): - x, y = this_lcl + 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: + x, y = this_lcl return x, y # Otherwise, find all LFCs that exist above the LCL # What is returned depends on which flag as described in the docstring @@ -508,8 +513,7 @@ def el(pressure, temperature, dewpt, parcel_temperature_profile=None, which='top if parcel_temperature_profile is None: new_stuff = parcel_profile_with_lcl(pressure, temperature, dewpt) pressure, temperature, _, parcel_temperature_profile = new_stuff - temperature = temperature.to('degC') - parcel_temperature_profile = parcel_temperature_profile.to('degC') + parcel_temperature_profile = parcel_temperature_profile.to(temperature.units) # If the top of the sounding parcel is warmer than the environment, there is no EL if parcel_temperature_profile[-1] > temperature[-1]: diff --git a/metpy/calc/tools.py b/metpy/calc/tools.py index 52e10251a3..00fd050286 100644 --- a/metpy/calc/tools.py +++ b/metpy/calc/tools.py @@ -1335,7 +1335,7 @@ def parse_angle(input_dir): Returns ------- - angle + `pint.Quantity` The angle in degrees """ diff --git a/metpy/cbook.py b/metpy/cbook.py index 7a3d51fd17..ae630c89f7 100644 --- a/metpy/cbook.py +++ b/metpy/cbook.py @@ -26,14 +26,15 @@ def is_string_like(s): path=pooch.os_cache('metpy'), base_url='https://github.com/Unidata/MetPy/raw/{version}/staticdata/', version='v' + __version__, - version_dev='master', - env='TEST_DATA_DIR') - -# Check if we're running from a git clone and if so, bash the path attribute with the path -# to git's local data store (un-versioned) -# Look for the staticdata directory (i.e. this is a git checkout) -if os.path.exists(os.path.join(os.path.dirname(__file__), '..', 'staticdata')): - POOCH.path = os.path.join(os.path.dirname(__file__), '..', 'staticdata') + 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', + os.path.join(os.path.dirname(__file__), '..', 'staticdata')) +if os.path.exists(dev_data_path): + POOCH.path = dev_data_path POOCH.load_registry(os.path.join(os.path.dirname(__file__), 'static-data-manifest.txt')) diff --git a/metpy/plots/ctables.py b/metpy/plots/ctables.py index f163ae8a3a..0dfe5b43d1 100644 --- a/metpy/plots/ctables.py +++ b/metpy/plots/ctables.py @@ -1,4 +1,4 @@ -# Copyright (c) 2014,2015,2017 MetPy Developers. +# Copyright (c) 2014,2015,2017,2019 MetPy Developers. # Distributed under the terms of the BSD 3-Clause License. # SPDX-License-Identifier: BSD-3-Clause """Work with custom color tables. @@ -46,10 +46,12 @@ def plot_color_gradients(cmap_category, cmap_list, nrows): import logging import os.path import posixpath +import warnings import matplotlib.colors as mcolors from pkg_resources import resource_listdir, resource_stream +from ..deprecation import metpyDeprecation from ..package_tools import Exporter exporter = Exporter(globals()) @@ -127,6 +129,13 @@ class ColortableRegistry(dict): matplotlib's Normalize instances to go with the colortable. """ + def __getitem__(self, key): + """Handle viridis deprecation.""" + if key == 'viridis': + warnings.warn('Viridis has been deprecated in v0.11. Please use ' + "matplotlib's 'viridis'.", metpyDeprecation) + return super(ColortableRegistry, self).__getitem__(key) + def scan_resource(self, pkg, path): r"""Scan a resource directory for colortable files and add them to the registry. @@ -175,8 +184,10 @@ def add_colortable(self, fobj, name): The name under which the color table will be stored """ - self[name] = read_colortable(fobj) - self[name + '_r'] = self[name][::-1] + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', category=metpyDeprecation) + self[name] = read_colortable(fobj) + 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. diff --git a/metpy/plots/skewt.py b/metpy/plots/skewt.py index 48fe66a31c..0d515713d0 100644 --- a/metpy/plots/skewt.py +++ b/metpy/plots/skewt.py @@ -299,6 +299,10 @@ def __init__(self, fig=None, rotation=30, subplot=None, rect=None): self.ax = fig.add_subplot(*subplot, projection='skewx', rotation=rotation) self.ax.grid(True) + self.mixing_lines = None + self.dry_adiabats = None + self.moist_adiabats = None + def plot(self, p, t, *args, **kwargs): r"""Plot data. @@ -448,6 +452,10 @@ def plot_dry_adiabats(self, t0=None, p=None, **kwargs): :class:`matplotlib.collections.LineCollection` """ + # Remove old lines + if self.dry_adiabats: + self.dry_adiabats.remove() + # Determine set of starting temps if necessary if t0 is None: xmin, xmax = self.ax.get_xlim() @@ -465,7 +473,8 @@ def plot_dry_adiabats(self, t0=None, p=None, **kwargs): kwargs.setdefault('colors', 'r') kwargs.setdefault('linestyles', 'dashed') kwargs.setdefault('alpha', 0.5) - return self.ax.add_collection(LineCollection(linedata, **kwargs)) + self.dry_adiabats = self.ax.add_collection(LineCollection(linedata, **kwargs)) + return self.dry_adiabats def plot_moist_adiabats(self, t0=None, p=None, **kwargs): r"""Plot moist adiabats. @@ -500,6 +509,10 @@ def plot_moist_adiabats(self, t0=None, p=None, **kwargs): :class:`matplotlib.collections.LineCollection` """ + # Remove old lines + if self.moist_adiabats: + self.moist_adiabats.remove() + # Determine set of starting temps if necessary if t0 is None: xmin, xmax = self.ax.get_xlim() @@ -518,7 +531,8 @@ def plot_moist_adiabats(self, t0=None, p=None, **kwargs): kwargs.setdefault('colors', 'b') kwargs.setdefault('linestyles', 'dashed') kwargs.setdefault('alpha', 0.5) - return self.ax.add_collection(LineCollection(linedata, **kwargs)) + self.moist_adiabats = self.ax.add_collection(LineCollection(linedata, **kwargs)) + return self.moist_adiabats def plot_mixing_lines(self, w=None, p=None, **kwargs): r"""Plot lines of constant mixing ratio. @@ -549,6 +563,10 @@ def plot_mixing_lines(self, w=None, p=None, **kwargs): :class:`matplotlib.collections.LineCollection` """ + # Remove old lines + if self.mixing_lines: + self.mixing_lines.remove() + # Default mixing level values if necessary if w is None: w = np.array([0.0004, 0.001, 0.002, 0.004, 0.007, 0.01, @@ -566,7 +584,8 @@ def plot_mixing_lines(self, w=None, p=None, **kwargs): kwargs.setdefault('colors', 'g') kwargs.setdefault('linestyles', 'dashed') kwargs.setdefault('alpha', 0.8) - return self.ax.add_collection(LineCollection(linedata, **kwargs)) + 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): r"""Shade area between two curves. diff --git a/metpy/plots/tests/test_ctables.py b/metpy/plots/tests/test_ctables.py index 108b4919f2..be19b8e9fd 100644 --- a/metpy/plots/tests/test_ctables.py +++ b/metpy/plots/tests/test_ctables.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015,2016,2017 MetPy Developers. +# Copyright (c) 2015,2016,2017,2019 MetPy Developers. # Distributed under the terms of the BSD 3-Clause License. # SPDX-License-Identifier: BSD-3-Clause """Tests for the `ctables` module.""" @@ -15,6 +15,7 @@ import numpy as np import pytest +from metpy.deprecation import MetpyDeprecationWarning from metpy.plots.ctables import ColortableRegistry, convert_gempak_table @@ -136,3 +137,10 @@ def test_gempak(): result = outfile.read() assert result == '(0.000000, 0.000000, 0.000000)\n(1.000000, 1.000000, 1.000000)\n' + + +def test_viridis(registry): + """Test viridis deprecation warning.""" + with pytest.warns(MetpyDeprecationWarning): + registry.scan_resource('metpy.plots', 'colortable_files') + registry.get_colortable('viridis') diff --git a/metpy/plots/tests/test_skewt.py b/metpy/plots/tests/test_skewt.py index 0311194ca3..a716be1c77 100644 --- a/metpy/plots/tests/test_skewt.py +++ b/metpy/plots/tests/test_skewt.py @@ -42,6 +42,11 @@ def test_skewt_api(): skew.plot_moist_adiabats() skew.plot_mixing_lines() + # Call again to hit removal statements + skew.plot_dry_adiabats() + skew.plot_moist_adiabats() + skew.plot_mixing_lines() + return fig diff --git a/metpy/static-data-manifest.txt b/metpy/static-data-manifest.txt index 276adc239e..f6cb419fe3 100644 --- a/metpy/static-data-manifest.txt +++ b/metpy/static-data-manifest.txt @@ -22,6 +22,7 @@ jan20_sounding.txt 3de8c3a9daeffbfec3b6de9c67e14fe42728c4d6c2024d4543e2e74d4fb57 linear_test.npz f1d22fe85cb602c8997d0008cbc44363da30fed805302c4bbb34bfbc7b37270a may22_sounding.txt 33cc9a2a6964cc6f0baa6bf9d8893f3d7b164c79953f695c1339e30189de19f0 may4_sounding.txt b3a7c3ee4b1bdb1e1961492265c23f42947dc627156cfacb5fdebac0ad0b4350 +mesonet_sample.txt cd6693e76983683898ae6121e2e9690978eeb8bca431693c3bdcacdb806cdf0c narr_example.nc 00bbe42b4dc90cc95cf5d12f3f5e7d5508cb2592d72dc8081ebacea8a526fb1c natural_neighbor_test.npz df7c07ee4ee05572552831ffea17f56ad664fbd2fc6c261d22a82baeecfa8a7a nearest_test.npz 85fb955573de48280067efc097b77d1314da88b6af3abc39a02b9540db63ef7f diff --git a/metpy/xarray.py b/metpy/xarray.py index 58c5fb5505..8c1204a4d4 100644 --- a/metpy/xarray.py +++ b/metpy/xarray.py @@ -1,7 +1,20 @@ -# Copyright (c) 2018 MetPy Developers. +# Copyright (c) 2018,2019 MetPy Developers. # Distributed under the terms of the BSD 3-Clause License. # SPDX-License-Identifier: BSD-3-Clause -"""Provide accessors to enhance interoperability between XArray and MetPy.""" +"""Provide accessors to enhance interoperability between xarray and MetPy. + +MetPy relies upon the `CF Conventions `_. to provide helpful +attributes and methods on xarray DataArrays and Dataset for working with +coordinate-related metadata. Also included are several attributes and methods for unit +operations. + +These accessors will be activated with any import of MetPy. Do not use the +``MetPyDataArrayAccessor`` or ``MetPyDatasetAccessor`` classes directly, instead, utilize the +applicable properties and methods via the ``.metpy`` attribute on an xarray DataArray or +Dataset. + +See Also: :doc:`xarray with MetPy Tutorial `. +""" from __future__ import absolute_import import functools @@ -78,15 +91,34 @@ @xr.register_dataarray_accessor('metpy') class MetPyDataArrayAccessor(object): - """Provide custom attributes and methods on XArray DataArray for MetPy functionality.""" + r"""Provide custom attributes and methods on xarray DataArrays for MetPy functionality. + + This accessor provides several convenient attributes and methods through the `.metpy` + attribute on a DataArray. For example, MetPy can identify the coordinate corresponding + to a particular axis (given sufficent metadata): + + >>> import xarray as xr + >>> temperature = xr.DataArray([[0, 1], [2, 3]], dims=('lat', 'lon'), + ... coords={'lat': [40, 41], 'lon': [-105, -104]}, + ... attrs={'units': 'degC'}) + >>> temperature.metpy.x + + array([-105, -104]) + Coordinates: + * lon (lon) int64 -105 -104 + Attributes: + _metpy_axis: X - def __init__(self, data_array): - """Initialize accessor with a DataArray.""" + """ + + def __init__(self, data_array): # noqa: D107 + # Initialize accessor with a DataArray. (Do not use directly). self._data_array = data_array self._units = self._data_array.attrs.get('units', 'dimensionless') @property def units(self): + """Return the units of this DataArray as a `pint.Quantity`.""" if self._units != '%': return units(self._units) else: @@ -94,7 +126,7 @@ def units(self): @property def unit_array(self): - """Return data values as a `pint.Quantity`.""" + """Return the data values of this DataArray as a `pint.Quantity`.""" return self._data_array.values * self.units @unit_array.setter @@ -109,7 +141,7 @@ def convert_units(self, units): @property def crs(self): - """Provide easy access to the `crs` coordinate.""" + """Return the coordinate reference system (CRS) as a CFProjection object.""" if 'crs' in self._data_array.coords: return self._data_array.coords['crs'].item() raise AttributeError('crs attribute is not available.') @@ -131,7 +163,17 @@ def _fixup_coordinate_map(self, coord_map): coord_map[axis] = self._data_array[coord_map[axis]] def assign_coordinates(self, coordinates): - """Assign the given coordinates to the given CF axis types.""" + """Assign the given coordinates to the given CF axis types. + + Parameters + ---------- + coordinates : dict or None + Mapping from axis types ('T', 'Z', 'Y', 'X') to coordinates of this DataArray. + Coordinates can either be specified directly or by their name. If ``None``, clears + the `_metpy_axis` attribute on all coordinates, which will trigger reparsing of + all coordinates on next access. + + """ if coordinates: # Assign the _metpy_axis attributes according to supplied mapping self._fixup_coordinate_map(coordinates) @@ -224,24 +266,42 @@ def _axis(self, axis): raise AttributeError("'" + axis + "' is not an interpretable axis.") def coordinates(self, *args): - """Return the coordinate variables corresponding to the given axes types.""" + """Return the coordinate variables corresponding to the given axes types. + + Parameters + ---------- + args : str + Strings describing the axes type(s) to obtain. Currently understood types are + 'time', 'vertical', 'y', and 'x'. + + Notes + ----- + This method is designed for use with mutliple coordinates; it returns a generator. To + access a single coordinate, use the appropriate attribute on the accessor, or use tuple + unpacking. + + """ for arg in args: yield self._axis(arg) @property def time(self): + """Return the time coordinate.""" return self._axis('time') @property def vertical(self): + """Return the vertical coordinate.""" return self._axis('vertical') @property def y(self): + """Return the y or latitude coordinate.""" return self._axis('y') @property def x(self): + """Return the x or longitude coordinate.""" return self._axis('x') def coordinates_identical(self, other): @@ -273,8 +333,12 @@ def as_timestamp(self): def find_axis_name(self, axis): """Return the name of the axis corresponding to the given identifier. - The given indentifer can be an axis number (integer), dimension coordinate name - (string) or a standard axis type (string). + Parameters + ---------- + axis : str or int + Identifier for an axis. Can be the an axis number (integer), dimension coordinate + name (string) or a standard axis type (string). + """ if isinstance(axis, int): # If an integer, use the corresponding dimension @@ -314,11 +378,11 @@ def __setitem__(self, key, value): @property def loc(self): - """Make the LocIndexer available as a property.""" + """Wrap DataArray.loc with an indexer to handle units and coordinate types.""" return self._LocIndexer(self._data_array) def sel(self, indexers=None, method=None, tolerance=None, drop=False, **indexers_kwargs): - """Wrap DataArray.sel to handle units.""" + """Wrap DataArray.sel to handle units and coordinate types.""" 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) @@ -326,10 +390,20 @@ def sel(self, indexers=None, method=None, tolerance=None, drop=False, **indexers @xr.register_dataset_accessor('metpy') class MetPyDatasetAccessor(object): - """Provide custom attributes and methods on XArray Dataset for MetPy functionality.""" + """Provide custom attributes and methods on XArray Datasets for MetPy functionality. + + This accessor provides parsing of CF metadata and unit-/coordinate-type-aware selection. + + >>> import xarray as xr + >>> from metpy.testing import get_test_data + >>> ds = xr.open_dataset(get_test_data('narr_example.nc', False)).metpy.parse_cf() + >>> print(ds['crs'].item()) + Projection: lambert_conformal_conic - def __init__(self, dataset): - """Initialize accessor with a Dataset.""" + """ + + def __init__(self, dataset): # noqa: D107 + # Initialize accessor with a Dataset. (Do not use directly). self._dataset = dataset def parse_cf(self, varname=None, coordinates=None): @@ -426,7 +500,7 @@ def __getitem__(self, key): @property def loc(self): - """Make the LocIndexer available as a property.""" + """Wrap Dataset.loc with an indexer to handle units and coordinate types.""" return self._LocIndexer(self._dataset) def sel(self, indexers=None, method=None, tolerance=None, drop=False, **indexers_kwargs): @@ -437,7 +511,17 @@ def sel(self, indexers=None, method=None, tolerance=None, drop=False, **indexers def check_axis(var, *axes): - """Check if var satisfies the criteria for any of the given axes.""" + """Check if the criteria for any of the given axes are satisfied. + + Parameters + ---------- + var : `xarray.DataArray` + DataArray belonging to the coordinate to be checked + axes : str + Axis type(s) to check for. Currently can check for 'time', 'vertical', 'y', 'lat', 'x', + and 'lon'. + + """ for axis in axes: # Check for # - standard name (CF option) @@ -549,3 +633,6 @@ def _to_magnitude(val, unit): data[coord_name].metpy.units) return indexers + + +__all__ = ('MetPyDataArrayAccessor', 'MetPyDatasetAccessor') diff --git a/setup.cfg b/setup.cfg index f858a5951b..853e9c6b0d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -81,6 +81,7 @@ multiline-quotes = double rst-roles = class, data, func, meth, mod rst-directives = plot docstring-convention = numpy +per-file-ignores = metpy/xarray.py:RST304 [tool:pytest] # https://github.com/matplotlib/pytest-mpl/issues/69 @@ -96,6 +97,7 @@ flake8-ignore = *.py F405 W503 RST902 metpy/interpolate/*.py RST306 metpy/io/__init__.py D999 metpy/plots/__init__.py D999 + metpy/xarray.py RST304 flake8-max-line-length = 95 doctest_optionflags = NORMALIZE_WHITESPACE mpl-results-path = test_output diff --git a/staticdata/mesonet_sample.txt b/staticdata/mesonet_sample.txt new file mode 100644 index 0000000000..298e07fcd7 --- /dev/null +++ b/staticdata/mesonet_sample.txt @@ -0,0 +1,121 @@ +STID,NAME,ST,LAT,LON,YR,MO,DA,HR,MI,TAIR,TDEW,RELH,CHIL,HEAT,WDIR,WSPD,WMAX,PRES,TMAX,TMIN,RAIN +ACME,Acme,OK,34.81,-98.02,2019,9,9,14,55, , , , , , , , , , , , +ADAX,Ada,OK,34.80,-96.67,2019,9,9,14,55,91,68,47, ,95,SSE,12,20,1014.72,91,72, +ALTU,Altus,OK,34.59,-99.34,2019,9,9,14,55,92,64,40, ,93,S,22,28,1011.61,92,73, +ALV2,Alva,OK,36.71,-98.71,2019,9,9,14,55,92,62,37, ,93,S,20,26,1011.31,93,72, +ANT2,Antlers,OK,34.25,-95.67,2019,9,9,14,55,94,63,36, ,95,SSE,9,15,1014.86,95,63, +APAC,Apache,OK,34.91,-98.29,2019,9,9,14,55,92,65,41, ,95,SSE,19,26,1013.18,93,71, +ARD2,Ardmore,OK,34.19,-97.09,2019,9,9,14,55,91,68,48, ,95,S,12,17,1014.36,91,72, +ARNE,Arnett,OK,36.07,-99.90,2019,9,9,14,55,89,61,39, ,89,S,19,31,1012.82,92,67,0.02 +BEAV,Beaver,OK,36.80,-100.53,2019,9,9,14,55,87,61,42, ,87,SSW,20,28,1011.56,89,69, +BESS,Bessie,OK,35.40,-99.06,2019,9,9,14,55,90,62,40, ,90,S,18,24,1012.42,90,75, +BIXB,Bixby,OK,35.96,-95.87,2019,9,9,14,55,92,71,51, ,98,SSE,13,19,1014.01,92,68, +BLAC,Blackwell,OK,36.75,-97.25,2019,9,9,14,55,93,61,35, ,93,S,17,22,1012.47,93,70, +BOIS,Boise City,OK,36.69,-102.50,2019,9,9,14,55,86,59,40, ,86,S,18,23,1013.07,86,62,0.43 +BREC,Breckinridge,OK,36.41,-97.69,2019,9,9,14,55,93,65,40, ,95,S,19,30,1012.31,94,74, +BRIS,Bristow,OK,35.78,-96.35,2019,9,9,14,55,90,67,47, ,94,S,9,14,1013.90,91,71, +BROK,Broken Bow,OK,34.04,-94.62,2019,9,9,14,55,95,65,37, ,97,SSW,6,12,1014.76,96,66, +BUFF,Buffalo,OK,36.83,-99.64,2019,9,9,14,55, , , , , , , , , ,91,72, +BURB,Burbank,OK,36.63,-96.81,2019,9,9,14,55,89,67,49, ,92,S,13,18,1013.22,91,71, +BURN,Burneyville,OK,33.89,-97.27,2019,9,9,14,55,90,70,52, ,96,SE,10,17,1014.19,90,65, +BUTL,Butler,OK,35.59,-99.27,2019,9,9,14,55,89,64,44, ,91,S,15,22,1012.62,92,77, +BYAR,Byars,OK,34.85,-97.00,2019,9,9,14,55,90,65,44, ,92,S,18,28,1014.68,91,73, +CAMA,Camargo,OK,36.03,-99.35,2019,9,9,14,55,89,64,44, ,91,S,25,30,1012.56,92,68,0.07 +CARL,Lake Carl Blackwell,OK,36.15,-97.29,2019,9,9,14,55,92,61,36, ,92,S,16,24,1012.44,93,72, +CENT,Centrahoma,OK,34.61,-96.33,2019,9,9,14,55,92,67,44, ,95,S,11,18,1014.45,92,67, +CHAN,Chandler,OK,35.65,-96.80,2019,9,9,14,55,90,64,41, ,92,SSE,14,23,1013.95,91,70, +CHER,Cherokee,OK,36.75,-98.36,2019,9,9,14,55,93,66,42, ,96,S,20,26,1011.34,94,75, +CHEY,Cheyenne,OK,35.55,-99.73,2019,9,9,14,55,88,61,41, ,88,SSW,18,26,1013.39,91,71, +CHIC,Chickasha,OK,35.03,-97.91,2019,9,9,14,55,94,62,35, ,95,S,20,26,1012.89,95,72, +CLAY,Clayton,OK,34.66,-95.33,2019,9,9,14,55,93,60,33, ,92,SSW,8,12,1014.77,93,65, +CLOU,Cloudy,OK,34.22,-95.25,2019,9,9,14,55,93,62,36, ,94,S,7,14,1015.58,94,66, +COOK,Cookson,OK,35.68,-94.85,2019,9,9,14,55,90,70,53, ,96,S,3,7,1015.57,90,69, +COPA,Copan,OK,36.91,-95.89,2019,9,9,14,55,91,68,47, ,95,S,15,21,1013.75,92,72, +DURA,Durant,OK,33.92,-96.32,2019,9,9,14,55,93,66,41, ,95,SSE,11,16,1014.54,93,69, +ELKC,Elk City,OK,35.33,-99.39,2019,9,9,14,55,88,61,40, ,88,S,20,26,1012.44,92,75, +ELRE,El Reno,OK,35.55,-98.04,2019,9,9,14,55,91,66,43, ,94,SSE,20,28,1012.99,93,68, +ERIC,Erick,OK,35.20,-99.80,2019,9,9,14,55,91,60,35, ,91,S,14,19,1012.59,92,72, +EUFA,Eufaula,OK,35.30,-95.66,2019,9,9,14,55,93,67,42, ,97,S,12,17,1014.18,93,71, +EVAX,Eva,OK,36.92,-101.78,2019,9,9,14,55,90,62,39, ,90,SSW,22,29,1012.17,90,65,0.04 +FAIR,Fairview,OK,36.26,-98.50,2019,9,9,14,55,94,64,38, ,96,SSW,19,27,1011.63,94,74, +FITT,Fittstown,OK,34.55,-96.72,2019,9,9,14,55,89,69,53, ,93,SSE,13,18,1015.08,89,67, +FORA,Foraker,OK,36.84,-96.43,2019,9,9,14,55,90,68,49, ,94,SSE,18,26,1013.63,90,68, +FREE,Freedom,OK,36.73,-99.14,2019,9,9,14,55,91,62,38, ,92,S,23,32,1011.57,94,69,0.05 +FTCB,Fort Cobb,OK,35.15,-98.47,2019,9,9,14,55,88,65,46, ,90,SSE,21,28,1012.64,91,68, +GOOD,Goodwell,OK,36.60,-101.60,2019,9,9,14,55,86,64,47, ,87,S,22,30,1011.97,88,66,0.01 +GRA2,Grandfield,OK,34.24,-98.74,2019,9,9,14,55,99,61,29, ,100,S,18,27,1012.18,99,76, +GUTH,Guthrie,OK,35.85,-97.48,2019,9,9,14,55,93,64,38, ,95,SSE,15,21,1012.52,94,74, +HASK,Haskell,OK,35.75,-95.64,2019,9,9,14,55,92,70,48, ,98,S,13,19,1014.26,93,69, +HECT,Hectorville,OK,35.84,-96.00,2019,9,9,14,55,92,65,41, ,94,S,9,16,1014.31,92,72, +HINT,Hinton,OK,35.48,-98.48,2019,9,9,14,55,89,65,45, ,91,SSE,16,24,1013.03,93,70, +HOBA,Hobart,OK,34.99,-99.05,2019,9,9,14,55,93,61,34, ,93,S,21,27,1012.36,93,74, +HOLD,Holdenville,OK,35.07,-96.36,2019,9,9,14,55,90,65,44, ,93,S,12,17,1014.52,91,70, +HOLL,Hollis,OK,34.69,-99.83,2019,9,9,14,55,94,59,32, ,93,S,18,25,1011.82,94,76, +HOOK,Hooker,OK,36.86,-101.23,2019,9,9,14,55,87,62,44, ,88,SSW,22,31,1011.87,89,68, +HUGO,Hugo,OK,34.03,-95.54,2019,9,9,14,55,93,65,40, ,95,SSW,14,20,1015.21,93,70, +IDAB,Idabel,OK,33.83,-94.88,2019,9,9,14,55,94,65,39, ,96,SW,10,14,1015.16,94,67, +INOL,Inola,OK,36.14,-95.45,2019,9,9,14,55,91,71,51, ,98,SSE,13,19,1014.71,92,67, +JAYX,Jay,OK,36.48,-94.78,2019,9,9,14,55,90,67,47, ,93,SSW,9,17,1015.86,91,72, +KENT,Kenton,OK,36.83,-102.88,2019,9,9,14,55,89,54,31, ,87,SSW,20,29,1012.63,90,61,0.49 +KETC,Ketchum Ranch,OK,34.53,-97.76,2019,9,9,14,55,92,64,40, ,94,SSE,15,24,1013.69,93,73, +KIN2,Kingfisher,OK,35.85,-97.95,2019,9,9,14,55,94,66,39, ,97,S,23,29,1012.13,96,75, +LAHO,Lahoma,OK,36.38,-98.11,2019,9,9,14,55,92,65,42, ,94,S,19,25,1012.04,93,71, +LANE,Lane,OK,34.31,-96.00,2019,9,9,14,55,91,66,43, ,94,S,10,19,1014.62,94,66, +MADI,Madill,OK,34.04,-96.94,2019,9,9,14,55,91,68,48, ,95,SSW,9,13,1014.30,91,70, +MANG,Mangum,OK,34.84,-99.42,2019,9,9,14,55,94,61,34, ,94,S,17,24,1011.88,94,69, +MARE,Marena,OK,36.06,-97.21,2019,9,9,14,55,92, , , , ,SSE,16,24,1012.92,92,71, +MAYR,May Ranch,OK,36.99,-99.01,2019,9,9,14,55,92,64,40, ,94,S,18,28,1011.77,93,71, +MCAL,McAlester,OK,34.88,-95.78,2019,9,9,14,55,93,64,38, ,94,S,6,13,1014.66,94,69, +MEDF,Medford,OK,36.79,-97.75,2019,9,9,14,55,94,65,38, ,96,SSE,23,33,1011.67,95,77, +MEDI,Medicine Park,OK,34.73,-98.57,2019,9,9,14,55,94,62,34, ,95,S,20,33,1013.01,95,72, +MIAM,Miami,OK,36.89,-94.84,2019,9,9,14,55,90,69,49, ,95,SSW,16,21,1014.93,91,74, +MINC,Minco,OK,35.27,-97.96,2019,9,9,14,55,91,65,42, ,94,S,23,28,1013.22,91,69, +MRSH,Marshall,OK,36.12,-97.61,2019,9,9,14,55,94,63,36, ,96,S,17,23,1012.03,95,75, +MTHE,Mt Herman,OK,34.31,-94.82,2019,9,9,14,55,90,64,41, ,92,SSE,9,15,1016.26,91,68, +NEWK,Newkirk,OK,36.90,-96.91,2019,9,9,14,55,90,66,45, ,92,S,15,21,1013.54,90,73, +NEWP,Newport,OK,34.23,-97.20,2019,9,9,14,55,90,70,52, ,95,SSE,14,19,1014.24,90,69, +NOWA,Nowata,OK,36.74,-95.61,2019,9,9,14,55,90,71,53, ,97,S,17,23,1014.08,91,71, +NRMN,Norman,OK,35.24,-97.46,2019,9,9,14,55,90,67,46, ,93,SSE,12,19,1013.85,91,73, +OILT,Oilton,OK,36.03,-96.50,2019,9,9,14,55,91,67,45, ,95,SSE,12,19,1013.71,91,72, +OKCE,Oklahoma City East,OK,35.47,-97.46,2019,9,9,14,55,91,65,42, ,93,SSE,12,21,1013.52,92,74, +OKEM,Okemah,OK,35.43,-96.26,2019,9,9,14,55,92,65,42, ,94,S,11,17,1014.54,94,70, +OKMU,Okmulgee,OK,35.58,-95.91,2019,9,9,14,55,92,64,40, ,94,S,10,16,1014.34,93,64, +PAUL,Pauls Valley,OK,34.72,-97.23,2019,9,9,14,55,90,69,50, ,94,SSE,11,19,1014.27,91,68, +PAWN,Pawnee,OK,36.36,-96.77,2019,9,9,14,55,89,66,46, ,92,S,13,19,1013.48,92,71, +PERK,Perkins,OK,36.00,-97.05,2019,9,9,14,55,93,60,33, ,93,SSE,19,26,1013.17,94,71, +PORT,Porter,OK,35.83,-95.56,2019,9,9,14,55,92,70,49, ,98,SSE,11,17,1014.72,92,72, +PRYO,Pryor,OK,36.37,-95.27,2019,9,9,14,55,90,72,55, ,97,S,12,18,1014.73,90,71, +PUTN,Putnam,OK,35.90,-98.96,2019,9,9,14,55,89,64,44, ,90,S,20,24,1013.27,93,75, +REDR,Red Rock,OK,36.36,-97.15,2019,9,9,14,55,93,63,37, ,94,S,15,21,1012.58,93,72, +RING,Ringling,OK,34.19,-97.59,2019,9,9,14,55,94,63,37, ,95,S,19,24,1013.81,94,68, +SALL,Sallisaw,OK,35.44,-94.80,2019,9,9,14,55,94,72,49, ,102,S,6,10,1014.76,94,71, +SEIL,Seiling,OK,36.19,-99.04,2019,9,9,14,55,93,63,37, ,94,SSW,17,24,1012.49,93,75, +SEMI,Seminole,OK,35.18,-96.70,2019,9,9,14,55,90,67,48, ,93,S,12,20,1014.50,90,70, +SHAW,Shawnee,OK,35.36,-96.95,2019,9,9,14,55,92,67,44, ,95,SSE,19,26,1014.27,93,71, +SKIA,Skiatook,OK,36.42,-96.04,2019,9,9,14,55,90,68,48, ,94,S,11,16,1014.16,91,71, +SLAP,Slapout,OK,36.60,-100.26,2019,9,9,14,55,89,62,40, ,90,SSW,22,30,1012.08,91,70,0.01 +SPEN,Spencer,OK,35.54,-97.34,2019,9,9,14,55,90,67,46, ,94,SSE,12,19,1013.65,91,74, +STIG,Stigler,OK,35.27,-95.18,2019,9,9,14,55,94,63,36, ,95,S,15,23,1014.46,93,66, +STIL,Stillwater,OK,36.12,-97.10,2019,9,9,14,55,93,60,34, ,92,S,11,19,1012.89,93,73, +STUA,Stuart,OK,34.88,-96.07,2019,9,9,14,55,94,64,38, ,96,S,13,22,1014.82,94,66, +SULP,Sulphur,OK,34.57,-96.95,2019,9,9,14,55,90,68,49, ,94,S,14,21,1014.86,90,71, +TAHL,Tahlequah,OK,35.97,-94.99,2019,9,9,14,55,90,70,51, ,95,S,11,16,1015.86,91,71, +TALA,Talala,OK,36.57,-95.75,2019,9,9,14,55,90,70,52, ,97,SSE,20,26,1014.34,91,71, +TALI,Talihina,OK,34.71,-95.01,2019,9,9,14,55,95,62,34, ,96,S,8,13,1015.15,95,64, +TIPT,Tipton,OK,34.44,-99.14,2019,9,9,14,55,97,60,30, ,97,S,20,27,1011.82,97,75, +TISH,Tishomingo,OK,34.33,-96.68,2019,9,9,14,55,90,69,50, ,95,SSE,16,22,1014.74,90,66, +TULN,Tulsa,OK,36.20,-95.94,2019,9,9,14,55,91,69,48, ,96,SSE,11,20,1014.16,91,76, +VALL,Valliant,OK,33.94,-95.11,2019,9,9,14,55,95,67,40, ,98,S,9,12,1015.15,95,67, +VINI,Vinita,OK,36.78,-95.22,2019,9,9,14,55,90,71,54, ,96,SSW,14,17,1014.88,90,70, +WAL2,Walters,OK,34.40,-98.35,2019,9,9,14,55,95,64,37, ,97,S,24,30,1012.66,95,70, +WASH,Washington,OK,34.98,-97.52,2019,9,9,14,55,93,68,44, ,97,S,12,17,1013.93,93,66, +WATO,Watonga,OK,35.84,-98.53,2019,9,9,14,55,90,66,44, ,93,S,20,29,1013.23,94,75, +WAUR,Waurika,OK,34.17,-97.99,2019,9,9,14,55,93,62,36, ,94,S,15,22,1013.44,93,71, +WEAT,Weatherford,OK,35.51,-98.78,2019,9,9,14,55,89,64,43, ,90,S,18,23,1012.83,89,74, +WEBR,Webbers Falls,OK,35.49,-95.12,2019,9,9,14,55,91,73,56, ,99,SSE,7,11,1014.49,91,71, +WEST,Westville,OK,36.01,-94.64,2019,9,9,14,55,87,70,57, ,91,S,6,11,1016.47,89,70, +WILB,Wilburton,OK,34.90,-95.35,2019,9,9,14,55,92,65,41, ,94,SSE,12,18,1014.74,93,65, +WIST,Wister,OK,34.98,-94.69,2019,9,9,14,55,94,61,34, ,95,SSW,14,19,1014.55,95,63, +WOOD,Woodward,OK,36.42,-99.42,2019,9,9,14,55,92,61,36, ,92,S,22,35,1012.47,92,69,0.02 +WYNO,Wynona,OK,36.52,-96.34,2019,9,9,14,55,91,66,44, ,93,SSE,20,29,1013.55,91,70, +YUKO,Yukon,OK,35.56,-97.76,2019,9,9,14,55,90,66,44, ,93,S,17,22,1013.40,91,73,