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

xarray Coordinate Identification Refactor #1058

Merged
merged 4 commits into from Jun 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 4 additions & 6 deletions metpy/calc/cross_sections.py
Expand Up @@ -13,7 +13,7 @@
from .basic import coriolis_parameter
from .tools import first_derivative
from ..package_tools import Exporter
from ..xarray import CFConventionHandler, check_matching_coordinates
from ..xarray import check_axis, check_matching_coordinates

exporter = Exporter(globals())

Expand All @@ -33,8 +33,7 @@ def distances_from_cross_section(cross):
A tuple of the x and y distances as DataArrays

"""
if (CFConventionHandler.check_axis(cross.metpy.x, 'lon')
and CFConventionHandler.check_axis(cross.metpy.y, 'lat')):
if check_axis(cross.metpy.x, 'lon') and check_axis(cross.metpy.y, 'lat'):
# Use pyproj to obtain x and y distances
from pyproj import Geod

Expand All @@ -53,8 +52,7 @@ def distances_from_cross_section(cross):
x = xr.DataArray(x, coords=lon.coords, dims=lon.dims, attrs={'units': 'meters'})
y = xr.DataArray(y, coords=lat.coords, dims=lat.dims, attrs={'units': 'meters'})

elif (CFConventionHandler.check_axis(cross.metpy.x, 'x')
and CFConventionHandler.check_axis(cross.metpy.y, 'y')):
elif check_axis(cross.metpy.x, 'x') and check_axis(cross.metpy.y, 'y'):

# Simply return what we have
x = cross.metpy.x
Expand All @@ -81,7 +79,7 @@ def latitude_from_cross_section(cross):

"""
y = cross.metpy.y
if CFConventionHandler.check_axis(y, 'lat'):
if check_axis(y, 'lat'):
return y
else:
import cartopy.crs as ccrs
Expand Down
14 changes: 7 additions & 7 deletions metpy/calc/tests/test_calc_tools.py
Expand Up @@ -986,13 +986,13 @@ def test_gradient_xarray(test_da_xy):
xr.testing.assert_allclose(deriv_p, truth_p)
assert deriv_p.metpy.units == truth_p.metpy.units

# Assert alternative specifications give same results
xr.testing.assert_identical(deriv_x_alt1, deriv_x)
xr.testing.assert_identical(deriv_y_alt1, deriv_y)
xr.testing.assert_identical(deriv_p_alt1, deriv_p)
xr.testing.assert_identical(deriv_x_alt2, deriv_x)
xr.testing.assert_identical(deriv_y_alt2, deriv_y)
xr.testing.assert_identical(deriv_p_alt2, deriv_p)
# Assert alternative specifications give same results (up to attribute differences)
xr.testing.assert_equal(deriv_x_alt1, deriv_x)
xr.testing.assert_equal(deriv_y_alt1, deriv_y)
xr.testing.assert_equal(deriv_p_alt1, deriv_p)
xr.testing.assert_equal(deriv_x_alt2, deriv_x)
xr.testing.assert_equal(deriv_y_alt2, deriv_y)
xr.testing.assert_equal(deriv_p_alt2, deriv_p)


def test_gradient_xarray_implicit_axes(test_da_xy):
Expand Down
8 changes: 4 additions & 4 deletions metpy/calc/tools.py
Expand Up @@ -25,7 +25,7 @@ def normalize_axis_index(a, n):
from ..interpolate.one_dimension import interpolate_1d, interpolate_nans_1d, log_interpolate_1d
from ..package_tools import Exporter
from ..units import atleast_1d, check_units, concatenate, diff, units
from ..xarray import CFConventionHandler, preprocess_xarray
from ..xarray import check_axis, preprocess_xarray

exporter = Exporter(globals())

Expand Down Expand Up @@ -925,13 +925,13 @@ def wrapper(f, **kwargs):
# Initialize new kwargs with the axis number
new_kwargs = {'axis': f.get_axis_num(axis)}

if f[axis].attrs.get('_metpy_axis') == 'T':
if check_axis(f[axis], 'time'):
# Time coordinate, need to convert to seconds from datetimes
new_kwargs['x'] = f[axis].metpy.as_timestamp().metpy.unit_array
elif CFConventionHandler.check_axis(f[axis], 'lon'):
elif check_axis(f[axis], 'lon'):
# Longitude coordinate, need to get grid deltas
new_kwargs['delta'], _ = grid_deltas_from_dataarray(f)
elif CFConventionHandler.check_axis(f[axis], 'lat'):
elif check_axis(f[axis], 'lat'):
# Latitude coordinate, need to get grid deltas
_, new_kwargs['delta'] = grid_deltas_from_dataarray(f)
else:
Expand Down
4 changes: 2 additions & 2 deletions metpy/interpolate/slices.py
Expand Up @@ -7,7 +7,7 @@
import xarray as xr

