Skip to content

xarray to and from iris #814

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

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies:
- pandas>=0.15.0
- pynio
- scipy
- iris
- pytest-cov
- cyordereddict
- pip:
Expand Down
162 changes: 152 additions & 10 deletions xarray/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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)
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible for a cube not to have variable name?

Copy link
Author

@nparley nparley Aug 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could possible be None. It's the CF variable name but I think the cube could have been made with out reading in a file so that the Name would be None.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick search in the Iris source gives this:

    @property
    def var_name(self):
        """The CF variable name for the object."""
        return self._var_name

    @var_name.setter
    def var_name(self, name):
        if name is not None:
            if not name:
                raise ValueError('An empty string is not a valid CF variable '
                                 'name.')
            elif set(name).intersection(string.whitespace):
                raise ValueError('{!r} is not a valid CF variable name because'
                                 ' it contains whitespace.'.format(name))
        self._var_name = name

So would conclude that's it's not an empty string or None

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)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to have coord_dims(coord) return a tuple that does not have length equal to it's number of dimensions?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coord_dims(coord):

Returns a tuple of the data dimensions relevant to the given coordinate.

So I think it's ok.

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's also possible to coord.var_name be None. We should probably raise an error in that case.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an xarray exception that might make sense for the error? Or just nameerror?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually just raise ValueError for cases like this where we cannot handle
the input.

On Wed, Aug 10, 2016 at 2:01 PM, Neil Parley [email protected]
wrote:

In xarray/convert.py
#814 (comment):

  •    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))
    
  • dims = [dim.var_name for dim in cube.dim_coords]

  • if not dims:

  • dims = ["dim{}".format(i) for i in range(cube.data.ndim)]

  • 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 coord_dims:
    
  •        coords[coord.var_name] = (coord_dims, coord.points, coord_attrs)
    

Is there an xarray exception that might make sense for the error? Or just
nameerror?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/pydata/xarray/pull/814/files/338ef6b7242c06d5107d2224c1df4cdb2a4b951c#r74329887,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABKS1vRRJWIawQDcDJYZxXjQEcbQJIjkks5qejw5gaJpZM4H9jGH
.

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)
13 changes: 13 additions & 0 deletions xarray/core/dataarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion xarray/test/test_dataarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down