diff --git a/.gitignore b/.gitignore index 0fe7b8b4720..0780fd24694 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .DS_Store *.swp .idea/ +.vscode/ *.pyc diff --git a/.travis.yml b/.travis.yml index ee33efbd120..e8a8be40ab5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,7 +49,7 @@ matrix: include: - python: 3.6 env: - - VERSIONS="numpy==1.13.0 matplotlib==2.1.0 scipy==1.0.0 pint==0.8 xarray==0.12.3 pandas==0.22.0" + - VERSIONS="numpy==1.16.0 matplotlib==2.1.0 scipy==1.0.0 pint==0.10.1 xarray==0.13.0 pandas==0.22.0" - TASK="coverage" - TEST_OUTPUT_CONTROL="" - python: "3.8-dev" @@ -126,6 +126,7 @@ before_script: script: - export TEST_DATA_DIR=${TRAVIS_BUILD_DIR}/staticdata; + - export NUMPY_EXPERIMENTAL_ARRAY_FUNCTION=1; - if [[ $TASK == "docs" ]]; then pushd docs; make clean overridecheck html linkcheck O=-W; diff --git a/docs/installguide.rst b/docs/installguide.rst index 21358764371..25209f4ab61 100644 --- a/docs/installguide.rst +++ b/docs/installguide.rst @@ -9,11 +9,11 @@ In general, MetPy tries to support minor versions of dependencies released withi years. For Python itself, that means supporting the last two minor releases. * matplotlib >= 2.1.0 -* numpy >= 1.13.0 +* numpy >= 1.16.0 * scipy >= 1.0.0 -* pint >= 0.8 +* pint >= 0.10.1 * pandas >= 0.22.0 -* xarray >= 0.12.3 +* xarray >= 0.13.0 * traitlets >= 4.3.0 * pooch >= 0.1 diff --git a/examples/Four_Panel_Map.py b/examples/Four_Panel_Map.py index cd9cfdb8bef..dfecde8de28 100644 --- a/examples/Four_Panel_Map.py +++ b/examples/Four_Panel_Map.py @@ -63,9 +63,9 @@ def plot_background(ax): # Do unit conversions to what we wish to plot vort_500 = vort_500 * 1e5 -surface_temp.metpy.convert_units('degF') -precip_water.metpy.convert_units('inches') -winds_300.metpy.convert_units('knots') +surface_temp = surface_temp.metpy.convert_units('degF') +precip_water = precip_water.metpy.convert_units('inches') +winds_300 = winds_300.metpy.convert_units('knots') ########################################### diff --git a/examples/cross_section.py b/examples/cross_section.py index 8e1774cc2d9..3bd1c854901 100644 --- a/examples/cross_section.py +++ b/examples/cross_section.py @@ -66,8 +66,8 @@ dims=specific_humidity.dims, attrs={'units': rh.units}) -cross['u_wind'].metpy.convert_units('knots') -cross['v_wind'].metpy.convert_units('knots') +cross['u_wind'] = cross['u_wind'].metpy.convert_units('knots') +cross['v_wind'] = cross['v_wind'].metpy.convert_units('knots') cross['t_wind'], cross['n_wind'] = mpcalc.cross_section_components(cross['u_wind'], cross['v_wind']) diff --git a/setup.cfg b/setup.cfg index 6d2fa8ec94b..c08ac4ac561 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,10 +39,10 @@ setup_requires = setuptools_scm python_requires = >=3.6 install_requires = matplotlib>=2.1.0 - numpy>=1.13.0 + numpy>=1.16.0 scipy>=1.0 - pint>=0.8 - xarray>=0.12.3 + pint>=0.10.1 + xarray>=0.13.0 pooch>=0.1 traitlets>=4.3.0 pandas>=0.22.0 diff --git a/src/metpy/calc/basic.py b/src/metpy/calc/basic.py index 19005b1eea2..5d24e816377 100644 --- a/src/metpy/calc/basic.py +++ b/src/metpy/calc/basic.py @@ -18,7 +18,7 @@ from .tools import wrap_output_like from .. import constants as mpconsts from ..package_tools import Exporter -from ..units import atleast_1d, check_units, masked_array, units +from ..units import check_units, masked_array, units from ..xarray import preprocess_xarray exporter = Exporter(globals()) @@ -90,7 +90,7 @@ def wind_direction(u, v, convention='from'): """ wdir = 90. * units.deg - np.arctan2(-v, -u) origshape = wdir.shape - wdir = atleast_1d(wdir) + wdir = np.atleast_1d(wdir) # Handle oceanographic convection if convention == 'to': @@ -244,8 +244,8 @@ def heat_index(temperature, relative_humidity, mask_undefined=True): windchill """ - temperature = atleast_1d(temperature) - relative_humidity = atleast_1d(relative_humidity) + temperature = np.atleast_1d(temperature) + relative_humidity = np.atleast_1d(relative_humidity) # assign units to relative_humidity if they currently are not present if not hasattr(relative_humidity, 'units'): relative_humidity = relative_humidity * units.dimensionless @@ -357,9 +357,9 @@ def apparent_temperature(temperature, relative_humidity, speed, face_level_winds """ is_not_scalar = isinstance(temperature.m, (list, tuple, np.ndarray)) - temperature = atleast_1d(temperature) - relative_humidity = atleast_1d(relative_humidity) - speed = atleast_1d(speed) + temperature = np.atleast_1d(temperature) + relative_humidity = np.atleast_1d(relative_humidity) + speed = np.atleast_1d(speed) # NB: mask_defined=True is needed to know where computed values exist wind_chill_temperature = windchill(temperature, speed, face_level_winds=face_level_winds, @@ -388,7 +388,7 @@ def apparent_temperature(temperature, relative_humidity, speed, face_level_winds if is_not_scalar: return app_temperature else: - return atleast_1d(app_temperature)[0] + return np.atleast_1d(app_temperature)[0] @exporter.export diff --git a/src/metpy/calc/cross_sections.py b/src/metpy/calc/cross_sections.py index 9bbd75d4ffc..bbae1520762 100644 --- a/src/metpy/calc/cross_sections.py +++ b/src/metpy/calc/cross_sections.py @@ -13,6 +13,7 @@ from .basic import coriolis_parameter from .tools import first_derivative from ..package_tools import Exporter +from ..units import units from ..xarray import check_axis, check_matching_coordinates exporter = Exporter(globals()) @@ -49,8 +50,8 @@ def distances_from_cross_section(cross): y = distance * np.cos(np.deg2rad(forward_az)) # Build into DataArrays - 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'}) + x = xr.DataArray(x * units.meter, coords=lon.coords, dims=lon.dims) + y = xr.DataArray(y * units.meter, coords=lat.coords, dims=lat.dims) elif check_axis(cross.metpy.x, 'x') and check_axis(cross.metpy.y, 'y'): @@ -86,8 +87,7 @@ def latitude_from_cross_section(cross): latitude = ccrs.Geodetic().transform_points(cross.metpy.cartopy_crs, cross.metpy.x.values, y.values)[..., 1] - latitude = xr.DataArray(latitude, coords=y.coords, dims=y.dims, - attrs={'units': 'degrees_north'}) + latitude = xr.DataArray(latitude * units.degrees_north, coords=y.coords, dims=y.dims) return latitude @@ -165,10 +165,6 @@ def cross_section_components(data_x, data_y, index='index'): component_tang = data_x * unit_tang[0] + data_y * unit_tang[1] component_norm = data_x * unit_norm[0] + data_y * unit_norm[1] - # Reattach units (only reliable attribute after operation) - component_tang.attrs = {'units': data_x.attrs['units']} - component_norm.attrs = {'units': data_x.attrs['units']} - return component_tang, component_norm @@ -207,9 +203,8 @@ def normal_component(data_x, data_y, index='index'): component_norm = data_x * unit_norm[0] + data_y * unit_norm[1] # Reattach only reliable attributes after operation - for attr in ('units', 'grid_mapping'): - if attr in data_x.attrs: - component_norm.attrs[attr] = data_x.attrs[attr] + if 'grid_mapping' in data_x.attrs: + component_norm.attrs['grid_mapping'] = data_x.attrs['grid_mapping'] return component_norm @@ -249,9 +244,8 @@ def tangential_component(data_x, data_y, index='index'): component_tang = data_x * unit_tang[0] + data_y * unit_tang[1] # Reattach only reliable attributes after operation - for attr in ('units', 'grid_mapping'): - if attr in data_x.attrs: - component_tang.attrs[attr] = data_x.attrs[attr] + if 'grid_mapping' in data_x.attrs: + component_tang.attrs['grid_mapping'] = data_x.attrs['grid_mapping'] return component_tang @@ -292,20 +286,16 @@ def absolute_momentum(u, v, index='index'): """ # Get the normal component of the wind - norm_wind = normal_component(u, v, index=index) - norm_wind.metpy.convert_units('m/s') + norm_wind = normal_component(u, v, index=index).metpy.convert_units('m/s') # Get other pieces of calculation (all as ndarrays matching shape of norm_wind) - latitude = latitude_from_cross_section(norm_wind) # in degrees_north + latitude = latitude_from_cross_section(norm_wind) _, latitude = xr.broadcast(norm_wind, latitude) - f = coriolis_parameter(np.deg2rad(latitude.values)).magnitude # in 1/s + f = coriolis_parameter(np.deg2rad(latitude.values)) x, y = distances_from_cross_section(norm_wind) - x.metpy.convert_units('meters') - y.metpy.convert_units('meters') + x = x.metpy.convert_units('meters') + y = y.metpy.convert_units('meters') _, x, y = xr.broadcast(norm_wind, x, y) - distance = np.hypot(x, y).values # in meters - - m = norm_wind + f * distance - m.attrs = {'units': norm_wind.attrs['units']} + distance = np.hypot(x.metpy.quantify(), y.metpy.quantify()) - return m + return norm_wind + f * distance diff --git a/src/metpy/calc/indices.py b/src/metpy/calc/indices.py index b569a26cc0c..f297e25ceb1 100644 --- a/src/metpy/calc/indices.py +++ b/src/metpy/calc/indices.py @@ -8,7 +8,7 @@ from .tools import _remove_nans, get_layer from .. import constants as mpconsts from ..package_tools import Exporter -from ..units import atleast_1d, check_units, concatenate, units +from ..units import check_units, concatenate, units from ..xarray import preprocess_xarray exporter = Exporter(globals()) @@ -258,7 +258,7 @@ def supercell_composite(mucape, effective_storm_helicity, effective_shear): supercell composite """ - effective_shear = np.clip(atleast_1d(effective_shear), None, 20 * units('m/s')) + effective_shear = np.clip(np.atleast_1d(effective_shear), None, 20 * units('m/s')) effective_shear[effective_shear < 10 * units('m/s')] = 0 * units('m/s') effective_shear = effective_shear / (20 * units('m/s')) @@ -306,12 +306,12 @@ def significant_tornado(sbcape, surface_based_lcl_height, storm_helicity_1km, sh significant tornado parameter """ - surface_based_lcl_height = np.clip(atleast_1d(surface_based_lcl_height), + surface_based_lcl_height = np.clip(np.atleast_1d(surface_based_lcl_height), 1000 * units.m, 2000 * units.m) surface_based_lcl_height[surface_based_lcl_height > 2000 * units.m] = 0 * units.m surface_based_lcl_height = ((2000. * units.m - surface_based_lcl_height) / (1000. * units.m)) - shear_6km = np.clip(atleast_1d(shear_6km), None, 30 * units('m/s')) + shear_6km = np.clip(np.atleast_1d(shear_6km), None, 30 * units('m/s')) shear_6km[shear_6km < 12.5 * units('m/s')] = 0 * units('m/s') shear_6km /= 20 * units('m/s') diff --git a/src/metpy/calc/kinematics.py b/src/metpy/calc/kinematics.py index 73caeff4ec9..5b7e876e4e1 100644 --- a/src/metpy/calc/kinematics.py +++ b/src/metpy/calc/kinematics.py @@ -9,7 +9,7 @@ from .. import constants as mpconsts from ..cbook import iterable from ..package_tools import Exporter -from ..units import atleast_2d, check_units, concatenate, units +from ..units import check_units, concatenate, units from ..xarray import preprocess_xarray exporter = Exporter(globals()) @@ -260,7 +260,7 @@ def advection(scalar, wind, deltas): # Make them be at least 2D (handling the 1D case) so that we can do the # multiply and sum below - grad, wind = atleast_2d(grad, wind) + grad, wind = np.atleast_2d(grad, wind) return (-grad * wind).sum(axis=0) diff --git a/src/metpy/calc/thermo.py b/src/metpy/calc/thermo.py index 95cf81f04b5..827ed4e5358 100644 --- a/src/metpy/calc/thermo.py +++ b/src/metpy/calc/thermo.py @@ -14,7 +14,7 @@ from ..cbook import broadcast_indices from ..interpolate.one_dimension import interpolate_1d from ..package_tools import Exporter -from ..units import atleast_1d, check_units, concatenate, units +from ..units import check_units, concatenate, units from ..xarray import preprocess_xarray exporter = Exporter(globals()) @@ -267,7 +267,7 @@ def dt(t, p): pressure = pressure.to('mbar') reference_pressure = reference_pressure.to('mbar') - temperature = atleast_1d(temperature) + temperature = np.atleast_1d(temperature) side = 'left' @@ -2374,9 +2374,9 @@ def wet_bulb_temperature(pressure, temperature, dewpoint): """ if not hasattr(pressure, 'shape'): - pressure = atleast_1d(pressure) - temperature = atleast_1d(temperature) - dewpoint = atleast_1d(dewpoint) + pressure = np.atleast_1d(pressure) + temperature = np.atleast_1d(temperature) + dewpoint = np.atleast_1d(dewpoint) it = np.nditer([pressure, temperature, dewpoint, None], op_dtypes=['float', 'float', 'float', 'float'], diff --git a/src/metpy/calc/tools.py b/src/metpy/calc/tools.py index 8282a7b3313..f6e450a02f0 100644 --- a/src/metpy/calc/tools.py +++ b/src/metpy/calc/tools.py @@ -16,7 +16,7 @@ from ..cbook import broadcast_indices, result_type from ..interpolate import interpolate_1d, log_interpolate_1d from ..package_tools import Exporter -from ..units import atleast_1d, check_units, concatenate, diff, units +from ..units import check_units, concatenate, units from ..xarray import check_axis, preprocess_xarray exporter = Exporter(globals()) @@ -1276,7 +1276,7 @@ def _process_deriv_args(f, kwargs): if 'x' in kwargs: raise ValueError('Cannot specify both "x" and "delta".') - delta = atleast_1d(kwargs['delta']) + delta = np.atleast_1d(kwargs['delta']) if delta.size == 1: diff_size = list(f.shape) diff_size[axis] -= 1 @@ -1288,7 +1288,7 @@ def _process_deriv_args(f, kwargs): delta = _broadcast_to_axis(delta, axis, n) elif 'x' in kwargs: x = _broadcast_to_axis(kwargs['x'], axis, n) - delta = diff(x, axis=axis) + delta = np.diff(x, axis=axis) else: raise ValueError('Must specify either "x" or "delta" for value positions.') @@ -1492,7 +1492,7 @@ def wrap_output_like(**wrap_kwargs): - As matched output (final returned value): * ``pint.Quantity`` - * ``xarray.DataArray`` + * ``xarray.DataArray`` wrapping a ``pint.Quantity`` (if matched output is not one of these types, we instead treat the match as if it was a dimenionless Quantity.) @@ -1572,16 +1572,27 @@ def wrapper(*args, **kwargs): def _wrap_output_like_matching_units(result, match): """Convert result to be like match with matching units for output wrapper.""" - match_units = str(getattr(match, 'units', '')) - output_xarray = isinstance(match, xr.DataArray) + if isinstance(match, xr.DataArray): + output_xarray = True + match_units = match.metpy.units + elif isinstance(match, units.Quantity): + output_xarray = False + match_units = match.units + else: + output_xarray = False + match_units = '' + if isinstance(result, xr.DataArray): - result.metpy.convert_units(match_units) - return result if output_xarray else result.metpy.unit_array + result = result.metpy.convert_units(match_units) + return result.metpy.quantify() if output_xarray else result.metpy.unit_array else: result = result.m_as(match_units) if isinstance(result, units.Quantity) else result if output_xarray: - return xr.DataArray(result, dims=match.dims, coords=match.coords, - attrs={'units': match_units}) + return xr.DataArray( + units.Quantity(result, match_units), + dims=match.dims, + coords=match.coords + ) else: return units.Quantity(result, match_units) @@ -1590,7 +1601,7 @@ def _wrap_output_like_not_matching_units(result, match): """Convert result to be like match without matching units for output wrapper.""" output_xarray = isinstance(match, xr.DataArray) if isinstance(result, xr.DataArray): - return result if output_xarray else result.metpy.unit_array + return result.metpy.quantify() if output_xarray else result.metpy.unit_array else: if isinstance(result, units.Quantity): result_magnitude = result.magnitude @@ -1600,7 +1611,10 @@ def _wrap_output_like_not_matching_units(result, match): result_units = '' if output_xarray: - return xr.DataArray(result_magnitude, dims=match.dims, coords=match.coords, - attrs={'units': result_units}) + return xr.DataArray( + units.Quantity(result_magnitude, result_units), + dims=match.dims, + coords=match.coords + ) else: return units.Quantity(result_magnitude, result_units) diff --git a/src/metpy/interpolate/slices.py b/src/metpy/interpolate/slices.py index 53c6ff8dc10..1d134c36e52 100644 --- a/src/metpy/interpolate/slices.py +++ b/src/metpy/interpolate/slices.py @@ -7,6 +7,7 @@ import xarray as xr from ..package_tools import Exporter +from ..units import units from ..xarray import check_axis exporter = Exporter(globals()) @@ -55,6 +56,13 @@ def interpolate_to_slice(data, points, interp_type='linear'): }, method=interp_type) data_sliced.coords['index'] = range(len(points)) + # Bug in xarray: interp strips units + if ( + isinstance(data.data, units.Quantity) + and not isinstance(data_sliced.data, units.Quantity) + ): + data_sliced.data = units.Quantity(data_sliced.data, data.data.units) + return data_sliced diff --git a/src/metpy/plots/declarative.py b/src/metpy/plots/declarative.py index 81715b43499..b60c4654445 100644 --- a/src/metpy/plots/declarative.py +++ b/src/metpy/plots/declarative.py @@ -928,7 +928,7 @@ def griddata(self): data_subset = data.metpy.sel(**subset).squeeze() if self.plot_units is not None: - data_subset.metpy.convert_units(self.plot_units) + data_subset = data_subset.metpy.convert_units(self.plot_units) self._griddata = data_subset return self._griddata @@ -1206,8 +1206,8 @@ def griddata(self): data_subset_v = v.metpy.sel(**subset).squeeze() if self.plot_units is not None: - data_subset_u.metpy.convert_units(self.plot_units) - data_subset_v.metpy.convert_units(self.plot_units) + data_subset_u = data_subset_u.metpy.convert_units(self.plot_units) + data_subset_v = data_subset_v.metpy.convert_units(self.plot_units) self._griddata_u = data_subset_u self._griddata_v = data_subset_v diff --git a/src/metpy/plots/station_plot.py b/src/metpy/plots/station_plot.py index 2bf5d6c6593..54bd7cf7bbd 100644 --- a/src/metpy/plots/station_plot.py +++ b/src/metpy/plots/station_plot.py @@ -10,7 +10,6 @@ from .wx_symbols import (current_weather, high_clouds, low_clouds, mid_clouds, pressure_tendency, sky_cover, wx_symbol_font) from ..package_tools import Exporter -from ..units import atleast_1d exporter = Exporter(globals()) @@ -58,8 +57,8 @@ def __init__(self, ax, x, y, fontsize=10, spacing=None, transform=None, **kwargs """ self.ax = ax - self.x = atleast_1d(x) - self.y = atleast_1d(y) + self.x = np.atleast_1d(x) + self.y = np.atleast_1d(y) self.fontsize = fontsize self.spacing = fontsize if spacing is None else spacing self.transform = transform diff --git a/src/metpy/testing.py b/src/metpy/testing.py index 458936657a7..ca4f9abb586 100644 --- a/src/metpy/testing.py +++ b/src/metpy/testing.py @@ -106,6 +106,11 @@ def check_and_drop_units(actual, desired): """ try: + # Convert DataArrays to Quantities + if isinstance(desired, xr.DataArray): + desired = desired.metpy.unit_array + if isinstance(actual, xr.DataArray): + actual = actual.metpy.unit_array # If the desired result has units, add dimensionless units if necessary, then # ensure that this is compatible to the desired result. if hasattr(desired, 'units'): diff --git a/src/metpy/units.py b/src/metpy/units.py index f2e8a568bdc..dde51b5f176 100644 --- a/src/metpy/units.py +++ b/src/metpy/units.py @@ -29,16 +29,17 @@ DimensionalityError = pint.DimensionalityError # Create registry, with preprocessors for UDUNITS-style powers (m2 s-2) and percent signs -try: - units = pint.UnitRegistry(autoconvert_offset_to_baseunit=True, - preprocessors=[functools.partial( - re.sub, - (r'(?<=[A-Za-z])(?![A-Za-z])(?>> import xarray as xr - >>> temperature = xr.DataArray([[0, 1], [2, 3]], dims=('lat', 'lon'), - ... coords={'lat': [40, 41], 'lon': [-105, -104]}, - ... attrs={'units': 'degC'}) + >>> from metpy.units import units + >>> temperature = xr.DataArray([[0, 1], [2, 3]] * units.degC, dims=('lat', 'lon'), + ... coords={'lat': [40, 41], 'lon': [-105, -104]}) >>> temperature.metpy.x array([-105, -104]) @@ -121,31 +121,68 @@ class MetPyDataArrayAccessor: 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) + """Return the units of this DataArray as a `pint.Unit`.""" + if isinstance(self._data_array.data, units.Quantity): + return self._data_array.data.units else: - return units('percent') + return units.parse_units(self._data_array.attrs.get('units', 'dimensionless')) + + @property + def magnitude(self): + """Return the magnitude of the data values of this DataArray (i.e., without units).""" + if isinstance(self._data_array.data, units.Quantity): + return self._data_array.data.magnitude + else: + return self._data_array.data @property def unit_array(self): """Return the data values of this DataArray as a `pint.Quantity`.""" - return self._data_array.values * self.units - - @unit_array.setter - def unit_array(self, values): - """Set data values from a `pint.Quantity`.""" - self._data_array.values = values.magnitude - self._units = self._data_array.attrs['units'] = str(values.units) + if isinstance(self._data_array.data, units.Quantity): + return self._data_array.data + else: + return units.Quantity(self._data_array.values, self.units) def convert_units(self, units): - """Convert the data values to different units in-place.""" - self.unit_array = self.unit_array.to(units) - return self._data_array # allow method chaining + """Return new DataArray with values converted to different units.""" + return self.quantify().copy(data=self.unit_array.to(units)) + + def convert_coordinate_units(self, coord, units): + """Return new DataArray with coordinate converted to different units.""" + new_coord_var = self._data_array[coord].copy( + data=self._data_array[coord].metpy.unit_array.m_as(units) + ) + new_coord_var.attrs['units'] = str(units) + return self._data_array.assign_coords(coords={coord: new_coord_var}) + + def quantify(self): + """Return a DataArray with the data converted to a `pint.Quantity`.""" + if ( + not isinstance(self._data_array.data, units.Quantity) + and np.issubdtype(self._data_array.data.dtype, np.number) + ): + # Only quantify if not already quantified and is quantifiable + quantified_dataarray = self._data_array.copy(data=self.unit_array) + if 'units' in quantified_dataarray.attrs: + del quantified_dataarray.attrs['units'] + else: + quantified_dataarray = self._data_array + return quantified_dataarray + + def dequantify(self): + """Return a DataArray with the data as magnitude and the units as an attribute.""" + if isinstance(self._data_array.data, units.Quantity): + # Only dequantify if quantified + dequantified_dataarray = self._data_array.copy( + data=self._data_array.data.magnitude + ) + dequantified_dataarray.attrs['units'] = str(self.units) + else: + dequantified_dataarray = self._data_array + return dequantified_dataarray @property def crs(self): @@ -617,11 +654,11 @@ def _has_coord(coord_type): log.warning('Found valid latitude/longitude coordinates, assuming ' 'latitude_longitude for projection grid_mapping variable') - # Rebuild the coordinates of the dataarray, and return - coords = dict(self._rebuild_coords(var, crs)) + # Rebuild the coordinates of the dataarray, and return quantified DataArray + var = self._rebuild_coords(var, crs) if crs is not None: - coords['crs'] = crs - return var.assign_coords(**coords) + var = var.assign_coords(coords={'crs': crs}) + return var.metpy.quantify() def _rebuild_coords(self, var, crs): """Clean up the units on the coordinate variables.""" @@ -629,19 +666,21 @@ def _rebuild_coords(self, var, crs): if (check_axis(coord_var, 'x', 'y') and not check_axis(coord_var, 'longitude', 'latitude')): try: - # Cannot modify an index inplace, so use copy - yield coord_name, coord_var.copy().metpy.convert_units('meters') + var = var.metpy.convert_coordinate_units(coord_name, 'meters') except DimensionalityError: # Radians! Attempt to use perspective point height conversion if crs is not None: - new_coord_var = coord_var.copy() height = crs['perspective_point_height'] - scaled_vals = new_coord_var.metpy.unit_array * (height * units.meters) - new_coord_var.metpy.unit_array = scaled_vals.to('meters') - yield coord_name, new_coord_var - else: - # Do nothing - yield coord_name, coord_var + new_coord_var = coord_var.copy( + data=( + coord_var.metpy.unit_array + * (height * units.meter) + ).m_as('meter') + ) + new_coord_var.attrs['units'] = 'meter' + var = var.assign_coords(coords={coord_name: new_coord_var}) + + return var class _LocIndexer: """Provide the unit-wrapped .loc indexer for datasets.""" @@ -855,12 +894,12 @@ def check_axis(var, *axes): if (axis in coordinate_criteria['units'] and ( ( coordinate_criteria['units'][axis]['match'] == 'dimensionality' - and (units.get_dimensionality(var.attrs.get('units')) + and (units.get_dimensionality(var.metpy.units) == units.get_dimensionality( coordinate_criteria['units'][axis]['units'])) ) or ( coordinate_criteria['units'][axis]['match'] == 'name' - and var.attrs.get('units') + and str(var.metpy.units) in coordinate_criteria['units'][axis]['units'] ))): return True @@ -976,28 +1015,11 @@ def wrapper(*args, **kwargs): return wrapper -# If DatetimeAccessor does not have a strftime (xarray <0.12.2), monkey patch one in -try: - from xarray.core.accessors import DatetimeAccessor - if not hasattr(DatetimeAccessor, 'strftime'): - def strftime(self, date_format): - """Format time as a string.""" - import pandas as pd - values = self._obj.data - values_as_series = pd.Series(values.ravel()) - strs = values_as_series.dt.strftime(date_format) - return strs.values.reshape(values.shape) - - DatetimeAccessor.strftime = strftime -except ImportError: - pass - - def _reassign_quantity_indexer(data, indexers): """Reassign a units.Quantity indexer to units of relevant coordinate.""" def _to_magnitude(val, unit): try: - return val.to(unit).m + return val.m_as(unit) except AttributeError: return val diff --git a/tests/calc/test_calc_tools.py b/tests/calc/test_calc_tools.py index bd9419dc010..319078434e8 100644 --- a/tests/calc/test_calc_tools.py +++ b/tests/calc/test_calc_tools.py @@ -1087,6 +1087,7 @@ def test_first_derivative_xarray_lonlat(test_da_lonlat): _, truth = xr.broadcast(test_da_lonlat, partial) truth.coords['crs'] = test_da_lonlat['crs'] truth.attrs['units'] = 'kelvin / meter' + truth.metpy.quantify() # Assert result matches expectation xr.testing.assert_allclose(deriv, truth) @@ -1102,6 +1103,7 @@ def test_first_derivative_xarray_time_and_default_axis(test_da_xy): deriv = first_derivative(test_da_xy) truth = xr.full_like(test_da_xy, -0.000777000777) truth.attrs['units'] = 'kelvin / second' + truth.metpy.quantify() xr.testing.assert_allclose(deriv, truth) assert deriv.metpy.units == truth.metpy.units @@ -1121,6 +1123,7 @@ def test_first_derivative_xarray_time_subsecond_precision(): truth = xr.full_like(test_da, 5.) truth.attrs['units'] = 'kelvin / second' + truth.metpy.quantify() xr.testing.assert_allclose(deriv, truth) assert deriv.metpy.units == truth.metpy.units @@ -1138,6 +1141,7 @@ def test_second_derivative_xarray_lonlat(test_da_lonlat): _, truth = xr.broadcast(test_da_lonlat, partial) truth.coords['crs'] = test_da_lonlat['crs'] truth.attrs['units'] = 'kelvin / meter^2' + truth.metpy.quantify() xr.testing.assert_allclose(deriv, truth) assert deriv.metpy.units == truth.metpy.units @@ -1152,9 +1156,11 @@ def test_gradient_xarray(test_da_xy): truth_x = xr.full_like(test_da_xy, -6.993007e-07) truth_x.attrs['units'] = 'kelvin / meter' + truth_x.metpy.quantify() truth_y = xr.full_like(test_da_xy, -2.797203e-06) truth_y.attrs['units'] = 'kelvin / meter' + truth_y.metpy.quantify() partial = xr.DataArray( np.array([0.04129204, 0.03330003, 0.02264402]), @@ -1163,6 +1169,7 @@ def test_gradient_xarray(test_da_xy): _, truth_p = xr.broadcast(test_da_xy, partial) truth_p.coords['crs'] = test_da_xy['crs'] truth_p.attrs['units'] = 'kelvin / hectopascal' + truth_p.metpy.quantify() # Assert results match expectations xr.testing.assert_allclose(deriv_x, truth_x) @@ -1188,9 +1195,11 @@ def test_gradient_xarray_implicit_axes(test_da_xy): truth_x = xr.full_like(data, -6.993007e-07) truth_x.attrs['units'] = 'kelvin / meter' + truth_x.metpy.quantify() truth_y = xr.full_like(data, -2.797203e-06) truth_y.attrs['units'] = 'kelvin / meter' + truth_y.metpy.quantify() xr.testing.assert_allclose(deriv_x, truth_x) assert deriv_x.metpy.units == truth_x.metpy.units @@ -1205,21 +1214,25 @@ def test_gradient_xarray_implicit_axes_transposed(test_da_lonlat): deriv_x, deriv_y = gradient(test_da) truth_x = xr.DataArray( - np.array([[-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06], - [-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06], - [-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06], - [-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06]]), + np.array( + [[-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06], + [-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06], + [-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06], + [-3.30782978e-06, -3.42816074e-06, -3.57012948e-06, -3.73759364e-06]] + ) * units('kelvin / meter'), dims=test_da.dims, - coords=test_da.coords, - attrs={'units': 'kelvin / meter'}) + coords=test_da.coords + ) truth_y = xr.DataArray( - np.array([[-1.15162805e-05, -1.15101023e-05, -1.15037894e-05, -1.14973413e-05], - [-1.15162805e-05, -1.15101023e-05, -1.15037894e-05, -1.14973413e-05], - [-1.15162805e-05, -1.15101023e-05, -1.15037894e-05, -1.14973413e-05], - [-1.15162805e-05, -1.15101023e-05, -1.15037894e-05, -1.14973413e-05]]), + np.array( + [[-1.15162805e-05, -1.15101023e-05, -1.15037894e-05, -1.14973413e-05], + [-1.15162805e-05, -1.15101023e-05, -1.15037894e-05, -1.14973413e-05], + [-1.15162805e-05, -1.15101023e-05, -1.15037894e-05, -1.14973413e-05], + [-1.15162805e-05, -1.15101023e-05, -1.15037894e-05, -1.14973413e-05]] + ) * units('kelvin / meter'), dims=test_da.dims, - coords=test_da.coords, - attrs={'units': 'kelvin / meter'}) + coords=test_da.coords + ) xr.testing.assert_allclose(deriv_x, truth_x) assert deriv_x.metpy.units == truth_x.metpy.units @@ -1240,6 +1253,7 @@ def test_laplacian_xarray_lonlat(test_da_lonlat): _, truth = xr.broadcast(test_da_lonlat, partial) truth.coords['crs'] = test_da_lonlat['crs'] truth.attrs['units'] = 'kelvin / meter^2' + truth.metpy.quantify() xr.testing.assert_allclose(laplac, truth) assert laplac.metpy.units == truth.metpy.units @@ -1285,33 +1299,31 @@ def test_remove_nans(): ( np.arange(4), xr.DataArray( - np.zeros(4), + np.zeros(4) * units.meter, dims=('x',), coords={'x': np.linspace(0, 1, 4)}, - attrs={'units': 'meter', 'description': 'Just some zeros'} + attrs={'description': 'Just some zeros'} ), False, xr.DataArray( - np.arange(4), + np.arange(4) * units.dimensionless, dims=('x',), - coords={'x': np.linspace(0, 1, 4)}, - attrs={'units': ''} + coords={'x': np.linspace(0, 1, 4)} ) ), ( np.arange(4), xr.DataArray( - np.zeros(4), + np.zeros(4) * units.meter, dims=('x',), coords={'x': np.linspace(0, 1, 4)}, - attrs={'units': 'meter', 'description': 'Just some zeros'} + attrs={'description': 'Just some zeros'} ), True, xr.DataArray( - np.arange(4), + np.arange(4) * units.meter, dims=('x',), - coords={'x': np.linspace(0, 1, 4)}, - attrs={'units': 'meter'} + coords={'x': np.linspace(0, 1, 4)} ) ), ([2, 4, 8] * units.kg, [0] * units.m, False, [2, 4, 8] * units.kg), @@ -1319,39 +1331,35 @@ def test_remove_nans(): ( [2, 4, 8] * units.kg, xr.DataArray( - np.zeros(3), + np.zeros(3) * units.meter, dims=('x',), - coords={'x': np.linspace(0, 1, 3)}, - attrs={'units': 'meter'} + coords={'x': np.linspace(0, 1, 3)} ), False, xr.DataArray( - [2, 4, 8], + [2, 4, 8] * units.kilogram, dims=('x',), - coords={'x': np.linspace(0, 1, 3)}, - attrs={'units': 'kilogram'} + coords={'x': np.linspace(0, 1, 3)} ) ), ( [2, 4, 8] * units.kg, xr.DataArray( - np.zeros(3), + np.zeros(3) * units.gram, dims=('x',), - coords={'x': np.linspace(0, 1, 3)}, - attrs={'units': 'gram'} + coords={'x': np.linspace(0, 1, 3)} ), True, xr.DataArray( - [2000, 4000, 8000], + [2000, 4000, 8000] * units.gram, dims=('x',), - coords={'x': np.linspace(0, 1, 3)}, - attrs={'units': 'gram'} + coords={'x': np.linspace(0, 1, 3)} ) ), ( xr.DataArray( - np.linspace(0, 1, 5), - attrs={'units': 'meter', 'description': 'A range of values'} + np.linspace(0, 1, 5) * units.meter, + attrs={'description': 'A range of values'} ), np.arange(4, dtype=np.float64), False, @@ -1359,8 +1367,8 @@ def test_remove_nans(): ), ( xr.DataArray( - np.linspace(0, 1, 5), - attrs={'units': 'meter', 'description': 'A range of values'} + np.linspace(0, 1, 5) * units.meter, + attrs={'description': 'A range of values'} ), [0] * units.kg, False, @@ -1368,8 +1376,8 @@ def test_remove_nans(): ), ( xr.DataArray( - np.linspace(0, 1, 5), - attrs={'units': 'meter', 'description': 'A range of values'} + np.linspace(0, 1, 5) * units.meter, + attrs={'description': 'A range of values'} ), [0] * units.cm, True, @@ -1377,36 +1385,36 @@ def test_remove_nans(): ), ( xr.DataArray( - np.linspace(0, 1, 5), - attrs={'units': 'meter', 'description': 'A range of values'} + np.linspace(0, 1, 5) * units.meter, + attrs={'description': 'A range of values'} ), xr.DataArray( - np.zeros(3), + np.zeros(3) * units.kilogram, dims=('x',), coords={'x': np.linspace(0, 1, 3)}, - attrs={'units': 'kilogram', 'description': 'Alternative data'} + attrs={'description': 'Alternative data'} ), False, xr.DataArray( - np.linspace(0, 1, 5), - attrs={'units': 'meter', 'description': 'A range of values'} + np.linspace(0, 1, 5) * units.meter, + attrs={'description': 'A range of values'} ) ), ( xr.DataArray( - np.linspace(0, 1, 5), - attrs={'units': 'meter', 'description': 'A range of values'} + np.linspace(0, 1, 5) * units.meter, + attrs={'description': 'A range of values'} ), xr.DataArray( - np.zeros(3), + np.zeros(3) * units.centimeter, dims=('x',), coords={'x': np.linspace(0, 1, 3)}, - attrs={'units': 'centimeter', 'description': 'Alternative data'} + attrs={'description': 'Alternative data'} ), True, xr.DataArray( - np.linspace(0, 100, 5), - attrs={'units': 'centimeter', 'description': 'A range of values'} + np.linspace(0, 100, 5) * units.centimeter, + attrs={'description': 'A range of values'} ) ), ]) @@ -1431,35 +1439,31 @@ def almost_identity(arg): ( [2, 4, 8] * units.kg, xr.DataArray( - np.zeros(3), + np.zeros(3) * units.meter, dims=('x',), - coords={'x': np.linspace(0, 1, 3)}, - attrs={'units': 'meter'} + coords={'x': np.linspace(0, 1, 3)} ) ), ( xr.DataArray( - np.linspace(0, 1, 5), - attrs={'units': 'meter'} + np.linspace(0, 1, 5) * units.meter ), [0] * units.kg ), ( xr.DataArray( - np.linspace(0, 1, 5), - attrs={'units': 'meter'} + np.linspace(0, 1, 5) * units.meter ), xr.DataArray( - np.zeros(3), + np.zeros(3) * units.kg, dims=('x',), - coords={'x': np.linspace(0, 1, 3)}, - attrs={'units': 'kilogram'} + coords={'x': np.linspace(0, 1, 3)} ) ), ( xr.DataArray( - np.linspace(0, 1, 5), - attrs={'units': 'meter', 'description': 'A range of values'} + np.linspace(0, 1, 5) * units.meter, + attrs={'description': 'A range of values'} ), np.arange(4, dtype=np.float64) ) @@ -1480,8 +1484,8 @@ def test_wrap_output_like_with_argument_kwarg(): def double(a): return units.Quantity(2) * a.metpy.unit_array - test = xr.DataArray([1, 3, 5, 7], attrs={'units': 'm'}) - expected = xr.DataArray([2, 6, 10, 14], attrs={'units': 'meter'}) + test = xr.DataArray([1, 3, 5, 7] * units.m) + expected = xr.DataArray([2, 6, 10, 14] * units.m) xr.testing.assert_identical(double(test), expected) diff --git a/tests/calc/test_cross_sections.py b/tests/calc/test_cross_sections.py index bd91765d8bc..fcb6cb36128 100644 --- a/tests/calc/test_cross_sections.py +++ b/tests/calc/test_cross_sections.py @@ -13,13 +13,14 @@ latitude_from_cross_section) from metpy.interpolate import cross_section from metpy.testing import assert_array_almost_equal, assert_xarray_allclose +from metpy.units import units @pytest.fixture() def test_cross_lonlat(): """Return cross section on a lon/lat grid with no time coordinate for use in tests.""" - data_u = np.linspace(-40, 40, 5 * 6 * 7).reshape((5, 6, 7)) - data_v = np.linspace(40, -40, 5 * 6 * 7).reshape((5, 6, 7)) + data_u = np.linspace(-40, 40, 5 * 6 * 7).reshape((5, 6, 7)) * units.knots + data_v = np.linspace(40, -40, 5 * 6 * 7).reshape((5, 6, 7)) * units.knots ds = xr.Dataset( { 'u_wind': (['isobaric', 'lat', 'lon'], data_u), @@ -46,8 +47,6 @@ def test_cross_lonlat(): ) } ) - ds['u_wind'].attrs['units'] = 'knots' - ds['v_wind'].attrs['units'] = 'knots' start, end = (30.5, 255.5), (44.5, 274.5) return cross_section(ds.metpy.parse_cf(), start, end, steps=7, interp_type='nearest') @@ -56,8 +55,8 @@ def test_cross_lonlat(): @pytest.fixture() def test_cross_xy(): """Return cross section on a x/y grid with a time coordinate for use in tests.""" - data_u = np.linspace(-25, 25, 5 * 6 * 7).reshape((1, 5, 6, 7)) - data_v = np.linspace(25, -25, 5 * 6 * 7).reshape((1, 5, 6, 7)) + data_u = np.linspace(-25, 25, 5 * 6 * 7).reshape((1, 5, 6, 7)) * units('m/s') + data_v = np.linspace(25, -25, 5 * 6 * 7).reshape((1, 5, 6, 7)) * units('m/s') ds = xr.Dataset( { 'u_wind': (['time', 'isobaric', 'y', 'x'], data_u), @@ -91,7 +90,6 @@ def test_cross_xy(): } ) ds['u_wind'].attrs = ds['v_wind'].attrs = { - 'units': 'm/s', 'grid_mapping': 'lambert_conformal' } ds['lambert_conformal'].attrs = { @@ -117,26 +115,24 @@ def test_distances_from_cross_section_given_lonlat(test_cross_lonlat): 1133651.21398864, 1417064.0174858, 1700476.82098296]) index = xr.DataArray(range(7), name='index', dims=['index']) true_x = xr.DataArray( - true_x_values, + true_x_values * units.meters, coords={ 'crs': test_cross_lonlat['crs'], 'lat': test_cross_lonlat['lat'], 'lon': test_cross_lonlat['lon'], 'index': index, }, - dims=['index'], - attrs={'units': 'meters'} + dims=['index'] ) true_y = xr.DataArray( - true_y_values, + true_y_values * units.meters, coords={ 'crs': test_cross_lonlat['crs'], 'lat': test_cross_lonlat['lat'], 'lon': test_cross_lonlat['lon'], 'index': index, }, - dims=['index'], - attrs={'units': 'meters'} + dims=['index'] ) assert_xarray_allclose(x, true_x) assert_xarray_allclose(y, true_y) @@ -152,7 +148,7 @@ def test_distances_from_cross_section_given_xy(test_cross_xy): def test_distances_from_cross_section_given_bad_coords(test_cross_xy): """Ensure an AttributeError is raised when the cross section lacks neeed coordinates.""" with pytest.raises(AttributeError): - distances_from_cross_section(test_cross_xy['u_wind'].drop('x')) + distances_from_cross_section(test_cross_xy['u_wind'].drop_vars('x')) def test_latitude_from_cross_section_given_lat(test_cross_lonlat): @@ -168,15 +164,14 @@ def test_latitude_from_cross_section_given_y(test_cross_xy): 43.0845549, 42.95]) index = xr.DataArray(range(7), name='index', dims=['index']) true_latitude = xr.DataArray( - true_latitude_values, + true_latitude_values * units.degrees_north, coords={ 'crs': test_cross_xy['crs'], 'y': test_cross_xy['y'], 'x': test_cross_xy['x'], 'index': index, }, - dims=['index'], - attrs={'units': 'degrees_north'} + dims=['index'] ) assert_xarray_allclose(latitude, true_latitude) @@ -235,11 +230,11 @@ def test_cross_section_components(test_cross_lonlat): -25.13011869, -29.45357997, -33.77704125], [-34.31747391, -38.64093519, -42.96439647, -47.28785775, -47.82829041, -52.15175169, -56.47521297]]) - true_tang_wind = xr.DataArray(true_tang_wind_values, + true_tang_wind = xr.DataArray(true_tang_wind_values * units.knots, coords=test_cross_lonlat['u_wind'].coords, dims=test_cross_lonlat['u_wind'].dims, attrs=test_cross_lonlat['u_wind'].attrs) - true_norm_wind = xr.DataArray(true_norm_wind_values, + true_norm_wind = xr.DataArray(true_norm_wind_values * units.knots, coords=test_cross_lonlat['u_wind'].coords, dims=test_cross_lonlat['u_wind'].dims, attrs=test_cross_lonlat['u_wind'].attrs) @@ -260,7 +255,7 @@ def test_tangential_component(test_cross_xy): 5.84347621, 6.49821458, 7.12201819], [8.85432685, 9.39001443, 9.90304096, 10.41945241, 10.93426433, 11.43606396, 11.90970179]]]) - true_tang_wind = xr.DataArray(true_tang_wind_values, + true_tang_wind = xr.DataArray(true_tang_wind_values * units('m/s'), coords=test_cross_xy['u_wind'].coords, dims=test_cross_xy['u_wind'].dims, attrs=test_cross_xy['u_wind'].attrs) @@ -280,7 +275,7 @@ def test_normal_component(test_cross_xy): -15.22809117, -17.53474865, -19.90214816], [-19.5758891, -21.71335642, -23.92155707, -26.18279611, -28.49467817, -30.8590159, -33.28110701]]]) - true_norm_wind = xr.DataArray(true_norm_wind_values, + true_norm_wind = xr.DataArray(true_norm_wind_values * units('m/s'), coords=test_cross_xy['u_wind'].coords, dims=test_cross_xy['u_wind'].dims, attrs=test_cross_xy['u_wind'].attrs) @@ -301,10 +296,9 @@ def test_absolute_momentum_given_lonlat(test_cross_lonlat): [-17.6544338, 10.29896833, 42.1895445, 77.67190477, 118.32104328, 159.84996612, 203.78899492]]) - true_momentum = xr.DataArray(true_momentum_values, + true_momentum = xr.DataArray(true_momentum_values * units('m/s'), coords=test_cross_lonlat['u_wind'].coords, - dims=test_cross_lonlat['u_wind'].dims, - attrs={'units': 'meter / second'}) + dims=test_cross_lonlat['u_wind'].dims) assert_xarray_allclose(momentum, true_momentum) @@ -321,8 +315,7 @@ def test_absolute_momentum_given_xy(test_cross_xy): 175.24900718, 225.76516831, 278.20450691], [117.43415353, 94.19367366, 93.23867305, 119.05994273, 161.98242018, 212.44090106, 264.82554806]]]) - true_momentum = xr.DataArray(true_momentum_values, + true_momentum = xr.DataArray(true_momentum_values * units('m/s'), coords=test_cross_xy['u_wind'].coords, - dims=test_cross_xy['u_wind'].dims, - attrs={'units': 'meter / second'}) + dims=test_cross_xy['u_wind'].dims) assert_xarray_allclose(momentum, true_momentum) diff --git a/tests/calc/test_kinematics.py b/tests/calc/test_kinematics.py index 44e61e4880d..e31ab8a7a6d 100644 --- a/tests/calc/test_kinematics.py +++ b/tests/calc/test_kinematics.py @@ -1035,7 +1035,7 @@ def data_4d(): data = xr.open_dataset(get_test_data('irma_gfs_example.nc', False)) data = data.metpy.parse_cf() data['Geopotential_height_isobaric'].attrs['units'] = 'm' - subset = data.drop(( + subset = data.drop_vars(( 'LatLon_361X720-0p25S-180p00E', 'Vertical_velocity_pressure_isobaric', 'isobaric1', 'Relative_humidity_isobaric', 'reftime' )).sel( diff --git a/tests/calc/test_thermo.py b/tests/calc/test_thermo.py index 5f9b5f9c461..57c1388d609 100644 --- a/tests/calc/test_thermo.py +++ b/tests/calc/test_thermo.py @@ -51,9 +51,9 @@ def test_relative_humidity_from_dewpoint_with_f(): def test_relative_humidity_from_dewpoint_xarray(): - """Test Relative Humidity calculation with xarray data arrays.""" + """Test Relative Humidity with xarray data arrays (quantified and unquantified).""" temp = xr.DataArray(25., attrs={'units': 'degC'}) - dewp = xr.DataArray(15., attrs={'units': 'degC'}) + dewp = xr.DataArray([15.] * units.degC) assert_almost_equal(relative_humidity_from_dewpoint(temp, dewp), 53.80 * units.percent, 2) diff --git a/tests/interpolate/test_slices.py b/tests/interpolate/test_slices.py index c254854ebfd..ad1264f6c6a 100644 --- a/tests/interpolate/test_slices.py +++ b/tests/interpolate/test_slices.py @@ -9,13 +9,14 @@ from metpy.interpolate import cross_section, geodesic, interpolate_to_slice from metpy.testing import assert_array_almost_equal +from metpy.units import units @pytest.fixture() def test_ds_lonlat(): """Return dataset on a lon/lat grid with no time coordinate for use in tests.""" - data_temp = np.linspace(250, 300, 5 * 6 * 7).reshape((5, 6, 7)) - data_rh = np.linspace(0, 1, 5 * 6 * 7).reshape((5, 6, 7)) + data_temp = np.linspace(250, 300, 5 * 6 * 7).reshape((5, 6, 7)) * units.kelvin + data_rh = np.linspace(0, 1, 5 * 6 * 7).reshape((5, 6, 7)) * units.dimensionless ds = xr.Dataset( { 'temperature': (['isobaric', 'lat', 'lon'], data_temp), @@ -42,15 +43,13 @@ def test_ds_lonlat(): ) } ) - ds['temperature'].attrs['units'] = 'kelvin' - ds['relative_humidity'].attrs['units'] = 'dimensionless' return ds.metpy.parse_cf() @pytest.fixture() def test_ds_xy(): """Return dataset on a x/y grid with a time coordinate for use in tests.""" - data_temperature = np.linspace(250, 300, 5 * 6 * 7).reshape((1, 5, 6, 7)) + data_temperature = np.linspace(250, 300, 5 * 6 * 7).reshape((1, 5, 6, 7)) * units.kelvin ds = xr.Dataset( { 'temperature': (['time', 'isobaric', 'y', 'x'], data_temperature), @@ -83,7 +82,6 @@ def test_ds_xy(): } ) ds['temperature'].attrs = { - 'units': 'kelvin', 'grid_mapping': 'lambert_conformal' } ds['lambert_conformal'].attrs = { @@ -169,7 +167,7 @@ def test_cross_section_dataarray_and_linear_interp(test_ds_xy): dims=['index'] ) data_truth = xr.DataArray( - truth_values, + truth_values * units.kelvin, name='temperature', coords={ 'time': data['time'], @@ -179,8 +177,7 @@ def test_cross_section_dataarray_and_linear_interp(test_ds_xy): 'y': data_truth_y, 'x': data_truth_x }, - dims=['time', 'isobaric', 'index'], - attrs={'units': 'kelvin'} + dims=['time', 'isobaric', 'index'] ) xr.testing.assert_allclose(data_truth, data_cross) diff --git a/tests/plots/test_mapping.py b/tests/plots/test_mapping.py index 1294a54d638..f7cbf67bca9 100644 --- a/tests/plots/test_mapping.py +++ b/tests/plots/test_mapping.py @@ -66,7 +66,7 @@ def test_globe_spheroid(): assert globe_params['a'] == 6367000 assert globe_params['b'] == 6360000 - + def test_aea(): """Test handling albers equal area projection.""" attrs = {'grid_mapping_name': 'albers_conical_equal_area', 'earth_radius': 6367000, @@ -89,7 +89,7 @@ def test_aea_minimal(): def test_aea_single_std_parallel(): """Test albers equal area with one standard parallel.""" - attrs = {'grid_mapping_name': 'albers_conical_equal_area', 'standard_parallel': 25} + attrs = {'grid_mapping_name': 'albers_conical_equal_area', 'standard_parallel': 20} crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.AlbersEqualArea) assert crs.proj4_params['lat_1'] == 20 diff --git a/tests/test_testing.py b/tests/test_testing.py index dd387add682..9efd83ab50f 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -6,9 +6,11 @@ import numpy as np import pytest +import xarray as xr from metpy.deprecation import MetpyDeprecationWarning -from metpy.testing import assert_array_almost_equal, check_and_silence_deprecation +from metpy.testing import (assert_array_almost_equal, check_and_drop_units, + check_and_silence_deprecation) # Test #1183: numpy.testing.assert_array* ignores any masked value, so work-around @@ -30,3 +32,13 @@ def test_masked_and_no_mask(): def test_deprecation_decorator(): """Make sure the deprecation checker works.""" warnings.warn('Testing warning.', MetpyDeprecationWarning) + + +def test_check_and_drop_units_with_dataarray(): + """Make sure check_and_drop_units functions properly with both arguments as DataArrays.""" + var_0 = xr.DataArray([[1, 2], [3, 4]], attrs={'units': 'cm'}) + var_1 = xr.DataArray([[0.01, 0.02], [0.03, 0.04]], attrs={'units': 'm'}) + actual, desired = check_and_drop_units(var_0, var_1) + assert isinstance(actual, np.ndarray) + assert isinstance(desired, np.ndarray) + np.testing.assert_array_almost_equal(actual, desired) diff --git a/tests/test_xarray.py b/tests/test_xarray.py index 158a0ef7b88..c26001bc795 100644 --- a/tests/test_xarray.py +++ b/tests/test_xarray.py @@ -57,7 +57,7 @@ def test_var_multidim_full(test_ds): @pytest.fixture def test_var_multidim_no_xy(test_var_multidim_full): """Provide a variable with multidimensional lat/lon coords but without x/y coords.""" - return test_var_multidim_full.drop(['y', 'x']) + return test_var_multidim_full.drop_vars(['y', 'x']) def test_projection(test_var): @@ -96,27 +96,68 @@ def test_unit_array(test_var): def test_units(test_var): """Test the units property on the accessor.""" - assert test_var.metpy.units == units('kelvin') + assert test_var.metpy.units == units.kelvin def test_units_percent(): - """Test that '%' is converted to 'percent'.""" + """Test that '%' is handled as 'percent'.""" test_var_percent = xr.open_dataset( get_test_data('irma_gfs_example.nc', as_file_obj=False))['Relative_humidity_isobaric'] - assert test_var_percent.metpy.units == units('percent') + assert test_var_percent.metpy.units == units.percent + + +def test_magnitude_with_quantity(test_var): + """Test magnitude property on accessor when data is a quantity.""" + assert isinstance(test_var.metpy.magnitude, np.ndarray) + np.testing.assert_array_almost_equal(test_var.metpy.magnitude, np.asarray(test_var.values)) + + +def test_magnitude_without_quantity(test_ds_generic): + """Test magnitude property on accessor when data is not a quantity.""" + assert isinstance(test_ds_generic['test'].data, np.ndarray) + np.testing.assert_array_equal( + test_ds_generic['test'].metpy.magnitude, + np.asarray(test_ds_generic['test'].values) + ) def test_convert_units(test_var): - """Test in-place conversion of units.""" - test_var.metpy.convert_units('degC') + """Test conversion of units.""" + result = test_var.metpy.convert_units('degC') - # Check that variable metadata is updated - assert units(test_var.attrs['units']) == units('degC') + # Check that units are updated without modifying original + assert result.metpy.units == units.degC + assert test_var.metpy.units == units.kelvin # Make sure we now get an array back with properly converted values - assert test_var.metpy.unit_array.units == units.degC - assert_almost_equal(test_var[0, 0, 0, 0], 18.44 * units.degC, 2) + assert_almost_equal(result[0, 0, 0, 0], 18.44 * units.degC, 2) + + +def test_convert_coordinate_units(test_ds_generic): + """Test conversion of coordinate units.""" + result = test_ds_generic['test'].metpy.convert_coordinate_units('b', 'percent') + assert result['b'].data[1] == 100. + assert result['b'].metpy.units == units.percent + + +def test_quantify(test_ds_generic): + """Test quantify method for converting data to Quantity.""" + original = test_ds_generic['test'].values + result = test_ds_generic['test'].metpy.quantify() + assert isinstance(result.data, units.Quantity) + assert result.data.units == units.dimensionless + assert 'units' not in result.attrs + np.testing.assert_array_almost_equal(result.data, units.Quantity(original)) + + +def test_dequantify(test_var): + """Test dequantify method for converting data away from Quantity.""" + original = test_var.data + result = test_var.metpy.dequantify() + assert isinstance(result.data, np.ndarray) + assert result.attrs['units'] == 'kelvin' + np.testing.assert_array_almost_equal(result.data, original.magnitude) def test_radian_projection_coords(): @@ -179,12 +220,6 @@ def func(a, b): assert_array_equal(func(data, b=data2), np.array([1001, 1001, 1001]) * units.m) -def test_strftime(): - """Test our monkey-patched xarray strftime.""" - data = xr.DataArray(np.datetime64('2000-01-01 01:00:00')) - assert '2000-01-01 01:00:00' == data.dt.strftime('%Y-%m-%d %H:%M:%S') - - def test_coordinates_basic_by_method(test_var): """Test that NARR example coordinates are like we expect using coordinates method.""" x, y, vertical, time = test_var.metpy.coordinates('x', 'y', 'vertical', 'time') @@ -439,7 +474,7 @@ def test_coordinates_identical_true(test_ds_generic): def test_coordinates_identical_false_number_of_coords(test_ds_generic): """Test coordinates identical method when false due to number of coordinates.""" - other_ds = test_ds_generic.drop('e') + other_ds = test_ds_generic.drop_vars('e') assert not test_ds_generic['test'].metpy.coordinates_identical(other_ds['test']) @@ -664,13 +699,6 @@ def test_coordinate_identification_shared_but_not_equal_coords(): assert ds['isobaric2'].identical(ds['u'].metpy.vertical) -def test_check_no_quantification_of_xarray_data(test_ds_generic): - """Test that .unit_array setter does not insert a `pint.Quantity` into the DataArray.""" - var = test_ds_generic['e'] - var.metpy.unit_array = [1000, 925, 850, 700, 500] * units.hPa - assert not isinstance(var.data, units.Quantity) - - def test_one_dimensional_lat_lon(test_ds_generic): """Test that 1D lat/lon coords are recognized as both x/y and longitude/latitude.""" test_ds_generic['d'].attrs['units'] = 'degrees_north' diff --git a/tests/units/test_units.py b/tests/units/test_units.py index 90ea0d0f5f1..072e9bb278d 100644 --- a/tests/units/test_units.py +++ b/tests/units/test_units.py @@ -14,8 +14,7 @@ from metpy.testing import assert_array_almost_equal, assert_array_equal from metpy.testing import assert_nan, set_agg_backend # noqa: F401 -from metpy.units import (atleast_1d, atleast_2d, check_units, concatenate, diff, - pandas_dataframe_to_unit_arrays, units) +from metpy.units import check_units, concatenate, pandas_dataframe_to_unit_arrays, units def test_concatenate(): @@ -61,29 +60,6 @@ def test_axvline(): return fig -def test_atleast1d_without_units(): - """Test that atleast_1d wrapper can handle plain arrays.""" - assert_array_equal(atleast_1d(1), np.array([1])) - assert_array_equal(atleast_1d([1, ], [2, ]), np.array([[1, ], [2, ]])) - - -def test_atleast2d_without_units(): - """Test that atleast_2d wrapper can handle plain arrays.""" - assert_array_equal(atleast_2d(1), np.array([[1]])) - - -def test_atleast2d_with_units(): - """Test that atleast_2d wrapper can handle plain array with units.""" - assert_array_equal( - atleast_2d(1 * units.degC), np.array([[1]]) * units.degC) - - -def test_units_diff(): - """Test our diff handles units properly.""" - assert_array_equal(diff(np.arange(20, 22) * units.degC), - np.array([1]) * units.delta_degC) - - # # Tests for unit-checking decorator # diff --git a/tutorials/xarray_tutorial.py b/tutorials/xarray_tutorial.py index 05d55f922c9..fc93a8c0909 100644 --- a/tutorials/xarray_tutorial.py +++ b/tutorials/xarray_tutorial.py @@ -82,7 +82,7 @@ # convert the the data from one unit to another (keeping it as a DataArray). For now, we'll # just use ``convert_units`` to convert our temperature to ``degC``. -data['temperature'].metpy.convert_units('degC') +data['temperature'] = data['temperature'].metpy.convert_units('degC') ######################################################################### # Coordinates