Skip to content

Commit 6c84ea4

Browse files
authored
Investigate and reduce benchmark changes for mesh timestamps (#6549)
* tests failing, pushing up for testing. * Testing a theory * Testing a theory * Hopefully, this should maintain benchmark improvements and return functionality * here's hoping it's that simple * here's hoping it's that simple * removed unnecessary timestamp.update() * self review comments * made update_from_mesh() force an update, even if timestamps appear alligned * Update components.py * Update test_MeshCoord.py
1 parent 2822889 commit 6c84ea4

File tree

3 files changed

+95
-40
lines changed

3 files changed

+95
-40
lines changed

lib/iris/coords.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -573,8 +573,9 @@ def reindent_data_string(text, n_indent):
573573

574574
def __setattr__(self, key, value):
575575
if getattr(self, "_mesh_timestamps", None) is not None:
576-
for timestamp in self._mesh_timestamps:
577-
timestamp.update()
576+
if key in ("points", "bounds", "_values", "indices"):
577+
for timestamp in self._mesh_timestamps:
578+
timestamp.update()
578579
object.__setattr__(self, key, value)
579580

580581
def __str__(self):

lib/iris/mesh/components.py

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2481,7 +2481,6 @@ def all_members(self):
24812481

24822482
@property
24832483
def _members(self):
2484-
self.timestamp.update()
24852484
return self._members_dict
24862485

24872486
@_members.setter
@@ -2776,6 +2775,7 @@ def __init__(
27762775
self._last_modified = None
27772776
self._updating = True
27782777
self._read_only = True
2778+
27792779
# Setup the metadata.
27802780
self._metadata_manager_temp = metadata_manager_factory(MeshCoordMetadata)
27812781

@@ -2820,20 +2820,25 @@ def __init__(
28202820
raise e
28212821
finally:
28222822
self._read_only = True
2823+
self._last_modified = self.mesh._last_modified
28232824
self._updating = False
28242825

28252826
def __getattribute__(self, item):
2826-
# Ensure that the MeshCoord is up to date whenever you use it
2827+
# Ensure that the MeshCoord is up to date each time you access an attribute.
28272828
# object.__getattribute__ bypasses this block to avoid infinite recursion
2828-
# by calling the method on the base class `object`
2829-
updating = object.__getattribute__(self, "_updating")
2830-
if updating is False and item != "update_from_mesh":
2829+
# by calling the method on the base class `object`.
2830+
mesh = object.__getattribute__(self, "_mesh")
2831+
mesh_last_modified = object.__getattribute__(mesh, "_last_modified")
2832+
self_last_modified = object.__getattribute__(self, "_last_modified")
2833+
if (
2834+
self_last_modified is None or self_last_modified < mesh_last_modified
2835+
) and item != "update_from_mesh":
28312836
object.__getattribute__(self, "update_from_mesh")()
28322837
return super().__getattribute__(item)
28332838

2834-
# Define accessors for MeshCoord-specific properties mesh/location/axis.
2839+
# Define accessors for MeshCoord-specific properties mesh/location/axis, and
2840+
# ensure every property on a MeshCoord is read-only.
28352841

2836-
# These are all read-only.
28372842
@property
28382843
def mesh(self):
28392844
return self._mesh
@@ -2943,7 +2948,8 @@ def climatological(self, value):
29432948
def points(self):
29442949
"""The coordinate points values as a NumPy array."""
29452950
try:
2946-
# the points property should return real data, but shouldn't realise the coord on the mesh
2951+
# The points property should return real data, but shouldn't
2952+
# realise the coord on the mesh.
29472953
return super().core_points().compute()
29482954
except AttributeError:
29492955
return super().core_points()
@@ -2957,6 +2963,8 @@ def points(self, value):
29572963

29582964
@property
29592965
def bounds(self):
2966+
# The bounds property should return real data, but shouldn't
2967+
# realise the coord on the mesh.
29602968
try:
29612969
return super().core_bounds().compute()
29622970
except AttributeError:
@@ -2973,9 +2981,8 @@ def bounds(self, value):
29732981

29742982
@property
29752983
def _metadata_manager(self):
2976-
# sets the metadata
2984+
# Fetches the metadata from the mesh every time any metadata is accessed.
29772985
use_metadict = self._load_metadata()
2978-
29792986
self._metadata_manager_temp.standard_name = use_metadict["standard_name"]
29802987
self._metadata_manager_temp.long_name = use_metadict["long_name"]
29812988
self._metadata_manager_temp.var_name = use_metadict["var_name"]
@@ -2999,22 +3006,27 @@ def __getitem__(self, keys):
29993006
return self.copy()
30003007

30013008
def update_from_mesh(self):
3002-
try:
3003-
object.__setattr__(self, "_updating", True)
3004-
if (self._last_modified is None) or (
3005-
self._last_modified < self.mesh._last_modified
3006-
):
3009+
"""Fetch and recalculate the points and bounds from the relevant coord on the mesh.
3010+
3011+
In most cases, updates should be done automatically, but this method can be used
3012+
if for some reason the points or bounds are out of date.
3013+
"""
3014+
updating = object.__getattribute__(self, "_updating")
3015+
if updating is False:
3016+
try:
3017+
object.__setattr__(self, "_updating", True)
3018+
# update points and bounds
30073019
points, bounds = self._load_points_and_bounds()
30083020
super(MeshCoord, self.__class__).points.fset(self, points)
30093021
super(MeshCoord, self.__class__).bounds.fset(self, bounds)
30103022
object.__setattr__(self, "_last_modified", self.mesh._last_modified)
3011-
# Ensure errors aren't bypassed
3012-
except Exception as e:
3013-
raise e
3014-
finally:
3015-
# if _updating isn't reset, this would mean the MeshCoord would never
3016-
# update from the attached Mesh, breaking the link
3017-
object.__setattr__(self, "_updating", False)
3023+
# Ensure errors aren't bypassed
3024+
except Exception as e:
3025+
raise e
3026+
finally:
3027+
# if _updating isn't reset, this would mean the MeshCoord would never
3028+
# update from the attached Mesh, breaking the link
3029+
object.__setattr__(self, "_updating", False)
30183030

30193031
def collapsed(self, dims_to_collapse=None):
30203032
"""Return a copy of this coordinate, which has been collapsed along the specified dimensions.
@@ -3188,6 +3200,7 @@ def _load_points_and_bounds(self):
31883200
# extra work to refactor the parent classes.
31893201
msg = "Cannot yet create a MeshCoord without points."
31903202
raise ValueError(msg)
3203+
31913204
return points, bounds
31923205

31933206
def _load_metadata(self):

lib/iris/tests/unit/mesh/components/test_MeshCoord.py

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,33 +1030,42 @@ def _set_all_coords(self, name, value):
10301030
c.__setattr__(name, value)
10311031

10321032
def test__last_modified(self):
1033-
# Ensure they are identical at creation.
1033+
# Ensure that mesh._last_modified is updated when you update the mesh, and
1034+
# and meshcoord._last_modified is updated to match that.
1035+
10341036
assert self.meshcoord.mesh._last_modified == self.timestamp_at_creation
10351037

10361038
self.coord_on_mesh.points = np.zeros(3)
10371039

10381040
assert self.meshcoord.mesh._last_modified == self.meshcoord._last_modified
10391041
assert self.meshcoord.mesh._last_modified != self.timestamp_at_creation
10401042

1041-
self.meshcoord.standard_name
1042-
1043-
assert self.meshcoord.mesh._last_modified == self.meshcoord._last_modified
1044-
assert self.meshcoord.mesh._last_modified != self.timestamp_at_creation
1045-
1046-
def test_points(self):
1043+
def test_points(self, mocker):
10471044
zeroes = np.zeros(3)
1045+
mocked = mocker.patch.object(MeshCoord, "_load_points_and_bounds")
1046+
mocked.return_value = (
1047+
zeroes,
1048+
np.zeros((3, 3)),
1049+
)
10481050
assert self.meshcoord.points.all() != zeroes.all()
10491051
self.coord_on_mesh.points = zeroes
10501052
assert self.meshcoord.points.all() == zeroes.all()
1053+
mocked.assert_called_once()
10511054

1052-
def test_bounds(self):
1055+
def test_bounds(self, mocker):
10531056
zero_bounds = np.zeros(3)
10541057
zero_points = np.zeros(15)
1058+
mocked = mocker.patch.object(MeshCoord, "_load_points_and_bounds")
1059+
mocked.return_value = (
1060+
zero_bounds,
1061+
np.zeros((3, 3)),
1062+
)
10551063
assert self.meshcoord.bounds.all() != zero_bounds.all()
10561064
# Node coords are used to calculate the meshcoord bounds
10571065
for nc in self.meshcoord.mesh.node_coords:
10581066
nc.points = zero_points
10591067
assert self.meshcoord.bounds.all() == zero_bounds.all()
1068+
mocked.assert_called_once()
10601069

10611070
@pytest.mark.parametrize(
10621071
"metadata_name, value",
@@ -1066,23 +1075,37 @@ def test_bounds(self):
10661075
("attributes", {"foo": 1}),
10671076
],
10681077
)
1069-
def test_basic_metadata(self, metadata_name, value):
1078+
def test_basic_metadata(self, metadata_name, value, mocker):
1079+
mocked = mocker.patch.object(MeshCoord, "_load_points_and_bounds")
1080+
10701081
self.coord_on_mesh.__setattr__(metadata_name, value)
10711082
assert self.meshcoord.__getattribute__(
10721083
metadata_name
10731084
) == self.coord_on_mesh.__getattribute__(metadata_name)
10741085

1075-
def test_units(self):
1086+
# Ensure updating metadata doesn't prompt the MeshCoord
1087+
# to update points and bounds.
1088+
mocked.assert_not_called()
1089+
1090+
def test_units(self, mocker):
1091+
mocked = mocker.patch.object(MeshCoord, "_load_points_and_bounds")
10761092
for c in self.mesh.coords():
10771093
c.units = "radians"
10781094
assert self.meshcoord.standard_name == self.coord_on_mesh.standard_name
1095+
# Ensure updating metadata doesn't prompt the MeshCoord
1096+
# to update points and bounds.
1097+
mocked.assert_not_called()
10791098

1080-
def test_standard_name(self):
1099+
def test_standard_name(self, mocker):
1100+
mocked = mocker.patch.object(MeshCoord, "_load_points_and_bounds")
10811101
for c in self.mesh.coords(axis="x"):
10821102
c.standard_name = "grid_longitude"
10831103
for c in self.mesh.coords(axis="y"):
10841104
c.standard_name = "grid_latitude"
10851105
assert self.meshcoord.standard_name == self.coord_on_mesh.standard_name
1106+
# Ensure updating metadata doesn't prompt the MeshCoord
1107+
# to update points and bounds.
1108+
mocked.assert_not_called()
10861109

10871110
def test_updates(self, mocker):
10881111
mocked = mocker.patch.object(MeshCoord, "_load_points_and_bounds")
@@ -1093,13 +1116,31 @@ def test_updates(self, mocker):
10931116
np.zeros((3, 3)),
10941117
)
10951118
self.coord_on_mesh.points = np.zeros(3)
1096-
# Only the first update should actually update
1097-
_ = self.meshcoord.update_from_mesh()
1098-
_ = self.meshcoord.update_from_mesh()
1119+
# Only the first time you access an attribute should fetch
1120+
# update the points and bounds.
1121+
1122+
self.meshcoord.standard_name
1123+
self.meshcoord.standard_name
10991124
mocked.assert_called_once()
11001125

11011126
# Ensure it updates more than once if the mesh has been updated
1102-
# a second time
1127+
# a second time.
11031128
self.coord_on_mesh.points = np.ones(3)
1104-
_ = self.meshcoord.update_from_mesh()
1129+
self.meshcoord.standard_name
1130+
assert mocked.call_count == 2
1131+
1132+
def test_update_from_mesh(self, mocker):
1133+
mocked = mocker.patch.object(MeshCoord, "_load_points_and_bounds")
1134+
mocked.return_value = (
1135+
np.zeros(
1136+
3,
1137+
),
1138+
np.zeros((3, 3)),
1139+
)
1140+
self.coord_on_mesh.points = np.zeros(3)
1141+
self.meshcoord.update_from_mesh()
1142+
mocked.assert_called_once()
1143+
1144+
# Ensure it forces an update, even if the mesh hasn't been updated.
1145+
self.meshcoord.update_from_mesh()
11051146
assert mocked.call_count == 2

0 commit comments

Comments
 (0)