from ..package_tools import Exporter
from ..xarray import CFConventionHandler
from ..xarray import check_axis

exporter = Exporter(globals())

Expand Down Expand Up @@ -166,7 +166,7 @@ def cross_section(data, start, end, steps=100, interp_type='linear'):
points_cross = geodesic(crs_data, start, end, steps)

# Patch points_cross to match given longitude range, whether [0, 360) or (-180, 180]
if CFConventionHandler.check_axis(x, 'lon') and (x > 180).any():
if check_axis(x, 'lon') and (x > 180).any():
points_cross[points_cross[:, 0] < 0, 0] += 360.

# Return the interpolated data
Expand Down
4 changes: 2 additions & 2 deletions metpy/interpolate/tests/test_slices.py
Expand Up @@ -227,9 +227,9 @@ def test_cross_section_dataset_and_nearest_interp(test_ds_lonlat):

def test_interpolate_to_slice_error_on_missing_coordinate(test_ds_lonlat):
"""Test that the proper error is raised with missing coordinate."""
# Use a variable with a coordinate dimension that has attributes removed
# Use a variable with a coordinate removed
data_bad = test_ds_lonlat['temperature'].copy()
data_bad['lat'].attrs = {}
del data_bad['lat']
path = np.array([[265.0, 30.],
[265.0, 36.],
[265.0, 42.]])
Expand Down
59 changes: 37 additions & 22 deletions metpy/tests/test_xarray.py
Expand Up @@ -13,7 +13,7 @@

from metpy.testing import assert_almost_equal, assert_array_equal, get_test_data
from metpy.units import units
from metpy.xarray import check_matching_coordinates, preprocess_xarray
from metpy.xarray import check_axis, check_matching_coordinates, preprocess_xarray


# Seed RandomState for deterministic tests
Expand Down Expand Up @@ -230,11 +230,11 @@ def test_missing_coordinate_type(test_ds_generic):
assert 'not available' in str(exc.value)


def test_assign_axes_not_overwrite(test_ds_generic):
"""Test that CFConventionHandler._assign_axis does not overwrite past axis attributes."""
def test_assign_coordinates_not_overwrite(test_ds_generic):
"""Test that assign_coordinates does not overwrite past axis attributes."""
data = test_ds_generic.copy()
data['c'].attrs['axis'] = 'X'
data.metpy._assign_axes({'Y': data['c']}, data['test'])
data['test'].metpy.assign_coordinates({'Y': data['c']})
assert data['c'].identical(data['test'].metpy.y)
assert data['c'].attrs['axis'] == 'X'

Expand All @@ -246,10 +246,8 @@ def test_resolve_axis_conflict_lonlat_and_xy(test_ds_generic):
test_ds_generic['d'].attrs['_CoordinateAxisType'] = 'GeoY'
test_ds_generic['e'].attrs['_CoordinateAxisType'] = 'Lat'

test_var = test_ds_generic.metpy.parse_cf('test')

assert test_var['b'].identical(test_var.metpy.x)
assert test_var['d'].identical(test_var.metpy.y)
assert test_ds_generic['test'].metpy.x.name == 'b'
assert test_ds_generic['test'].metpy.y.name == 'd'


def test_resolve_axis_conflict_double_lonlat(test_ds_generic):
Expand All @@ -259,8 +257,12 @@ def test_resolve_axis_conflict_double_lonlat(test_ds_generic):
test_ds_generic['d'].attrs['_CoordinateAxisType'] = 'Lat'
test_ds_generic['e'].attrs['_CoordinateAxisType'] = 'Lon'

with pytest.warns(UserWarning, match='Specify the unique'):
test_ds_generic.metpy.parse_cf('test')
with pytest.warns(UserWarning, match='More than one x coordinate'):
with pytest.raises(AttributeError):
test_ds_generic['test'].metpy.x
with pytest.warns(UserWarning, match='More than one y coordinate'):
with pytest.raises(AttributeError):
test_ds_generic['test'].metpy.y


def test_resolve_axis_conflict_double_xy(test_ds_generic):
Expand All @@ -270,8 +272,12 @@ def test_resolve_axis_conflict_double_xy(test_ds_generic):
test_ds_generic['d'].attrs['standard_name'] = 'projection_x_coordinate'
test_ds_generic['e'].attrs['standard_name'] = 'projection_y_coordinate'

