Skip to content

Commit

Permalink
Merge pull request #1483 from jthielen/cartopy-to-pyproj-calc
Browse files Browse the repository at this point in the history
Use PyProj instead of Cartopy for internal coordinate transforms and rename crs coordinate
  • Loading branch information
dopplershift authored Oct 9, 2020
2 parents 6bf9e25 + ee2117d commit f8d2fcd
Show file tree
Hide file tree
Showing 26 changed files with 700 additions and 698 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ jobs:

- name: Enable linkchecker for PRs
if: ${{ github.event_name == 'pull_request' && matrix.check-links == true }}
run: echo "::set-env name=LINKCHECKER::linkcheck"
run: echo "LINKCHECKER=linkcheck" >> $GITHUB_ENV

- name: Build docs
run: |
Expand All @@ -120,7 +120,7 @@ jobs:
# branch that's not master (which is confined to n.nn.x above) or on a tag.
- name: Set doc version
if: ${{ github.event_name != 'push' || !contains(github.ref, 'master') }}
run: echo "::set-env name=DOC_VERSION::v$(python -c 'import metpy; print(metpy.__version__.rsplit(".", maxsplit=2)[0])')"
run: echo "DOC_VERSION=v$(python -c 'import metpy; print(metpy.__version__.rsplit(".", maxsplit=2)[0])')" >> $GITHUB_ENV

- name: Upload to GitHub Pages
if: ${{ github.event_name != 'pull_request' && matrix.experimental == false }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/linting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
curl -sfL \
https://github.com/reviewdog/reviewdog/raw/master/install.sh | \
sh -s -- -b $HOME/bin
echo ::add-path::$HOME/bin
echo "$HOME/bin" >> $GITHUB_PATH
- name: Run flake8
env:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/tests-conda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ jobs:
path: test_output/

- name: Upload coverage
if: ${{ always() }}
uses: codecov/codecov-action@v1
with:
name: conda-${{ matrix.python-version }}-${{ runner.os }}
1 change: 1 addition & 0 deletions .github/workflows/tests-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ jobs:
path: test_output/

- name: Upload coverage
if: ${{ always() }}
uses: codecov/codecov-action@v1
with:
name: pypi-${{ matrix.python-version }}-${{ matrix.dep-versions }}-${{ matrix.no-extras }}-${{ runner.os }}
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ script:
fi

after_script:
- if [[ $TASK != "coverage" ]]; then
- if [[ $TASK == "coverage" ]]; then
pip install codecov codacy-coverage;
coverage xml;
codecov -X gcov -f coverage.xml -e TRAVIS_PYTHON_VERSION;
Expand Down
13 changes: 7 additions & 6 deletions ci/Current.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
importlib_metadata==2.0.0
importlib_resources==3.0.0
matplotlib==3.3.2
numpy==1.19.1
scipy==1.5.2
pandas==1.1.3
pooch==1.2.0
pint==0.16.1
xarray==0.16.1
pyproj==2.6.1.post1
scipy==1.5.2
traitlets==4.3.3
pooch==1.2.0
pandas==1.1.3
importlib_metadata==2.0.0
importlib_resources==3.0.0
xarray==0.16.1
13 changes: 7 additions & 6 deletions ci/Minimum
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
importlib_metadata==1.0.0
importlib_resources==1.3.0
matplotlib==2.1.0
numpy==1.16.0
scipy==1.0.0
pandas==0.22.0
pint==0.10.1
xarray==0.14.1
traitlets==4.3.0
pooch==0.1
pandas==0.22.0
importlib_metadata==1.0.0
importlib_resources==1.3.0
pyproj==2.3.0
scipy==1.0.0
traitlets==4.3.0
xarray==0.14.1
5 changes: 3 additions & 2 deletions ci/Prerelease
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
matplotlib>=0.0.dev0
numpy>=0.0.dev0
scipy>=0.0.dev0
traitlets>=0.0.dev0
pooch>=0.0.dev0
pandas>=0.0.dev0
pyproj>=0.0.dev0
scipy>=0.0.dev0
traitlets>=0.0.dev0
1 change: 0 additions & 1 deletion ci/extra_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
cartopy==0.18.0
pyproj==2.6.1.post1
15 changes: 7 additions & 8 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import numpy
import pandas
import pooch
import pyproj
import pytest
import scipy
import traitlets
Expand All @@ -25,10 +26,10 @@
def pytest_report_header(config, startdir):
"""Add dependency information to pytest output."""
return (f'Dep Versions: Matplotlib {matplotlib.__version__}, '
f'NumPy {numpy.__version__}, SciPy {scipy.__version__}, '
f'Xarray {xarray.__version__}, Pint {pint.__version__}, '
f'Pandas {pandas.__version__}, Traitlets {traitlets.__version__}, '
f'Pooch {pooch.version.full_version}')
f'NumPy {numpy.__version__}, Pandas {pandas.__version__}, '
f'Pint {pint.__version__}, Pooch {pooch.version.full_version}\n'
f'\tPyProj {pyproj.__version__}, SciPy {scipy.__version__}, '
f'Traitlets {traitlets.__version__}, Xarray {xarray.__version__}')


