From 974d9dbadaead22f45e2a327699971767f5004e5 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Tue, 16 Feb 2021 13:45:41 +0000 Subject: [PATCH 1/6] add mesh coordinate manager --- lib/iris/experimental/ugrid.py | 478 ++++++++++++++++++++++++++------- 1 file changed, 386 insertions(+), 92 deletions(-) diff --git a/lib/iris/experimental/ugrid.py b/lib/iris/experimental/ugrid.py index 488212ff9b..e33ee57b4a 100644 --- a/lib/iris/experimental/ugrid.py +++ b/lib/iris/experimental/ugrid.py @@ -10,6 +10,7 @@ """ +from collections import Mapping, namedtuple from functools import wraps import dask.array as da @@ -17,7 +18,10 @@ from .. import _lazy_data as _lazy from ..common.metadata import ( + _hexdigest, BaseMetadata, + CoordMetadata, + DimCoordMetadata, metadata_manager_factory, SERVICES, SERVICES_COMBINE, @@ -25,7 +29,9 @@ SERVICES_DIFFERENCE, ) from ..common.lenient import _lenient_service as lenient_service -from ..coords import _DimensionalMetadata +from ..coords import _DimensionalMetadata, AuxCoord +from ..exceptions import CoordinateNotFoundError +from ..util import guess_coord_axis __all__ = [ @@ -35,6 +41,14 @@ ] +# Mesh coordinate manager namedtuples. +Mesh1DCoords = namedtuple("Mesh1DCoords", ["node_x", "node_y", "edge_x", "edge_y"]) +Mesh2DCoords = namedtuple("Mesh2DCoords", ["node_x", "node_y", "edge_x", "edge_y", "face_x", "face_y"]) +MeshNodeCoords = namedtuple("MeshNodeCoords", ["node_x", "node_y"]) +MeshEdgeCoords = namedtuple("MeshEdgeCoords", ["edge_x", "edge_y"]) +MeshFaceCoords = namedtuple("MeshFaceCoords", ["face_x", "face_y"]) + + class Connectivity(_DimensionalMetadata): """ A CF-UGRID topology connectivity, describing the topological relationship @@ -768,6 +782,8 @@ def equal(self, other, lenient=None): # - don't provide public methods to return the coordinate and connectivity # managers # +# - validate both managers contents e.g., shape? more...? +# # """ # def __init__( # self, @@ -880,7 +896,7 @@ def equal(self, other, lenient=None): # # def coords( # self, -# name_or_coord=None, +# item=None, # standard_name=None, # long_name=None, # var_name=None, @@ -911,6 +927,7 @@ def equal(self, other, lenient=None): # # def add_coords(self, node_x=None, node_y=None, edge_x=None, edge_y=None, face_x=None, face_y=None): # # this supports adding a new coord to the manager, but also replacing an existing coord +# # ignore face_x and face_y appropriately given the topology_dimension # self._coord_manager.add(...) # # def add_connectivities(self, *args): @@ -998,97 +1015,374 @@ def equal(self, other, lenient=None): # """ # return self._metadata_manager.topology_dimension # -# # -# # - validate coord_systems -# # - validate climatological -# # - use guess_coord_axis (iris.utils) -# # - others? -# # -# class _Mesh1DCoordinateManager: -# REQUIRED = ( -# "node_x", -# "node_y", -# ) -# OPTIONAL = ( -# "edge_x", -# "edge_y", -# ) -# def __init__(self, node_x, node_y, edge_x=None, edge_y=None): -# # required -# self.node_x = node_x -# self.node_y = node_y -# # optional -# self.edge_x = edge_x -# self.edge_y = edge_y -# -# # WOO-GA - this can easily get out of sync with the self attributes. -# # choose the container wisely e.g., could be an dict..., also the self -# # attributes may need to be @property's that access the chosen _members container -# self._members = [ ... ] -# -# def __iter__(self): -# for member in self._members: -# yield member -# -# def __getstate__(self): -# pass -# -# def __setstate__(self, state): -# pass -# -# def coord(self, **kwargs): -# # see Cube.coord for pattern, checking for a single result -# return self.coords(**kwargs)[0] -# -# def coords(self, ...): -# # see Cube.coords for relevant patterns -# # return [ ... ] -# pass -# -# def add(self, **kwargs): -# pass -# -# def remove(self, ...): -# # needs to respect the minimum UGRID contract -# # use logging/warning to flag items not removed - highlight in doc-string -# # don't raise an exception -# -# def __str__(self): -# pass -# -# def __repr__(self): -# pass -# -# def __eq__(self, other): -# # Full equality could be MASSIVE, so we want to avoid that. -# # Ideally we want a mesh signature from LFRic for comparison, although this would -# # limit Iris' relevance outside MO. -# # TL;DR: unknown quantity. -# raise NotImplemented -# -# def __ne__(self, other): -# # See __eq__ -# raise NotImplemented -# -# -# class _Mesh2DCoordinateManager(_Mesh1DCoordinateManager): -# OPTIONAL = ( -# "edge_x", -# "edge_y", -# "face_x", -# "face_y", -# ) -# def __init__(self, node_x, node_y, edge_x=None, edge_y=None, face_x=None, face_y=None): -# # optional -# self.face_x = face_x -# self.face_y = face_y -# -# super().__init__(node_x, node_y, edge_x=edge_x, edge_y=edge_y) -# -# # does the order matter? -# self._members.extend([self.face_x, self.face_y]) -# # + +class _Mesh1DCoordinateManager: + """ + + TBD: require clarity on coord_systems validation + TBD: require clarity on __eq__ support + TBD: rationalise self.coords() logic with other manager and Cube + + """ + REQUIRED = ( + "node_x", + "node_y", + ) + OPTIONAL = ( + "edge_x", + "edge_y", + ) + + def __init__(self, node_x, node_y, edge_x=None, edge_y=None): + # initialise all the coordinates + self.ALL = self.REQUIRED + self.OPTIONAL + self._members = {member: None for member in self.ALL} + + # required coordinates + self.node_x = node_x + self.node_y = node_y + # optional coordinates + self.edge_x = edge_x + self.edge_y = edge_y + + def __iter__(self): + for item in self._members.items(): + yield item + + def __getstate__(self): + pass + + def __setstate__(self, state): + pass + + def _satisfied(self): + # is this needed? + return all([getattr(self, member) is not None for member in self.REQUIRED]) + + def _node_setter(self, axis, coord): + axis = axis.lower() + member = f"node_{axis}" + if coord is None: + emsg = f"{member!r} is a required coordinate, cannot set to 'None'." + raise ValueError(emsg) + if not isinstance(coord, AuxCoord): + emsg = f"{member!r} requires to be an 'AuxCoord', got {type(coord)}." + raise TypeError(emsg) + guess_axis = guess_coord_axis(coord) + if guess_axis and guess_axis.lower() != axis: + emsg = f"{member!r} requires an {axis}-axis like 'AuxCoord', got an {guess_axis.lower()}-axis like." + raise TypeError(emsg) + if coord.climatological: + emsg = f"{member!r} cannot be a climatological 'AuxCoord'." + raise TypeError(emsg) + node_shape = self.node_shape + if node_shape is not None and coord.shape != node_shape: + emsg = f"{member!r} requires to have shape {node_shape!r}, got {coord.shape!r}." + raise ValueError(emsg) + self._members[member] = coord + + def _edge_setter(self, axis, coord): + axis = axis.lower() + member = f"edge_{axis}" + if coord is not None: + if not isinstance(coord, AuxCoord): + emsg = f"{member!r} requires to be a 'AuxCoord', got {type(coord)}." + raise TypeError(emsg) + guess_axis = guess_coord_axis(coord) + if guess_axis and guess_axis.lower() != axis: + emsg = f"{member!r} requires an {axis}-axis like 'AuxCoord', got an {guess_axis.lower()}-axis like." + raise TypeError(emsg) + if coord.climatological: + emsg = f"{member!r} cannot be a climatological 'AuxCoord'." + raise TypeError(emsg) + edge_shape = self.edge_shape + if edge_shape is not None and coord.shape != edge_shape: + emsg = f"{member!r} requires to have shape {edge_shape!r}, got {coord.shape!r}." + raise ValueError(emsg) + self._members[member] = coord + + @property + def node_x(self): + return self._members["node_x"] + + @node_x.setter + def node_x(self, coord): + self._node_setter("x", coord) + + @property + def node_y(self): + return self._members["node_y"] + + @node_y.setter + def node_y(self, coord): + self._node_setter("y", coord) + + @property + def edge_x(self): + return self._members["edge_x"] + + @edge_x.setter + def edge_x(self, coord): + self._edge_setter("x", coord) + + @property + def edge_y(self): + return self._members["edge_y"] + + @edge_y.setter + def edge_y(self, coord): + self._edge_setter("y", coord) + + def _shape(self, location): + coord = getattr(self, f"{location}_x") + shape = coord.shape if coord is not None else None + if shape is None: + coord = getattr(self, f"{location}_y") + if coord is not None: + shape = coord.shape + return shape + + @property + def node_shape(self): + return self._shape("node") + + @property + def edge_shape(self): + return self._shape("edge") + + @property + def all_coords(self): + return Mesh1DCoords(**self._members) + + @property + def node_coords(self): + return MeshNodeCoords(node_x=self.node_x, node_y=self.node_y) + + @property + def edge_coords(self): + return MeshEdgeCoords(edge_x=self.edge_x, edge_y=self.edge_y) + + def _coords( + self, + coords, + item=None, + standard_name=None, + long_name=None, + var_name=None, + attributes=None, + axis=None, + ): + """ + TDB: support coord_systems + + """ + name = None + coord = None + + if isinstance(item, str): + name = item + else: + coord = item + + if name is not None: + coords = [ + member + for member in coords + if member.name() == name + ] + + if standard_name is not None: + coords = [ + member + for member in coords + if member.standard_name == standard_name + ] + + if long_name is not None: + coords = [ + member + for member in coords + if member.long_name == long_name + ] + + if var_name is not None: + coords = [ + member + for member in coords + if member.var_name == var_name + ] + + if axis is not None: + axis = axis.upper() + guess_axis = guess_coord_axis + coords = [ + member + for member in coords + if guess_axis(member) == axis + ] + + if attributes is not None: + if not isinstance(attributes, Mapping): + emsg = ( + "The attributes keyword was expecting a dictionary " + f"type, but got a {type(attributes)} instead." + ) + raise ValueError(emsg) + + def attr_filter(member): + return all( + k in member.attributes and _hexdigest(member.attributes[k]) == _hexdigest(v) + for k, v in attributes.items() + ) + + coords = [ + member + for member in coords + if attr_filter(member) + ] + + if coord is not None: + if hasattr(coord, "__class__") and coord.__class__ in ( + CoordMetadata, + DimCoordMetadata, + ): + target_metadata = coord + else: + target_metadata = coord.metadata + coords = [ + member + for member in coords + if member.metadata == target_metadata + ] + + return coords + + def coord(self, **kwargs): + coords = self.coords(**kwargs) + if len(coords) > 1: + emsg = ( + f"Expected to find exactly 1 coordinate, but found {len(coords)}. " + f"They were: {', '.join(coord.name() for coord in coords)}." + ) + raise CoordinateNotFoundError(emsg) + if len(coords) == 0: + item = kwargs["item"] + if item is not None: + if not isinstance(item, str): + item = item.name() + name = item or kwargs["standard_name"] or kwargs["long_name"] or kwargs["var_name"] or None + name = "" if name is None else f"{name} " + emsg = f"Expected to find exactly 1 {name}coordinate, but found none." + raise CoordinateNotFoundError(emsg) + return coords[0] + + def coords( + self, + item=None, + standard_name=None, + long_name=None, + var_name=None, + attributes=None, + axis=None, + node=None, + edge=None, + ): + coords = [] + + # rationalise the tri-state behaviour + if node is None and edge is None: + node = edge = True + else: + node = True if node else False + edge = True if edge else False + + if node: + coords.extend([coord for coord in self.node_coords if coord is not None]) + + if edge: + coords.extend([coord for coord in self.edge_coords if coord is not None]) + + return self._coords( + coords, + item=item, + standard_name=standard_name, + long_name=long_name, + var_name=var_name, + attributes=attributes, + axis=axis, + ) + + def add(self, node_x=None, node_y=None, edge_x=None, edge_y=None): + """ + use self.remove to remove a coordinate e.g., using the pattern self.add(edge_x=None) + will not remove the edge_x coordinate + + """ + # deal with the special case where both required members are changing + if node_x is not None and node_y is not None: + cache_x = self.node_x + cache_y = self.node_y + self._members["node_x"] = None + self._members["node_y"] = None + try: + self.node_x = node_x + self.node_y = node_y + except (TypeError, ValueError): + # restore previous valid state + self._members["node_x"] = cache_x + self._members["node_y"] = cache_y + # now, re-raise the exception + raise + else: + # deal with either, or none, of the required members + if node_x is not None: + self.node_x = node_x + if node_y is not None: + self.node_y = node_y + + # deal with the optional members + if edge_x is not None: + self.edge_x = edge_x + if edge_y is not None: + self.edge_y = edge_y + + # def remove(self, ...): + # # needs to respect the minimum UGRID contract + # # use logging/warning to flag items not removed - highlight in doc-string + # # don't raise an exception + + def __repr__(self): + args = [f"{member}={coord!r}" for member, coord in self if coord is not None] + return f"{self.__class__.__name__}({', '.join(args)})" + + def __eq__(self, other): + # TBD + raise NotImplemented + + def __ne__(self, other): + # TBD + raise NotImplemented + + +class _Mesh2DCoordinateManager(_Mesh1DCoordinateManager): + OPTIONAL = ( + "edge_x", + "edge_y", + "face_x", + "face_y", + ) + def __init__(self, node_x, node_y, edge_x=None, edge_y=None, face_x=None, face_y=None): + # optional + self.face_x = face_x + self.face_y = face_y + + super().__init__(node_x, node_y, edge_x=edge_x, edge_y=edge_y) + + # does the order matter? + self._members.extend([self.face_x, self.face_y]) + + # # keep an eye on the __init__ inheritance # class _Mesh1DConnectivityManager: # REQUIRED = ( From 803d9f315c70521efb51589941a333288debae1a Mon Sep 17 00:00:00 2001 From: Bill Little Date: Wed, 17 Feb 2021 00:57:10 +0000 Subject: [PATCH 2/6] wip --- lib/iris/experimental/ugrid.py | 492 ++++++++++++++++++++++++--------- 1 file changed, 368 insertions(+), 124 deletions(-) diff --git a/lib/iris/experimental/ugrid.py b/lib/iris/experimental/ugrid.py index e33ee57b4a..42d8cfc10a 100644 --- a/lib/iris/experimental/ugrid.py +++ b/lib/iris/experimental/ugrid.py @@ -29,6 +29,7 @@ SERVICES_DIFFERENCE, ) from ..common.lenient import _lenient_service as lenient_service +from ..config import get_logger from ..coords import _DimensionalMetadata, AuxCoord from ..exceptions import CoordinateNotFoundError from ..util import guess_coord_axis @@ -41,9 +42,18 @@ ] +# Configure the logger. +logger = get_logger(__name__, fmt="[%(cls)s.%(funcName)s]") + + # Mesh coordinate manager namedtuples. -Mesh1DCoords = namedtuple("Mesh1DCoords", ["node_x", "node_y", "edge_x", "edge_y"]) -Mesh2DCoords = namedtuple("Mesh2DCoords", ["node_x", "node_y", "edge_x", "edge_y", "face_x", "face_y"]) +Mesh1DCoords = namedtuple( + "Mesh1DCoords", ["node_x", "node_y", "edge_x", "edge_y"] +) +Mesh2DCoords = namedtuple( + "Mesh2DCoords", + ["node_x", "node_y", "edge_x", "edge_y", "face_x", "face_y"], +) MeshNodeCoords = namedtuple("MeshNodeCoords", ["node_x", "node_y"]) MeshEdgeCoords = namedtuple("MeshEdgeCoords", ["edge_x", "edge_y"]) MeshFaceCoords = namedtuple("MeshFaceCoords", ["face_x", "face_y"]) @@ -1017,6 +1027,7 @@ def equal(self, other, lenient=None): # # + class _Mesh1DCoordinateManager: """ @@ -1025,6 +1036,7 @@ class _Mesh1DCoordinateManager: TBD: rationalise self.coords() logic with other manager and Cube """ + REQUIRED = ( "node_x", "node_y", @@ -1056,50 +1068,36 @@ def __getstate__(self): def __setstate__(self, state): pass - def _satisfied(self): - # is this needed? - return all([getattr(self, member) is not None for member in self.REQUIRED]) - - def _node_setter(self, axis, coord): + def _setter(self, location, axis, coord, shape): axis = axis.lower() - member = f"node_{axis}" - if coord is None: - emsg = f"{member!r} is a required coordinate, cannot set to 'None'." - raise ValueError(emsg) - if not isinstance(coord, AuxCoord): - emsg = f"{member!r} requires to be an 'AuxCoord', got {type(coord)}." - raise TypeError(emsg) - guess_axis = guess_coord_axis(coord) - if guess_axis and guess_axis.lower() != axis: - emsg = f"{member!r} requires an {axis}-axis like 'AuxCoord', got an {guess_axis.lower()}-axis like." - raise TypeError(emsg) - if coord.climatological: - emsg = f"{member!r} cannot be a climatological 'AuxCoord'." - raise TypeError(emsg) - node_shape = self.node_shape - if node_shape is not None and coord.shape != node_shape: - emsg = f"{member!r} requires to have shape {node_shape!r}, got {coord.shape!r}." + member = f"{location}_{axis}" + + # enforce the UGRID minimum coordinate requirement + if location == "node" and coord is None: + emsg = ( + f"{member!r} is a required coordinate, cannot set to 'None'." + ) raise ValueError(emsg) - self._members[member] = coord - def _edge_setter(self, axis, coord): - axis = axis.lower() - member = f"edge_{axis}" if coord is not None: if not isinstance(coord, AuxCoord): - emsg = f"{member!r} requires to be a 'AuxCoord', got {type(coord)}." + emsg = f"{member!r} requires to be an 'AuxCoord', got {type(coord)}." raise TypeError(emsg) + guess_axis = guess_coord_axis(coord) + if guess_axis and guess_axis.lower() != axis: emsg = f"{member!r} requires an {axis}-axis like 'AuxCoord', got an {guess_axis.lower()}-axis like." raise TypeError(emsg) + if coord.climatological: emsg = f"{member!r} cannot be a climatological 'AuxCoord'." raise TypeError(emsg) - edge_shape = self.edge_shape - if edge_shape is not None and coord.shape != edge_shape: - emsg = f"{member!r} requires to have shape {edge_shape!r}, got {coord.shape!r}." + + if shape is not None and coord.shape != shape: + emsg = f"{member!r} requires to have shape {shape!r}, got {coord.shape!r}." raise ValueError(emsg) + self._members[member] = coord @property @@ -1108,7 +1106,9 @@ def node_x(self): @node_x.setter def node_x(self, coord): - self._node_setter("x", coord) + self._setter( + location="node", axis="x", coord=coord, shape=self.node_shape + ) @property def node_y(self): @@ -1116,7 +1116,9 @@ def node_y(self): @node_y.setter def node_y(self, coord): - self._node_setter("y", coord) + self._setter( + location="node", axis="y", coord=coord, shape=self.node_shape + ) @property def edge_x(self): @@ -1124,7 +1126,9 @@ def edge_x(self): @edge_x.setter def edge_x(self, coord): - self._edge_setter("x", coord) + self._setter( + location="edge", axis="x", coord=coord, shape=self.edge_shape + ) @property def edge_y(self): @@ -1132,7 +1136,9 @@ def edge_y(self): @edge_y.setter def edge_y(self, coord): - self._edge_setter("y", coord) + self._setter( + location="edge", axis="y", coord=coord, shape=self.edge_shape + ) def _shape(self, location): coord = getattr(self, f"{location}_x") @@ -1145,11 +1151,11 @@ def _shape(self, location): @property def node_shape(self): - return self._shape("node") + return self._shape(location="node") @property def edge_shape(self): - return self._shape("edge") + return self._shape(location="edge") @property def all_coords(self): @@ -1164,17 +1170,17 @@ def edge_coords(self): return MeshEdgeCoords(edge_x=self.edge_x, edge_y=self.edge_y) def _coords( - self, - coords, - item=None, - standard_name=None, - long_name=None, - var_name=None, - attributes=None, - axis=None, + self, + members, + item=None, + standard_name=None, + long_name=None, + var_name=None, + attributes=None, + axis=None, ): """ - TDB: support coord_systems + TDB: support coord_systems? """ name = None @@ -1186,41 +1192,30 @@ def _coords( coord = item if name is not None: - coords = [ - member - for member in coords - if member.name() == name - ] + members = {k: v for k, v in members.items() if v.name() == name} if standard_name is not None: - coords = [ - member - for member in coords - if member.standard_name == standard_name - ] + members = { + k: v + for k, v in members.items() + if v.standard_name == standard_name + } if long_name is not None: - coords = [ - member - for member in coords - if member.long_name == long_name - ] + members = { + k: v for k, v in members.items() if v.long_name == long_name + } if var_name is not None: - coords = [ - member - for member in coords - if member.var_name == var_name - ] + members = { + k: v for k, v in members.items() if v.var_name == var_name + } if axis is not None: axis = axis.upper() - guess_axis = guess_coord_axis - coords = [ - member - for member in coords - if guess_axis(member) == axis - ] + members = { + k: v for k, v in members.items() if guess_coord_axis(v) == axis + } if attributes is not None: if not isinstance(attributes, Mapping): @@ -1230,17 +1225,14 @@ def _coords( ) raise ValueError(emsg) - def attr_filter(member): + def _filter(coord): return all( - k in member.attributes and _hexdigest(member.attributes[k]) == _hexdigest(v) + k in coord.attributes + and _hexdigest(coord.attributes[k]) == _hexdigest(v) for k, v in attributes.items() ) - coords = [ - member - for member in coords - if attr_filter(member) - ] + members = {k: v for k, v in members.items() if _filter(v)} if coord is not None: if hasattr(coord, "__class__") and coord.__class__ in ( @@ -1250,46 +1242,98 @@ def attr_filter(member): target_metadata = coord else: target_metadata = coord.metadata - coords = [ - member - for member in coords - if member.metadata == target_metadata - ] - return coords + members = { + k: v + for k, v in members.items() + if v.metadata == target_metadata + } + + return members + + def _coord(self, **kwargs): + asdict = kwargs["asdict"] + kwargs["asdict"] = True + members = self.coords(**kwargs) - def coord(self, **kwargs): - coords = self.coords(**kwargs) - if len(coords) > 1: + if len(members) > 1: + names = ", ".join( + f"{member}={coord!r}" for member, coord in members.items() + ) emsg = ( - f"Expected to find exactly 1 coordinate, but found {len(coords)}. " - f"They were: {', '.join(coord.name() for coord in coords)}." + f"Expected to find exactly 1 coordinate, but found {len(members)}. " + f"They were: {names}." ) raise CoordinateNotFoundError(emsg) - if len(coords) == 0: + + if len(members) == 0: item = kwargs["item"] if item is not None: if not isinstance(item, str): item = item.name() - name = item or kwargs["standard_name"] or kwargs["long_name"] or kwargs["var_name"] or None - name = "" if name is None else f"{name} " - emsg = f"Expected to find exactly 1 {name}coordinate, but found none." + name = ( + item + or kwargs["standard_name"] + or kwargs["long_name"] + or kwargs["var_name"] + or None + ) + name = "" if name is None else f"{name!r} " + emsg = ( + f"Expected to find exactly 1 {name}coordinate, but found none." + ) raise CoordinateNotFoundError(emsg) - return coords[0] - def coords( - self, - item=None, - standard_name=None, - long_name=None, - var_name=None, - attributes=None, - axis=None, - node=None, - edge=None, + if not asdict: + members = list(members.values())[0] + + return members + + def coord( + self, + item=None, + standard_name=None, + long_name=None, + var_name=None, + attributes=None, + axis=None, + node=None, + edge=None, + asdict=True, ): - coords = [] + return self._coord( + item=item, + standard_name=standard_name, + long_name=long_name, + var_name=var_name, + attributes=attributes, + axis=axis, + node=node, + edge=edge, + asdict=asdict, + ) + + @staticmethod + def _filter_members(members): + """Remove non-None coordinate members.""" + return { + member: coord + for member, coord in members._asdict().items() + if coord is not None + } + def coords( + self, + item=None, + standard_name=None, + long_name=None, + var_name=None, + attributes=None, + axis=None, + node=None, + edge=None, + asdict=True, + ): # rationalise the tri-state behaviour if node is None and edge is None: node = edge = True @@ -1297,14 +1341,14 @@ def coords( node = True if node else False edge = True if edge else False + members = {} if node: - coords.extend([coord for coord in self.node_coords if coord is not None]) - + members.update(self._filter_members(self.node_coords)) if edge: - coords.extend([coord for coord in self.edge_coords if coord is not None]) + members.update(self._filter_members(self.edge_coords)) - return self._coords( - coords, + result = self._coords( + members, item=item, standard_name=standard_name, long_name=long_name, @@ -1313,10 +1357,15 @@ def coords( axis=axis, ) + if not asdict: + result = list(result.values()) + + return result + def add(self, node_x=None, node_y=None, edge_x=None, edge_y=None): """ - use self.remove to remove a coordinate e.g., using the pattern self.add(edge_x=None) - will not remove the edge_x coordinate + use self.remove(edge_x=True) to remove a coordinate e.g., using the + pattern self.add(edge_x=None) will not remove the edge_x coordinate """ # deal with the special case where both required members are changing @@ -1325,6 +1374,7 @@ def add(self, node_x=None, node_y=None, edge_x=None, edge_y=None): cache_y = self.node_y self._members["node_x"] = None self._members["node_y"] = None + try: self.node_x = node_x self.node_y = node_y @@ -1347,13 +1397,57 @@ def add(self, node_x=None, node_y=None, edge_x=None, edge_y=None): if edge_y is not None: self.edge_y = edge_y - # def remove(self, ...): - # # needs to respect the minimum UGRID contract - # # use logging/warning to flag items not removed - highlight in doc-string - # # don't raise an exception + def _remove(self, **kwargs): + result = {} + members = self.coords(**kwargs) + + for member in members.keys(): + if member in self.REQUIRED: + dmsg = f"Ignoring request to remove required coordinate {member!r}" + logger.debug(dmsg, extra=dict(cls=self.__class__.__name__)) + else: + result[member] = members[member] + setattr(self, member, None) + + return result + + def remove( + self, + item=None, + standard_name=None, + long_name=None, + var_name=None, + attributes=None, + axis=None, + node=None, + edge=None, + ): + return self._remove( + item=item, + standard_name=standard_name, + long_name=long_name, + var_name=var_name, + attributes=attributes, + axis=axis, + node=node, + edge=edge, + asdict=True, + ) + + def __str__(self): + args = [ + f"{member}=True" + for member, coord in self + if coord is not None + ] + return f"{self.__class__.__name__}({', '.join(args)})" def __repr__(self): - args = [f"{member}={coord!r}" for member, coord in self if coord is not None] + args = [ + f"{member}={coord!r}" + for member, coord in self + if coord is not None + ] return f"{self.__class__.__name__}({', '.join(args)})" def __eq__(self, other): @@ -1372,15 +1466,165 @@ class _Mesh2DCoordinateManager(_Mesh1DCoordinateManager): "face_x", "face_y", ) - def __init__(self, node_x, node_y, edge_x=None, edge_y=None, face_x=None, face_y=None): - # optional + + def __init__( + self, + node_x, + node_y, + edge_x=None, + edge_y=None, + face_x=None, + face_y=None, + ): + super().__init__(node_x, node_y, edge_x=edge_x, edge_y=edge_y) + + # optional coordinates self.face_x = face_x self.face_y = face_y - super().__init__(node_x, node_y, edge_x=edge_x, edge_y=edge_y) + @property + def face_x(self): + return self._members["face_x"] + + @face_x.setter + def face_x(self, coord): + self._setter( + location="face", axis="x", coord=coord, shape=self.face_shape + ) + + @property + def face_y(self): + return self._members["face_y"] + + @face_y.setter + def face_y(self, coord): + self._setter( + location="face", axis="y", coord=coord, shape=self.face_shape + ) + + @property + def face_shape(self): + return self._shape(location="face") + + @property + def all_coords(self): + return Mesh2DCoords(**self._members) + + @property + def face_coords(self): + return MeshFaceCoords(face_x=self.face_x, face_y=self.face_y) + + def coord( + self, + item=None, + standard_name=None, + long_name=None, + var_name=None, + attributes=None, + axis=None, + node=None, + edge=None, + face=None, + asdict=True, + ): + return self._coord( + item=item, + standard_name=standard_name, + long_name=long_name, + var_name=var_name, + attributes=attributes, + axis=axis, + node=node, + edge=edge, + face=face, + asdict=asdict, + ) + + def coords( + self, + item=None, + standard_name=None, + long_name=None, + var_name=None, + attributes=None, + axis=None, + node=None, + edge=None, + face=None, + asdict=True, + ): + # rationalise the tri-state behaviour + if node is None and edge is None and face is None: + node = edge = face = True + else: + node = True if node else False + edge = True if edge else False + face = True if face else False + + members = {} + if node: + members.update(self._filter_members(self.node_coords)) + if edge: + members.update(self._filter_members(self.edge_coords)) + if face: + members.update(self._filter_members(self.face_coords)) + + result = self._coords( + members, + item=item, + standard_name=standard_name, + long_name=long_name, + var_name=var_name, + attributes=attributes, + axis=axis, + ) - # does the order matter? - self._members.extend([self.face_x, self.face_y]) + if not asdict: + result = list(result.values()) + + return result + + def add( + self, + node_x=None, + node_y=None, + edge_x=None, + edge_y=None, + face_x=None, + face_y=None, + ): + super().add(node_x=node_x, node_y=node_y, edge_x=edge_x, edge_y=edge_y) + + # deal with the optional members + if face_x is not None: + self.face_x = face_x + if face_y is not None: + self.face_y = face_y + + def remove( + self, + item=None, + standard_name=None, + long_name=None, + var_name=None, + attributes=None, + axis=None, + node=None, + edge=None, + face=None, + ): + return self._remove( + item=item, + standard_name=standard_name, + long_name=long_name, + var_name=var_name, + attributes=attributes, + axis=axis, + node=node, + edge=edge, + face=face, + asdict=True, + ) # # keep an eye on the __init__ inheritance From f26d32983efafa18856a60ac958a0771d74bbce7 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Wed, 17 Feb 2021 12:28:06 +0000 Subject: [PATCH 3/6] make shape methods private + reorganise method order --- lib/iris/experimental/ugrid.py | 487 +++++++++++++++++---------------- 1 file changed, 249 insertions(+), 238 deletions(-) diff --git a/lib/iris/experimental/ugrid.py b/lib/iris/experimental/ugrid.py index 42d8cfc10a..656b5a9e3d 100644 --- a/lib/iris/experimental/ugrid.py +++ b/lib/iris/experimental/ugrid.py @@ -38,6 +38,11 @@ __all__ = [ "Connectivity", "ConnectivityMetadata", + "Mesh1DCoords", + "Mesh2DCoords", + "MeshEdgeCoords", + "MeshFaceCoords", + "MeshNodeCoords", "MeshMetadata", ] @@ -1058,119 +1063,79 @@ def __init__(self, node_x, node_y, edge_x=None, edge_y=None): self.edge_x = edge_x self.edge_y = edge_y + def __eq__(self, other): + # TBD + raise NotImplementedError + + def __getstate__(self): + # TBD + pass + def __iter__(self): for item in self._members.items(): yield item - def __getstate__(self): - pass + def __ne__(self, other): + # TBD + raise NotImplementedError + + def __repr__(self): + args = [ + f"{member}={coord!r}" + for member, coord in self + if coord is not None + ] + return f"{self.__class__.__name__}({', '.join(args)})" def __setstate__(self, state): pass - def _setter(self, location, axis, coord, shape): - axis = axis.lower() - member = f"{location}_{axis}" + def __str__(self): + args = [ + f"{member}=True" for member, coord in self if coord is not None + ] + return f"{self.__class__.__name__}({', '.join(args)})" - # enforce the UGRID minimum coordinate requirement - if location == "node" and coord is None: + def _coord(self, **kwargs): + asdict = kwargs["asdict"] + kwargs["asdict"] = True + members = self.coords(**kwargs) + + if len(members) > 1: + names = ", ".join( + f"{member}={coord!r}" for member, coord in members.items() + ) emsg = ( - f"{member!r} is a required coordinate, cannot set to 'None'." + f"Expected to find exactly 1 coordinate, but found {len(members)}. " + f"They were: {names}." ) - raise ValueError(emsg) - - if coord is not None: - if not isinstance(coord, AuxCoord): - emsg = f"{member!r} requires to be an 'AuxCoord', got {type(coord)}." - raise TypeError(emsg) - - guess_axis = guess_coord_axis(coord) - - if guess_axis and guess_axis.lower() != axis: - emsg = f"{member!r} requires an {axis}-axis like 'AuxCoord', got an {guess_axis.lower()}-axis like." - raise TypeError(emsg) - - if coord.climatological: - emsg = f"{member!r} cannot be a climatological 'AuxCoord'." - raise TypeError(emsg) - - if shape is not None and coord.shape != shape: - emsg = f"{member!r} requires to have shape {shape!r}, got {coord.shape!r}." - raise ValueError(emsg) - - self._members[member] = coord - - @property - def node_x(self): - return self._members["node_x"] - - @node_x.setter - def node_x(self, coord): - self._setter( - location="node", axis="x", coord=coord, shape=self.node_shape - ) - - @property - def node_y(self): - return self._members["node_y"] - - @node_y.setter - def node_y(self, coord): - self._setter( - location="node", axis="y", coord=coord, shape=self.node_shape - ) - - @property - def edge_x(self): - return self._members["edge_x"] - - @edge_x.setter - def edge_x(self, coord): - self._setter( - location="edge", axis="x", coord=coord, shape=self.edge_shape - ) - - @property - def edge_y(self): - return self._members["edge_y"] - - @edge_y.setter - def edge_y(self, coord): - self._setter( - location="edge", axis="y", coord=coord, shape=self.edge_shape - ) - - def _shape(self, location): - coord = getattr(self, f"{location}_x") - shape = coord.shape if coord is not None else None - if shape is None: - coord = getattr(self, f"{location}_y") - if coord is not None: - shape = coord.shape - return shape - - @property - def node_shape(self): - return self._shape(location="node") - - @property - def edge_shape(self): - return self._shape(location="edge") + raise CoordinateNotFoundError(emsg) - @property - def all_coords(self): - return Mesh1DCoords(**self._members) + if len(members) == 0: + item = kwargs["item"] + if item is not None: + if not isinstance(item, str): + item = item.name() + name = ( + item + or kwargs["standard_name"] + or kwargs["long_name"] + or kwargs["var_name"] + or None + ) + name = "" if name is None else f"{name!r} " + emsg = ( + f"Expected to find exactly 1 {name}coordinate, but found none." + ) + raise CoordinateNotFoundError(emsg) - @property - def node_coords(self): - return MeshNodeCoords(node_x=self.node_x, node_y=self.node_y) + if not asdict: + members = list(members.values())[0] - @property - def edge_coords(self): - return MeshEdgeCoords(edge_x=self.edge_x, edge_y=self.edge_y) + return members + @staticmethod def _coords( - self, members, item=None, standard_name=None, @@ -1251,43 +1216,164 @@ def _filter(coord): return members - def _coord(self, **kwargs): - asdict = kwargs["asdict"] - kwargs["asdict"] = True + @staticmethod + def _filter_members(members): + """Remove non-None coordinate members.""" + return { + member: coord + for member, coord in members._asdict().items() + if coord is not None + } + + def _remove(self, **kwargs): + result = {} members = self.coords(**kwargs) - if len(members) > 1: - names = ", ".join( - f"{member}={coord!r}" for member, coord in members.items() - ) - emsg = ( - f"Expected to find exactly 1 coordinate, but found {len(members)}. " - f"They were: {names}." - ) - raise CoordinateNotFoundError(emsg) + for member in members.keys(): + if member in self.REQUIRED: + dmsg = f"Ignoring request to remove required coordinate {member!r}" + logger.debug(dmsg, extra=dict(cls=self.__class__.__name__)) + else: + result[member] = members[member] + setattr(self, member, None) - if len(members) == 0: - item = kwargs["item"] - if item is not None: - if not isinstance(item, str): - item = item.name() - name = ( - item - or kwargs["standard_name"] - or kwargs["long_name"] - or kwargs["var_name"] - or None - ) - name = "" if name is None else f"{name!r} " + return result + + def _setter(self, location, axis, coord, shape): + axis = axis.lower() + member = f"{location}_{axis}" + + # enforce the UGRID minimum coordinate requirement + if location == "node" and coord is None: emsg = ( - f"Expected to find exactly 1 {name}coordinate, but found none." + f"{member!r} is a required coordinate, cannot set to 'None'." ) - raise CoordinateNotFoundError(emsg) + raise ValueError(emsg) - if not asdict: - members = list(members.values())[0] + if coord is not None: + if not isinstance(coord, AuxCoord): + emsg = f"{member!r} requires to be an 'AuxCoord', got {type(coord)}." + raise TypeError(emsg) - return members + guess_axis = guess_coord_axis(coord) + + if guess_axis and guess_axis.lower() != axis: + emsg = f"{member!r} requires an {axis}-axis like 'AuxCoord', got an {guess_axis.lower()}-axis like." + raise TypeError(emsg) + + if coord.climatological: + emsg = f"{member!r} cannot be a climatological 'AuxCoord'." + raise TypeError(emsg) + + if shape is not None and coord.shape != shape: + emsg = f"{member!r} requires to have shape {shape!r}, got {coord.shape!r}." + raise ValueError(emsg) + + self._members[member] = coord + + def _shape(self, location): + coord = getattr(self, f"{location}_x") + shape = coord.shape if coord is not None else None + if shape is None: + coord = getattr(self, f"{location}_y") + if coord is not None: + shape = coord.shape + return shape + + @property + def _edge_shape(self): + return self._shape(location="edge") + + @property + def _node_shape(self): + return self._shape(location="node") + + @property + def all_coords(self): + return Mesh1DCoords(**self._members) + + @property + def edge_coords(self): + return MeshEdgeCoords(edge_x=self.edge_x, edge_y=self.edge_y) + + @property + def edge_x(self): + return self._members["edge_x"] + + @edge_x.setter + def edge_x(self, coord): + self._setter( + location="edge", axis="x", coord=coord, shape=self._edge_shape + ) + + @property + def edge_y(self): + return self._members["edge_y"] + + @edge_y.setter + def edge_y(self, coord): + self._setter( + location="edge", axis="y", coord=coord, shape=self._edge_shape + ) + + @property + def node_coords(self): + return MeshNodeCoords(node_x=self.node_x, node_y=self.node_y) + + @property + def node_x(self): + return self._members["node_x"] + + @node_x.setter + def node_x(self, coord): + self._setter( + location="node", axis="x", coord=coord, shape=self._node_shape + ) + + @property + def node_y(self): + return self._members["node_y"] + + @node_y.setter + def node_y(self, coord): + self._setter( + location="node", axis="y", coord=coord, shape=self._node_shape + ) + + def add(self, node_x=None, node_y=None, edge_x=None, edge_y=None): + """ + use self.remove(edge_x=True) to remove a coordinate e.g., using the + pattern self.add(edge_x=None) will not remove the edge_x coordinate + + """ + # deal with the special case where both required members are changing + if node_x is not None and node_y is not None: + cache_x = self.node_x + cache_y = self.node_y + self._members["node_x"] = None + self._members["node_y"] = None + + try: + self.node_x = node_x + self.node_y = node_y + except (TypeError, ValueError): + # restore previous valid state + self._members["node_x"] = cache_x + self._members["node_y"] = cache_y + # now, re-raise the exception + raise + else: + # deal with either, or none, of the required members + if node_x is not None: + self.node_x = node_x + if node_y is not None: + self.node_y = node_y + + # deal with the optional members + if edge_x is not None: + self.edge_x = edge_x + if edge_y is not None: + self.edge_y = edge_y def coord( self, @@ -1313,15 +1399,6 @@ def coord( asdict=asdict, ) - @staticmethod - def _filter_members(members): - """Remove non-None coordinate members.""" - return { - member: coord - for member, coord in members._asdict().items() - if coord is not None - } - def coords( self, item=None, @@ -1362,55 +1439,6 @@ def coords( return result - def add(self, node_x=None, node_y=None, edge_x=None, edge_y=None): - """ - use self.remove(edge_x=True) to remove a coordinate e.g., using the - pattern self.add(edge_x=None) will not remove the edge_x coordinate - - """ - # deal with the special case where both required members are changing - if node_x is not None and node_y is not None: - cache_x = self.node_x - cache_y = self.node_y - self._members["node_x"] = None - self._members["node_y"] = None - - try: - self.node_x = node_x - self.node_y = node_y - except (TypeError, ValueError): - # restore previous valid state - self._members["node_x"] = cache_x - self._members["node_y"] = cache_y - # now, re-raise the exception - raise - else: - # deal with either, or none, of the required members - if node_x is not None: - self.node_x = node_x - if node_y is not None: - self.node_y = node_y - - # deal with the optional members - if edge_x is not None: - self.edge_x = edge_x - if edge_y is not None: - self.edge_y = edge_y - - def _remove(self, **kwargs): - result = {} - members = self.coords(**kwargs) - - for member in members.keys(): - if member in self.REQUIRED: - dmsg = f"Ignoring request to remove required coordinate {member!r}" - logger.debug(dmsg, extra=dict(cls=self.__class__.__name__)) - else: - result[member] = members[member] - setattr(self, member, None) - - return result - def remove( self, item=None, @@ -1434,30 +1462,6 @@ def remove( asdict=True, ) - def __str__(self): - args = [ - f"{member}=True" - for member, coord in self - if coord is not None - ] - return f"{self.__class__.__name__}({', '.join(args)})" - - def __repr__(self): - args = [ - f"{member}={coord!r}" - for member, coord in self - if coord is not None - ] - return f"{self.__class__.__name__}({', '.join(args)})" - - def __eq__(self, other): - # TBD - raise NotImplemented - - def __ne__(self, other): - # TBD - raise NotImplemented - class _Mesh2DCoordinateManager(_Mesh1DCoordinateManager): OPTIONAL = ( @@ -1482,6 +1486,25 @@ def __init__( self.face_x = face_x self.face_y = face_y + def __getstate__(self): + # TBD + pass + + def __setstate__(self, state): + pass + + @property + def _face_shape(self): + return self._shape(location="face") + + @property + def all_coords(self): + return Mesh2DCoords(**self._members) + + @property + def face_coords(self): + return MeshFaceCoords(face_x=self.face_x, face_y=self.face_y) + @property def face_x(self): return self._members["face_x"] @@ -1489,7 +1512,7 @@ def face_x(self): @face_x.setter def face_x(self, coord): self._setter( - location="face", axis="x", coord=coord, shape=self.face_shape + location="face", axis="x", coord=coord, shape=self._face_shape ) @property @@ -1499,20 +1522,25 @@ def face_y(self): @face_y.setter def face_y(self, coord): self._setter( - location="face", axis="y", coord=coord, shape=self.face_shape + location="face", axis="y", coord=coord, shape=self._face_shape ) - @property - def face_shape(self): - return self._shape(location="face") - - @property - def all_coords(self): - return Mesh2DCoords(**self._members) + def add( + self, + node_x=None, + node_y=None, + edge_x=None, + edge_y=None, + face_x=None, + face_y=None, + ): + super().add(node_x=node_x, node_y=node_y, edge_x=edge_x, edge_y=edge_y) - @property - def face_coords(self): - return MeshFaceCoords(face_x=self.face_x, face_y=self.face_y) + # deal with the optional members + if face_x is not None: + self.face_x = face_x + if face_y is not None: + self.face_y = face_y def coord( self, @@ -1584,23 +1612,6 @@ def coords( return result - def add( - self, - node_x=None, - node_y=None, - edge_x=None, - edge_y=None, - face_x=None, - face_y=None, - ): - super().add(node_x=node_x, node_y=node_y, edge_x=edge_x, edge_y=edge_y) - - # deal with the optional members - if face_x is not None: - self.face_x = face_x - if face_y is not None: - self.face_y = face_y - def remove( self, item=None, From aded75b8be6fc28dc36a2ae190eab1b546452efc Mon Sep 17 00:00:00 2001 From: Bill Little Date: Fri, 19 Feb 2021 00:26:59 +0000 Subject: [PATCH 4/6] review actions --- lib/iris/experimental/ugrid.py | 606 ++++++++++++++------------------- 1 file changed, 256 insertions(+), 350 deletions(-) diff --git a/lib/iris/experimental/ugrid.py b/lib/iris/experimental/ugrid.py index 656b5a9e3d..0b3631b5ce 100644 --- a/lib/iris/experimental/ugrid.py +++ b/lib/iris/experimental/ugrid.py @@ -800,6 +800,10 @@ def equal(self, other, lenient=None): # - validate both managers contents e.g., shape? more...? # # """ +# +# # TBD: for volume support, include 3 +# TOPOLOGY_DIMENSIONS = (1, 2) +# # def __init__( # self, # topology_dimension, @@ -814,48 +818,58 @@ def equal(self, other, lenient=None): # node_coords_and_axes=None, # [(coord, "x"), (coord, "y")] this is a stronger contract, not relying on guessing # edge_coords_and_axes=None, # ditto # face_coords_and_axes=None, # ditto -# connectivities=None, # [Connectivity, [Connectivity], ...] +# # connectivities=None, # [Connectivity, [Connectivity], ...] # ): # # TODO: support volumes. # # TODO: support (coord, "z") # -# # These are strings, if None is provided then assign the default string. +# # these are strings, if None is provided then assign the default string. # self.node_dimension = node_dimension # self.edge_dimension = edge_dimension # self.face_dimension = face_dimension # # self._metadata_manager = metadata_manager_factory(MeshMetadata) # +# # topology_dimension is read-only, so assign directly to the metadata manager +# if topology_dimension not in self.TOPOLOGY_DIMENSIONS: +# emsg = f"Expected 'topology_dimension' in range {self.TOPOLOGY_DIMENSIONS!r}, got {topology_dimension!r}." +# raise ValueError(emsg) # self._metadata_manager.topology_dimension = topology_dimension # +# # assign the metadata to the metadata manager # self.standard_name = standard_name # self.long_name = long_name # self.var_name = var_name # self.units = units # self.attributes = attributes # -# # based on the topology_dimension create the appropriate coordinate manager -# # with some intelligence -# self._coord_manager = ... -# -# # based on the topology_dimension create the appropriate connectivity manager -# # with some intelligence -# self._connectivity_manager = ... -# -# @property -# def all_coords(self): -# # return a namedtuple -# # coords = mesh.all_coords -# # coords.face_x, coords.edge_y -# pass -# -# @property -# def node_coords(self): -# # return a namedtuple -# # node_coords = mesh.node_coords -# # node_coords.x -# # node_coords.y -# pass +# # based on the topology_dimension, create the appropriate coordinate manager +# kwargs = {} +# if node_coords_and_axes is not None: +# for coord, axis in node_coords_and_axes: +# kwargs[f"node_{axis}"] = coord +# if edge_coords_and_axes is not None: +# for coord, axis in edge_coords_and_axes: +# kwargs[f"edge_{axis}"] = coord +# if face_coords_and_axes is not None: +# for coord, axis in face_coords_and_axes: +# kwargs[f"face_{axis}"] = coord +# +# if self.topology_dimension == 1: +# self._coord_manager = _Mesh1DCoordinateManager(**kwargs) +# elif self.topology_dimension == 2: +# self._coord_manager = _Mesh2DCoordinateManager(**kwargs) +# else: +# emsg = f"Unsupported 'topology_dimension', got {topology_dimension!r}." +# raise NotImplementedError(emsg) +# +# # # based on the topology_dimension create the appropriate connectivity manager +# # # with some intelligence +# # self._connectivity_manager = ... +# +# # @property +# # def all_coords(self): +# # return._coord_manager.all_coords # # @property # def edge_coords(self): @@ -868,95 +882,105 @@ def equal(self, other, lenient=None): # pass # # @property -# def all_connectivities(self): +# def node_coords(self): # # return a namedtuple -# # conns = mesh.all_connectivities -# # conns.edge_node, conns.boundary_node -# pass -# -# @property -# def face_node_connectivity(self): -# # required -# return self._connectivity_manager.face_node -# -# @property -# def edge_node_connectivity(self): -# # optionally required -# return self._connectivity_manager.edge_node -# -# @property -# def face_edge_connectivity(self): -# # optional -# return self._connectivity_manager.face_edge -# -# @property -# def face_face_connectivity(self): -# # optional -# return self._connectivity_manager.face_face -# -# @property -# def edge_face_connectivity(self): -# # optional -# return self._connectivity_manager.edge_face -# -# @property -# def boundary_node_connectivity(self): -# # optional -# return self._connectivity_manager.boundard_node -# -# def coord(self, ...): -# # as Cube.coord i.e., ensure that one and only one coord-like is returned -# # otherwise raise and exception -# pass -# -# def coords( -# self, -# item=None, -# standard_name=None, -# long_name=None, -# var_name=None, -# attributes=None, -# axis=None, -# node=False, -# edge=False, -# face=False, -# ): -# # do we support the coord_system kwargs? -# self._coord_manager.coords(...) -# -# def connectivity(self, ...): -# pass -# -# def connectivities( -# self, -# name_or_coord=None, -# standard_name=None, -# long_name=None, -# var_name=None, -# attributes=None, -# node=False, -# edge=False, -# face=False, -# ): +# # node_coords = mesh.node_coords +# # node_coords.x +# # node_coords.y # pass # -# def add_coords(self, node_x=None, node_y=None, edge_x=None, edge_y=None, face_x=None, face_y=None): -# # this supports adding a new coord to the manager, but also replacing an existing coord -# # ignore face_x and face_y appropriately given the topology_dimension -# self._coord_manager.add(...) -# -# def add_connectivities(self, *args): -# # this supports adding a new connectivity to the manager, but also replacing an existing connectivity -# self._connectivity_manager.add(*args) # -# def remove_coords(self, ...): -# # could provide the "name", "metadata", "coord"-instance -# # this could use mesh.coords() to find the coords -# self._coord_manager.remove(...) # -# def remove_connectivities(self, ...): -# # needs to respect the minimum UGRID contract -# self._connectivity_manager.remove(...) +# # @property +# # def all_connectivities(self): +# # # return a namedtuple +# # # conns = mesh.all_connectivities +# # # conns.edge_node, conns.boundary_node +# # pass +# # +# # @property +# # def face_node_connectivity(self): +# # # required +# # return self._connectivity_manager.face_node +# # +# # @property +# # def edge_node_connectivity(self): +# # # optionally required +# # return self._connectivity_manager.edge_node +# # +# # @property +# # def face_edge_connectivity(self): +# # # optional +# # return self._connectivity_manager.face_edge +# # +# # @property +# # def face_face_connectivity(self): +# # # optional +# # return self._connectivity_manager.face_face +# # +# # @property +# # def edge_face_connectivity(self): +# # # optional +# # return self._connectivity_manager.edge_face +# # +# # @property +# # def boundary_node_connectivity(self): +# # # optional +# # return self._connectivity_manager.boundary_node +# +# # def coord(self, ...): +# # # as Cube.coord i.e., ensure that one and only one coord-like is returned +# # # otherwise raise and exception +# # pass +# # +# # def coords( +# # self, +# # item=None, +# # standard_name=None, +# # long_name=None, +# # var_name=None, +# # attributes=None, +# # axis=None, +# # node=False, +# # edge=False, +# # face=False, +# # ): +# # # do we support the coord_system kwargs? +# # self._coord_manager.coords(...) +# +# # def connectivity(self, ...): +# # pass +# # +# # def connectivities( +# # self, +# # name_or_coord=None, +# # standard_name=None, +# # long_name=None, +# # var_name=None, +# # attributes=None, +# # node=False, +# # edge=False, +# # face=False, +# # ): +# # pass +# +# # def add_coords(self, node_x=None, node_y=None, edge_x=None, edge_y=None, face_x=None, face_y=None): +# # # this supports adding a new coord to the manager, but also replacing an existing coord +# # # ignore face_x and face_y appropriately given the topology_dimension +# # self._coord_manager.add(...) +# # +# # # def add_connectivities(self, *args): +# # # # this supports adding a new connectivity to the manager, but also replacing an existing connectivity +# # # self._connectivity_manager.add(*args) +# # +# # def remove_coords(self, ...): +# # # could provide the "name", "metadata", "coord"-instance +# # # this could use mesh.coords() to find the coords +# # self._coord_manager.remove(...) +# +# # def remove_connectivities(self, ...): +# # # needs to respect the minimum UGRID contract +# # self._connectivity_manager.remove(...) # # def __eq__(self, other): # # Full equality could be MASSIVE, so we want to avoid that. @@ -975,8 +999,8 @@ def equal(self, other, lenient=None): # def __repr__(self): # pass # -# def __unicode__(self, ...): -# pass +# # def __unicode__(self, ...): +# # pass # # def __getstate__(self): # pass @@ -992,47 +1016,47 @@ def equal(self, other, lenient=None): # # after using MeshCoord.guess_points(), the user may wish to add the associated MeshCoord.points into # # the Mesh as face_coordinates. # -# def to_AuxCoord(self, location, axis): -# # factory method -# # return the lazy AuxCoord(...) for the given location and axis -# -# def to_AuxCoords(self, location): -# # factory method -# # return the lazy AuxCoord(...), AuxCoord(...) -# -# def to_MeshCoord(self, location, axis): -# # factory method -# # return MeshCoord(..., location=location, axis=axis) -# # use Connectivity.indices_by_src() for fetching indices. -# -# def to_MeshCoords(self, location): -# # factory method -# # return MeshCoord(..., location=location, axis="x"), MeshCoord(..., location=location, axis="y") -# # use Connectivity.indices_by_src() for fetching indices. -# -# def dimension_names_reset(self, node=False, face=False, edge=False): -# # reset to defaults like this (suggestion) -# -# def dimension_names(self, node=None, face=None, edge=None): -# # e.g., only set self.node iff node != None. these attributes will -# # always be set to a user provided string or the default string. -# # return a namedtuple of dict-like -# -# @property -# def cf_role(self): -# return "mesh_topology" -# -# @property -# def topology_dimension(self): -# """ -# read-only -# -# """ -# return self._metadata_manager.topology_dimension -# +# # def to_AuxCoord(self, location, axis): +# # # factory method +# # # return the lazy AuxCoord(...) for the given location and axis +# # +# # def to_AuxCoords(self, location): +# # # factory method +# # # return the lazy AuxCoord(...), AuxCoord(...) +# # +# # def to_MeshCoord(self, location, axis): +# # # factory method +# # # return MeshCoord(..., location=location, axis=axis) +# # # use Connectivity.indices_by_src() for fetching indices. +# # +# # def to_MeshCoords(self, location): +# # # factory method +# # # return MeshCoord(..., location=location, axis="x"), MeshCoord(..., location=location, axis="y") +# # # use Connectivity.indices_by_src() for fetching indices. +# # +# # def dimension_names_reset(self, node=False, face=False, edge=False): +# # # reset to defaults like this (suggestion) +# # +# # def dimension_names(self, node=None, face=None, edge=None): +# # # e.g., only set self.node iff node != None. these attributes will +# # # always be set to a user provided string or the default string. +# # # return a namedtuple of dict-like +# # +# # @property +# # def cf_role(self): +# # return "mesh_topology" +# # +# # @property +# # def topology_dimension(self): +# # """ +# # read-only +# # +# # """ +# # return self._metadata_manager.topology_dimension # + class _Mesh1DCoordinateManager: """ @@ -1065,7 +1089,7 @@ def __init__(self, node_x, node_y, edge_x=None, edge_y=None): def __eq__(self, other): # TBD - raise NotImplementedError + return NotImplemented def __getstate__(self): # TBD @@ -1077,7 +1101,7 @@ def __iter__(self): def __ne__(self, other): # TBD - raise NotImplementedError + raise NotImplemented def __repr__(self): args = [ @@ -1096,46 +1120,8 @@ def __str__(self): ] return f"{self.__class__.__name__}({', '.join(args)})" - def _coord(self, **kwargs): - asdict = kwargs["asdict"] - kwargs["asdict"] = True - members = self.coords(**kwargs) - - if len(members) > 1: - names = ", ".join( - f"{member}={coord!r}" for member, coord in members.items() - ) - emsg = ( - f"Expected to find exactly 1 coordinate, but found {len(members)}. " - f"They were: {names}." - ) - raise CoordinateNotFoundError(emsg) - - if len(members) == 0: - item = kwargs["item"] - if item is not None: - if not isinstance(item, str): - item = item.name() - name = ( - item - or kwargs["standard_name"] - or kwargs["long_name"] - or kwargs["var_name"] - or None - ) - name = "" if name is None else f"{name!r} " - emsg = ( - f"Expected to find exactly 1 {name}coordinate, but found none." - ) - raise CoordinateNotFoundError(emsg) - - if not asdict: - members = list(members.values())[0] - - return members - @staticmethod - def _coords( + def _filters( members, item=None, standard_name=None, @@ -1216,18 +1202,9 @@ def _filter(coord): return members - @staticmethod - def _filter_members(members): - """Remove non-None coordinate members.""" - return { - member: coord - for member, coord in members._asdict().items() - if coord is not None - } - def _remove(self, **kwargs): result = {} - members = self.coords(**kwargs) + members = self.filters(**kwargs) for member in members.keys(): if member in self.REQUIRED: @@ -1258,7 +1235,7 @@ def _setter(self, location, axis, coord, shape): guess_axis = guess_coord_axis(coord) if guess_axis and guess_axis.lower() != axis: - emsg = f"{member!r} requires an {axis}-axis like 'AuxCoord', got an {guess_axis.lower()}-axis like." + emsg = f"{member!r} requires a {axis}-axis like 'AuxCoord', got a {guess_axis.lower()}-axis like." raise TypeError(emsg) if coord.climatological: @@ -1289,7 +1266,7 @@ def _node_shape(self): return self._shape(location="node") @property - def all_coords(self): + def all_members(self): return Mesh1DCoords(**self._members) @property @@ -1340,66 +1317,75 @@ def node_y(self, coord): location="node", axis="y", coord=coord, shape=self._node_shape ) - def add(self, node_x=None, node_y=None, edge_x=None, edge_y=None): - """ - use self.remove(edge_x=True) to remove a coordinate e.g., using the - pattern self.add(edge_x=None) will not remove the edge_x coordinate + def _add(self, coords): + member_x, member_y = coords._fields - """ - # deal with the special case where both required members are changing - if node_x is not None and node_y is not None: - cache_x = self.node_x - cache_y = self.node_y - self._members["node_x"] = None - self._members["node_y"] = None + # deal with the special case where both members are changing + if coords[0] is not None and coords[1] is not None: + cache_x = self._members[member_x] + cache_y = self._members[member_y] + self._members[member_x] = None + self._members[member_y] = None try: - self.node_x = node_x - self.node_y = node_y + setattr(self, member_x, coords[0]) + setattr(self, member_y, coords[1]) except (TypeError, ValueError): # restore previous valid state - self._members["node_x"] = cache_x - self._members["node_y"] = cache_y + self._members[member_x] = cache_x + self._members[member_y] = cache_y # now, re-raise the exception raise else: - # deal with either, or none, of the required members - if node_x is not None: - self.node_x = node_x - if node_y is not None: - self.node_y = node_y - - # deal with the optional members - if edge_x is not None: - self.edge_x = edge_x - if edge_y is not None: - self.edge_y = edge_y - - def coord( - self, - item=None, - standard_name=None, - long_name=None, - var_name=None, - attributes=None, - axis=None, - node=None, - edge=None, - asdict=True, - ): - return self._coord( - item=item, - standard_name=standard_name, - long_name=long_name, - var_name=var_name, - attributes=attributes, - axis=axis, - node=node, - edge=edge, - asdict=asdict, - ) + # deal with the case where one or no member is changing + if coords[0] is not None: + setattr(self, member_x, coords[0]) + if coords[1] is not None: + setattr(self, member_y, coords[1]) + + def add(self, node_x=None, node_y=None, edge_x=None, edge_y=None): + """ + use self.remove(edge_x=True) to remove a coordinate e.g., using the + pattern self.add(edge_x=None) will not remove the edge_x coordinate + + """ + self._add(MeshNodeCoords(node_x, node_y)) + self._add(MeshEdgeCoords(edge_x, edge_y)) + + def filter(self, **kwargs): + result = self.filters(**kwargs) + + if len(result) > 1: + names = ", ".join( + f"{member}={coord!r}" for member, coord in result.items() + ) + emsg = ( + f"Expected to find exactly 1 coordinate, but found {len(result)}. " + f"They were: {names}." + ) + raise CoordinateNotFoundError(emsg) - def coords( + if len(result) == 0: + item = kwargs["item"] + if item is not None: + if not isinstance(item, str): + item = item.name() + name = ( + item + or kwargs["standard_name"] + or kwargs["long_name"] + or kwargs["var_name"] + or None + ) + name = "" if name is None else f"{name!r} " + emsg = ( + f"Expected to find exactly 1 {name}coordinate, but found none." + ) + raise CoordinateNotFoundError(emsg) + + return result + + def filters( self, item=None, standard_name=None, @@ -1409,22 +1395,29 @@ def coords( axis=None, node=None, edge=None, - asdict=True, + face=None, ): # rationalise the tri-state behaviour - if node is None and edge is None: - node = edge = True - else: - node = True if node else False - edge = True if edge else False + args = [node, edge, face] + state = not any(set(filter(lambda arg: arg is not None, args))) + node, edge, face = map(lambda arg: arg if arg is not None else state, args) + + def func(args): + return args[1] is not None members = {} if node: - members.update(self._filter_members(self.node_coords)) + members.update(dict(filter(func, self.node_coords._asdict().items()))) if edge: - members.update(self._filter_members(self.edge_coords)) + members.update(dict(filter(func, self.edge_coords._asdict().items()))) + if hasattr(self, "face_coords"): + if face: + members.update(dict(filter(func, self.face_coords._asdict().items()))) + else: + dmsg = "Ignoring request to filter non-existent 'face_coords'" + logger.debug(dmsg, extra=dict(cls=self.__class__.__name__)) - result = self._coords( + result = self._filters( members, item=item, standard_name=standard_name, @@ -1434,9 +1427,6 @@ def coords( axis=axis, ) - if not asdict: - result = list(result.values()) - return result def remove( @@ -1459,7 +1449,6 @@ def remove( axis=axis, node=node, edge=edge, - asdict=True, ) @@ -1486,19 +1475,12 @@ def __init__( self.face_x = face_x self.face_y = face_y - def __getstate__(self): - # TBD - pass - - def __setstate__(self, state): - pass - @property def _face_shape(self): return self._shape(location="face") @property - def all_coords(self): + def all_members(self): return Mesh2DCoords(**self._members) @property @@ -1535,82 +1517,7 @@ def add( face_y=None, ): super().add(node_x=node_x, node_y=node_y, edge_x=edge_x, edge_y=edge_y) - - # deal with the optional members - if face_x is not None: - self.face_x = face_x - if face_y is not None: - self.face_y = face_y - - def coord( - self, - item=None, - standard_name=None, - long_name=None, - var_name=None, - attributes=None, - axis=None, - node=None, - edge=None, - face=None, - asdict=True, - ): - return self._coord( - item=item, - standard_name=standard_name, - long_name=long_name, - var_name=var_name, - attributes=attributes, - axis=axis, - node=node, - edge=edge, - face=face, - asdict=asdict, - ) - - def coords( - self, - item=None, - standard_name=None, - long_name=None, - var_name=None, - attributes=None, - axis=None, - node=None, - edge=None, - face=None, - asdict=True, - ): - # rationalise the tri-state behaviour - if node is None and edge is None and face is None: - node = edge = face = True - else: - node = True if node else False - edge = True if edge else False - face = True if face else False - - members = {} - if node: - members.update(self._filter_members(self.node_coords)) - if edge: - members.update(self._filter_members(self.edge_coords)) - if face: - members.update(self._filter_members(self.face_coords)) - - result = self._coords( - members, - item=item, - standard_name=standard_name, - long_name=long_name, - var_name=var_name, - attributes=attributes, - axis=axis, - ) - - if not asdict: - result = list(result.values()) - - return result + self._add(MeshFaceCoords(face_x, face_y)) def remove( self, @@ -1634,7 +1541,6 @@ def remove( node=node, edge=edge, face=face, - asdict=True, ) From 96dd0bf7fe09214be51d2282c68b53de0075736c Mon Sep 17 00:00:00 2001 From: Bill Little Date: Fri, 19 Feb 2021 02:45:43 +0000 Subject: [PATCH 5/6] partial mesh --- lib/iris/experimental/ugrid.py | 716 +++++++++++++++++++-------------- 1 file changed, 424 insertions(+), 292 deletions(-) diff --git a/lib/iris/experimental/ugrid.py b/lib/iris/experimental/ugrid.py index 0b3631b5ce..ee356f1c55 100644 --- a/lib/iris/experimental/ugrid.py +++ b/lib/iris/experimental/ugrid.py @@ -29,6 +29,7 @@ SERVICES_DIFFERENCE, ) from ..common.lenient import _lenient_service as lenient_service +from ..common.mixin import CFVariableMixin from ..config import get_logger from ..coords import _DimensionalMetadata, AuxCoord from ..exceptions import CoordinateNotFoundError @@ -51,6 +52,12 @@ logger = get_logger(__name__, fmt="[%(cls)s.%(funcName)s]") +# Mesh dimension names namedtuples. +Mesh1DNames = namedtuple("Mesh1DNames", ["node_dimension", "edge_dimension"]) +Mesh2DNames = namedtuple( + "Mesh2DNames", ["node_dimension", "edge_dimension", "face_dimension"] +) + # Mesh coordinate manager namedtuples. Mesh1DCoords = namedtuple( "Mesh1DCoords", ["node_x", "node_y", "edge_x", "edge_y"] @@ -766,295 +773,411 @@ def equal(self, other, lenient=None): return super().equal(other, lenient=lenient) -# class Mesh(CFVariableMixin): -# """ -# -# .. todo:: -# -# .. questions:: -# -# - decide on the verbose/succinct version of __str__ vs __repr__ -# -# .. notes:: -# -# - the mesh is location agnostic -# -# - no need to support volume at mesh level, yet -# -# - topology_dimension -# - use for fast equality between Mesh instances -# - checking connectivity dimensionality, specifically the highest dimensonality of the -# "geometric element" being added i.e., reference the src_location/tgt_location -# - used to honour and enforce the minimum UGRID connectivity contract -# -# - support pickling -# -# - copy is off the table!! -# -# - MeshCoord.guess_points() -# - MeshCoord.to_AuxCoord() -# -# - don't provide public methods to return the coordinate and connectivity -# managers -# -# - validate both managers contents e.g., shape? more...? -# -# """ -# -# # TBD: for volume support, include 3 -# TOPOLOGY_DIMENSIONS = (1, 2) -# -# def __init__( -# self, -# topology_dimension, -# standard_name=None, -# long_name=None, -# var_name=None, -# units=None, -# attributes=None, -# node_dimension=None, -# edge_dimension=None, -# face_dimension=None, -# node_coords_and_axes=None, # [(coord, "x"), (coord, "y")] this is a stronger contract, not relying on guessing -# edge_coords_and_axes=None, # ditto -# face_coords_and_axes=None, # ditto -# # connectivities=None, # [Connectivity, [Connectivity], ...] -# ): -# # TODO: support volumes. -# # TODO: support (coord, "z") -# -# # these are strings, if None is provided then assign the default string. -# self.node_dimension = node_dimension -# self.edge_dimension = edge_dimension -# self.face_dimension = face_dimension -# -# self._metadata_manager = metadata_manager_factory(MeshMetadata) -# -# # topology_dimension is read-only, so assign directly to the metadata manager -# if topology_dimension not in self.TOPOLOGY_DIMENSIONS: -# emsg = f"Expected 'topology_dimension' in range {self.TOPOLOGY_DIMENSIONS!r}, got {topology_dimension!r}." -# raise ValueError(emsg) -# self._metadata_manager.topology_dimension = topology_dimension -# -# # assign the metadata to the metadata manager -# self.standard_name = standard_name -# self.long_name = long_name -# self.var_name = var_name -# self.units = units -# self.attributes = attributes -# -# # based on the topology_dimension, create the appropriate coordinate manager -# kwargs = {} -# if node_coords_and_axes is not None: -# for coord, axis in node_coords_and_axes: -# kwargs[f"node_{axis}"] = coord -# if edge_coords_and_axes is not None: -# for coord, axis in edge_coords_and_axes: -# kwargs[f"edge_{axis}"] = coord -# if face_coords_and_axes is not None: -# for coord, axis in face_coords_and_axes: -# kwargs[f"face_{axis}"] = coord -# -# if self.topology_dimension == 1: -# self._coord_manager = _Mesh1DCoordinateManager(**kwargs) -# elif self.topology_dimension == 2: -# self._coord_manager = _Mesh2DCoordinateManager(**kwargs) -# else: -# emsg = f"Unsupported 'topology_dimension', got {topology_dimension!r}." -# raise NotImplementedError(emsg) -# -# # # based on the topology_dimension create the appropriate connectivity manager -# # # with some intelligence -# # self._connectivity_manager = ... -# -# # @property -# # def all_coords(self): -# # return._coord_manager.all_coords -# -# @property -# def edge_coords(self): -# # as above -# pass -# -# @property -# def face_coords(self): -# # as above -# pass -# -# @property -# def node_coords(self): -# # return a namedtuple -# # node_coords = mesh.node_coords -# # node_coords.x -# # node_coords.y -# pass -# -# -# -# # @property -# # def all_connectivities(self): -# # # return a namedtuple -# # # conns = mesh.all_connectivities -# # # conns.edge_node, conns.boundary_node -# # pass -# # -# # @property -# # def face_node_connectivity(self): -# # # required -# # return self._connectivity_manager.face_node -# # -# # @property -# # def edge_node_connectivity(self): -# # # optionally required -# # return self._connectivity_manager.edge_node -# # -# # @property -# # def face_edge_connectivity(self): -# # # optional -# # return self._connectivity_manager.face_edge -# # -# # @property -# # def face_face_connectivity(self): -# # # optional -# # return self._connectivity_manager.face_face -# # -# # @property -# # def edge_face_connectivity(self): -# # # optional -# # return self._connectivity_manager.edge_face -# # -# # @property -# # def boundary_node_connectivity(self): -# # # optional -# # return self._connectivity_manager.boundary_node -# -# # def coord(self, ...): -# # # as Cube.coord i.e., ensure that one and only one coord-like is returned -# # # otherwise raise and exception -# # pass -# # -# # def coords( -# # self, -# # item=None, -# # standard_name=None, -# # long_name=None, -# # var_name=None, -# # attributes=None, -# # axis=None, -# # node=False, -# # edge=False, -# # face=False, -# # ): -# # # do we support the coord_system kwargs? -# # self._coord_manager.coords(...) -# -# # def connectivity(self, ...): -# # pass -# # -# # def connectivities( -# # self, -# # name_or_coord=None, -# # standard_name=None, -# # long_name=None, -# # var_name=None, -# # attributes=None, -# # node=False, -# # edge=False, -# # face=False, -# # ): -# # pass -# -# # def add_coords(self, node_x=None, node_y=None, edge_x=None, edge_y=None, face_x=None, face_y=None): -# # # this supports adding a new coord to the manager, but also replacing an existing coord -# # # ignore face_x and face_y appropriately given the topology_dimension -# # self._coord_manager.add(...) -# # -# # # def add_connectivities(self, *args): -# # # # this supports adding a new connectivity to the manager, but also replacing an existing connectivity -# # # self._connectivity_manager.add(*args) -# # -# # def remove_coords(self, ...): -# # # could provide the "name", "metadata", "coord"-instance -# # # this could use mesh.coords() to find the coords -# # self._coord_manager.remove(...) -# -# # def remove_connectivities(self, ...): -# # # needs to respect the minimum UGRID contract -# # self._connectivity_manager.remove(...) -# -# def __eq__(self, other): -# # Full equality could be MASSIVE, so we want to avoid that. -# # Ideally we want a mesh signature from LFRic for comparison, although this would -# # limit Iris' relevance outside MO. -# # TL;DR: unknown quantity. -# raise NotImplemented -# -# def __ne__(self, other): -# # See __eq__ -# raise NotImplemented -# -# def __str__(self): -# pass -# -# def __repr__(self): -# pass -# -# # def __unicode__(self, ...): -# # pass -# -# def __getstate__(self): -# pass -# -# def __setstate__(self, state): -# pass -# -# def xml_element(self): -# pass -# -# # the MeshCoord will always have bounds, perhaps points. However the MeshCoord.guess_points() may -# # be a very useful part of its behaviour. -# # after using MeshCoord.guess_points(), the user may wish to add the associated MeshCoord.points into -# # the Mesh as face_coordinates. -# -# # def to_AuxCoord(self, location, axis): -# # # factory method -# # # return the lazy AuxCoord(...) for the given location and axis -# # -# # def to_AuxCoords(self, location): -# # # factory method -# # # return the lazy AuxCoord(...), AuxCoord(...) -# # -# # def to_MeshCoord(self, location, axis): -# # # factory method -# # # return MeshCoord(..., location=location, axis=axis) -# # # use Connectivity.indices_by_src() for fetching indices. -# # -# # def to_MeshCoords(self, location): -# # # factory method -# # # return MeshCoord(..., location=location, axis="x"), MeshCoord(..., location=location, axis="y") -# # # use Connectivity.indices_by_src() for fetching indices. -# # -# # def dimension_names_reset(self, node=False, face=False, edge=False): -# # # reset to defaults like this (suggestion) -# # -# # def dimension_names(self, node=None, face=None, edge=None): -# # # e.g., only set self.node iff node != None. these attributes will -# # # always be set to a user provided string or the default string. -# # # return a namedtuple of dict-like -# # -# # @property -# # def cf_role(self): -# # return "mesh_topology" -# # -# # @property -# # def topology_dimension(self): -# # """ -# # read-only -# # -# # """ -# # return self._metadata_manager.topology_dimension -# +class Mesh(CFVariableMixin): + """ + + .. todo:: + + .. questions:: + + - decide on the verbose/succinct version of __str__ vs __repr__ + + .. notes:: + + - the mesh is location agnostic + + - no need to support volume at mesh level, yet + + - topology_dimension + - use for fast equality between Mesh instances + - checking connectivity dimensionality, specifically the highest dimensonality of the + "geometric element" being added i.e., reference the src_location/tgt_location + - used to honour and enforce the minimum UGRID connectivity contract + + - support pickling + + - copy is off the table!! + + - MeshCoord.guess_points() + - MeshCoord.to_AuxCoord() + - don't provide public methods to return the coordinate and connectivity + managers + + - validate both managers contents e.g., shape? more...? + + """ + + # TBD: for volume and/or z-axis support include axis "z" and/or dimension "3" + AXES = ("x", "y") + TOPOLOGY_DIMENSIONS = (1, 2) + + def __init__( + self, + topology_dimension, + node_coords_and_axes, + standard_name=None, + long_name=None, + var_name=None, + units=None, + attributes=None, + edge_coords_and_axes=None, + face_coords_and_axes=None, + # connectivities=None, + node_dimension=None, + edge_dimension=None, + face_dimension=None, + ): + # TODO: support volumes. + # TODO: support (coord, "z") + + self._metadata_manager = metadata_manager_factory(MeshMetadata) + + # topology_dimension is read-only, so assign directly to the metadata manager + if topology_dimension not in self.TOPOLOGY_DIMENSIONS: + emsg = f"Expected 'topology_dimension' in range {self.TOPOLOGY_DIMENSIONS!r}, got {topology_dimension!r}." + raise ValueError(emsg) + self._metadata_manager.topology_dimension = topology_dimension + + # TBD: these are strings, if None is provided then assign the default string. + self.node_dimension = node_dimension + self.edge_dimension = edge_dimension + self.face_dimension = face_dimension + + # assign the metadata to the metadata manager + self.standard_name = standard_name + self.long_name = long_name + self.var_name = var_name + self.units = units + self.attributes = attributes + + # based on the topology_dimension, create the appropriate coordinate manager + def normalise(location, axis): + result = str(axis).lower() + if result not in self.AXES: + emsg = f"Invalid axis specified for {location} coordinate {coord.name()!r}, got {axis!r}." + raise ValueError(emsg) + return f"{location}_{axis}" + + kwargs = {} + for coord, axis in node_coords_and_axes: + kwargs[normalise("node", axis)] = coord + if edge_coords_and_axes is not None: + for coord, axis in edge_coords_and_axes: + kwargs[normalise("edge", axis)] = coord + if face_coords_and_axes is not None: + for coord, axis in face_coords_and_axes: + kwargs[normalise("face", axis)] = coord + + # check the UGRID minimum requirement for coordinates + if "node_x" not in kwargs: + emsg = f"Require a node coordinate that is x-axis like to be provided." + raise ValueError(emsg) + if "node_y" not in kwargs: + emsg = f"Require a node coordinate that is y-axis like to be provided." + raise ValueError(emsg) + + if self.topology_dimension == 1: + self._coord_manager = _Mesh1DCoordinateManager(**kwargs) + elif self.topology_dimension == 2: + self._coord_manager = _Mesh2DCoordinateManager(**kwargs) + else: + emsg = f"Unsupported 'topology_dimension', got {topology_dimension!r}." + raise NotImplementedError(emsg) + + # based on the topology_dimension, create the appropriate connectivity manager + # self._connectivity_manager = ... + + def __eq__(self, other): + # TBD + raise NotImplemented + + def __getstate__(self): + # TBD + pass + + def __ne__(self, other): + # TBD + raise NotImplemented + + def __repr__(self): + # TBD + args = [] + return f"{self.__class__.__name__}({', '.join(args)})" + + def __setstate__(self, state): + # TBD + pass + + def __str__(self): + # TBD + args = [] + return f"{self.__class__.__name__}({', '.join(args)})" + + @property + def all_coords(self): + return self._coord_manager.all_members + + @property + def edge_dimension(self): + return self._edge_dimension + + @edge_dimension.setter + def edge_dimension(self, name): + if not name or not isinstance(name, str): + self._edge_dimension = f"Mesh{self.topology_dimension}d_edge" + else: + self._edge_dimension = name + + @property + def edge_coords(self): + return self._coord_manager.edge_coords + + @property + def face_dimension(self): + return self._face_dimension + + @face_dimension.setter + def face_dimension(self, name): + if not name or not isinstance(name, str): + self._face_dimension = f"Mesh{self.topology_dimension}d_face" + else: + self._face_dimension = name + + @property + def face_coords(self): + return self._coord_manager.face_coords + + @property + def node_dimension(self): + return self._node_dimension + + @node_dimension.setter + def node_dimension(self, name): + if not name or not isinstance(name, str): + self._node_dimension = f"Mesh{self.topology_dimension}d_node" + else: + self._node_dimension = name + + @property + def node_coords(self): + return self._coord_manager.node_coords + + # @property + # def all_connectivities(self): + # # return a namedtuple + # # conns = mesh.all_connectivities + # # conns.edge_node, conns.boundary_node + # pass + # + # @property + # def face_node_connectivity(self): + # # required + # return self._connectivity_manager.face_node + # + # @property + # def edge_node_connectivity(self): + # # optionally required + # return self._connectivity_manager.edge_node + # + # @property + # def face_edge_connectivity(self): + # # optional + # return self._connectivity_manager.face_edge + # + # @property + # def face_face_connectivity(self): + # # optional + # return self._connectivity_manager.face_face + # + # @property + # def edge_face_connectivity(self): + # # optional + # return self._connectivity_manager.edge_face + # + # @property + # def boundary_node_connectivity(self): + # # optional + # return self._connectivity_manager.boundary_node + + def add_coords( + self, + node_x=None, + node_y=None, + edge_x=None, + edge_y=None, + face_x=None, + face_y=None, + ): + self._coord_manager.add( + node_x=node_x, + node_y=node_y, + edge_x=edge_x, + edge_y=edge_y, + face_x=face_x, + face_y=face_y, + ) + + # def add_connectivities(self, *args): + # # this supports adding a new connectivity to the manager, but also replacing an existing connectivity + # self._connectivity_manager.add(*args) + + # def connectivities( + # self, + # name_or_coord=None, + # standard_name=None, + # long_name=None, + # var_name=None, + # attributes=None, + # node=False, + # edge=False, + # face=False, + # ): + # pass + + # def connectivity(self, ...): + # pass + + def coord( + self, + item=None, + standard_name=None, + long_name=None, + var_name=None, + attributes=None, + axis=None, + node=None, + edge=None, + face=None, + ): + return self._coord_manager.filter( + item=item, + standard_name=standard_name, + long_name=long_name, + var_name=var_name, + attributes=attributes, + axis=axis, + node=node, + edge=edge, + face=face, + ) + + def coords( + self, + item=None, + standard_name=None, + long_name=None, + var_name=None, + attributes=None, + axis=None, + node=False, + edge=False, + face=False, + ): + return self._coord_manager.filters( + item=item, + standard_name=standard_name, + long_name=long_name, + var_name=var_name, + attributes=attributes, + axis=axis, + node=node, + edge=edge, + face=face, + ) + + # def remove_connectivities(self, ...): + # # needs to respect the minimum UGRID contract + # self._connectivity_manager.remove(...) + + def remove_coords( + self, + item=None, + standard_name=None, + long_name=None, + var_name=None, + attributes=None, + axis=None, + node=None, + edge=None, + face=None, + ): + self._coord_manager.remove( + item=item, + standard_name=standard_name, + long_name=long_name, + var_name=var_name, + attributes=attributes, + axis=axis, + node=node, + edge=edge, + face=face, + ) + + def xml_element(self): + # TBD + pass + + # the MeshCoord will always have bounds, perhaps points. However the MeshCoord.guess_points() may + # be a very useful part of its behaviour. + # after using MeshCoord.guess_points(), the user may wish to add the associated MeshCoord.points into + # the Mesh as face_coordinates. + + # def to_AuxCoord(self, location, axis): + # # factory method + # # return the lazy AuxCoord(...) for the given location and axis + # + # def to_AuxCoords(self, location): + # # factory method + # # return the lazy AuxCoord(...), AuxCoord(...) + # + # def to_MeshCoord(self, location, axis): + # # factory method + # # return MeshCoord(..., location=location, axis=axis) + # # use Connectivity.indices_by_src() for fetching indices. + # + # def to_MeshCoords(self, location): + # # factory method + # # return MeshCoord(..., location=location, axis="x"), MeshCoord(..., location=location, axis="y") + # # use Connectivity.indices_by_src() for fetching indices. + + def dimension_names_reset(self, node=False, edge=False, face=False): + if node: + self.node_dimension = None + if edge: + self.edge_dimension = None + if face: + self.face_dimension = None + if self.topology_dimension == 1: + result = Mesh1DNames(self.node_dimension, self.edge_dimension) + else: + result = Mesh2DNames( + self.node_dimension, self.edge_dimension, self.face_dimension + ) + return result + + def dimension_names(self, node=None, edge=None, face=None): + if node: + self.node_dimension = node + if edge: + self.edge_dimension = edge + if face: + self.face_dimension = face + if self.topology_dimension == 1: + result = Mesh1DNames(self.node_dimension, self.edge_dimension) + else: + result = Mesh2DNames( + self.node_dimension, self.edge_dimension, self.node_dimension + ) + return result + + @property + def cf_role(self): + return "mesh_topology" + + @property + def topology_dimension(self): + return self._metadata_manager.topology_dimension class _Mesh1DCoordinateManager: @@ -1112,6 +1235,7 @@ def __repr__(self): return f"{self.__class__.__name__}({', '.join(args)})" def __setstate__(self, state): + # TBD pass def __str__(self): @@ -1400,19 +1524,27 @@ def filters( # rationalise the tri-state behaviour args = [node, edge, face] state = not any(set(filter(lambda arg: arg is not None, args))) - node, edge, face = map(lambda arg: arg if arg is not None else state, args) + node, edge, face = map( + lambda arg: arg if arg is not None else state, args + ) def func(args): return args[1] is not None members = {} if node: - members.update(dict(filter(func, self.node_coords._asdict().items()))) + members.update( + dict(filter(func, self.node_coords._asdict().items())) + ) if edge: - members.update(dict(filter(func, self.edge_coords._asdict().items()))) + members.update( + dict(filter(func, self.edge_coords._asdict().items())) + ) if hasattr(self, "face_coords"): if face: - members.update(dict(filter(func, self.face_coords._asdict().items()))) + members.update( + dict(filter(func, self.face_coords._asdict().items())) + ) else: dmsg = "Ignoring request to filter non-existent 'face_coords'" logger.debug(dmsg, extra=dict(cls=self.__class__.__name__)) From acb0eb31422d654b9e69d0a625e7f4bf7632325f Mon Sep 17 00:00:00 2001 From: Bill Little Date: Fri, 19 Feb 2021 02:50:00 +0000 Subject: [PATCH 6/6] wip --- lib/iris/experimental/ugrid.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/iris/experimental/ugrid.py b/lib/iris/experimental/ugrid.py index ee356f1c55..002c40952f 100644 --- a/lib/iris/experimental/ugrid.py +++ b/lib/iris/experimental/ugrid.py @@ -871,10 +871,14 @@ def normalise(location, axis): # check the UGRID minimum requirement for coordinates if "node_x" not in kwargs: - emsg = f"Require a node coordinate that is x-axis like to be provided." + emsg = ( + "Require a node coordinate that is x-axis like to be provided." + ) raise ValueError(emsg) if "node_y" not in kwargs: - emsg = f"Require a node coordinate that is y-axis like to be provided." + emsg = ( + "Require a node coordinate that is y-axis like to be provided." + ) raise ValueError(emsg) if self.topology_dimension == 1: @@ -890,7 +894,7 @@ def normalise(location, axis): def __eq__(self, other): # TBD - raise NotImplemented + return NotImplemented def __getstate__(self): # TBD @@ -898,7 +902,7 @@ def __getstate__(self): def __ne__(self, other): # TBD - raise NotImplemented + return NotImplemented def __repr__(self): # TBD @@ -1224,7 +1228,7 @@ def __iter__(self): def __ne__(self, other): # TBD - raise NotImplemented + return NotImplemented def __repr__(self): args = [