with pytest.warns(UserWarning, match='Specify the unique'):
test_ds_generic.metpy.parse_cf('test')
with pytest.warns(UserWarning, match='More than one x coordinate'):
with pytest.raises(AttributeError):
test_ds_generic['test'].metpy.x
with pytest.warns(UserWarning, match='More than one y coordinate'):
with pytest.raises(AttributeError):
test_ds_generic['test'].metpy.y


def test_resolve_axis_conflict_double_x_with_single_dim(test_ds_generic):
Expand All @@ -280,18 +286,17 @@ def test_resolve_axis_conflict_double_x_with_single_dim(test_ds_generic):
test_ds_generic.coords['f'] = ('e', np.linspace(0, 1, 5))
test_ds_generic['f'].attrs['standard_name'] = 'projection_x_coordinate'

test_var = test_ds_generic.metpy.parse_cf('test')

assert test_var['e'].identical(test_var.metpy.x)
assert test_ds_generic['test'].metpy.x.name == 'e'


def test_resolve_axis_conflict_double_vertical(test_ds_generic):
"""Test _resolve_axis_conflict with double vertical coordinates."""
test_ds_generic['b'].attrs['units'] = 'hPa'
test_ds_generic['c'].attrs['units'] = 'Pa'

with pytest.warns(UserWarning, match='Specify the unique'):
test_ds_generic.metpy.parse_cf('test')
with pytest.warns(UserWarning, match='More than one vertical coordinate'):
with pytest.raises(AttributeError):
test_ds_generic['test'].metpy.vertical


criterion_matches = [
Expand Down Expand Up @@ -334,7 +339,7 @@ def test_resolve_axis_conflict_double_vertical(test_ds_generic):
def test_check_axis_criterion_match(test_ds_generic, test_tuple):
"""Test the variety of possibilities for check_axis in the criterion match."""
test_ds_generic['e'].attrs[test_tuple[0]] = test_tuple[1]
assert test_ds_generic.metpy.check_axis(test_ds_generic['e'], test_tuple[2])
assert check_axis(test_ds_generic['e'], test_tuple[2])


unit_matches = [
Expand All @@ -360,7 +365,7 @@ def test_check_axis_criterion_match(test_ds_generic, test_tuple):
def test_check_axis_unit_match(test_ds_generic, test_tuple):
"""Test the variety of possibilities for check_axis in the unit match."""
test_ds_generic['e'].attrs['units'] = test_tuple[0]
assert test_ds_generic.metpy.check_axis(test_ds_generic['e'], test_tuple[1])
assert check_axis(test_ds_generic['e'], test_tuple[1])


regex_matches = [
Expand Down Expand Up @@ -401,7 +406,7 @@ def test_check_axis_unit_match(test_ds_generic, test_tuple):
def test_check_axis_regular_expression_match(test_ds_generic, test_tuple):
"""Test the variety of possibilities for check_axis in the regular expression match."""
data = test_ds_generic.rename({'e': test_tuple[0]})
assert data.metpy.check_axis(data[test_tuple[0]], test_tuple[1])
assert check_axis(data[test_tuple[0]], test_tuple[1])


def test_narr_example_variable_without_grid_mapping(test_ds):
Expand Down Expand Up @@ -512,8 +517,10 @@ def test_data_array_sel_dict_with_units(test_var):
def test_data_array_sel_kwargs_with_units(test_var):
"""Test .sel on the metpy accessor with kwargs and axis type."""
truth = test_var.loc[:, 500.][..., 122]
assert truth.identical(test_var.metpy.sel(vertical=5e4 * units.Pa, x=-16.569 * units.km,
tolerance=1., method='nearest'))
selection = test_var.metpy.sel(vertical=5e4 * units.Pa, x=-16.569 * units.km,
tolerance=1., method='nearest')
selection.metpy.assign_coordinates(None) # truth was not parsed for coordinates
assert truth.identical(selection)


def test_dataset_loc_with_units(test_ds):
Expand All @@ -535,3 +542,11 @@ def test_dataset_loc_without_dict(test_ds):
"""Test that .metpy.loc for Datasets raises error when used with a non-dict."""
with pytest.raises(TypeError):
test_ds.metpy.loc[:, 700 * units.hPa]


def test_dataset_parse_cf_keep_attrs(test_ds):
"""Test that .parse_cf() does not remove attributes on the parsed dataset."""
parsed_ds = test_ds.metpy.parse_cf()

assert parsed_ds.attrs # Must be non-empty
assert parsed_ds.attrs == test_ds.attrs # Must match