@pytest.fixture(autouse=True)
Expand All @@ -45,6 +46,7 @@ def ccrs():
Any testing function/fixture that needs access to ``cartopy.crs`` can simply add this to
their parameter list.
"""
return pytest.importorskip('cartopy.crs')

Expand All @@ -55,15 +57,14 @@ def cfeature():
Any testing function/fixture that needs access to ``cartopy.feature`` can simply add this
to their parameter list.
"""
return pytest.importorskip('cartopy.feature')


@pytest.fixture()
def test_da_lonlat():
"""Return a DataArray with a lon/lat grid and no time coordinate for use in tests."""
pytest.importorskip('cartopy')

data = numpy.linspace(300, 250, 3 * 4 * 4).reshape((3, 4, 4))
ds = xarray.Dataset(
{'temperature': (['isobaric', 'lat', 'lon'], data)},
Expand Down Expand Up @@ -96,8 +97,6 @@ def test_da_lonlat():
@pytest.fixture()
def test_da_xy():
"""Return a DataArray with a x/y grid and a time coordinate for use in tests."""
pytest.importorskip('cartopy')

data = numpy.linspace(300, 250, 3 * 3 * 4 * 4).reshape((3, 3, 4, 4))
ds = xarray.Dataset(
{'temperature': (['time', 'isobaric', 'y', 'x'], data),
Expand Down
9 changes: 5 additions & 4 deletions docs/installguide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ years. For Python itself, that means supporting the last two minor releases.

* matplotlib >= 2.1.0
* numpy >= 1.16.0
* scipy >= 1.0.0
* pint >= 0.10.1
* pandas >= 0.22.0
* xarray >= 0.14.1
* traitlets >= 4.3.0
* pint >= 0.10.1
* pooch >= 0.1
* pyproj >= 2.3.0
* scipy >= 1.0.0
* traitlets >= 4.3.0
* xarray >= 0.14.1

------------
Installation
Expand Down
11 changes: 6 additions & 5 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,17 @@ include_package_data = True
setup_requires = setuptools_scm
python_requires = >=3.6
install_requires =
importlib_metadata>=1.0.0; python_version < '3.8'
importlib_resources>=1.3.0; python_version < '3.9'
matplotlib>=2.1.0
numpy>=1.16.0
scipy>=1.0
pandas>=0.22.0
pint>=0.10.1
xarray>=0.14.1
pooch>=0.1
pyproj>=2.3.0,<3.0
scipy>=1.0
traitlets>=4.3.0
pandas>=0.22.0
importlib_metadata>=1.0.0; python_version < '3.8'
importlib_resources>=1.3.0; python_version < '3.9'
xarray>=0.14.1

[options.packages.find]
where = src
Expand Down
15 changes: 8 additions & 7 deletions src/metpy/calc/cross_sections.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ def distances_from_cross_section(cross):
"""
if check_axis(cross.metpy.x, 'longitude') and check_axis(cross.metpy.y, 'latitude'):
# Use pyproj to obtain x and y distances
from pyproj import Geod

g = Geod(cross.metpy.cartopy_crs.proj4_init)
g = cross.metpy.pyproj_crs.get_geod()
lon = cross.metpy.x
lat = cross.metpy.y

Expand Down Expand Up @@ -83,10 +81,13 @@ def latitude_from_cross_section(cross):
if check_axis(y, 'latitude'):
return y
else:
import cartopy.crs as ccrs
latitude = ccrs.Geodetic().transform_points(cross.metpy.cartopy_crs,
cross.metpy.x.values,
y.values)[..., 1]
from pyproj import Proj
latitude = Proj(cross.metpy.pyproj_crs)(
cross.metpy.x.values,
y.values,
inverse=True,
radians=False
)[1]
latitude = xr.DataArray(latitude * units.degrees_north, coords=y.coords, dims=y.dims)
return latitude

Expand Down
36 changes: 17 additions & 19 deletions src/metpy/calc/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import numpy as np
from numpy.core.numeric import normalize_axis_index
import numpy.ma as ma
from pyproj import Geod
from scipy.spatial import cKDTree
import xarray as xr

Expand Down Expand Up @@ -763,7 +764,7 @@ def take(indexer):

@exporter.export
@preprocess_and_wrap()
def lat_lon_grid_deltas(longitude, latitude, x_dim=-1, y_dim=-2, **kwargs):
def lat_lon_grid_deltas(longitude, latitude, x_dim=-1, y_dim=-2, geod=None):
r"""Calculate the actual delta between grid points that are in latitude/longitude format.
Parameters
Expand All @@ -778,8 +779,9 @@ def lat_lon_grid_deltas(longitude, latitude, x_dim=-1, y_dim=-2, **kwargs):
axis number for the x dimension, defaults to -1.
y_dim : int
axis number for the y dimesion, defaults to -2.
kwargs
Other keyword arguments to pass to :class:`~pyproj.Geod`
geod : `pyproj.Geod` or ``None``
PyProj Geod to use for forward azimuth and distance calculations. If ``None``, use a
default spherical ellipsoid.
Returns
-------
Expand All @@ -797,8 +799,6 @@ def lat_lon_grid_deltas(longitude, latitude, x_dim=-1, y_dim=-2, **kwargs):
array-like type). It will also "densify" your data if using Dask or lazy-loading.
"""
from pyproj import Geod

# Inputs must be the same number of dimensions
if latitude.ndim != longitude.ndim:
raise ValueError('Latitude and longitude must have the same number of dimensions.')
Expand All @@ -819,11 +819,10 @@ def lat_lon_grid_deltas(longitude, latitude, x_dim=-1, y_dim=-2, **kwargs):
take_y = make_take(latitude.ndim, y_dim)
take_x = make_take(latitude.ndim, x_dim)

geod_args = {'ellps': 'sphere'}
if kwargs:
geod_args = kwargs

g = Geod(**geod_args)
if geod is None:
g = Geod(ellps='sphere')
else:
g = geod

forward_az, _, dy = g.inv(longitude[take_y(slice(None, -1))],
latitude[take_y(slice(None, -1))],
Expand All @@ -842,7 +841,7 @@ def lat_lon_grid_deltas(longitude, latitude, x_dim=-1, y_dim=-2, **kwargs):

@exporter.export
@preprocess_and_wrap()
def azimuth_range_to_lat_lon(azimuths, ranges, center_lon, center_lat, **kwargs):
def azimuth_range_to_lat_lon(azimuths, ranges, center_lon, center_lat, geod=None):
"""Convert azimuth and range locations in a polar coordinate system to lat/lon coordinates.
Pole refers to the origin of the coordinate system.
Expand All @@ -858,8 +857,9 @@ def azimuth_range_to_lat_lon(azimuths, ranges, center_lon, center_lat, **kwargs)
The latitude of the pole in decimal degrees
center_lon : float
The longitude of the pole in decimal degrees
kwargs
arbitrary keyword arguments to pass to pyproj.Geod (e.g. 'ellps')
geod : `pyproj.Geod` or ``None``
PyProj Geod to use for forward azimuth and distance calculations. If ``None``, use a
default spherical ellipsoid.
Returns
-------
Expand All @@ -870,12 +870,10 @@ def azimuth_range_to_lat_lon(azimuths, ranges, center_lon, center_lat, **kwargs)
Credit to Brian Blaylock for the original implementation.
"""
from pyproj import Geod

geod_args = {'ellps': 'sphere'}
if kwargs:
geod_args = kwargs
g = Geod(**geod_args)
if geod is None:
g = Geod(ellps='sphere')
else:
g = geod

rng2d, az2d = np.meshgrid(ranges, azimuths)
lats = np.full(az2d.shape, center_lat)
Expand Down
15 changes: 8 additions & 7 deletions src/metpy/interpolate/slices.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ def geodesic(crs, start, end, steps):
Parameters
----------
crs: `cartopy.crs`
Cartopy Coordinate Reference System to use for the output
crs: `pyproj.CRS`
PyProj Coordinate Reference System to use for the output
start: (2, ) array_like
A latitude-longitude pair designating the start point of the geodesic (units are
degrees north and degrees east).
Expand All @@ -96,18 +96,19 @@ def geodesic(crs, start, end, steps):
cross_section
"""
import cartopy.crs as ccrs
from pyproj import Geod
from pyproj import Proj

g = crs.get_geod()
p = Proj(crs)

# Geod.npts only gives points *in between* the start and end, and we want to include
# the endpoints.
g = Geod(crs.proj4_init)
geodesic = np.concatenate([
np.array(start[::-1])[None],
np.array(g.npts(start[1], start[0], end[1], end[0], steps - 2)),
np.array(end[::-1])[None]
]).transpose()
points = crs.transform_points(ccrs.Geodetic(), *geodesic)[:, :2]
points = np.stack(p(geodesic[0], geodesic[1], inverse=False, radians=False), axis=-1)

return points

Expand Down Expand Up @@ -162,7 +163,7 @@ def cross_section(data, start, end, steps=100, interp_type='linear'):

# Get the projection and coordinates
try:
crs_data = data.metpy.cartopy_crs
crs_data = data.metpy.pyproj_crs
x = data.metpy.x
except AttributeError:
raise ValueError('Data missing required coordinate information. Verify that '
Expand Down
6 changes: 6 additions & 0 deletions src/metpy/plots/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ def to_cartopy(self):

return proj_handler(self._attrs, globe)

def to_pyproj(self):
"""Convert to a PyProj CRS."""
import pyproj

return pyproj.CRS.from_cf(self._attrs)

def to_dict(self):
"""Get the dictionary of metadata attributes."""
return self._attrs.copy()
Expand Down
13 changes: 0 additions & 13 deletions src/metpy/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,6 @@ def wrapped(*args, **kwargs):
return wrapped


def needs_pyproj(test_func):
"""Decorate a test function or fixture as requiring PyProj.
Will skip the decorated test, or any test using the decorated fixture, if ``pyproj`` is
unable to be imported.
"""
@functools.wraps(test_func)
def wrapped(*args, **kwargs):
pytest.importorskip('pyproj')
return test_func(*args, **kwargs)
return wrapped


def get_upper_air_data(date, station):
"""Get upper air observations from the test data cache.
Expand Down
Loading

0 comments on commit f8d2fcd

Please sign in to comment.