Skip to content

Commit

Permalink
is_discrete_axis
Browse files Browse the repository at this point in the history
  • Loading branch information
davidhassell committed Jun 13, 2024
1 parent c6817b8 commit 0b97250
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 99 deletions.
11 changes: 9 additions & 2 deletions Changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@ version NEXTVERSION

**2024-??-??**

* New method: `cf.Field.is_discrete_axis`
(https://github.com/NCAS-CMS/cf-python/issues/784)
* Include the UM version as a field property when reading UM files
(https://github.com/NCAS-CMS/cf-python/issues/777)
* Fix bug where `cf.example_fields` returned a `list`
of Fields rather than a `Fieldlist`
* Include the UM version as a field property when reading UM files
(https://github.com/NCAS-CMS/cf-python/issues/777)
* Fix bug where `cf.example_fields` returned a `list` of Fields rather
than a `Fieldlist`
(https://github.com/NCAS-CMS/cf-python/issues/725)
* Fix bug where combining UGRID fields erroneously creates an extra
axis and broadcasts over it
(https://github.com/NCAS-CMS/cf-python/issues/784)
* Fix bug where `cf.normalize_slice` doesn't correctly
handle certain cyclic slices
(https://github.com/NCAS-CMS/cf-python/issues/774)
Expand Down
209 changes: 112 additions & 97 deletions cf/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,26 +191,29 @@
"__ge__",
)

#_xxx = namedtuple(
# "data_dimension", ["size", "axis", "keys", "coords", "coord_type", "scalar#"]
#)

@dataclass()
class _data_dimension:
"""TODO
class _Axis_characterisation:
"""Characterise a domain axis.

Used by `_binary_operation` to help with ascertaining if there is
a common axis in two fields.

.. versionaddedd:: NEXTVERSION

"""

# The size of the axis, a positive integer.
size: int = -1
# The domain axis identifier. E.g. 'domainaxis0'
axis: str = ""
keys: tuple = ()
# The coordinate constructs that characterize the axis
coords: tuple = ()
coord_type: tuple = ()
# The identifiers of the coordinate
# constructs. E.g. ('dimensioncoordinate1',)
keys: tuple = ()
# Whether or not the axis is spanned by the field's data array
axis_in_data_axes: bool = True


# _empty_set = set()


class Field(mixin.FieldDomain, mixin.PropertiesData, cfdm.Field):
Expand Down Expand Up @@ -1001,92 +1004,100 @@ def _binary_operation(self, other, method):
data_axes = f.get_data_axes()
for axis in f.domain_axes(todict=True):
identity = None
key = None
coord = None
coord_type = None

key, dim_coord = f.dimension_coordinate(
item=True, default=(None, None), filter_by_axis=(axis,)
)
if dim_coord is not None:
# This axis of the domain has a dimension
# coordinate
identity = dim_coord.identity(strict=True, default=None)
if identity is None:
# Dimension coordinate has no identity, but it
# may have a recognised axis.
for ctype in ("T", "X", "Y", "Z"):
if getattr(dim_coord, ctype, False):
identity = ctype
break

if identity is None and relaxed_identities:
identity = dim_coord.identity(relaxed=True, default=None)

keys = (key,)
coords = (dim_coord,)
if self.is_discrete_axis(axis):
# This is a discrete axis whose identity is
# inferred from all of its auxiliary coordinates
x = {}
for key, aux_coord in f.auxiliary_coordinates(
filter_by_axis=(axis,),
axis_mode="and",
todict=True,
).items():
identity = aux_coord.identity(
strict=True, default=None
)
if identity is None and relaxed_identities:
identity = aux_coord.identity(
relaxed=True, default=None
)
if identity is not None:
x[identity] = key

if x:
# E.g. {2:3, 4:6, 1:7} -> (1, 2, 4), (7, 3, 6)
identity, keys = tuple(zip(*sorted(x.items())))
coords = tuple(
f.auxiliary_coordinate(key) for key in keys
)
else:
identity = None
keys = ()
coords = ()
else:
discrete_axis = f.has_property('featureType') or f.domain_topologies(todict=True)
if discrete_axis:
x = {}
for key, aux_coord in f.auxiliary_coordinates(
filter_by_axis=(axis,),
axis_mode="exact", todict=True
).items():
identity = aux_coord.identity(strict=True, default=None)
if identity is None and relaxed_identities:
identity = aux_coord.identity(
relaxed=True, default=None
)
if identity is not None:
x[identity] = key

if x:
# E.g. {2:3, 4:6, 1:7} -> (1, 2, 4), (7, 3, 6)
identity, keys = tuple(zip(*sorted(x.items())))
coords = tuple(f.auxiliary_coordinate(key)
for key in keys)
else:
identity = None
keys = None
coords = None
else:
key, dim_coord = f.dimension_coordinate(
item=True, default=(None, None), filter_by_axis=(axis,)
)
if dim_coord is not None:
# This non-discrete axis has a dimension
# coordinate
identity = dim_coord.identity(
strict=True, default=None
)
if identity is None:
# Dimension coordinate has no identity,
# but it may have a recognised axis.
for ctype in ("T", "X", "Y", "Z"):
if getattr(dim_coord, ctype, False):
identity = ctype
break

if identity is None and relaxed_identities:
identity = dim_coord.identity(
relaxed=True, default=None
)

keys = (key,)
coords = (dim_coord,)
else:
key, aux_coord = f.auxiliary_coordinate(
item=True,
default=(None, None),
filter_by_axis=(axis,),
axis_mode="exact",
)
if aux_coord is not None:
# This non-discrete axis of the domain
# does not have a dimension coordinate but
# it does have exactly one 1-d auxiliary
# coordinate, so that will do.
# This non-discrete axis does not have a
# dimension coordinate but it does have
# exactly one 1-d auxiliary coordinate, so
# that will do.
coords = (aux_coord,)
identity = aux_coord.identity(strict=True, default=None)
identity = aux_coord.identity(
strict=True, default=None
)
if identity is None and relaxed_identities:
identity = aux_coord.identity(
relaxed=True, default=None
)


if identity is None:
identity = i
else:
coord_type = coords[0].construct_type

out[identity] = _data_dimension(
out[identity] = _Axis_characterisation(
size=f.domain_axis(axis).get_size(),
axis=axis,
keys=keys,
coords=coords,
coord_type=coord_type,
axis_in_data_axes=axis in data_axes,
)

for identity, y in tuple(out1.items()):
missing_axis_inserted = False
if not y.axis_in_data_axes and identity in out0 and isinstance(identity, str):
if (
not y.axis_in_data_axes
and identity in out0
and isinstance(identity, str)
):
a = out0[identity]
if a.size > 1:
# Put missing axis into data axes
Expand All @@ -1098,14 +1109,18 @@ def _binary_operation(self, other, method):

for identity, a in tuple(out0.items()):
missing_axis_inserted = False
if not a.axis_in_data_axes and identity in out1 and isinstance(identity, str):
if (
not a.axis_in_data_axes
and identity in out1
and isinstance(identity, str)
):
y = out1[identity]
if y.size > 1:
# Put missing axis into data axes
field0.insert_dimension(a.axis, position=0, inplace=True)
missing_axis_inserted = True

if not missing_axis_inserted and not a.axis_in_data_axes:
if not missing_axis_inserted and not a.axis_in_data_axes:
del out0[identity]

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

# Dictionary of size > 1 axes from field1 which will replace
# matching size 1 axes in field0. E.g. {'domainaxis1':
# data_dimension(
# size=8,
# axis='domainaxis1',
# key='dimensioncoordinate1',
# coord=<CF DimensionCoordinate: longitude(8) degrees_east>,
# coord_type='dimension_coordinate',
# scalar=False
# )
# matching size 1 axes in field0.
#
# E.g. {'domainaxis1': _Axis_characterisation(
# size=8,
# axis='domainaxis1',
# keys=('dimensioncoordinate1',),
# coords=(CF DimensionCoordinate: longitude(8) degrees_east>,),
# axis_in_data_axes=True)
# }
axes_to_replace_from_field1 = {}

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

for a_key, a_coord, y_key, y_coord in zip(a.keys, a.coords, y.keys, y.coords):
for a_key, a_coord, y_key, y_coord in zip(
a.keys, a.coords, y.keys, y.coords
):
# Ensure matching axis directions
if y_coord.direction() != a_coord.direction():
other.flip(y.axis, inplace=True)

# Check for matching coordinate values
if not y_coord._equivalent_data(a_coord):
raise ValueError(
f"Can't combine size {y.size} {identity!r} axes with "
f"non-matching coordinate values"
)

# Check coord refs
refs1 = field1.get_coordinate_reference(construct=y_key, key=True)
refs0 = field0.get_coordinate_reference(construct=a_key, key=True)

refs1 = field1.get_coordinate_reference(
construct=y_key, key=True
)
refs0 = field0.get_coordinate_reference(
construct=a_key, key=True
)

n_refs = len(refs1)
n0_refs = len(refs0)

if n_refs != n0_refs:
raise ValueError(
f"Can't combine {self.__class__.__name__!r} with "
f"{other.__class__.__name__!r} because the coordinate "
f"references have different lengths: {n_refs} and "
f"{n0_refs}."
)

n_equivalent_refs = 0
for ref1 in refs1:
for ref0 in refs0[:]:
Expand All @@ -1261,12 +1281,12 @@ def _binary_operation(self, other, method):
n_equivalent_refs += 1
refs0.remove(ref0)
break

if n_equivalent_refs != n_refs:
raise ValueError(
f"Can't combine {self.__class__.__name__!r} with "
f"{other.__class__.__name__!r} because the fields have "
"incompatible coordinate references."
f"{other.__class__.__name__!r} because the fields "
"have incompatible coordinate references."
)

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

for cm in self.cell_methods(todict=True).values():

axes = cm.get_axis_identities(None)
if axes is None:
axes = cm.get_axes(None)

for axis in axes:
for axis in cm.get_axes(()):
if axis in axis_map:
continue

Expand Down
Loading

0 comments on commit 0b97250

Please sign in to comment.