From 13e7ee113bb71c9c996556ab95ed764543e21fa4 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Mon, 19 Aug 2024 16:05:29 +0100 Subject: [PATCH 01/16] added functional timestamps --- lib/iris/mesh/components.py | 169 +++++++++++++++++++++--------------- 1 file changed, 101 insertions(+), 68 deletions(-) diff --git a/lib/iris/mesh/components.py b/lib/iris/mesh/components.py index a5936388f8..2e7a39b95c 100644 --- a/lib/iris/mesh/components.py +++ b/lib/iris/mesh/components.py @@ -1138,6 +1138,10 @@ def all_connectivities(self): """All the :class:`~iris.mesh.Connectivity` instances of the :class:`MeshXY`.""" return self._connectivity_manager.all_members + @property + def timestamp(self): + return MAX(self._coord_manager.timestamp, self._connectivity_manager.timestamp) + @property def all_coords(self): """All the :class:`~iris.coords.AuxCoord` coordinates of the :class:`MeshXY`.""" @@ -2001,6 +2005,7 @@ def __init__(self, node_x, node_y, edge_x=None, edge_y=None): # optional coordinates self.edge_x = edge_x self.edge_y = edge_y + self.timestamp = datetime.now() def __eq__(self, other): # TBD: this is a minimalist implementation and requires to be revisited @@ -2031,6 +2036,7 @@ def __str__(self): return f"{self.__class__.__name__}({', '.join(args)})" def _remove(self, **kwargs): + self.timestamp = datetime.now() result = {} members = self.filters(**kwargs) @@ -2045,6 +2051,7 @@ def _remove(self, **kwargs): return result def _setter(self, element, axis, coord, shape): + self.timestamp = datetime.now() axis = axis.lower() member = f"{element}_{axis}" @@ -2138,6 +2145,7 @@ def node_y(self, coord): self._setter(element="node", axis="y", coord=coord, shape=self._node_shape) def _add(self, coords): + self.timestamp = datetime.now() member_x, member_y = coords._fields # deal with the special case where both members are changing @@ -2377,6 +2385,7 @@ def __init__(self, *connectivities): self.ALL = self.REQUIRED + self.OPTIONAL self._members = {member: None for member in self.ALL} self.add(*connectivities) + self.timestamp = datetime.now() def __eq__(self, other): # TBD: this is a minimalist implementation and requires to be revisited @@ -2423,6 +2432,7 @@ def add(self, *connectivities): # manager. # No warning is raised for duplicate cf_roles - user is trusted to # validate their outputs. + self.timestamp = datetime.now() add_dict = {} for connectivity in connectivities: if not isinstance(connectivity, Connectivity): @@ -2564,6 +2574,7 @@ def remove( contains_edge=None, contains_face=None, ): + self.timestamp = datetime.now() removal_dict = self.filters( item=item, standard_name=standard_name, @@ -2724,74 +2735,7 @@ def __init__( raise ValueError(msg) # Held in metadata, readable as self.axis, but cannot set it. self._metadata_manager.axis = axis - - points, bounds = self._construct_access_arrays() - if points is None: - # TODO: we intend to support this in future, but it will require - # extra work to refactor the parent classes. - msg = "Cannot yet create a MeshCoord without points." - raise ValueError(msg) - - # Get the 'coord identity' metadata from the relevant node-coordinate. - node_coord = self.mesh.coord(location="node", axis=self.axis) - node_metadict = node_coord.metadata._asdict() - # Use node metadata, unless location is face/edge. - use_metadict = node_metadict.copy() - if location != "node": - # Location is either "edge" or "face" - get the relevant coord. - location_coord = self.mesh.coord(location=location, axis=axis) - - # Take the MeshCoord metadata from the 'location' coord. - use_metadict = location_coord.metadata._asdict() - unit_unknown = Unit(None) - - # N.B. at present, coords in a MeshXY are stored+accessed by 'axis', which - # means they must have a standard_name. So ... - # (a) the 'location' (face/edge) coord *always* has a usable phenomenon - # identity. - # (b) we still want to check that location+node coords have the same - # phenomenon (i.e. physical meaning identity + units), **but** ... - # (c) we will accept/ignore some differences : not just "var_name", but - # also "long_name" *and* "attributes". So it is *only* "standard_name" - # and "units" that cause an error if they differ. - for key in ("standard_name", "units"): - bounds_value = use_metadict[key] - nodes_value = node_metadict[key] - if key == "units" and ( - bounds_value == unit_unknown or nodes_value == unit_unknown - ): - # Allow "any" unit to match no-units (for now) - continue - if bounds_value != nodes_value: - - def fix_repr(val): - # Tidy values appearance by converting Unit to string, and - # wrapping strings in '', but leaving other types as a - # plain str() representation. - if isinstance(val, Unit): - val = str(val) - if isinstance(val, str): - val = repr(val) - return val - - nodes_value, bounds_value = [ - fix_repr(val) for val in (nodes_value, bounds_value) - ] - msg = ( - f"Node coordinate {node_coord!r} disagrees with the " - f"{location} coordinate {location_coord!r}, " - f'in having a "{key}" value of {nodes_value} ' - f"instead of {bounds_value}." - ) - raise ValueError(msg) - - # Don't use 'coord_system' as a constructor arg, since for - # MeshCoords it is deduced from the mesh. - # (Otherwise a non-None coord_system breaks the 'copy' operation) - use_metadict.pop("coord_system") - - # Call parent constructor to handle the common constructor args. - super().__init__(points, bounds=bounds, **use_metadict) + self._load_points_and_bounds() # Define accessors for MeshCoord-specific properties mesh/location/axis. # These are all read-only. @@ -2808,6 +2752,31 @@ def location(self): def axis(self): return self._metadata_manager.axis + @property + def points(self): + """The coordinate points values as a NumPy array.""" + if self.timestamp < self._mesh.timestamp or self.timestamp is None: + self._load_points_and_bounds() + return self._values + + @points.setter + def points(self, value): + if value: + msg = "Cannot set 'points' on a MeshCoord." + raise ValueError(msg) + + @property + def bounds(self): + if self.timestamp < self._mesh.timestamp or self.timestamp is None: + self._load_points_and_bounds() + return self.bounds + + @bounds.setter + def bounds(self, value): + if value: + msg = "Cannot set 'bounds' on a MeshCoord." + raise ValueError(msg) + # Provide overrides to mimic the Coord-specific properties that are not # supported by MeshCoord, i.e. "coord_system" and "climatological". # These mimic the Coord properties, but always return fixed 'null' values. @@ -3017,6 +2986,70 @@ def summary(self, *args, **kwargs): result = "\n".join(lines) return result + def _load_points_and_bounds(self): + points, bounds = self._construct_access_arrays() + if points is None: + # TODO: we intend to support this in future, but it will require + # extra work to refactor the parent classes. + msg = "Cannot yet create a MeshCoord without points." + raise ValueError(msg) + + # Get the 'coord identity' metadata from the relevant node-coordinate. + node_coord = self.mesh.coord(include_nodes=True, axis=self.axis) + node_metadict = node_coord.metadata._asdict() + # Use node metadata, unless location is face/edge. + use_metadict = node_metadict.copy() + if location != "node": + # Location is either "edge" or "face" - get the relevant coord. + kwargs = {f"include_{location}s": True, "axis": axis} + location_coord = self.mesh.coord(**kwargs) + + # Take the MeshCoord metadata from the 'location' coord. + use_metadict = location_coord.metadata._asdict() + unit_unknown = Unit(None) + + # N.B. at present, coords in a Mesh are stored+accessed by 'axis', which + # means they must have a standard_name. So ... + # (a) the 'location' (face/edge) coord *always* has a usable phenomenon + # identity. + # (b) we still want to check that location+node coords have the same + # phenomenon (i.e. physical meaning identity + units), **but** ... + # (c) we will accept/ignore some differences : not just "var_name", but + # also "long_name" *and* "attributes". So it is *only* "standard_name" + # and "units" that cause an error if they differ. + for key in ("standard_name", "units"): + bounds_value = use_metadict[key] + nodes_value = node_metadict[key] + if key == "units" and ( + bounds_value == unit_unknown or nodes_value == unit_unknown + ): + # Allow "any" unit to match no-units (for now) + continue + if bounds_value != nodes_value: + + def fix_repr(val): + # Tidy values appearance by converting Unit to string, and + # wrapping strings in '', but leaving other types as a + # plain str() representation. + if isinstance(val, Unit): + val = str(val) + if isinstance(val, str): + val = repr(val) + return val + + nodes_value, bounds_value = [ + fix_repr(val) for val in (nodes_value, bounds_value) + ] + msg = ( + f"Node coordinate {node_coord!r} disagrees with the " + f"{location} coordinate {location_coord!r}, " + f'in having a "{key}" value of {nodes_value} ' + f"instead of {bounds_value}." + ) + raise ValueError(msg) + super().__init__(points, bounds=bounds, **use_metadict) + self.timestamp = self._mesh.timestamp + def _construct_access_arrays(self): """Build lazy points and bounds arrays. From 7b32abe214330d294b7f540c8774a2fca668781c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 15:07:00 +0000 Subject: [PATCH 02/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lib/iris/mesh/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/mesh/components.py b/lib/iris/mesh/components.py index 2e7a39b95c..1d9fbb0fc6 100644 --- a/lib/iris/mesh/components.py +++ b/lib/iris/mesh/components.py @@ -3021,7 +3021,7 @@ def _load_points_and_bounds(self): bounds_value = use_metadict[key] nodes_value = node_metadict[key] if key == "units" and ( - bounds_value == unit_unknown or nodes_value == unit_unknown + bounds_value == unit_unknown or nodes_value == unit_unknown ): # Allow "any" unit to match no-units (for now) continue From 824c8ae9683344c45b3133fe1b600ef5e4ae116a Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 20 Aug 2024 02:11:29 +0100 Subject: [PATCH 03/16] wip --- lib/iris/mesh/components.py | 40 +++++++++++++++------------ lib/iris/tests/unit/cube/test_Cube.py | 4 +++ 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/lib/iris/mesh/components.py b/lib/iris/mesh/components.py index 2e7a39b95c..149d241596 100644 --- a/lib/iris/mesh/components.py +++ b/lib/iris/mesh/components.py @@ -30,6 +30,7 @@ from ..exceptions import ConnectivityNotFoundError, CoordinateNotFoundError from ..util import array_equal, clip_string, guess_coord_axis from ..warnings import IrisVagueMetadataWarning +from datetime import datetime # Configure the logger. logger = get_logger(__name__, propagate=True, handler=False) @@ -1140,7 +1141,8 @@ def all_connectivities(self): @property def timestamp(self): - return MAX(self._coord_manager.timestamp, self._connectivity_manager.timestamp) + """The most recent time and date that the mesh was edited.""" + return max(self._coord_manager.timestamp, self._connectivity_manager.timestamp) @property def all_coords(self): @@ -2005,6 +2007,8 @@ def __init__(self, node_x, node_y, edge_x=None, edge_y=None): # optional coordinates self.edge_x = edge_x self.edge_y = edge_y + # makes a note of when the mesh coordinates were last edited, for use in + # ensuring MeshCoords are up to date self.timestamp = datetime.now() def __eq__(self, other): @@ -2385,6 +2389,8 @@ def __init__(self, *connectivities): self.ALL = self.REQUIRED + self.OPTIONAL self._members = {member: None for member in self.ALL} self.add(*connectivities) + # makes a note of when the mesh connectivities were last edited, for use in + # ensuring MeshCoords are up to date self.timestamp = datetime.now() def __eq__(self, other): @@ -2735,7 +2741,8 @@ def __init__( raise ValueError(msg) # Held in metadata, readable as self.axis, but cannot set it. self._metadata_manager.axis = axis - self._load_points_and_bounds() + points, bounds = self._load_points_and_bounds() + super().__init__(points, bounds=bounds, **use_metadict) # Define accessors for MeshCoord-specific properties mesh/location/axis. # These are all read-only. @@ -2755,25 +2762,25 @@ def axis(self): @property def points(self): """The coordinate points values as a NumPy array.""" - if self.timestamp < self._mesh.timestamp or self.timestamp is None: - self._load_points_and_bounds() - return self._values + if self.timestamp < self.mesh.timestamp or self.timestamp is None: + self._values, _, _ = self._load_points_and_bounds() + return super._values @points.setter def points(self, value): - if value: + if len(value) > 0: msg = "Cannot set 'points' on a MeshCoord." raise ValueError(msg) @property def bounds(self): if self.timestamp < self._mesh.timestamp or self.timestamp is None: - self._load_points_and_bounds() - return self.bounds + _, self.bounds, _ = self._load_points_and_bounds() + return super.bounds @bounds.setter def bounds(self, value): - if value: + if len(value) > 0 and self.bounds: msg = "Cannot set 'bounds' on a MeshCoord." raise ValueError(msg) @@ -2995,20 +3002,19 @@ def _load_points_and_bounds(self): raise ValueError(msg) # Get the 'coord identity' metadata from the relevant node-coordinate. - node_coord = self.mesh.coord(include_nodes=True, axis=self.axis) + node_coord = self.mesh.coord(location="node", axis=self.axis) node_metadict = node_coord.metadata._asdict() # Use node metadata, unless location is face/edge. use_metadict = node_metadict.copy() - if location != "node": + if self.location != "node": # Location is either "edge" or "face" - get the relevant coord. - kwargs = {f"include_{location}s": True, "axis": axis} - location_coord = self.mesh.coord(**kwargs) + location_coord = self.mesh.coord(location=location, axis=self.axis) # Take the MeshCoord metadata from the 'location' coord. use_metadict = location_coord.metadata._asdict() unit_unknown = Unit(None) - # N.B. at present, coords in a Mesh are stored+accessed by 'axis', which + # N.B. at present, coords in a MeshXY are stored+accessed by 'axis', which # means they must have a standard_name. So ... # (a) the 'location' (face/edge) coord *always* has a usable phenomenon # identity. @@ -3042,13 +3048,13 @@ def fix_repr(val): ] msg = ( f"Node coordinate {node_coord!r} disagrees with the " - f"{location} coordinate {location_coord!r}, " + f"{self.location} coordinate {location_coord!r}, " f'in having a "{key}" value of {nodes_value} ' f"instead of {bounds_value}." ) raise ValueError(msg) - super().__init__(points, bounds=bounds, **use_metadict) - self.timestamp = self._mesh.timestamp + self.timestamp = self.mesh.timestamp + return points, bounds, use_metadict def _construct_access_arrays(self): """Build lazy points and bounds arrays. diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 878a793448..64d8e8f24c 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2058,6 +2058,10 @@ def test_mesh(self): result = self.cube.mesh self.assertIs(result, self.mesh) + def test_mesh_timestamp(self): + result = self.cube.mesh.timestamp + + def test_no_mesh(self): # Replace standard setUp cube with a no-mesh version. _add_test_meshcube(self, nomesh=True) From 9abb8e2fa997a3f30c5f682103aaeafb2c429a59 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 20 Aug 2024 02:17:52 +0100 Subject: [PATCH 04/16] wip --- lib/iris/tests/unit/cube/test_Cube.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 64d8e8f24c..207fe8555f 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2060,7 +2060,7 @@ def test_mesh(self): def test_mesh_timestamp(self): result = self.cube.mesh.timestamp - + self.assertNotNone(result) def test_no_mesh(self): # Replace standard setUp cube with a no-mesh version. From 75a471f74452c9931f69ca56b97850778c3b31b9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 01:18:49 +0000 Subject: [PATCH 05/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lib/iris/mesh/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/mesh/components.py b/lib/iris/mesh/components.py index 7d201d02fd..887702d813 100644 --- a/lib/iris/mesh/components.py +++ b/lib/iris/mesh/components.py @@ -13,6 +13,7 @@ from collections import namedtuple from collections.abc import Container from contextlib import contextmanager +from datetime import datetime from typing import Iterable import warnings @@ -30,7 +31,6 @@ from ..exceptions import ConnectivityNotFoundError, CoordinateNotFoundError from ..util import array_equal, clip_string, guess_coord_axis from ..warnings import IrisVagueMetadataWarning -from datetime import datetime # Configure the logger. logger = get_logger(__name__, propagate=True, handler=False) From 378e6428aa6a3afbdd1eb6cc60451908d74166ec Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 20 Aug 2024 09:42:14 +0100 Subject: [PATCH 06/16] a couple of small changes --- lib/iris/mesh/components.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/iris/mesh/components.py b/lib/iris/mesh/components.py index 7d201d02fd..8e120ad01a 100644 --- a/lib/iris/mesh/components.py +++ b/lib/iris/mesh/components.py @@ -2741,7 +2741,7 @@ def __init__( raise ValueError(msg) # Held in metadata, readable as self.axis, but cannot set it. self._metadata_manager.axis = axis - points, bounds = self._load_points_and_bounds() + points, bounds, use_metadict = self._load_points_and_bounds() super().__init__(points, bounds=bounds, **use_metadict) # Define accessors for MeshCoord-specific properties mesh/location/axis. @@ -2763,7 +2763,7 @@ def axis(self): def points(self): """The coordinate points values as a NumPy array.""" if self.timestamp < self.mesh.timestamp or self.timestamp is None: - self._values, _, _ = self._load_points_and_bounds() + self.points, self.bounds, _ = self._load_points_and_bounds() return super._values @points.setter @@ -2775,7 +2775,7 @@ def points(self, value): @property def bounds(self): if self.timestamp < self._mesh.timestamp or self.timestamp is None: - _, self.bounds, _ = self._load_points_and_bounds() + self.points, self.bounds, _ = self._load_points_and_bounds() return super.bounds @bounds.setter @@ -3008,7 +3008,7 @@ def _load_points_and_bounds(self): use_metadict = node_metadict.copy() if self.location != "node": # Location is either "edge" or "face" - get the relevant coord. - location_coord = self.mesh.coord(location=location, axis=self.axis) + location_coord = self.mesh.coord(location=self.location, axis=self.axis) # Take the MeshCoord metadata from the 'location' coord. use_metadict = location_coord.metadata._asdict() From 56f994cf399d46359e2c891a5a67da3bc0b34e16 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 20 Aug 2024 10:04:42 +0100 Subject: [PATCH 07/16] fixed super calls --- lib/iris/mesh/components.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iris/mesh/components.py b/lib/iris/mesh/components.py index 614b838a51..2c9fd37407 100644 --- a/lib/iris/mesh/components.py +++ b/lib/iris/mesh/components.py @@ -2764,7 +2764,7 @@ def points(self): """The coordinate points values as a NumPy array.""" if self.timestamp < self.mesh.timestamp or self.timestamp is None: self.points, self.bounds, _ = self._load_points_and_bounds() - return super._values + return super().points @points.setter def points(self, value): @@ -2776,7 +2776,7 @@ def points(self, value): def bounds(self): if self.timestamp < self._mesh.timestamp or self.timestamp is None: self.points, self.bounds, _ = self._load_points_and_bounds() - return super.bounds + return super().bounds @bounds.setter def bounds(self, value): From 092e3548b245745bd31e0a0e5331612777e839ac Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 20 Aug 2024 11:21:00 +0100 Subject: [PATCH 08/16] easy review comments --- lib/iris/mesh/components.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/iris/mesh/components.py b/lib/iris/mesh/components.py index 2c9fd37407..ec43b70437 100644 --- a/lib/iris/mesh/components.py +++ b/lib/iris/mesh/components.py @@ -1141,7 +1141,8 @@ def all_connectivities(self): @property def timestamp(self): - """The most recent time and date that the mesh was edited.""" + """The most recent time and date that the mesh coordinates and or connecitivities + were edited.""" return max(self._coord_manager.timestamp, self._connectivity_manager.timestamp) @property @@ -2742,6 +2743,11 @@ def __init__( # Held in metadata, readable as self.axis, but cannot set it. self._metadata_manager.axis = axis points, bounds, use_metadict = self._load_points_and_bounds() + # Don't use 'coord_system' as a constructor arg, since for + # MeshCoords it is deduced from the mesh. + # (Otherwise a non-None coord_system breaks the 'copy' operation) + use_metadict.pop("coord_system") + super().__init__(points, bounds=bounds, **use_metadict) # Define accessors for MeshCoord-specific properties mesh/location/axis. @@ -2774,7 +2780,7 @@ def points(self, value): @property def bounds(self): - if self.timestamp < self._mesh.timestamp or self.timestamp is None: + if self.timestamp < self.mesh.timestamp or self.timestamp is None: self.points, self.bounds, _ = self._load_points_and_bounds() return super().bounds From 3cbe9c07b6ee386489757b93fe625416dc2d129d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:21:43 +0000 Subject: [PATCH 09/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lib/iris/mesh/components.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/iris/mesh/components.py b/lib/iris/mesh/components.py index ec43b70437..3aa7ccab7a 100644 --- a/lib/iris/mesh/components.py +++ b/lib/iris/mesh/components.py @@ -1142,7 +1142,8 @@ def all_connectivities(self): @property def timestamp(self): """The most recent time and date that the mesh coordinates and or connecitivities - were edited.""" + were edited. + """ return max(self._coord_manager.timestamp, self._connectivity_manager.timestamp) @property From 0fc009937cca24332ab07e2c391615f3729ad79a Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 20 Aug 2024 15:18:47 +0100 Subject: [PATCH 10/16] Implemented metadata property. Currently fails on circular --- lib/iris/mesh/components.py | 67 ++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/lib/iris/mesh/components.py b/lib/iris/mesh/components.py index 3aa7ccab7a..c4d289523c 100644 --- a/lib/iris/mesh/components.py +++ b/lib/iris/mesh/components.py @@ -2001,7 +2001,7 @@ class _Mesh1DCoordinateManager: 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} + self._members_dict = {member: None for member in self.ALL} # required coordinates self.node_x = node_x @@ -2023,6 +2023,7 @@ def __getstate__(self): def __iter__(self): for item in self._members.items(): yield item + # ELIASISCOOL def __ne__(self, other): result = self.__eq__(other) @@ -2106,9 +2107,20 @@ def _edge_shape(self): def _node_shape(self): return self._shape(element="node") + @property + def _members(self): + self.timestamp = datetime.now() + return self._members_dict + + @_members.setter + def _members(self, value): + self.timestamp = datetime.now() + self._members_dict = value + @property def all_members(self): return Mesh1DCoords(**self._members) + # ELIASISCOOL @property def edge_coords(self): @@ -2118,6 +2130,8 @@ def edge_coords(self): def edge_x(self): return self._members["edge_x"] + # ELIASISCOOL + @edge_x.setter def edge_x(self, coord): self._setter(element="edge", axis="x", coord=coord, shape=self._edge_shape) @@ -2126,6 +2140,8 @@ def edge_x(self, coord): def edge_y(self): return self._members["edge_y"] + # ELIASISCOOL + @edge_y.setter def edge_y(self, coord): self._setter(element="edge", axis="y", coord=coord, shape=self._edge_shape) @@ -2138,6 +2154,8 @@ def node_coords(self): def node_x(self): return self._members["node_x"] + # ELIASISCOOL + @node_x.setter def node_x(self, coord): self._setter(element="node", axis="x", coord=coord, shape=self._node_shape) @@ -2389,7 +2407,7 @@ def __init__(self, *connectivities): raise ValueError(message) self.ALL = self.REQUIRED + self.OPTIONAL - self._members = {member: None for member in self.ALL} + self._members_dict = {member: None for member in self.ALL} self.add(*connectivities) # makes a note of when the mesh connectivities were last edited, for use in # ensuring MeshCoords are up to date @@ -2434,6 +2452,16 @@ def __str__(self): def all_members(self): return NotImplemented + @property + def _members(self): + self.timestamp = datetime.now() + return self._members_dict + + @_members.setter + def _members(self, value): + self.timestamp = datetime.now() + self._members_dict = value + def add(self, *connectivities): # Since Connectivity classes include their cf_role, no setters will be # provided, just a means to add one or more connectivities to the @@ -2711,7 +2739,7 @@ def __init__( axis, ): # Setup the metadata. - self._metadata_manager = metadata_manager_factory(MeshCoordMetadata) + self._metadata_manager_temp = metadata_manager_factory(MeshCoordMetadata) # Validate and record the class-specific constructor args. if not isinstance(mesh, MeshXY): @@ -2743,7 +2771,8 @@ def __init__( raise ValueError(msg) # Held in metadata, readable as self.axis, but cannot set it. self._metadata_manager.axis = axis - points, bounds, use_metadict = self._load_points_and_bounds() + points, bounds = self._load_points_and_bounds() + use_metadict = self._load_metadata() # Don't use 'coord_system' as a constructor arg, since for # MeshCoords it is deduced from the mesh. # (Otherwise a non-None coord_system breaks the 'copy' operation) @@ -2770,7 +2799,9 @@ def axis(self): def points(self): """The coordinate points values as a NumPy array.""" if self.timestamp < self.mesh.timestamp or self.timestamp is None: - self.points, self.bounds, _ = self._load_points_and_bounds() + points, bounds = self._load_points_and_bounds() + super(MeshCoord, self.__class__).points.fset(self, points) + super(MeshCoord, self.__class__).bounds.fset(self, bounds) return super().points @points.setter @@ -2782,7 +2813,9 @@ def points(self, value): @property def bounds(self): if self.timestamp < self.mesh.timestamp or self.timestamp is None: - self.points, self.bounds, _ = self._load_points_and_bounds() + points, bounds = self._load_points_and_bounds() + super(MeshCoord, self.__class__).points.fset(self, points) + super(MeshCoord, self.__class__).bounds.fset(self, bounds) return super().bounds @bounds.setter @@ -2790,6 +2823,20 @@ def bounds(self, value): if len(value) > 0 and self.bounds: msg = "Cannot set 'bounds' on a MeshCoord." raise ValueError(msg) + else: + super(MeshCoord, self.__class__).bounds.fset(self, value) + + @property + def _metadata_manager(self): + # An explanatory comment. + use_metadict = self._load_metadata() + self._metadata_manager_temp.standard_name = something + # Etcetera for all standard coord metadata + # THIS INCLUDES DETERMINING THE CORRECT VALUE, AS IN + # THE CURRENT BLOCK WITHIN _load_points_and_bounds + + return self._metadata_manager_temp + # Provide overrides to mimic the Coord-specific properties that are not # supported by MeshCoord, i.e. "coord_system" and "climatological". @@ -3007,7 +3054,10 @@ def _load_points_and_bounds(self): # extra work to refactor the parent classes. msg = "Cannot yet create a MeshCoord without points." raise ValueError(msg) + self.timestamp = self.mesh.timestamp + return points, bounds + def _load_metadata(self): # Get the 'coord identity' metadata from the relevant node-coordinate. node_coord = self.mesh.coord(location="node", axis=self.axis) node_metadict = node_coord.metadata._asdict() @@ -3034,7 +3084,7 @@ def _load_points_and_bounds(self): bounds_value = use_metadict[key] nodes_value = node_metadict[key] if key == "units" and ( - bounds_value == unit_unknown or nodes_value == unit_unknown + bounds_value == unit_unknown or nodes_value == unit_unknown ): # Allow "any" unit to match no-units (for now) continue @@ -3060,8 +3110,7 @@ def fix_repr(val): f"instead of {bounds_value}." ) raise ValueError(msg) - self.timestamp = self.mesh.timestamp - return points, bounds, use_metadict + return use_metadict def _construct_access_arrays(self): """Build lazy points and bounds arrays. From 67c9ed0017c9ff8f0bd8009d413f20d25b1b5345 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:21:29 +0000 Subject: [PATCH 11/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lib/iris/mesh/components.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iris/mesh/components.py b/lib/iris/mesh/components.py index c4d289523c..8c9984d59f 100644 --- a/lib/iris/mesh/components.py +++ b/lib/iris/mesh/components.py @@ -2120,6 +2120,7 @@ def _members(self, value): @property def all_members(self): return Mesh1DCoords(**self._members) + # ELIASISCOOL @property @@ -2837,7 +2838,6 @@ def _metadata_manager(self): return self._metadata_manager_temp - # Provide overrides to mimic the Coord-specific properties that are not # supported by MeshCoord, i.e. "coord_system" and "climatological". # These mimic the Coord properties, but always return fixed 'null' values. @@ -3084,7 +3084,7 @@ def _load_metadata(self): bounds_value = use_metadict[key] nodes_value = node_metadict[key] if key == "units" and ( - bounds_value == unit_unknown or nodes_value == unit_unknown + bounds_value == unit_unknown or nodes_value == unit_unknown ): # Allow "any" unit to match no-units (for now) continue From d8ad4dd5f4cdf25871e56e45e7f7ecbbf8decf68 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Thu, 22 Aug 2024 15:24:23 +0100 Subject: [PATCH 12/16] bounds are circular looping --- lib/iris/mesh/components.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/iris/mesh/components.py b/lib/iris/mesh/components.py index c4d289523c..f64edefcbf 100644 --- a/lib/iris/mesh/components.py +++ b/lib/iris/mesh/components.py @@ -2760,7 +2760,7 @@ def __init__( ) raise ValueError(msg) # Held in metadata, readable as self.location, but cannot set it. - self._metadata_manager.location = location + self._metadata_manager_temp.location = location if axis not in MeshXY.AXES: # The valid axes are defined by the MeshXY class. @@ -2770,7 +2770,7 @@ def __init__( ) raise ValueError(msg) # Held in metadata, readable as self.axis, but cannot set it. - self._metadata_manager.axis = axis + self._metadata_manager_temp.axis = axis points, bounds = self._load_points_and_bounds() use_metadict = self._load_metadata() # Don't use 'coord_system' as a constructor arg, since for @@ -2806,7 +2806,7 @@ def points(self): @points.setter def points(self, value): - if len(value) > 0: + if len(value) != 0 or not(value is None): msg = "Cannot set 'points' on a MeshCoord." raise ValueError(msg) @@ -2820,7 +2820,7 @@ def bounds(self): @bounds.setter def bounds(self, value): - if len(value) > 0 and self.bounds: + if len(value) != 0 or not(value is None) and self.bounds: msg = "Cannot set 'bounds' on a MeshCoord." raise ValueError(msg) else: @@ -2830,11 +2830,13 @@ def bounds(self, value): def _metadata_manager(self): # An explanatory comment. use_metadict = self._load_metadata() - self._metadata_manager_temp.standard_name = something - # Etcetera for all standard coord metadata - # THIS INCLUDES DETERMINING THE CORRECT VALUE, AS IN - # THE CURRENT BLOCK WITHIN _load_points_and_bounds - + self._metadata_manager_temp.standard_name = use_metadict["standard_name"] + self._metadata_manager_temp.long_name = use_metadict["long_name"] + self._metadata_manager_temp.var_name = use_metadict["var_name"] + self._metadata_manager_temp.units = use_metadict["units"] + self._metadata_manager_temp.attributes = use_metadict["attributes"] + self._metadata_manager_temp.coord_system = use_metadict["coord_system"] + self._metadata_manager_temp.climatological = use_metadict["climatological"] return self._metadata_manager_temp @@ -3054,18 +3056,20 @@ def _load_points_and_bounds(self): # extra work to refactor the parent classes. msg = "Cannot yet create a MeshCoord without points." raise ValueError(msg) - self.timestamp = self.mesh.timestamp - return points, bounds + self.timestamp = self.mesh.timestamp + return points, bounds def _load_metadata(self): + axis = self._metadata_manager_temp.axis + location = self._metadata_manager_temp.location # Get the 'coord identity' metadata from the relevant node-coordinate. - node_coord = self.mesh.coord(location="node", axis=self.axis) + node_coord = self.mesh.coord(location="node", axis=axis) node_metadict = node_coord.metadata._asdict() # Use node metadata, unless location is face/edge. use_metadict = node_metadict.copy() - if self.location != "node": + if location != "node": # Location is either "edge" or "face" - get the relevant coord. - location_coord = self.mesh.coord(location=self.location, axis=self.axis) + location_coord = self.mesh.coord(location=location, axis=axis) # Take the MeshCoord metadata from the 'location' coord. use_metadict = location_coord.metadata._asdict() @@ -3105,7 +3109,7 @@ def fix_repr(val): ] msg = ( f"Node coordinate {node_coord!r} disagrees with the " - f"{self.location} coordinate {location_coord!r}, " + f"{location} coordinate {location_coord!r}, " f'in having a "{key}" value of {nodes_value} ' f"instead of {bounds_value}." ) From 050d47bb2f9bad74fd6319e4cc8bfe3fbe81ad85 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 22 Aug 2024 14:25:39 +0000 Subject: [PATCH 13/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lib/iris/mesh/components.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iris/mesh/components.py b/lib/iris/mesh/components.py index 4dda67b3e7..24fd00689d 100644 --- a/lib/iris/mesh/components.py +++ b/lib/iris/mesh/components.py @@ -2807,7 +2807,7 @@ def points(self): @points.setter def points(self, value): - if len(value) != 0 or not(value is None): + if len(value) != 0 or not (value is None): msg = "Cannot set 'points' on a MeshCoord." raise ValueError(msg) @@ -2821,7 +2821,7 @@ def bounds(self): @bounds.setter def bounds(self, value): - if len(value) != 0 or not(value is None) and self.bounds: + if len(value) != 0 or not (value is None) and self.bounds: msg = "Cannot set 'bounds' on a MeshCoord." raise ValueError(msg) else: From 9c0b61da28ed87e17abb69c98a0f181fdb27017c Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Thu, 3 Oct 2024 16:44:16 +0100 Subject: [PATCH 14/16] fixed bounds issue --- lib/iris/mesh/components.py | 36 +++++++++++++++++++-------- lib/iris/tests/unit/cube/test_Cube.py | 2 +- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/iris/mesh/components.py b/lib/iris/mesh/components.py index 4dda67b3e7..7ad5585d59 100644 --- a/lib/iris/mesh/components.py +++ b/lib/iris/mesh/components.py @@ -2739,6 +2739,7 @@ def __init__( location, axis, ): + self._read_only_points_and_bounds = True # Setup the metadata. self._metadata_manager_temp = metadata_manager_factory(MeshCoordMetadata) @@ -2778,8 +2779,8 @@ def __init__( # MeshCoords it is deduced from the mesh. # (Otherwise a non-None coord_system breaks the 'copy' operation) use_metadict.pop("coord_system") - - super().__init__(points, bounds=bounds, **use_metadict) + with self._writable_points_and_bounds(): + super().__init__(points, bounds=bounds, **use_metadict) # Define accessors for MeshCoord-specific properties mesh/location/axis. # These are all read-only. @@ -2796,6 +2797,19 @@ def location(self): def axis(self): return self._metadata_manager.axis + @contextmanager + def _writable_points_and_bounds(self): + """ + Context manager to allow bounds and points to be set during __init__. + `points` currently doesn't encounter any issues without this manager, but + is included here for future proofing. + """ + try: + self._read_only_points_and_bounds = False + yield + finally: + self._read_only_points_and_bounds = True + @property def points(self): """The coordinate points values as a NumPy array.""" @@ -2807,9 +2821,10 @@ def points(self): @points.setter def points(self, value): - if len(value) != 0 or not(value is None): - msg = "Cannot set 'points' on a MeshCoord." - raise ValueError(msg) + if self._read_only_points_and_bounds: + if len(value) != 0 or not(value is None): + msg = "Cannot set 'points' on a MeshCoord." + raise ValueError(msg) @property def bounds(self): @@ -2821,11 +2836,12 @@ def bounds(self): @bounds.setter def bounds(self, value): - if len(value) != 0 or not(value is None) and self.bounds: - msg = "Cannot set 'bounds' on a MeshCoord." - raise ValueError(msg) - else: - super(MeshCoord, self.__class__).bounds.fset(self, value) + if self._read_only_points_and_bounds: + if len(value) != 0: #or not(value is None) and self.bounds: + msg = "Cannot set 'bounds' on a MeshCoord." + raise ValueError(msg) + else: + super(MeshCoord, self.__class__).bounds.fset(self, value) @property def _metadata_manager(self): diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 207fe8555f..37736e82d7 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2060,7 +2060,7 @@ def test_mesh(self): def test_mesh_timestamp(self): result = self.cube.mesh.timestamp - self.assertNotNone(result) + self.assertIsNotNone(result) def test_no_mesh(self): # Replace standard setUp cube with a no-mesh version. From 99c995ad7fb4d4a870ea1f19443674bd6dc89305 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Thu, 3 Oct 2024 16:53:57 +0100 Subject: [PATCH 15/16] merge conflicts --- lib/iris/mesh/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/mesh/components.py b/lib/iris/mesh/components.py index 7ad5585d59..a3c2d7b941 100644 --- a/lib/iris/mesh/components.py +++ b/lib/iris/mesh/components.py @@ -14,7 +14,7 @@ from collections.abc import Container from contextlib import contextmanager from datetime import datetime -from typing import Iterable +from typing import Iterable, Literal import warnings from cf_units import Unit From adbb97b339662a4f70c3e3618afa747827e3cee6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:57:27 +0000 Subject: [PATCH 16/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lib/iris/mesh/components.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/iris/mesh/components.py b/lib/iris/mesh/components.py index 9440bdb833..702bcbe510 100644 --- a/lib/iris/mesh/components.py +++ b/lib/iris/mesh/components.py @@ -2802,8 +2802,7 @@ def axis(self): @contextmanager def _writable_points_and_bounds(self): - """ - Context manager to allow bounds and points to be set during __init__. + """Context manager to allow bounds and points to be set during __init__. `points` currently doesn't encounter any issues without this manager, but is included here for future proofing. """ @@ -2825,7 +2824,7 @@ def points(self): @points.setter def points(self, value): if self._read_only_points_and_bounds: - if len(value) != 0 or not(value is None): + if len(value) != 0 or not (value is None): msg = "Cannot set 'points' on a MeshCoord." raise ValueError(msg) @@ -2840,7 +2839,7 @@ def bounds(self): @bounds.setter def bounds(self, value): if self._read_only_points_and_bounds: - if len(value) != 0: #or not(value is None) and self.bounds: + if len(value) != 0: # or not(value is None) and self.bounds: msg = "Cannot set 'bounds' on a MeshCoord." raise ValueError(msg) else: