Skip to content

Commit 0b97250

Browse files
committed
is_discrete_axis
1 parent c6817b8 commit 0b97250

File tree

6 files changed

+242
-99
lines changed

6 files changed

+242
-99
lines changed

Changelog.rst

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@ version NEXTVERSION
33

44
**2024-??-??**
55

6+
* New method: `cf.Field.is_discrete_axis`
7+
(https://github.com/NCAS-CMS/cf-python/issues/784)
68
* Include the UM version as a field property when reading UM files
79
(https://github.com/NCAS-CMS/cf-python/issues/777)
8-
* Fix bug where `cf.example_fields` returned a `list`
9-
of Fields rather than a `Fieldlist`
10+
* Include the UM version as a field property when reading UM files
11+
(https://github.com/NCAS-CMS/cf-python/issues/777)
12+
* Fix bug where `cf.example_fields` returned a `list` of Fields rather
13+
than a `Fieldlist`
1014
(https://github.com/NCAS-CMS/cf-python/issues/725)
15+
* Fix bug where combining UGRID fields erroneously creates an extra
16+
axis and broadcasts over it
17+
(https://github.com/NCAS-CMS/cf-python/issues/784)
1118
* Fix bug where `cf.normalize_slice` doesn't correctly
1219
handle certain cyclic slices
1320
(https://github.com/NCAS-CMS/cf-python/issues/774)

cf/field.py

Lines changed: 112 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -191,26 +191,29 @@
191191
"__ge__",
192192
)
193193

194-
#_xxx = namedtuple(
195-
# "data_dimension", ["size", "axis", "keys", "coords", "coord_type", "scalar#"]
196-
#)
197194

198195
@dataclass()
199-
class _data_dimension:
200-
"""TODO
196+
class _Axis_characterisation:
197+
"""Characterise a domain axis.
198+
199+
Used by `_binary_operation` to help with ascertaining if there is
200+
a common axis in two fields.
201201

202202
.. versionaddedd:: NEXTVERSION
203203

204204
"""
205+
206+
# The size of the axis, a positive integer.
205207
size: int = -1
208+
# The domain axis identifier. E.g. 'domainaxis0'
206209
axis: str = ""
207-
keys: tuple = ()
210+
# The coordinate constructs that characterize the axis
208211
coords: tuple = ()
209-
coord_type: tuple = ()
212+
# The identifiers of the coordinate
213+
# constructs. E.g. ('dimensioncoordinate1',)
214+
keys: tuple = ()
215+
# Whether or not the axis is spanned by the field's data array
210216
axis_in_data_axes: bool = True
211-
212-
213-
# _empty_set = set()
214217

215218

216219
class Field(mixin.FieldDomain, mixin.PropertiesData, cfdm.Field):
@@ -1001,92 +1004,100 @@ def _binary_operation(self, other, method):
10011004
data_axes = f.get_data_axes()
10021005
for axis in f.domain_axes(todict=True):
10031006
identity = None
1004-
key = None
1005-
coord = None
1006-
coord_type = None
10071007

1008-
key, dim_coord = f.dimension_coordinate(
1009-
item=True, default=(None, None), filter_by_axis=(axis,)
1010-
)
1011-
if dim_coord is not None:
1012-
# This axis of the domain has a dimension
1013-
# coordinate
1014-
identity = dim_coord.identity(strict=True, default=None)
1015-
if identity is None:
1016-
# Dimension coordinate has no identity, but it
1017-
# may have a recognised axis.
1018-
for ctype in ("T", "X", "Y", "Z"):
1019-
if getattr(dim_coord, ctype, False):
1020-
identity = ctype
1021-
break
1022-
1023-
if identity is None and relaxed_identities:
1024-
identity = dim_coord.identity(relaxed=True, default=None)
1025-
1026-
keys = (key,)
1027-
coords = (dim_coord,)
1008+
if self.is_discrete_axis(axis):
1009+
# This is a discrete axis whose identity is
1010+
# inferred from all of its auxiliary coordinates
1011+
x = {}
1012+
for key, aux_coord in f.auxiliary_coordinates(
1013+
filter_by_axis=(axis,),
1014+
axis_mode="and",
1015+
todict=True,
1016+
).items():
1017+
identity = aux_coord.identity(
1018+
strict=True, default=None
1019+
)
1020+
if identity is None and relaxed_identities:
1021+
identity = aux_coord.identity(
1022+
relaxed=True, default=None
1023+
)
1024+
if identity is not None:
1025+
x[identity] = key
1026+
1027+
if x:
1028+
# E.g. {2:3, 4:6, 1:7} -> (1, 2, 4), (7, 3, 6)
1029+
identity, keys = tuple(zip(*sorted(x.items())))
1030+
coords = tuple(
1031+
f.auxiliary_coordinate(key) for key in keys
1032+
)
1033+
else:
1034+
identity = None
1035+
keys = ()
1036+
coords = ()
10281037
else:
1029-
discrete_axis = f.has_property('featureType') or f.domain_topologies(todict=True)
1030-
if discrete_axis:
1031-
x = {}
1032-
for key, aux_coord in f.auxiliary_coordinates(
1033-
filter_by_axis=(axis,),
1034-
axis_mode="exact", todict=True
1035-
).items():
1036-
identity = aux_coord.identity(strict=True, default=None)
1037-
if identity is None and relaxed_identities:
1038-
identity = aux_coord.identity(
1039-
relaxed=True, default=None
1040-
)
1041-
if identity is not None:
1042-
x[identity] = key
1043-
1044-
if x:
1045-
# E.g. {2:3, 4:6, 1:7} -> (1, 2, 4), (7, 3, 6)
1046-
identity, keys = tuple(zip(*sorted(x.items())))
1047-
coords = tuple(f.auxiliary_coordinate(key)
1048-
for key in keys)
1049-
else:
1050-
identity = None
1051-
keys = None
1052-
coords = None
1053-
else:
1038+
key, dim_coord = f.dimension_coordinate(
1039+
item=True, default=(None, None), filter_by_axis=(axis,)
1040+
)
1041+
if dim_coord is not None:
1042+
# This non-discrete axis has a dimension
1043+
# coordinate
1044+
identity = dim_coord.identity(
1045+
strict=True, default=None
1046+
)
1047+
if identity is None:
1048+
# Dimension coordinate has no identity,
1049+
# but it may have a recognised axis.
1050+
for ctype in ("T", "X", "Y", "Z"):
1051+
if getattr(dim_coord, ctype, False):
1052+
identity = ctype
1053+
break
1054+
1055+
if identity is None and relaxed_identities:
1056+
identity = dim_coord.identity(
1057+
relaxed=True, default=None
1058+
)
1059+
1060+
keys = (key,)
1061+
coords = (dim_coord,)
1062+
else:
10541063
key, aux_coord = f.auxiliary_coordinate(
10551064
item=True,
10561065
default=(None, None),
10571066
filter_by_axis=(axis,),
10581067
axis_mode="exact",
10591068
)
10601069
if aux_coord is not None:
1061-
# This non-discrete axis of the domain
1062-
# does not have a dimension coordinate but
1063-
# it does have exactly one 1-d auxiliary
1064-
# coordinate, so that will do.
1070+
# This non-discrete axis does not have a
1071+
# dimension coordinate but it does have
1072+
# exactly one 1-d auxiliary coordinate, so
1073+
# that will do.
10651074
coords = (aux_coord,)
1066-
identity = aux_coord.identity(strict=True, default=None)
1075+
identity = aux_coord.identity(
1076+
strict=True, default=None
1077+
)
10671078
if identity is None and relaxed_identities:
10681079
identity = aux_coord.identity(
10691080
relaxed=True, default=None
10701081
)
1071-
10721082

10731083
if identity is None:
10741084
identity = i
1075-
else:
1076-
coord_type = coords[0].construct_type
10771085

1078-
out[identity] = _data_dimension(
1086+
out[identity] = _Axis_characterisation(
10791087
size=f.domain_axis(axis).get_size(),
10801088
axis=axis,
10811089
keys=keys,
10821090
coords=coords,
1083-
coord_type=coord_type,
10841091
axis_in_data_axes=axis in data_axes,
10851092
)
10861093

10871094
for identity, y in tuple(out1.items()):
10881095
missing_axis_inserted = False
1089-
if not y.axis_in_data_axes and identity in out0 and isinstance(identity, str):
1096+
if (
1097+
not y.axis_in_data_axes
1098+
and identity in out0
1099+
and isinstance(identity, str)
1100+
):
10901101
a = out0[identity]
10911102
if a.size > 1:
10921103
# Put missing axis into data axes
@@ -1098,14 +1109,18 @@ def _binary_operation(self, other, method):
10981109

10991110
for identity, a in tuple(out0.items()):
11001111
missing_axis_inserted = False
1101-
if not a.axis_in_data_axes and identity in out1 and isinstance(identity, str):
1112+
if (
1113+
not a.axis_in_data_axes
1114+
and identity in out1
1115+
and isinstance(identity, str)
1116+
):
11021117
y = out1[identity]
11031118
if y.size > 1:
11041119
# Put missing axis into data axes
11051120
field0.insert_dimension(a.axis, position=0, inplace=True)
11061121
missing_axis_inserted = True
11071122

1108-
if not missing_axis_inserted and not a.axis_in_data_axes:
1123+
if not missing_axis_inserted and not a.axis_in_data_axes:
11091124
del out0[identity]
11101125

11111126
squeeze1 = []
@@ -1116,15 +1131,14 @@ def _binary_operation(self, other, method):
11161131
axes_added_from_field1 = []
11171132

11181133
# Dictionary of size > 1 axes from field1 which will replace
1119-
# matching size 1 axes in field0. E.g. {'domainaxis1':
1120-
# data_dimension(
1121-
# size=8,
1122-
# axis='domainaxis1',
1123-
# key='dimensioncoordinate1',
1124-
# coord=<CF DimensionCoordinate: longitude(8) degrees_east>,
1125-
# coord_type='dimension_coordinate',
1126-
# scalar=False
1127-
# )
1134+
# matching size 1 axes in field0.
1135+
#
1136+
# E.g. {'domainaxis1': _Axis_characterisation(
1137+
# size=8,
1138+
# axis='domainaxis1',
1139+
# keys=('dimensioncoordinate1',),
1140+
# coords=(CF DimensionCoordinate: longitude(8) degrees_east>,),
1141+
# axis_in_data_axes=True)
11281142
# }
11291143
axes_to_replace_from_field1 = {}
11301144

@@ -1225,33 +1239,39 @@ def _binary_operation(self, other, method):
12251239
f"{a.size} {identity!r} axis"
12261240
)
12271241

1228-
for a_key, a_coord, y_key, y_coord in zip(a.keys, a.coords, y.keys, y.coords):
1242+
for a_key, a_coord, y_key, y_coord in zip(
1243+
a.keys, a.coords, y.keys, y.coords
1244+
):
12291245
# Ensure matching axis directions
12301246
if y_coord.direction() != a_coord.direction():
12311247
other.flip(y.axis, inplace=True)
1232-
1248+
12331249
# Check for matching coordinate values
12341250
if not y_coord._equivalent_data(a_coord):
12351251
raise ValueError(
12361252
f"Can't combine size {y.size} {identity!r} axes with "
12371253
f"non-matching coordinate values"
12381254
)
1239-
1255+
12401256
# Check coord refs
1241-
refs1 = field1.get_coordinate_reference(construct=y_key, key=True)
1242-
refs0 = field0.get_coordinate_reference(construct=a_key, key=True)
1243-
1257+
refs1 = field1.get_coordinate_reference(
1258+
construct=y_key, key=True
1259+
)
1260+
refs0 = field0.get_coordinate_reference(
1261+
construct=a_key, key=True
1262+
)
1263+
12441264
n_refs = len(refs1)
12451265
n0_refs = len(refs0)
1246-
1266+
12471267
if n_refs != n0_refs:
12481268
raise ValueError(
12491269
f"Can't combine {self.__class__.__name__!r} with "
12501270
f"{other.__class__.__name__!r} because the coordinate "
12511271
f"references have different lengths: {n_refs} and "
12521272
f"{n0_refs}."
12531273
)
1254-
1274+
12551275
n_equivalent_refs = 0
12561276
for ref1 in refs1:
12571277
for ref0 in refs0[:]:
@@ -1261,12 +1281,12 @@ def _binary_operation(self, other, method):
12611281
n_equivalent_refs += 1
12621282
refs0.remove(ref0)
12631283
break
1264-
1284+
12651285
if n_equivalent_refs != n_refs:
12661286
raise ValueError(
12671287
f"Can't combine {self.__class__.__name__!r} with "
1268-
f"{other.__class__.__name__!r} because the fields have "
1269-
"incompatible coordinate references."
1288+
f"{other.__class__.__name__!r} because the fields "
1289+
"have incompatible coordinate references."
12701290
)
12711291

12721292
# Change the domain axis sizes in field0 so that they match
@@ -1473,12 +1493,7 @@ def _conform_cell_methods(self):
14731493
axis_map = {}
14741494

14751495
for cm in self.cell_methods(todict=True).values():
1476-
1477-
axes = cm.get_axis_identities(None)
1478-
if axes is None:
1479-
axes = cm.get_axes(None)
1480-
1481-
for axis in axes:
1496+
for axis in cm.get_axes(()):
14821497
if axis in axis_map:
14831498
continue
14841499

0 commit comments

Comments
 (0)