Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ def _dotv(version):
"members": True,
"member-order": "alphabetical",
"undoc-members": True,
"private-members": False,
"private-members": "_MeshIndexSet",
"special-members": False,
# Enums are most valuable when documented as concisely as possible.
"inherited-members": "Enum,IntEnum,ReprEnum,StrEnum",
Expand Down
3 changes: 2 additions & 1 deletion lib/iris/analysis/_interpolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from iris._lazy_data import map_complete_blocks
from iris.coords import AuxCoord, DimCoord
from iris.exceptions import MonotonicityError
import iris.util

_DEFAULT_DTYPE = np.float16
Expand Down Expand Up @@ -478,7 +479,7 @@ def _validate(self):
# Check monotonic.
if not iris.util.monotonic(coord.points, strict=True):
msg = "Cannot interpolate over the non-monotonic coordinate {}."
raise ValueError(msg.format(coord.name()))
raise MonotonicityError(msg.format(coord.name()))

def _points(self, sample_points, data, data_dims=None):
"""Interpolate at the specified points.
Expand Down
111 changes: 111 additions & 0 deletions lib/iris/common/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class _NamedTupleMeta(ABCMeta):
def __new__(mcs, name, bases, namespace):
names = []

# "Inherit" the base classes' `_fields`
for base in bases:
if hasattr(base, "_fields"):
base_names = getattr(base, "_fields")
Expand All @@ -130,6 +131,8 @@ def __new__(mcs, name, bases, namespace):
base_names = (base_names,)
names.extend(base_names)

# _members are fields that are specific in this NamedTuple.
# Add these as fields.
if "_members" in namespace and not getattr(
namespace["_members"], "__isabstractmethod__", False
):
Expand Down Expand Up @@ -1596,6 +1599,114 @@ def equal(self, other, lenient=None):
return super().equal(other, lenient=lenient)


class MeshIndexSetMetadata(BaseMetadata):
"""Metadata container for a :class:`~iris.mesh.components._MeshIndexSet`."""

_members = ("mesh", "location", "start_index")

__slots__ = ()

@wraps(BaseMetadata.__eq__, assigned=("__doc__",), updated=())
@lenient_service
def __eq__(self, other):
return super().__eq__(other)

def _combine_lenient(self, other):
"""Perform lenient combination of metadata members for _MeshIndexSet.

Parameters
----------
other : MeshIndexSetMetadata
The other metadata participating in the lenient combination.

Returns
-------
A list of combined metadata member values.

"""

# It is actually "strict" : return None except where members are equal.
def func(field):
left = getattr(self, field)
right = getattr(other, field)
return left if left == right else None

# Note that, we use "_members" not "_fields".
values = [func(field) for field in self._members]
# Perform lenient combination of the other parent members.
result = super()._combine_lenient(other)
result.extend(values)

return result

def _compare_lenient(self, other):
"""Perform lenient equality of metadata members for _MeshIndexSet.

Parameters
----------
other : MeshIndexSetMetadata
The other metadata participating in the lenient comparison.

Returns
-------
bool

"""
# Perform "strict" comparison for the _MeshIndexSet specific members
# 'mesh', 'location', 'start_index' : for equality, they must all match.
result = all(
[getattr(self, field) == getattr(other, field) for field in self._members]
)
if result:
# Perform lenient comparison of the other parent members.
result = super()._compare_lenient(other)

return result

def _difference_lenient(self, other):
"""Perform lenient difference of metadata members for _MeshIndexSet.

Parameters
----------
other : MeshIndexSetMetadata
The other metadata participating in the lenient difference.

Returns
-------
A list of different metadata member values.

"""

# Perform "strict" difference for location / axis.
def func(field):
left = getattr(self, field)
right = getattr(other, field)
return None if left == right else (left, right)

# Note that, we use "_members" not "_fields".
values = [func(field) for field in self._members]
# Perform lenient difference of the other parent members.
result = super()._difference_lenient(other)
result.extend(values)

return result

@wraps(BaseMetadata.combine, assigned=("__doc__",), updated=())
@lenient_service
def combine(self, other, lenient=None):
return super().combine(other, lenient=lenient)

@wraps(BaseMetadata.difference, assigned=("__doc__",), updated=())
@lenient_service
def difference(self, other, lenient=None):
return super().difference(other, lenient=lenient)

@wraps(BaseMetadata.equal, assigned=("__doc__",), updated=())
@lenient_service
def equal(self, other, lenient=None):
return super().equal(other, lenient=lenient)


class MeshCoordMetadata(BaseMetadata):
"""Metadata container for a :class:`~iris.coords.MeshCoord`."""

Expand Down
29 changes: 23 additions & 6 deletions lib/iris/coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from functools import lru_cache
from itertools import zip_longest
import operator
from typing import cast
import warnings
import zlib

Expand Down Expand Up @@ -277,7 +278,18 @@ def _sanitise_array(self, src, ndmin):
@property
def _values(self):
"""The _DimensionalMetadata values as a NumPy array."""
return self._values_dm.data.view()

def data_id():
return id(self._values_dm.core_data())

original_id = data_id()
result = self._values_dm.data.view()
if data_id() != original_id:
# Realisation has occurred - potential effect on deferred mesh computations
# (MeshCoord, _MeshIndexSet).
for timestamp in self._mesh_timestamps:
timestamp.update()
return result

@_values.setter
def _values(self, values):
Expand Down Expand Up @@ -319,7 +331,7 @@ def summary(
precision=None,
convert_dates=True,
_section_indices=None,
):
) -> str:
r"""Make a printable text summary.

