Skip to content

Commit

Permalink
Add NameConstraint with relaxed name loading (SciTools#3463)
Browse files Browse the repository at this point in the history
  • Loading branch information
bjlittle authored and pp-mo committed Jan 14, 2020
1 parent 3981ae6 commit e9fcace
Show file tree
Hide file tree
Showing 11 changed files with 780 additions and 22 deletions.
26 changes: 22 additions & 4 deletions docs/iris/src/userguide/loading_iris_cubes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -166,18 +166,36 @@ As we have seen, loading the following file creates several Cubes::
cubes = iris.load(filename)

Specifying a name as a constraint argument to :py:func:`iris.load` will mean
only cubes with a matching :meth:`name <iris.cube.Cube.name>`
only cubes with matching :meth:`name <iris.cube.Cube.names>`
will be returned::

filename = iris.sample_data_path('uk_hires.pp')
cubes = iris.load(filename, 'specific_humidity')
cubes = iris.load(filename, 'surface_altitude')

To constrain the load to multiple distinct constraints, a list of constraints
Note that, the provided name will match against either the standard name,
long name, NetCDF variable name or STASH metadata of a cube. Therefore, the
previous example using the ``surface_altitude`` standard name constraint can
also be achieved using the STASH value of ``m01s00i033``::

filename = iris.sample_data_path('uk_hires.pp')
cubes = iris.load(filename, 'm01s00i033')

If further specific name constraint control is required i.e., to constrain
against a combination of standard name, long name, NetCDF variable name and/or
STASH metadata, consider using the :class:`iris.NameConstraint`. For example,
to constrain against both a standard name of ``surface_altitude`` **and** a STASH
of ``m01s00i033``::

filename = iris.sample_data_path('uk_hires.pp')
constraint = iris.NameConstraint(standard_name='surface_altitude', STASH='m01s00i033')
cubes = iris.load(filename, constraint)

To constrain the load to multiple distinct constraints, a list of constraints
can be provided. This is equivalent to running load once for each constraint
but is likely to be more efficient::

filename = iris.sample_data_path('uk_hires.pp')
cubes = iris.load(filename, ['air_potential_temperature', 'specific_humidity'])
cubes = iris.load(filename, ['air_potential_temperature', 'surface_altitude'])

The :class:`iris.Constraint` class can be used to restrict coordinate values
on load. For example, to constrain the load to match
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* The :class:`~iris.NameConstraint` provides richer name constraint matching when loading or extracting against cubes, by supporting a constraint against any combination of ``standard_name``, ``long_name``, NetCDF ``var_name`` and ``STASH`` from the attributes dictionary of a :class:`~iris.cube.Cube`.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Cubes and coordinates now have a new ``names`` property that contains a tuple of the ``standard_name``, ``long_name``, NetCDF ``var_name``, and ``STASH`` attributes metadata.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Name constraint matching against cubes during loading or extracting has been relaxed from strictly matching against the :meth:`~iris.cube.Cube.name`, to matching against either the ``standard_name``, ``long_name``, NetCDF ``var_name``, or ``STASH`` attributes metadata of a cube.
20 changes: 16 additions & 4 deletions lib/iris/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,26 @@ def callback(cube, field, filename):
__version__ = '2.4.0rc0'

# Restrict the names imported when using "from iris import *"
__all__ = ['load', 'load_cube', 'load_cubes', 'load_raw',
'save', 'Constraint', 'AttributeConstraint', 'sample_data_path',
'site_configuration', 'Future', 'FUTURE',
'IrisDeprecation']
__all__ = [
"load",
"load_cube",
"load_cubes",
"load_raw",
"save",
"Constraint",
"AttributeConstraint",
"NameConstraint",
"sample_data_path",
"site_configuration",
"Future",
"FUTURE",
"IrisDeprecation",
]


Constraint = iris._constraints.Constraint
AttributeConstraint = iris._constraints.AttributeConstraint
NameConstraint = iris._constraints.NameConstraint


class Future(threading.local):
Expand Down
111 changes: 107 additions & 4 deletions lib/iris/_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ def __init__(self, name=None, cube_func=None, coord_values=None, **kwargs):
Args:
* name: string or None
If a string, it is used as the name to match against Cube.name().
If a string, it is used as the name to match against the
`~iris.cube.Cube.names` property.
* cube_func: callable or None
If a callable, it must accept a Cube as its first and only argument
and return either True or False.
Expand Down Expand Up @@ -140,7 +141,9 @@ def _coordless_match(self, cube):
"""
match = True
if self._name:
match = self._name == cube.name()
# Require to also check against cube.name() for the fallback
# "unknown" default case, when there is no name metadata available.
match = self._name in cube.names or self._name == cube.name()
if match and self._cube_func:
match = self._cube_func(cube)
return match
Expand Down Expand Up @@ -454,7 +457,7 @@ def __init__(self, **attributes):
"""
self._attributes = attributes
Constraint.__init__(self, cube_func=self._cube_func)
super().__init__(cube_func=self._cube_func)

def _cube_func(self, cube):
match = True
Expand All @@ -477,4 +480,104 @@ def _cube_func(self, cube):
return match

def __repr__(self):
return 'AttributeConstraint(%r)' % self._attributes
return "AttributeConstraint(%r)" % self._attributes


class NameConstraint(Constraint):
"""Provides a simple Cube name based :class:`Constraint`."""

def __init__(
self,
standard_name="none",
long_name="none",
var_name="none",
STASH="none",
):
"""
Provides a simple Cube name based :class:`Constraint`, which matches
against each of the names provided, which may be either standard name,
long name, NetCDF variable name and/or the STASH from the attributes
dictionary.
The name constraint will only succeed if *all* of the provided names
match.
Kwargs:
* standard_name:
A string or callable representing the standard name to match
against.
* long_name:
A string or callable representing the long name to match against.
* var_name:
A string or callable representing the NetCDF variable name to match
against.
* STASH:
A string or callable representing the UM STASH code to match
against.
.. note::
The default value of each of the keyword arguments is the string
"none", rather than the singleton None, as None may be a legitimate
value to be matched against e.g., to constrain against all cubes
where the standard_name is not set, then use standard_name=None.
Returns:
* Boolean
Example usage::
iris.NameConstraint(long_name='air temp', var_name=None)
iris.NameConstraint(long_name=lambda name: 'temp' in name)
iris.NameConstraint(standard_name='air_temperature',
STASH=lambda stash: stash.item == 203)
"""
self.standard_name = standard_name
self.long_name = long_name
self.var_name = var_name
self.STASH = STASH
self._names = ("standard_name", "long_name", "var_name", "STASH")
super().__init__(cube_func=self._cube_func)

def _cube_func(self, cube):
def matcher(target, value):
if callable(value):
result = False
if target is not None:
#
# Don't pass None through into the callable. Users should
# use the "name=None" pattern instead. Otherwise, users
# will need to explicitly handle the None case, which is
# unnecessary and pretty darn ugly e.g.,
#
# lambda name: name is not None and name.startswith('ick')
#
result = value(target)
else:
result = value == target
return result

match = True
for name in self._names:
expected = getattr(self, name)
if expected != "none":
if name == "STASH":
actual = cube.attributes.get(name)
else:
actual = getattr(cube, name)
match = matcher(actual, expected)
# Make this is a short-circuit match.
if match is False:
break

return match

def __repr__(self):
names = []
for name in self._names:
value = getattr(self, name)
if value != "none":
names.append("{}={!r}".format(name, value))
return "{}({})".format(self.__class__.__name__, ", ".join(names))
42 changes: 42 additions & 0 deletions lib/iris/_cube_coord_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from six.moves import (filter, input, map, range, zip) # noqa
import six


from collections import namedtuple
import re
import string

Expand All @@ -31,6 +33,30 @@
_TOKEN_PARSE = re.compile(r'''^[a-zA-Z0-9][\w\.\+\-@]*$''')


class Names(
namedtuple("Names", ["standard_name", "long_name", "var_name", "STASH"])
):
"""
Immutable container for name metadata.
Args:
* standard_name:
A string representing the CF Conventions and Metadata standard name, or
None.
* long_name:
A string representing the CF Conventions and Metadata long name, or
None
* var_name:
A string representing the associated NetCDF variable name, or None.
* STASH:
A string representing the `~iris.fileformats.pp.STASH` code, or None.
"""

__slots__ = ()


def get_valid_standard_name(name):
# Standard names are optionally followed by a standard name
# modifier, separated by one or more blank spaces
Expand Down Expand Up @@ -177,6 +203,22 @@ def _check(item):

return result

@property
def names(self):
"""
A tuple containing all of the metadata names. This includes the
standard name, long name, NetCDF variable name, and attributes
STASH name.
"""
standard_name = self.standard_name
long_name = self.long_name
var_name = self.var_name
stash_name = self.attributes.get("STASH")
if stash_name is not None:
stash_name = str(stash_name)
return Names(standard_name, long_name, var_name, stash_name)

def rename(self, name):
"""
Changes the human-readable name.
Expand Down
Loading

0 comments on commit e9fcace

Please sign in to comment.