diff --git a/Changelog.rst b/Changelog.rst index e112ae4b38..b351b250dc 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -1,3 +1,14 @@ +Version NEXTVERSION +-------------- + +**2026-10-??** + +* Fix problems creating CFA variables with `cf.write` from + aggregation-promoted field ancillaries + (https://github.com/NCAS-CMS/cf-python/issues/971) + +---- + Version 3.21.0 -------------- diff --git a/cf/aggregate.py b/cf/aggregate.py index 149be58d67..f70c22db6a 100644 --- a/cf/aggregate.py +++ b/cf/aggregate.py @@ -3845,41 +3845,13 @@ def _create_hash_and_first_values( # ------------------------------------------------------------ # Field ancillaries + # + # No need to calculate actual hash values for field + # ancillaries (which can be very slow), because they always + # get concatenated regardless of their values. # ------------------------------------------------------------ - if donotchecknonaggregatingaxes: - for anc in m.field_anc.values(): - anc["hash_value"] = (None,) - else: - for anc in m.field_anc.values(): - key = anc["key"] - canonical_units = anc["units"] - - field_anc = constructs[key] - - c_axes = anc["axes"] - canonical_axes = anc["canonical_axes"] - if c_axes != canonical_axes: - # Transpose the field ancillary so that it has the - # canonical axis order - iaxes = [c_axes.index(axis) for axis in canonical_axes] - field_anc = field_anc.transpose(iaxes) - - sort_indices, needs_sorting = _sort_indices(m, canonical_axes) - - # Get the hash of the data array - h = _get_hfl( - field_anc, - canonical_units, - sort_indices, - needs_sorting, - False, - False, - hfl_cache, - rtol, - atol, - ) - - anc["hash_value"] = (h,) + for anc in m.field_anc.values(): + anc["hash_value"] = (None,) # ------------------------------------------------------------ # Domain ancillaries @@ -5048,6 +5020,14 @@ def _fix_promoted_field_ancillaries(output_meta, axes_aggregated): fa.squeeze(squeeze, inplace=True) fa_axes = [a for i, a in enumerate(fa_axes) if i not in squeeze] + # Note that this field ancillary can be written as a + # CF-netCDF aggregation variable. (The previous indexing + # and squeezing will likely have set the aggregation write + # status to False, but we know that promoted field + # ancillaries, which only contain a single broadcast + # value, are safe in this regard.) + fa.data._nc_set_aggregation_write_status(True) + # Record the field ancillary as being able to be written # as a CF-netCDF aggregation 'value' variable fa.data._nc_set_aggregation_fragment_type("value") diff --git a/cf/test/test_CFA.py b/cf/test/test_CFA.py index 324fe38bf8..f2ee8fef34 100644 --- a/cf/test/test_CFA.py +++ b/cf/test/test_CFA.py @@ -181,7 +181,7 @@ def test_CFA_constructs(self): f = cf.read(tmpfile1, cfa_write="all")[0] # No constructs - cf.write(f, tmpfile2, cfa={"constructs": []}) + cf.write(f, tmpfile2, cfa=[]) nc = netCDF4.Dataset(tmpfile2, "r") for var in nc.variables.values(): attrs = var.ncattrs() @@ -311,6 +311,9 @@ def test_CFA_cfa(self): cf.write(f, tmpfile2, cfa="field") g = cf.read(tmpfile2, cfa_write="field")[0] + cf.write(f, tmpfile2, cfa=["field"]) + g = cf.read(tmpfile2, cfa_write="field")[0] + # Default of cfa="auto" - check that aggregation variable # gets written cf.write(g, cfa_file) @@ -345,7 +348,7 @@ def test_CFA_cfa(self): nc.close() # Check bad values of cfa - for cfa in (False, True, (), []): + for cfa in (False, True): with self.assertRaises(ValueError): cf.write(g, cfa_file, cfa=cfa) diff --git a/cf/test/test_aggregate.py b/cf/test/test_aggregate.py index e4684d6f87..27af7a2505 100644 --- a/cf/test/test_aggregate.py +++ b/cf/test/test_aggregate.py @@ -1,6 +1,7 @@ import datetime import faulthandler import os +import tempfile import unittest import warnings @@ -10,6 +11,14 @@ import cf +n_tmpfiles = 1 +tmpfiles = [ + tempfile.mkstemp("_test_aggregate.nc", dir=os.getcwd())[1] + for i in range(n_tmpfiles) +] +[tmpfile] = tmpfiles + + # To facilitate the testing of logging outputs (see test_aggregate_verbosity) log_name = __name__ logger = cf.logging.getLogger(log_name) @@ -345,6 +354,8 @@ def test_aggregate_promote_field_ancillaries(self): self.assertTrue((anc[:2] == "bar_a").all()) self.assertTrue((anc[2:] == "bar_b").all()) + cf.write(c, tmpfile, cfa="field_ancillary") + def test_aggregate_cells(self): """Test the 'cells' keyword of cf.aggregate""" f = cf.example_field(0)