Skip to content

Commit 32875ed

Browse files
bjlittlepp-mo
authored andcommitted
Add NameConstraint with relaxed name loading (SciTools#3463)
1 parent b7b3486 commit 32875ed

File tree

11 files changed

+780
-22
lines changed

11 files changed

+780
-22
lines changed

docs/iris/src/userguide/loading_iris_cubes.rst

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,18 +166,36 @@ As we have seen, loading the following file creates several Cubes::
166166
cubes = iris.load(filename)
167167

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

172172
filename = iris.sample_data_path('uk_hires.pp')
173-
cubes = iris.load(filename, 'specific_humidity')
173+
cubes = iris.load(filename, 'surface_altitude')
174174

175-
To constrain the load to multiple distinct constraints, a list of constraints
175+
Note that, the provided name will match against either the standard name,
176+
long name, NetCDF variable name or STASH metadata of a cube. Therefore, the
177+
previous example using the ``surface_altitude`` standard name constraint can
178+
also be achieved using the STASH value of ``m01s00i033``::
179+
180+
filename = iris.sample_data_path('uk_hires.pp')
181+
cubes = iris.load(filename, 'm01s00i033')
182+
183+
If further specific name constraint control is required i.e., to constrain
184+
against a combination of standard name, long name, NetCDF variable name and/or
185+
STASH metadata, consider using the :class:`iris.NameConstraint`. For example,
186+
to constrain against both a standard name of ``surface_altitude`` **and** a STASH
187+
of ``m01s00i033``::
188+
189+
filename = iris.sample_data_path('uk_hires.pp')
190+
constraint = iris.NameConstraint(standard_name='surface_altitude', STASH='m01s00i033')
191+
cubes = iris.load(filename, constraint)
192+
193+
To constrain the load to multiple distinct constraints, a list of constraints
176194
can be provided. This is equivalent to running load once for each constraint
177195
but is likely to be more efficient::
178196

179197
filename = iris.sample_data_path('uk_hires.pp')
180-
cubes = iris.load(filename, ['air_potential_temperature', 'specific_humidity'])
198+
cubes = iris.load(filename, ['air_potential_temperature', 'surface_altitude'])
181199

182200
The :class:`iris.Constraint` class can be used to restrict coordinate values
183201
on load. For example, to constrain the load to match
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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`.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

lib/iris/__init__.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,26 @@ def callback(cube, field, filename):
124124
__version__ = '2.4.0rc0'
125125

126126
# Restrict the names imported when using "from iris import *"
127-
__all__ = ['load', 'load_cube', 'load_cubes', 'load_raw',
128-
'save', 'Constraint', 'AttributeConstraint', 'sample_data_path',
129-
'site_configuration', 'Future', 'FUTURE',
130-
'IrisDeprecation']
127+
__all__ = [
128+
"load",
129+
"load_cube",
130+
"load_cubes",
131+
"load_raw",
132+
"save",
133+
"Constraint",
134+
"AttributeConstraint",
135+
"NameConstraint",
136+
"sample_data_path",
137+
"site_configuration",
138+
"Future",
139+
"FUTURE",
140+
"IrisDeprecation",
141+
]
131142

132143

133144
Constraint = iris._constraints.Constraint
134145
AttributeConstraint = iris._constraints.AttributeConstraint
146+
NameConstraint = iris._constraints.NameConstraint
135147

136148

137149
class Future(threading.local):

lib/iris/_constraints.py

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ def __init__(self, name=None, cube_func=None, coord_values=None, **kwargs):
5252
Args:
5353
5454
* name: string or None
55-
If a string, it is used as the name to match against Cube.name().
55+
If a string, it is used as the name to match against the
56+
`~iris.cube.Cube.names` property.
5657
* cube_func: callable or None
5758
If a callable, it must accept a Cube as its first and only argument
5859
and return either True or False.
@@ -140,7 +141,9 @@ def _coordless_match(self, cube):
140141
"""
141142
match = True
142143
if self._name:
143-
match = self._name == cube.name()
144+
# Require to also check against cube.name() for the fallback
145+
# "unknown" default case, when there is no name metadata available.
146+
match = self._name in cube.names or self._name == cube.name()
144147
if match and self._cube_func:
145148
match = self._cube_func(cube)
146149
return match
@@ -454,7 +457,7 @@ def __init__(self, **attributes):
454457
455458
"""
456459
self._attributes = attributes
457-
Constraint.__init__(self, cube_func=self._cube_func)
460+
super().__init__(cube_func=self._cube_func)
458461

459462
def _cube_func(self, cube):
460463
match = True
@@ -477,4 +480,104 @@ def _cube_func(self, cube):
477480
return match
478481

479482
def __repr__(self):
480-
return 'AttributeConstraint(%r)' % self._attributes
483+
return "AttributeConstraint(%r)" % self._attributes
484+
485+
486+
class NameConstraint(Constraint):
487+
"""Provides a simple Cube name based :class:`Constraint`."""
488+
489+
def __init__(
490+
self,
491+
standard_name="none",
492+
long_name="none",
493+
var_name="none",
494+
STASH="none",
495+
):
496+
"""
497+
Provides a simple Cube name based :class:`Constraint`, which matches
498+
against each of the names provided, which may be either standard name,
499+
long name, NetCDF variable name and/or the STASH from the attributes
500+
dictionary.
501+
502+
The name constraint will only succeed if *all* of the provided names
503+
match.
504+
505+
Kwargs:
506+
* standard_name:
507+
A string or callable representing the standard name to match
508+
against.
509+
* long_name:
510+
A string or callable representing the long name to match against.
511+
* var_name:
512+
A string or callable representing the NetCDF variable name to match
513+
against.
514+
* STASH:
515+
A string or callable representing the UM STASH code to match
516+
against.
517+
518+
.. note::
519+
The default value of each of the keyword arguments is the string
520+
"none", rather than the singleton None, as None may be a legitimate
521+
value to be matched against e.g., to constrain against all cubes
522+
where the standard_name is not set, then use standard_name=None.
523+
524+
Returns:
525+
* Boolean
526+
527+
Example usage::
528+
529+
iris.NameConstraint(long_name='air temp', var_name=None)
530+
531+
iris.NameConstraint(long_name=lambda name: 'temp' in name)
532+
533+
iris.NameConstraint(standard_name='air_temperature',
534+
STASH=lambda stash: stash.item == 203)
535+
536+
"""
537+
self.standard_name = standard_name
538+
self.long_name = long_name
539+
self.var_name = var_name
540+
self.STASH = STASH
541+
self._names = ("standard_name", "long_name", "var_name", "STASH")
542+
super().__init__(cube_func=self._cube_func)
543+
544+
def _cube_func(self, cube):
545+
def matcher(target, value):
546+
if callable(value):
547+
result = False
548+
if target is not None:
549+
#
550+
# Don't pass None through into the callable. Users should
551+
# use the "name=None" pattern instead. Otherwise, users
552+
# will need to explicitly handle the None case, which is
553+
# unnecessary and pretty darn ugly e.g.,
554+
#
555+
# lambda name: name is not None and name.startswith('ick')
556+
#
557+
result = value(target)
558+
else:
559+
result = value == target
560+
return result
561+
562+
match = True
563+
for name in self._names:
564+
expected = getattr(self, name)
565+
if expected != "none":
566+
if name == "STASH":
567+
actual = cube.attributes.get(name)
568+
else:
569+
actual = getattr(cube, name)
570+
match = matcher(actual, expected)
571+
# Make this is a short-circuit match.
572+
if match is False:
573+
break
574+
575+
return match
576+
577+
def __repr__(self):
578+
names = []
579+
for name in self._names:
580+
value = getattr(self, name)
581+
if value != "none":
582+
names.append("{}={!r}".format(name, value))
583+
return "{}({})".format(self.__class__.__name__, ", ".join(names))

lib/iris/_cube_coord_common.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from six.moves import (filter, input, map, range, zip) # noqa
2020
import six
2121

22+
23+
from collections import namedtuple
2224
import re
2325
import string
2426

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

3335

36+
class Names(
37+
namedtuple("Names", ["standard_name", "long_name", "var_name", "STASH"])
38+
):
39+
"""
40+
Immutable container for name metadata.
41+
42+
Args:
43+
44+
* standard_name:
45+
A string representing the CF Conventions and Metadata standard name, or
46+
None.
47+
* long_name:
48+
A string representing the CF Conventions and Metadata long name, or
49+
None
50+
* var_name:
51+
A string representing the associated NetCDF variable name, or None.
52+
* STASH:
53+
A string representing the `~iris.fileformats.pp.STASH` code, or None.
54+
55+
"""
56+
57+
__slots__ = ()
58+
59+
3460
def get_valid_standard_name(name):
3561
# Standard names are optionally followed by a standard name
3662
# modifier, separated by one or more blank spaces
@@ -177,6 +203,22 @@ def _check(item):
177203

178204
return result
179205

206+
@property
207+
def names(self):
208+
"""
209+
A tuple containing all of the metadata names. This includes the
210+
standard name, long name, NetCDF variable name, and attributes
211+
STASH name.
212+
213+
"""
214+
standard_name = self.standard_name
215+
long_name = self.long_name
216+
var_name = self.var_name
217+
stash_name = self.attributes.get("STASH")
218+
if stash_name is not None:
219+
stash_name = str(stash_name)
220+
return Names(standard_name, long_name, var_name, stash_name)
221+
180222
def rename(self, name):
181223
"""
182224
Changes the human-readable name.

0 commit comments

Comments
 (0)