Parameters
Expand Down Expand Up @@ -609,6 +621,7 @@ def reindent_data_string(text, n_indent):
show = val is not None and val is not False
if show:
if name == "attributes":
val = cast(dict, val)
# Use a multi-line form for this.
add_output(newline_indent)
add_output("attributes:", section="attributes")
Expand Down Expand Up @@ -2473,7 +2486,7 @@ def _guess_bounds(self, bound_position=0.5, monthly=False, yearly=False):
# XXX Consider moving into DimCoord
# ensure we have monotonic points
if not self.is_monotonic():
raise ValueError(
raise iris.exceptions.MonotonicityError(
"Need monotonic points to generate bounds for %s" % self.name()
)

Expand Down Expand Up @@ -3047,7 +3060,9 @@ def _new_points_requirements(self, points):
raise TypeError(emsg.format(self.name(), self.__class__.__name__))
if points.size > 1 and not iris.util.monotonic(points, strict=True):
emsg = "The {!r} {} points array must be strictly monotonic."
raise ValueError(emsg.format(self.name(), self.__class__.__name__))
raise iris.exceptions.MonotonicityError(
emsg.format(self.name(), self.__class__.__name__)
)

@property
def _values(self):
Expand Down Expand Up @@ -3127,7 +3142,7 @@ def _new_bounds_requirements(self, bounds):
)
if not monotonic:
emsg = "The {!r} {} bounds array must be strictly monotonic."
raise ValueError(
raise iris.exceptions.MonotonicityError(
emsg.format(self.name(), self.__class__.__name__)
)
directions.add(direction)
Expand All @@ -3137,7 +3152,9 @@ def _new_bounds_requirements(self, bounds):
"The direction of monotonicity for {!r} {} must "
"be consistent across all bounds."
)
raise ValueError(emsg.format(self.name(), self.__class__.__name__))
raise iris.exceptions.MonotonicityError(
emsg.format(self.name(), self.__class__.__name__)
)

if n_bounds == 2:
# Make ordering of bounds consistent with coord's direction
Expand Down
18 changes: 11 additions & 7 deletions lib/iris/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from functools import partial, reduce
import itertools
import operator
from typing import TYPE_CHECKING, Any, Optional, TypeAlias, TypeGuard
from typing import TYPE_CHECKING, Any, Literal, Optional, TypeAlias, TypeGuard
import warnings
from xml.dom.minidom import Document

Expand Down Expand Up @@ -1638,7 +1638,13 @@ def is_mesh_coord(anycoord: iris.coords.Coord) -> TypeGuard[MeshCoord]:
ownval=location,
)
)
mesh_dims = (self.mesh_dim(),)
mesh_dim = self.mesh_dim()
mesh_dims: tuple[int] | tuple[()]
if mesh_dim is None:
# Scalar coordinate.
mesh_dims = ()
else:
mesh_dims = (mesh_dim,)
if data_dims != mesh_dims:
raise iris.exceptions.CannotAddError(
msg.format(
Expand Down Expand Up @@ -2605,7 +2611,7 @@ def mesh_dim(self) -> int | None:
if coord is None:
result = None
else:
(result,) = self.coord_dims(coord) # result is a 1-tuple
(result,) = self.coord_dims(coord) or (None,)
return result

def cell_measures(
Expand Down Expand Up @@ -3286,8 +3292,7 @@ def new_ancillary_variable_dims(av_):
coord_keys = tuple([full_slice[dim] for dim in self.coord_dims(coord)])
try:
new_coord = coord[coord_keys]
except ValueError:
# TODO make this except more specific to catch monotonic error
except iris.exceptions.MonotonicityError:
# Attempt to slice it by converting to AuxCoord first
new_coord = iris.coords.AuxCoord.from_coord(coord)[coord_keys]
aux_coords.append((new_coord, new_coord_dims(coord)))
Expand All @@ -3312,8 +3317,7 @@ def new_ancillary_variable_dims(av_):
else:
dim_coords.append((new_coord, new_dims))
shape += new_coord.core_points().shape
except ValueError:
# TODO make this except more specific to catch monotonic error
except iris.exceptions.MonotonicityError:
# Attempt to slice it by converting to AuxCoord first
new_coord = iris.coords.AuxCoord.from_coord(coord)[coord_keys]
aux_coords.append((new_coord, new_dims))
Expand Down
6 changes: 6 additions & 0 deletions lib/iris/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,9 @@ class CFParseError(IrisError):
"""Raised when a string associated with a CF defined syntax could not be parsed."""

pass


class MonotonicityError(ValueError):
"""Raised when a coordinate values are not monotonic."""

pass
Loading
Loading