diff --git a/.travis.yml b/.travis.yml index e887ec60467..c2ebc362afd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ matrix: - python: 2.7 env: CONDA_ENV=py27-min - python: 2.7 - env: CONDA_ENV=py27-cdat+pynio + env: CONDA_ENV=py27-cdat+iris+pynio - python: 3.3 env: CONDA_ENV=py33 - python: 3.4 diff --git a/ci/requirements-py27-cdat+pynio.yml b/ci/requirements-py27-cdat+iris+pynio.yml similarity index 95% rename from ci/requirements-py27-cdat+pynio.yml rename to ci/requirements-py27-cdat+iris+pynio.yml index 53aafb058e1..d9cb5c7643e 100644 --- a/ci/requirements-py27-cdat+pynio.yml +++ b/ci/requirements-py27-cdat+iris+pynio.yml @@ -10,6 +10,7 @@ dependencies: - pandas>=0.15.0 - pynio - scipy + - iris - pytest-cov - cyordereddict - pip: diff --git a/xarray/convert.py b/xarray/convert.py index 5c4624f2d01..d06f2836821 100644 --- a/xarray/convert.py +++ b/xarray/convert.py @@ -3,24 +3,42 @@ import numpy as np from .core.dataarray import DataArray +from .core.pycompat import OrderedDict from .conventions import ( maybe_encode_timedelta, maybe_encode_datetime, decode_cf) -ignored_attrs = set(['name', 'tileIndex']) +cdms2_ignored_attrs = set(['name', 'tileIndex']) +iris_forbidden_keys = set( + ['standard_name', 'long_name', 'units', 'bounds', 'axis', + 'calendar', 'leap_month', 'leap_year', 'month_lengths', + 'coordinates', 'grid_mapping', 'climatology', + 'cell_methods', 'formula_terms', 'compress', + 'missing_value', 'add_offset', 'scale_factor', + 'valid_max', 'valid_min', 'valid_range', '_FillValue']) +cell_methods_strings = set(['point', 'sum', 'maximum', 'median', 'mid_range', + 'minimum', 'mean', 'mode', 'standard_deviation', + 'variance']) + + +def encode(var): + return maybe_encode_timedelta(maybe_encode_datetime(var.variable)) + + +def _filter_attrs(attrs, ignored_attrs): + """ Return attrs that are not in ignored_attrs + """ + return dict((k, v) for k, v in attrs.items() if k not in ignored_attrs) def from_cdms2(variable): """Convert a cdms2 variable into an DataArray """ - def get_cdms2_attrs(var): - return dict((k, v) for k, v in var.attributes.items() - if k not in ignored_attrs) - values = np.asarray(variable) name = variable.id - coords = [(v.id, np.asarray(v), get_cdms2_attrs(v)) + coords = [(v.id, np.asarray(v), + _filter_attrs(v.attributes, cdms2_ignored_attrs)) for v in variable.getAxisList()] - attrs = get_cdms2_attrs(variable) + attrs = _filter_attrs(variable.attributes, cdms2_ignored_attrs) dataarray = DataArray(values, coords=coords, name=name, attrs=attrs) return decode_cf(dataarray.to_dataset())[dataarray.name] @@ -31,9 +49,6 @@ def to_cdms2(dataarray): # we don't want cdms2 to be a hard dependency import cdms2 - def encode(var): - return maybe_encode_timedelta(maybe_encode_datetime(var.variable)) - def set_cdms2_attrs(var, attrs): for k, v in attrs.items(): setattr(var, k, v) @@ -49,3 +64,130 @@ def set_cdms2_attrs(var, attrs): cdms2_var = cdms2.createVariable(var.values, axes=axes, id=dataarray.name) set_cdms2_attrs(cdms2_var, var.attrs) return cdms2_var + + +def _pick_attrs(attrs, keys): + """ Return attrs with keys in keys list + """ + return dict((k, v) for k, v in attrs.items() if k in keys) + + +def _get_iris_args(attrs): + """ Converts the xarray attrs into args that can be passed into Iris + """ + # iris.unit is deprecated in Iris v1.9 + import cf_units + args = {'attributes': _filter_attrs(attrs, iris_forbidden_keys)} + args.update(_pick_attrs(attrs, ('standard_name', 'long_name',))) + unit_args = _pick_attrs(attrs, ('calendar',)) + if 'units' in attrs: + args['units'] = cf_units.Unit(attrs['units'], **unit_args) + return args + + +# TODO: Add converting bounds from xarray to Iris and back +def to_iris(dataarray): + """ Convert a DataArray into a Iris Cube + """ + # Iris not a hard dependency + import iris + try: + from iris.fileformats.netcdf import parse_cell_methods + except ImportError: + # prior to v1.10 + from iris.fileformats._pyke_rules.compiled_krb.fc_rules_cf_fc \ + import _parse_cell_methods as parse_cell_methods + + dim_coords = [] + aux_coords = [] + + for coord_name in dataarray.coords: + coord = encode(dataarray.coords[coord_name]) + coord_args = _get_iris_args(coord.attrs) + coord_args['var_name'] = coord_name + axis = None + if coord.dims: + axis = dataarray.get_axis_num(coord.dims) + if coord_name in dataarray.dims: + iris_coord = iris.coords.DimCoord(coord.values, **coord_args) + dim_coords.append((iris_coord, axis)) + else: + iris_coord = iris.coords.AuxCoord(coord.values, **coord_args) + aux_coords.append((iris_coord, axis)) + + args = _get_iris_args(dataarray.attrs) + args['var_name'] = dataarray.name + args['dim_coords_and_dims'] = dim_coords + args['aux_coords_and_dims'] = aux_coords + if 'cell_methods' in dataarray.attrs: + args['cell_methods'] = parse_cell_methods( + dataarray.name, dataarray.attrs['cell_methods']) + + cube = iris.cube.Cube(dataarray.to_masked_array(), **args) + return cube + + +def _iris_obj_to_attrs(obj): + """ Return a dictionary of attrs when given a Iris object + """ + attrs = {'standard_name': obj.standard_name, + 'long_name': obj.long_name} + if obj.units.calendar: + attrs['calendar'] = obj.units.calendar + if obj.units.origin != '1': + attrs['units'] = obj.units.origin + attrs.update(obj.attributes) + return dict((k, v) for k, v in attrs.items() if v is not None) + + +def _iris_cell_methods_to_str(cell_methods_obj): + """ Converts a Iris cell methods into a string + """ + cell_methods = [] + for cell_method in cell_methods_obj: + names = ''.join(['{}: '.format(n) for n in cell_method.coord_names]) + intervals = ' '.join(['interval: {}'.format(interval) + for interval in cell_method.intervals]) + comments = ' '.join(['comment: {}'.format(comment) + for comment in cell_method.comments]) + extra = ' '.join([intervals, comments]).strip() + if extra: + extra = ' ({})'.format(extra) + cell_methods.append(names + cell_method.method + extra) + return ' '.join(cell_methods) + + +def from_iris(cube): + """ Convert a Iris cube into an DataArray + """ + import iris.exceptions + name = cube.var_name + dims = [] + for i in xrange(cube.ndim): + try: + dim_coord = cube.coord(dim_coords=True, dimensions=(i,)) + dims.append(dim_coord.var_name) + except iris.exceptions.CoordinateNotFoundError: + dims.append("dim_{}".format(i)) + + coords = OrderedDict() + + for coord in cube.coords(): + coord_attrs = _iris_obj_to_attrs(coord) + coord_dims = [dims[i] for i in cube.coord_dims(coord)] + if not coord.var_name: + raise ValueError('Coordinate has no var_name') + if coord_dims: + coords[coord.var_name] = (coord_dims, coord.points, coord_attrs) + else: + coords[coord.var_name] = ((), + np.asscalar(coord.points), coord_attrs) + + array_attrs = _iris_obj_to_attrs(cube) + cell_methods = _iris_cell_methods_to_str(cube.cell_methods) + if cell_methods: + array_attrs['cell_methods'] = cell_methods + dataarray = DataArray(cube.data, coords=coords, name=name, + attrs=array_attrs, dims=dims) + decoded_ds = decode_cf(dataarray._to_temp_dataset()) + return dataarray._from_temp_dataset(decoded_ds) diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py index 868961ee653..a18e7d18b33 100644 --- a/xarray/core/dataarray.py +++ b/xarray/core/dataarray.py @@ -1121,6 +1121,19 @@ def from_cdms2(cls, variable): from ..convert import from_cdms2 return from_cdms2(variable) + def to_iris(self): + """Convert this array into a iris.cube.Cube + """ + from ..convert import to_iris + return to_iris(self) + + @classmethod + def from_iris(cls, cube): + """Convert a iris.cube.Cube into an xarray.DataArray + """ + from ..convert import from_iris + return from_iris(cube) + def _all_compat(self, other, compat_str): """Helper function for equals and identical""" def compat(x, y): diff --git a/xarray/test/test_dataarray.py b/xarray/test/test_dataarray.py index e873280721b..adca0f47a87 100644 --- a/xarray/test/test_dataarray.py +++ b/xarray/test/test_dataarray.py @@ -10,7 +10,7 @@ Coordinate, Variable) from xarray.core.pycompat import iteritems, OrderedDict from xarray.core.common import _full_like - +from xarray.conventions import maybe_encode_datetime from xarray.test import (TestCase, ReturnItem, source_ndarray, unittest, requires_dask, requires_bottleneck)