Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use PyProj instead of Cartopy for internal coordinate transforms and rename crs coordinate #1483

Merged
merged 9 commits into from
Oct 9, 2020
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
dopplershift marked this conversation as resolved.
Show resolved Hide resolved
- 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