diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 248c968086..7554683fcc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12"] os: [macos, ubuntu, windows] steps: diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d25fdcdbeb..f0a315583a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -45,7 +45,7 @@ jobs: with: output-dir: dist env: - CIBW_BUILD: "cp39-* cp310-* cp311-*" + CIBW_BUILD: "cp39-* cp310-* cp311-* cp312*" CIBW_SKIP: "*-musllinux_*" # numpy doesn't have wheels for musllinux so we can't build some quickly and without bloating CIBW_ARCHS_LINUX: "x86_64" CIBW_ARCHS_MACOS: x86_64 arm64 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 2df62f2769..ab3f488d9e 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -160,7 +160,7 @@ documentation style. For more on the NumPy documentation style: -- https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt +- https://github.com/numpy/numpy/blob/main/doc/HOWTO_DOCUMENT.rst.txt An example: @@ -340,9 +340,9 @@ GitHub When contributing to pyart, the changes created should be in a new branch under your forked repository. Let's say the user is adding a new map display. -Instead of creating that new function in your master branch. Create a new +Instead of creating that new function in your main branch. Create a new branch called ‘cartopy_map’. If everything checks out and the admin -accepts the pull request, you can then merge the master branch and +accepts the pull request, you can then merge the main branch and cartopy_map branch. To delete a branch both locally and remotely, if done with it:: @@ -362,16 +362,15 @@ To create a new branch:: If you have a branch with changes that have not been added to a pull request but you would like to start a new branch with a different task in mind. It -is recommended that your new branch is based on your master. First:: - - git checkout master +is recommended that your new branch is based on your main. First:: + git checkout main Then:: git checkout -b This way, your new branch is not a combination of your other task branch and -the new task branch, but is based on the original master branch. +the new task branch, but is based on the original main branch. Typing `git status` will not only inform the user of what files have been modified and untracked, it will also inform the user of which branch they diff --git a/INSTALL.rst b/INSTALL.rst index 26d88c99f4..7975fa24b9 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -49,10 +49,10 @@ without these packages. * `Basemap `_ But Cartopy is recommended as basemap will no longer have support. -* `xarray `_ +* `pyproj `_ -* `pytest `_ Obtaining the latest source =========================== diff --git a/README.rst b/README.rst index a73b86efef..01a38d9337 100644 --- a/README.rst +++ b/README.rst @@ -173,7 +173,7 @@ Other related open source software for working with weather radar data: Dependencies ============ -Py-ART is tested to work under Python 3.9, 3.10 and 3.11 +Py-ART is tested to work under Python 3.9, 3.10, 3.11, and 3.12. The required dependencies to install Py-ART in addition to Python are: diff --git a/doc/environment.yml b/doc/environment.yml index 009e2ca6be..0c10630d2b 100644 --- a/doc/environment.yml +++ b/doc/environment.yml @@ -33,6 +33,7 @@ dependencies: - sphinx-copybutton - nbsphinx - pre_commit + - cmweather - pip - pip: - pooch diff --git a/doc/source/changelog.md b/doc/source/changelog.md index ea1b478dc2..82412d4a1a 100644 --- a/doc/source/changelog.md +++ b/doc/source/changelog.md @@ -1,5 +1,40 @@ # Changelog +## Py-ART 1.16.1 + +([full changelog](https://github.com/ARM-DOE/pyart/compare/v1.16.0...a91195baac7e8afcffa9aae3a62dc27f32e8ab1a)) + +### Enhancements made + +- ENH: cmweather used to now import colormaps [#1452](https://github.com/ARM-DOE/pyart/pull/1452) ([@zssherman](https://github.com/zssherman)) +- EN: Update accessor.py [#1457](https://github.com/ARM-DOE/pyart/pull/1457) ([@syedhamidali](https://github.com/syedhamidali)) +- ENH: Add new xradar functionality [#1456](https://github.com/ARM-DOE/pyart/pull/1456) ([@mgrover1](https://github.com/mgrover1)) + +### Bugs fixed + +- FIX: Fix the colormap in xradar examples [#1474](https://github.com/ARM-DOE/pyart/pull/1474) ([@mgrover1](https://github.com/mgrover1)) +- FIX: Fix the small typo in xradar grid example [#1471](https://github.com/ARM-DOE/pyart/pull/1471) ([@mgrover1](https://github.com/mgrover1)) +- FIX: Fix failing ci isinstance [#1448](https://github.com/ARM-DOE/pyart/pull/1448) ([@mgrover1](https://github.com/mgrover1)) + +### Documentation improvements + +- DOC: Add dealias example with xradar [#1472](https://github.com/ARM-DOE/pyart/pull/1472) ([@mgrover1](https://github.com/mgrover1)) +- DOC: Add xradar gridding example [#1470](https://github.com/ARM-DOE/pyart/pull/1470) ([@mgrover1](https://github.com/mgrover1)) +- DOC: Add docs for using xradar and Py-ART together [#1469](https://github.com/ARM-DOE/pyart/pull/1469) ([@mgrover1](https://github.com/mgrover1)) +- DOC: Update outdated installation instructions [#1462](https://github.com/ARM-DOE/pyart/pull/1462) ([@mgrover1](https://github.com/mgrover1)) + +### Other merged PRs + +- ADD: Add python 3.12 to ci and wheels [#1473](https://github.com/ARM-DOE/pyart/pull/1473) ([@mgrover1](https://github.com/mgrover1)) +- ADD: Add gridding support from the xradar object [#1468](https://github.com/ARM-DOE/pyart/pull/1468) ([@mgrover1](https://github.com/mgrover1)) +- ADD: Update changelog for recent releases [#1450](https://github.com/ARM-DOE/pyart/pull/1450) ([@mgrover1](https://github.com/mgrover1)) + +### Contributors to this release + +([GitHub contributors page for this release](https://github.com/ARM-DOE/pyart/graphs/contributors?from=2023-08-18&to=2023-10-13&type=c)) + +[@mgrover1](https://github.com/search?q=repo%3AARM-DOE%2Fpyart+involves%3Amgrover1+updated%3A2023-08-18..2023-10-13&type=Issues) | [@review-notebook-app](https://github.com/search?q=repo%3AARM-DOE%2Fpyart+involves%3Areview-notebook-app+updated%3A2023-08-18..2023-10-13&type=Issues) | [@scollis](https://github.com/search?q=repo%3AARM-DOE%2Fpyart+involves%3Ascollis+updated%3A2023-08-18..2023-10-13&type=Issues) | [@syedhamidali](https://github.com/search?q=repo%3AARM-DOE%2Fpyart+involves%3Asyedhamidali+updated%3A2023-08-18..2023-10-13&type=Issues) | [@zssherman](https://github.com/search?q=repo%3AARM-DOE%2Fpyart+involves%3Azssherman+updated%3A2023-08-18..2023-10-13&type=Issues) + ## Py-ART 1.16.0 ([full changelog](https://github.com/ARM-DOE/pyart/compare/v1.15.2...ba7f3533438db44b60e1c41e7ba17dfd6350497f)) diff --git a/doc/source/conf.py b/doc/source/conf.py index bc612a5b16..0a6380bfb6 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -266,10 +266,10 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), - "numpy": ("https://docs.scipy.org/doc/numpy/", None), + "numpy": ("https://numpy.org/doc/stable", None), "scipy": ("https://docs.scipy.org/doc/scipy/reference/", None), "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), - "matplotlib": ("https://matplotlib.org", None), + "matplotlib": ("https://matplotlib.org/stable", None), } # Add myst extensions diff --git a/doc/source/index.rst b/doc/source/index.rst index 257014053e..085a6eec16 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -161,6 +161,8 @@ Short Courses Various short courses on Py-ART and open source radar software have been given which contain tutorial like materials and additional examples. +* `2023 AMS, Open Source Radar Short Course `_ +* `2022 ERAD, Open Source Radar Short Course `_ * `2015 AMS, Open Source Radar Short Course `_ * `2015 ARM/ASR Meeting, PyART, the Python ARM Radar Toolkit `_ * `2014 ARM/ASM Meeting, Py-ART tutorial `_ @@ -183,7 +185,7 @@ environment based on the `environment.yml `_ - -* `CyLP `_ or - `PyGLPK `_ or - `CVXOPT `_ and their dependencies. - -* `Cartopy `_ or -* `Basemap `_ But Cartopy is recommended as - basemap will no longer have support. - -* `xarray `_ -* `pyproj `_ - -* `pytest `_ - -Obtaining the latest source -=========================== - -The latest source code for Py-ART can be obtained from the GitHub repository, -https://github.com/ARM-DOE/pyart. - -The latest source can be checked out using - -:: - - $ git clone https://github.com/ARM-DOE/pyart.git - - -Installing from Source -====================== - -The path to the TRMM RSL library must be provided during install. This can -either be done by setting the ``RSL_PATH`` environmentation variable. In bash -this can be done using ``export RSL_PATH=/path/to/rsl/``. If this location is -not specified, some common locations will be searched. Note that the location -provided should be the root TRMM RSL path, under which both a `lib` and -`include` directory are contained, the default location is ``/lib/local/trmm``. -If using CyLP, a path for the coincbc directory is needed. This can be done -using ``export COIN_INSTALL_DIR=/path/to/coincbc/``. When using CyLP, on some -systems, installing the Anaconda compilers is needed. These can be found here: -https://docs.conda.io/projects/conda-build/en/latest/resources/compiler-tools.html - -After specifying the TRMM RSL path Py-ART can be installed globally using - -:: - - $ python setup.py install - -of locally using - -:: - - $ python setup.py install --user - -If you prefer to use Py-ART without installing, simply add the this path to -your ``PYTHONPATH`` (directory or with a .pth file) and compile the extension -in-place. - -:: - - $ python setup.py build_ext -i - -You can also install Py-ART in development mode by using - -:: - - $ pip install -e . - -Frequently asked questions -========================== - -* I'm getting a no 'io' module after installing pyart with pip. - - There is a pyart on pip that is a different package. Make sure to do:: - - pip install arm_pyart - - and not:: - - pip install pyart - -* I'm getting a segfault or compile error with CyLP in newer Python versions - when installing in an environment. - - Anaconda has its own compilers now on conda-forge. Theres can be found here: - https://docs.conda.io/projects/conda-build/en/latest/resources/compiler-tools.html - Once the proper compilers are installed, reinstall CyLP. - -* I'm getting a segfault or another error in python when using - ``pyart.io.read_rsl()`` with IRIS/other files. - - This is due to a bug in RSL, and can be remedied by adding - ``-fno-stack-protector -D_FORTIFY_SOURCE=0`` to the CFLAGS parameter of the - makefile of RSL. This issue has been fixed with the release of rsl-v1.44. - -* I'm having trouble getting PyGLPK to compile on my 64-bit operating system. - - Change the line in the setup.py file from - - :: - - define_macros = macros, extra_compile_args=['-m32'], extra_link_args=['-m32'], - - to - - :: - - define_macros = macros, extra_compile_args=['-m64'], extra_link_args=['-m64'], - - Then build and install PyGLPK as recommended in the PYGLPK README.txt file. - -* When running basemap, I get an error 'KeyError: PROJ_LIB'. - - Basemap is not being supported beyond 2020, some of these errors relate - to it not playing nicely with newer versions of packages. We recommend using - Cartopy instead, but some users have been able to use: - import os - os.environ['PROJ_LIB'] = 'C:/Users/xx Username xxx/Anaconda3/Lib/site-packages/mpl_toolkits/basemap' - To get basemap working, but again Cartopy should be used instead of Basemap. +.. include:: ../../../INSTALL.rst diff --git a/doc/source/userguide/contributors_guide.rst b/doc/source/userguide/contributors_guide.rst index 219af44e08..b1cd2f37dc 100644 --- a/doc/source/userguide/contributors_guide.rst +++ b/doc/source/userguide/contributors_guide.rst @@ -1,428 +1 @@ -Contributor's Guide -=================== - - -The Python ARM Radar Toolkit (Py-ART) -------------------------------------- - -The Python ARM Radar Toolkit, Py-ART, is an open source Python module -containing a growing collection of weather radar algorithms and utilities -build on top of the Scientific Python stack and distributed under the -3-Clause BSD license. Py-ART is used by the -`Atmospheric Radiation Measurement (ARM) Climate Research Facility -`_ for working with data from a number of precipitation -and cloud radars, but has been designed so that it can be used by others in -the radar and atmospheric communities to examine, processes, and analyze -data from many types of weather radars. - - -Important Links ---------------- - -- Official source code repository: https://github.com/ARM-DOE/pyart -- HTML documentation: https://arm-doe.github.io/pyart -- Examples: https://arm-doe.github.io/pyart/examples/ -- Mailing List: http://groups.google.com/group/pyart-users/ -- Issue Tracker: https://github.com/ARM-DOE/pyart/issues - - -Citing ------- - -If you use the Python ARM Radar Toolkit (Py-ART) to prepare a publication -please cite: - - Helmus, J.J. & Collis, S.M., (2016). The Python ARM Radar Toolkit - (Py-ART), a Library for Working with Weather Radar Data in the Python - Programming Language. Journal of Open Research Software. 4(1), p.e25. - DOI: http://doi.org/10.5334/jors.119 - -Py-ART implements many published scientific methods which should *also* be -cited if you make use of them. Refer to the **References** section in the -documentation of the functions used for information on these citations. - - -Install -------- - -The easiest method for installing Py-ART is to use the conda packages from -the latest release. To do this you must download and install -`Anaconda `_ or -`Miniconda `_. -Then use the following command in a terminal or command prompt to install -the latest version of Py-ART:: - - conda install -c conda-forge arm_pyart - -To update an older version of Py-ART to the latest release use:: - - conda update -c conda-forge arm_pyart - - -Resources ---------- - -Pyart: - -- https://github.com/openradar/AMS-Short-Course-on-Open-Source-Radar-Software -- https://github.com/EVS-ATMOS/pyart_short_course -- https://www.youtube.com/watch?v=diiP-Q3bKZw -- http://arm-doe.github.io/pyart/dev/auto_examples/index.html - -Git: - -- https://git-scm.com/book/en/v2 - - -Code Style ----------- - -Py-ART follows pep8 coding standards. To make sure your code follows the -pep8 style, you can use a variety of tools that can check for you. Two -popular pep8 check modules are pycodestyle and pylint. - -For more on pep8 style: - -- https://www.python.org/dev/peps/pep-0008/ - -To install pycodestyle:: - - conda install pycodestyle - -To use pycodestyle:: - - pycodestyle filename - -To install pylint:: - - conda install pylint - -To get a detailed pylint report:: - - pylint filename - -If you want to just see what line number and the issue, just use:: - - pylint -r n filename - -Both of these tools are highly configurable to suit a user's taste. Refer to -the tools documentation for details on this process. - -- https://pycodestyle.readthedocs.io/en/latest/ -- https://www.pylint.org/ - - -Python File Setup ------------------ - -In a new .py file, the top of the code should have a brief introduction to -the module. - -An example: - -.. code-block:: python - - """ - Retrieval of VADs from a radar object. - - """ - -Following the introduction code, modules are then added. To follow pep8 -standards, modules should be added in the order of: - - 1. Standard library imports. - 2. Related third party imports. - 3. Local application/library specific imports. - -For example: - -.. code-block:: python - - import glob - import os - - import numpy as np - import numpy.ma as ma - from scipy.interpolate import interp1d - - from ..core import HorizontalWindProfile - -Following the main function def line, but before the code within it, a doc -string is needed to explain arguments, returns, references if needed, and -other helpful information. These documentation standards follow the NumPy -documentation style. - -For more on the NumPy documentation style: - -- https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt - -An example: - -.. code-block:: python - - def velocity_azimuth_display( - radar, velocity=None, z_want=None, valid_ray_min=16, - gatefilter=False, window=2): - - """ - Velocity azimuth display. - - Parameters - ---------- - radar : Radar - Radar object used. - velocity : string - Velocity field to use for VAD calculation. - If None, the default velocity field will be used. - - Other Parameters - ---------------- - z_want : array - Height array user would like for the VAD - calculation. None will result in a z_want of - np.linspace and use of _inverse_dist_squared - and _Average1D functions. Note, height must have - same shape as expected u_wind and v_wind if user - provides z_want. - valid_ray_min : int - Amount of rays required to include that level in - the VAD calculation. - gatefilter : GateFilter - Used to correct the velocity field before its use - in the VAD calculation. Uses Py-ART's region dealiaser. - window : int - Value to use for window calculation in _Averag1D - function. - - Returns - ------- - height : array - Heights in meters above sea level at which horizontal - winds were sampled. - speed : array - Horizontal wind speed in meters per second at each height. - direction : array - Horizontal wind direction in degrees at each height. - u_wind : array - U-wind mean in meters per second. - v_wind : array - V-wind mean in meters per second. - - Reference - ---------- - K. A. Browning and R. Wexler, 1968: The Determination - of Kinematic Properties of a Wind Field Using Doppler - Radar. J. Appl. Meteor., 7, 105–113 - - """ - -As seen, each argument has what type of object it is, an explanation of -what it is, mention of units, and if an argument has a default value, a -statement of what that default value is and why. - -Private or smaller functions and classes can have a single line explanation. - -An example: - -.. code-block:: python - - def u_wind(self): - """ U component of horizontal wind in meters per second. """ - - -Testing -------- - -When adding a new function to pyart it is important to add your function to -the __init__.py file under the corresponding pyart folder. - -Create a test for your function and have assert from numpy testing test the -known values to the calculated values. If changes are made in the future to -pyart, pytest will use the test created to see if the function is still valid and -produces the same values. It works that, it takes known values that are -obtained from the function, and when pytest is ran, it takes the test -function and reruns the function and compares the results to the original. - -An example: - -.. code-block:: python - - def test_vad(): - test_radar = pyart.testing.make_target_radar() - height = np.arange(0, 1000, 200) - speed = np.ones_like(height) * 5 - direction = np.array([0, 90, 180, 270, 45]) - profile = pyart.core.HorizontalWindProfile( - height, speed, direction) - sim_vel = pyart.util.simulated_vel_from_profile( - test_radar, profile) - - test_radar.add_field('velocity', sim_vel, - replace_existing=True) - - velocity = 'velocity' - z_start = 0 - z_end = 10 - z_count = 5 - - vad_height = ([0., 2.5, 5., 7.5, 10.]) - vad_speed = ([4.98665725, 4.94020686, 4.88107152, - 4.81939374, 4.75851962]) - vad_direction = ([359.84659496, 359.30240553, 358.58658589, - 357.81073051, 357.01353486]) - u_wind = ([0.01335138, 0.06014712, 0.12039762, - 0.18410404, 0.24791911]) - v_wind = ([-4.98663937, -4.9398407, -4.87958641, - -4.81587601, -4.75205693]) - - vad = pyart.retrieve.velocity_azimuth_display(test_radar, - velocity, - z_start, z_end, - z_count) - - assert_almost_equal(vad.height, vad_height, 3) - assert_almost_equal(vad.speed, vad_speed, 3) - assert_almost_equal(vad.direction, vad_direction, 3) - assert_almost_equal(vad.u_wind, u_wind, 3) - assert_almost_equal(vad.v_wind, v_wind, 3) - -Pytest is used to run unit tests in pyart. - -It is recommended to install pyart in “editable” mode for pytest testing. -From within the main pyart directory:: - - pip install -e . - -This lets you change your source code and rerun tests at will. - -To install pytest:: - - conda install pytest - -To run all tests in pyart with pytest from outside the pyart directory:: - - pytest --pyargs pyart - -All test with increase verbosity:: - - pytest -v - -Just one file:: - - pytest filename - -Note: When an example shows filename as such:: - - pytest filename - -filename is the filename and location, such as:: - - pytest /home/user/pyart/pyart/io/tests/test_cfradial.py - -Relative paths can also be used:: - - cd pyart - pytest ./pyart/retrieve/tests/test_vad.py - -For more on pytest: - -- https://docs.pytest.org/en/latest/ - - -GitHub ------- - -When contributing to pyart, the changes created should be in a new branch -under your forked repository. Let's say the user is adding a new map display. -Instead of creating that new function in your master branch. Create a new -branch called ‘cartopy_map’. If everything checks out and the admin -accepts the pull request, you can then merge the master branch and -cartopy_map branch. - -To delete a branch both locally and remotely, if done with it:: - - git push origin --delete - git branch -d - -or in this case:: - - git push origin --delete cartopy_map - git branch -d cartopy_map - - -To create a new branch:: - - git checkout -b - -If you have a branch with changes that have not been added to a pull request -but you would like to start a new branch with a different task in mind. It -is recommended that your new branch is based on your master. First:: - - git checkout master - -Then:: - - git checkout -b - -This way, your new branch is not a combination of your other task branch and -the new task branch, but is based on the original master branch. - -Typing `git status` will not only inform the user of what files have been -modified and untracked, it will also inform the user of which branch they -are currently on. - -To switch between branches, simply type:: - - git checkout - -When commiting to GitHub, start the statement with a acronym such as -‘ADD:’ depending on what your commiting, could be ‘MAINT:’ or -‘BUG:’ or more. Then following should be a short statement such as -“ADD: Adding cartopy map display.”, but after the short statement, before -finishing the quotations, hit enter and in your terminal you can then type -a more in depth description on what your commiting. - -A set of recommended acronymns can be found at: - -- https://docs.scipy.org/doc/numpy/dev/gitwash/development_workflow.html - -If you would like to type your commit in the terminal and skip the default -editor:: - - git commit -m "STY: Removing whitespace from vad.py pep8." - -To use the default editor(in Linux, usually VIM), simply type:: - - git commit - -One thing to keep in mind is before doing a pull request, update your -branches with the original upstream repository. - -This could be done by:: - - git fetch upstream - -After fetching, a git merge is needed to pull in the changes. - -This is done by:: - - git merge upstream/master - -To prevent a merge commit:: - - git merge --ff-only upstream/master - -After creating a pull request through GitHub, two outside checkers, -Appveyor and TravisCI will determine if the code past all checks. If the -code fails either tests, as the pull request sits, make changes to fix the -code and when pushed to GitHub, the pull request will automatically update -and TravisCI and Appveyor will automatically rerun. - -Blog Posts ----------- - -You can also contribute by adding blog posts. To get started with -blog posts, check out the blog post notebook template in the -`doc/source/blog_posts/` directory. Move the blog post to a -directory with the corresponding year (ex. `2022`), and follow the -contribution process outlined above. +.. include:: ../../../CONTRIBUTING.rst diff --git a/doc/source/userguide/setting_up_an_environment.rst b/doc/source/userguide/setting_up_an_environment.rst index 814d32ab14..0fbb90804e 100644 --- a/doc/source/userguide/setting_up_an_environment.rst +++ b/doc/source/userguide/setting_up_an_environment.rst @@ -73,7 +73,7 @@ if you want Jupyter Notebook to run in that enviroment with those packages:: while that environment is activated. Another way to create a conda environment is by doing it from scratch using the conda create command. An example of this:: - conda create -n pyart_env -c conda-forge python=3.6 arm_pyart netCDF4 + conda create -n pyart_env -c conda-forge arm_pyart netCDF4 cartopy scipy numpy matplotlib This will also create an environment called pyart_env that can be activate the diff --git a/examples/xradar/README.txt b/examples/xradar/README.txt new file mode 100644 index 0000000000..0ae5d93f85 --- /dev/null +++ b/examples/xradar/README.txt @@ -0,0 +1,6 @@ +.. _xradar_examples: + +Xradar Examples +------------------ + +Examples of using Xradar with Py-ART to accomplish different tasks. diff --git a/examples/xradar/plot_dealias_xradar.py b/examples/xradar/plot_dealias_xradar.py new file mode 100644 index 0000000000..9e78a49b10 --- /dev/null +++ b/examples/xradar/plot_dealias_xradar.py @@ -0,0 +1,108 @@ +""" +================================================= +Dealias Radial Velocities Using Xradar and Py-ART +================================================= + +An example which uses xradar and Py-ART to dealias radial velocities. + +""" + +# Author: Max Grover (mgrover@anl.gov) +# License: BSD 3 clause + + +import cartopy.crs as ccrs +import matplotlib.pyplot as plt +import xradar as xd + +import pyart +from pyart.testing import get_test_data + +# Locate the test data and read in using xradar +filename = get_test_data("swx_20120520_0641.nc") +tree = xd.io.open_cfradial1_datatree(filename) + +# Give the tree Py-ART radar methods +radar = pyart.xradar.Xradar(tree) + +# Determine the nyquist velocity using the maximum radial velocity from the first sweep +nyq = radar["sweep_0"]["mean_doppler_velocity"].max().values + +# Set the nyquist to what we captured above +# Calculate the velocity texture +vel_texture = pyart.retrieve.calculate_velocity_texture( + radar, vel_field="mean_doppler_velocity", nyq=nyq +) +radar.add_field("velocity_texture", vel_texture, replace_existing=True) + +########################################## +# **Visualize our velocity texture field** +# Let's use the RadarMapDisplay to visualize the texture field + +fig = plt.figure(figsize=[8, 8]) +display = pyart.graph.RadarMapDisplay(radar) +display.plot_ppi_map( + "velocity_texture", + sweep=2, + resolution="50m", + vmin=0, + vmax=10, + projection=ccrs.PlateCarree(), + cmap="pyart_balance", +) +plt.show() + +################################################## +# **Determine a Velocity Texture Threshold Value** +# +# We can use the xradar/xarray plotting functionality here +radar["sweep_0"]["velocity_texture"].plot.hist() +plt.show() + +############################# +# **Apply a Gatefilter Mask** +# +# We now apply this threshold, along with a reflectivity threshold, +# and make use of the region-based dealiasing algorithm + +# Configure the gatefilter +gatefilter = pyart.filters.GateFilter(radar) +gatefilter.exclude_above("velocity_texture", 4) +gatefilter.exclude_below("corrected_reflectivity_horizontal", 0) + +# At this point, we can simply used dealias_region_based to dealias the velocities +# and then add the new field to the radar. +velocity_dealiased = pyart.correct.dealias_region_based( + radar, + vel_field="mean_doppler_velocity", + nyquist_vel=nyq, + centered=True, + gatefilter=gatefilter, +) +radar.add_field("corrected_velocity", velocity_dealiased, replace_existing=True) + +############################################# +# **Visualize the Cleaned Radial Velocities** +# +# We can visualize the uncorrected and corrected radial velocity fields +fig = plt.figure(figsize=(14, 5)) +display = pyart.graph.RadarMapDisplay(radar) +ax1 = plt.subplot(121) +display.plot_ppi( + "mean_doppler_velocity", + cmap="twilight_shifted", + vmin=-40, + vmax=40, + colorbar_label="Uncorrected Radial Velocity (m/s)", + ax=ax1, +) +ax2 = plt.subplot(122) +display.plot_ppi( + "corrected_velocity", + cmap="twilight_shifted", + vmin=-40, + vmax=40, + colorbar_label="Corrected Radial Velocity (m/s)", + ax=ax2, +) +plt.show() diff --git a/examples/xradar/plot_grid_xradar.py b/examples/xradar/plot_grid_xradar.py new file mode 100644 index 0000000000..8a6e00f0c8 --- /dev/null +++ b/examples/xradar/plot_grid_xradar.py @@ -0,0 +1,40 @@ +""" +================================= +Grid Data Using Xradar and Py-ART +================================= + +An example which uses xradar and Py-ART to grid a PPI file. + +""" + +# Author: Max Grover (mgrover@anl.gov) +# License: BSD 3 clause + + +import xradar as xd + +import pyart +from pyart.testing import get_test_data + +# Locate the test data and read in using xradar +filename = get_test_data("swx_20120520_0641.nc") +tree = xd.io.open_cfradial1_datatree(filename) + +# Give the tree Py-ART radar methods +radar = pyart.xradar.Xradar(tree) + +# Grid using 11 vertical levels, and 101 horizontal grid cells at a resolution on 1 km +grid = pyart.map.grid_from_radars( + (radar,), + grid_shape=(11, 101, 101), + grid_limits=( + (0.0, 10_000), + (-50_000.0, 50_000.0), + (-50_000, 50_000.0), + ), +) + +display = pyart.graph.GridMapDisplay(grid) +display.plot_grid( + "reflectivity_horizontal", level=0, vmin=-20, vmax=60, cmap="pyart_ChaseSpectral" +) diff --git a/examples/xradar/plot_xradar.py b/examples/xradar/plot_xradar.py new file mode 100644 index 0000000000..708adffe72 --- /dev/null +++ b/examples/xradar/plot_xradar.py @@ -0,0 +1,30 @@ +""" +================================== +Plot a PPI Using Xradar and Py-ART +================================== + +An example which uses xradar and Py-ART to create a PPI plot of a Cfradial file. + +""" + +# Author: Max Grover (mgrover@anl.gov) +# License: BSD 3 clause + + +import xradar as xd + +import pyart +from pyart.testing import get_test_data + +# Locate the test data and read in using xradar +filename = get_test_data("swx_20120520_0641.nc") +tree = xd.io.open_cfradial1_datatree(filename) + +# Give the tree Py-ART radar methods +radar = pyart.xradar.Xradar(tree) + +# Plot the Reflectivity Field (corrected_reflectivity_horizontal) +display = pyart.graph.RadarMapDisplay(radar) +display.plot_ppi( + "corrected_reflectivity_horizontal", cmap="pyart_ChaseSpectral", vmin=-20, vmax=70 +) diff --git a/pyart/xradar/accessor.py b/pyart/xradar/accessor.py index 7673242208..cbe11ef7d2 100644 --- a/pyart/xradar/accessor.py +++ b/pyart/xradar/accessor.py @@ -4,9 +4,172 @@ """ +import copy + +import numpy as np +import pandas as pd +from datatree import DataTree, formatting, formatting_html +from datatree.treenode import NodePath +from xarray import concat +from xarray.core import utils + +from ..core.transforms import antenna_vectors_to_cartesian + + class Xradar: - def __init__(self, xradar): + def __init__(self, xradar, default_sweep="sweep_0", scan_type=None): self.xradar = xradar + self.scan_type = scan_type or "ppi" + self.combined_sweeps = self._combine_sweeps(self.xradar) + self.fields = self._find_fields(self.combined_sweeps) + self.time = dict( + data=(self.combined_sweeps.time - self.combined_sweeps.time.min()).astype( + "int64" + ) + / 1e9, + units=f"seconds since {pd.to_datetime(self.combined_sweeps.time.min().values).strftime('%Y-%m-%d %H:%M:%S.0')}", + calendar="gregorian", + ) + self.range = dict(data=self.combined_sweeps.range.values) + self.azimuth = dict(data=self.combined_sweeps.azimuth.values) + self.elevation = dict(data=self.combined_sweeps.elevation.values) + self.fixed_angle = dict(data=self.combined_sweeps.sweep_fixed_angle.values) + self.antenna_transition = None + self.latitude = dict( + data=np.expand_dims(self.xradar["latitude"].values, axis=0) + ) + self.longitude = dict( + data=np.expand_dims(self.xradar["longitude"].values, axis=0) + ) + self.altitude = dict( + data=np.expand_dims(self.xradar["altitude"].values, axis=0) + ) + self.sweep_end_ray_index = dict( + data=self.combined_sweeps.ngates.groupby("sweep_number").max().values + ) + self.sweep_start_ray_index = dict( + data=self.combined_sweeps.ngates.groupby("sweep_number").min().values + ) + self.metadata = dict(**self.xradar.attrs) + self.ngates = len(self.range["data"]) + self.nrays = len(self.azimuth["data"]) + self.nsweeps = len(self.xradar.sweep_group_name) + self.instrument_parameters = dict(**self.xradar["radar_parameters"].attrs) + self.init_gate_x_y_z() + self.init_gate_alt() + + def __repr__(self): + return formatting.datatree_repr(self.xradar) + + def _repr_html_(self): + return formatting_html.datatree_repr(self.xradar) + + def __getitem__(self: DataTree, key): + """ + Access child nodes, variables, or coordinates stored anywhere in this tree. + + Returned object will be either a DataTree or DataArray object depending on whether the key given points to a + child or variable. + + Parameters + ---------- + key : str + Name of variable / child within this node, or unix-like path to variable / child within another node. + + Returns + ------- + Union[DataTree, DataArray] + """ + + # Either: + if utils.is_dict_like(key): + # dict-like indexing + raise NotImplementedError("Should this index over whole tree?") + elif isinstance(key, str): + # path-like: a name of a node/variable, or path to a node/variable + path = NodePath(key) + return self.xradar._get_item(path) + elif utils.is_list_like(key): + # iterable of variable names + raise NotImplementedError( + "Selecting via tags is deprecated, and selecting multiple items should be " + "implemented via .subset" + ) + else: + raise ValueError(f"Invalid format for key: {key}") + + # Iterators + + def iter_start(self): + """Return an iterator over the sweep start indices.""" + return (s for s in self.sweep_start_ray_index["data"]) + + def iter_end(self): + """Return an iterator over the sweep end indices.""" + return (s for s in self.sweep_end_ray_index["data"]) + + def iter_start_end(self): + """Return an iterator over the sweep start and end indices.""" + return ((s, e) for s, e in zip(self.iter_start(), self.iter_end())) + + def iter_slice(self): + """Return an iterator which returns sweep slice objects.""" + return (slice(s, e + 1) for s, e in self.iter_start_end()) + + def iter_field(self, field_name): + """Return an iterator which returns sweep field data.""" + self.check_field_exists(field_name) + return (self.fields[field_name]["data"][s] for s in self.iter_slice()) + + def iter_azimuth(self): + """Return an iterator which returns sweep azimuth data.""" + return (self.azimuth["data"][s] for s in self.iter_slice()) + + def iter_elevation(self): + """Return an iterator which returns sweep elevation data.""" + return (self.elevation["data"][s] for s in self.iter_slice()) + + def add_field(self, field_name, dic, replace_existing=False): + """ + Add a field to the object. + + Parameters + ---------- + field_name : str + Name of the field to add to the dictionary of fields. + dic : dict + Dictionary contain field data and metadata. + replace_existing : bool, optional + True to replace the existing field with key field_name if it + exists, loosing any existing data. False will raise a ValueError + when the field already exists. + + """ + # check that the field dictionary to add is valid + if field_name in self.fields and replace_existing is False: + err = "A field with name: %s already exists" % (field_name) + raise ValueError(err) + if "data" not in dic: + raise KeyError("dic must contain a 'data' key") + if dic["data"].shape != (self.nrays, self.ngates): + t = (self.nrays, self.ngates) + err = "'data' has invalid shape, should be (%i, %i)" % t + raise ValueError(err) + # add the field + self.fields[field_name] = dic + for sweep in range(self.nsweeps): + sweep_ds = ( + self.xradar[f"sweep_{sweep}"].to_dataset().drop_duplicates("azimuth") + ) + sweep_ds[field_name] = ( + ("azimuth", "range"), + self.fields[field_name]["data"][self.get_slice(sweep)], + ) + attrs = dic.copy() + del attrs["data"] + sweep_ds[field_name].attrs = attrs + self.xradar[f"sweep_{sweep}"].ds = sweep_ds + return def get_field(self, sweep, field_name, copy=False): """ @@ -32,13 +195,30 @@ def get_field(self, sweep, field_name, copy=False): data : array Array containing data for the requested sweep and field. """ - data = self.xradar[f"sweep_{sweep}"][field_name].values - + self.check_field_exists(field_name) + s = self.get_slice(sweep) + data = self.fields[field_name]["data"][s] if copy: return data.copy() else: return data + def check_field_exists(self, field_name): + """ + Check that a field exists in the fields dictionary. + + If the field does not exist raise a KeyError. + + Parameters + ---------- + field_name : str + Name of field to check. + + """ + if field_name not in self.fields: + raise KeyError("Field not available: " + field_name) + return + def get_gate_x_y_z(self, sweep, edges=False, filter_transitions=False): """ Return the x, y and z gate locations in meters for a given sweep. @@ -75,8 +255,175 @@ def get_gate_x_y_z(self, sweep, edges=False, filter_transitions=False): """ # Check to see if the data needs to be georeferenced - if "x" not in self.xradar[f"sweep_{0}"].coords: - self.xradar = self.xradar.xradar.georeference() + if "x" not in self.xradar[f"sweep_{sweep}"].coords: + self.combined_sweeps = self.combined_sweeps.xradar.georeference() + + data = self.combined_sweeps.sel(sweep_number=sweep) + return data["x"].values, data["y"].values, data["z"].values + + def init_gate_x_y_z(self): + """Initialize or reset the gate_{x, y, z} attributes.""" + + ranges = self.range["data"] + azimuths = self.azimuth["data"] + elevations = self.elevation["data"] + cartesian_coords = antenna_vectors_to_cartesian( + ranges, azimuths, elevations, edges=False + ) + + if not hasattr(self, "gate_x"): + self.gate_x = dict() + + if not hasattr(self, "gate_y"): + self.gate_y = dict() + + if not hasattr(self, "gate_z"): + self.gate_z = dict() + + self.gate_x = dict(data=cartesian_coords[0]) + self.gate_y = dict(data=cartesian_coords[1]) + self.gate_z = dict(data=cartesian_coords[2]) + + def init_gate_alt(self): + if not hasattr(self, "gate_altitude"): + self.gate_altitude = dict() + + try: + self.gate_altitude = dict(data=self.altitude["data"] + self.gate_z["data"]) + except ValueError: + self.gate_altitude = dict( + data=np.mean(self.altitude["data"]) + self.gate_z["data"] + ) + + def _combine_sweeps(self, radar): + # Loop through and extract the different datasets + ds_list = [] + for sweep in radar.sweep_group_name.values: + ds_list.append(radar[sweep].ds.drop_duplicates("azimuth")) + + # Merge based on the sweep number + merged = concat(ds_list, dim="sweep_number") + + # Stack the sweep number and azimuth together + stacked = merged.stack(gates=["sweep_number", "azimuth"]).transpose() + + # Drop the missing gates + cleaned = stacked.where(stacked.time == stacked.time.dropna("gates")) + + # Add in number of gates variable + cleaned["ngates"] = ("gates", np.arange(len(cleaned.gates))) + + # Return the non-missing times, ensuring valid data is returned + return cleaned + + def add_filter(self, gatefilter, replace_existing=False, include_fields=None): + """ + Updates the radar object with an applied gatefilter provided + by the user that masks values in fields within the radar object. + + Parameters + ---------- + gatefilter : GateFilter + GateFilter instance. This filter will exclude equal to + the conditions provided in the gatefilter and mask values + in fields specified or all fields if include_fields is None. + replace_existing : bool, optional + If True, replaces the fields in the radar object with + copies of those fields with the applied gatefilter. + False will return new fields with the appended 'filtered_' + prefix. + include_fields : list, optional + List of fields to have filtered applied to. If none, all + fields will have applied filter. + + """ + # If include_fields is None, sets list to all fields to include. + if include_fields is None: + include_fields = [*self.fields.keys()] + + try: + # Replace current fields with masked versions with applied gatefilter. + if replace_existing: + for field in include_fields: + self.fields[field]["data"] = np.ma.masked_where( + gatefilter.gate_excluded, self.fields[field]["data"] + ) + # Add new fields with prefix 'filtered_' + else: + for field in include_fields: + field_dict = copy.deepcopy(self.fields[field]) + field_dict["data"] = np.ma.masked_where( + gatefilter.gate_excluded, field_dict["data"] + ) + self.add_field( + "filtered_" + field, field_dict, replace_existing=True + ) + + # If fields don't match up throw an error. + except KeyError: + raise KeyError( + field + " not found in the original radar object, " + "please check that names in the include_fields list " + "match those in the radar object." + ) + return + + def get_nyquist_vel(self, sweep, check_uniform=True): + """ + Return the Nyquist velocity in meters per second for a given sweep. + + Raises a LookupError if the Nyquist velocity is not available, an + Exception is raised if the velocities are not uniform in the sweep + unless check_uniform is set to False. + + Parameters + ---------- + sweep : int + Sweep number to retrieve data for, 0 based. + check_uniform : bool + True to check to perform a check on the Nyquist velocities that + they are uniform in the sweep, False will skip this check and + return the velocity of the first ray in the sweep. + + Returns + ------- + nyquist_velocity : float + Array containing the Nyquist velocity in m/s for a given sweep. + + """ + s = self.get_slice(sweep) + try: + nyq_vel = self.instrument_parameters["nyquist_velocity"]["data"][s] + except TypeError: + raise LookupError("Nyquist velocity unavailable") + if check_uniform: + if np.any(nyq_vel != nyq_vel[0]): + raise Exception("Nyquist velocities are not uniform in sweep") + return float(nyq_vel[0]) + + def get_start(self, sweep): + """Return the starting ray index for a given sweep.""" + return int(self.combined_sweeps.ngates.sel(sweep_number=sweep).min()) + + def get_end(self, sweep): + """Return the ending ray for a given sweep.""" + return self.sweep_end_ray_index["data"][sweep] + + def get_start_end(self, sweep): + """Return the starting and ending ray for a given sweep.""" + return self.get_start(sweep), self.get_end(sweep) + + def get_slice(self, sweep): + """Return a slice for selecting rays for a given sweep.""" + start, end = self.get_start_end(sweep) + return slice(start, end + 1) - data = self.xradar[f"sweep_{sweep}"].xradar.georeference() - return data["x"].values, data["y"].values, data["x"].values + def _find_fields(self, ds): + fields = {} + for field in self.combined_sweeps.variables: + if self.combined_sweeps[field].dims == ("gates", "range"): + fields[field] = { + "data": self.combined_sweeps[field].values, + **self.combined_sweeps[field].attrs, + } + return fields diff --git a/tests/xradar/test_accessor.py b/tests/xradar/test_accessor.py index 5664267fa9..113f8be3dd 100644 --- a/tests/xradar/test_accessor.py +++ b/tests/xradar/test_accessor.py @@ -1,4 +1,6 @@ +import numpy as np import xradar as xd +from numpy.testing import assert_allclose from open_radar_data import DATASETS import pyart @@ -9,22 +11,50 @@ def test_get_field(filename=filename): dtree = xd.io.open_cfradial1_datatree( filename, - first_dim="time", optional=False, ) radar = pyart.xradar.Xradar(dtree) reflectivity = radar.get_field(0, "DBZ") - assert reflectivity.shape == (483, 996) + assert reflectivity.shape == (480, 996) def test_get_gate_x_y_z(filename=filename): dtree = xd.io.open_cfradial1_datatree( filename, - first_dim="time", optional=False, ) radar = pyart.xradar.Xradar(dtree) x, y, z = radar.get_gate_x_y_z(0) - assert x.shape == (483, 996) - assert y.shape == (483, 996) - assert z.shape == (483, 996) + assert x.shape == (480, 996) + assert y.shape == (480, 996) + assert z.shape == (480, 996) + + +def test_add_field(filename=filename): + dtree = xd.io.open_cfradial1_datatree( + filename, + optional=False, + ) + radar = pyart.xradar.Xradar(dtree) + new_field = radar.fields["DBZ"] + radar.add_field("reflectivity", new_field) + assert "reflectivity" in radar.fields + assert radar["sweep_0"]["reflectivity"].shape == radar["sweep_0"]["DBZ"].shape + + +def test_grid(filename=filename): + dtree = xd.io.open_cfradial1_datatree( + filename, + optional=False, + ) + radar = pyart.xradar.Xradar(dtree) + grid = pyart.map.grid_from_radars( + (radar,), + grid_shape=(1, 11, 11), + grid_limits=((2000, 2000), (-100_000.0, 100_000.0), (-100_000.0, 100_000.0)), + fields=["DBZ"], + ) + assert_allclose(grid.x["data"], np.arange(-100_000, 120_000, 20_000)) + assert_allclose( + grid.fields["DBZ"]["data"][0, -1, 0], np.array(0.4243435), rtol=1e-03 + )