From 5f865b4d9e1490513c882953cddc5d20fa3dbaf6 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Fri, 30 May 2025 15:49:06 +0100 Subject: [PATCH 01/24] dev --- cfdm/mixin/fielddomain.py | 69 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/cfdm/mixin/fielddomain.py b/cfdm/mixin/fielddomain.py index 7e7316553..b110f768c 100644 --- a/cfdm/mixin/fielddomain.py +++ b/cfdm/mixin/fielddomain.py @@ -1052,6 +1052,75 @@ def coordinate_references(self, *identities, **filter_kwargs): **filter_kwargs, ) + def create_1d_coordinates(self): + """TODOHEAL + + .. versionadded:: NEXTVERSION + + """ + crs = self.coordinate_reference('grid_mapping_name:healpix', + default=None) + if crs is None: + return + + key, healpix_indices = self.coordinate(???, item=True, default=(None, None)) + if key is None: + return + + axis = self.get_domain_axes(key)[0] + dx = healpix_indices.to_dask_array( + _force_mask_hardness=False, _force_to_memory=False + ) + + nside = 2**crs.coordinate_conversion.get_property('refinement_level') + order = crs.coordinate_conversion.get_property('healpix_index_method') + hp = astropy_healpix.HEALPix(nside=nside, order=order) + + def lon_coordinates(a): + return hp.healpix_to_lonlat(a)[0].degree + + def lat_coordinates(a): + return hp.healpix_to_lonlat(a)[1].degree + + def lon_bounds(a): + return hp.boundaries_lonlat(a)[0].degree + + def lat_bounds(a): + return hp.boundaries_lonlat(a)[1].degree + + meta = np.array((), dtype=np.dtype('float64')) + + # Latitude coordinates + dx = dx.map_blocks(lat_coordinates, meta=meta) + lat = self._AuxiliaryCoordinate( + data=self._Data(dx, 'degrees_north'), + properties={'standard_name': 'latitude'} + copy=False + ) + + # Longitude coordinates + dx = dx.map_blocks(lon_coordinates, meta=meta) + lon = self._AuxiliaryCoordinate( + data=self._Data(dx, 'degrees_east'),, + properties={'standard_name': 'longitude'} + copy=False + ) + + # Latitude bounds + dx = da.blockwise(lat_bounds, "ij", dx, "i", new_axes={'j': 4}, + meta=meta) + bounds = self._Bounds(data=dx) + lat.set_bounds(bounds) + + # Longitude bounds + dx = da.blockwise(lon_bounds, "ij", dx, "i", new_axes={'j': 4}, + meta=meta) + bounds = self._Bounds(data=dx) + lon.set_bounds(bounds) + + self.set_construct(lat, axes=axis, copy=False) + self.set_construct(lon, axes=axis, copy=False) + def del_construct(self, *identity, default=ValueError(), **filter_kwargs): """Remove a metadata construct. From b1411bfae6927a2170a79227d9ce89f533548355 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Mon, 9 Jun 2025 18:45:30 +0100 Subject: [PATCH 02/24] dev --- cfdm/mixin/fielddomain.py | 167 ++++++++++++++++++++++++++++---------- 1 file changed, 123 insertions(+), 44 deletions(-) diff --git a/cfdm/mixin/fielddomain.py b/cfdm/mixin/fielddomain.py index 643af1e6f..5f05319a5 100644 --- a/cfdm/mixin/fielddomain.py +++ b/cfdm/mixin/fielddomain.py @@ -1,7 +1,14 @@ import logging import re -from ..decorators import _manage_log_level_via_verbosity +import dask.array as da +import numpy as np + +from ..decorators import ( + _inplace_enabled, + _inplace_enabled_define_and_cleanup, + _manage_log_level_via_verbosity, +) logger = logging.getLogger(__name__) @@ -1085,75 +1092,147 @@ def coordinate_references(self, *identities, **filter_kwargs): **filter_kwargs, ) - def create_1d_coordinates(self): - """TODOHEAL + @_inplace_enabled(default=False) + def create_1d_coordinates(self, persist=False, key=False, inplace=False): + """TODOHEALPIX. .. versionadded:: NEXTVERSION + :Parameters: + + persist: `bool`, optional + TODOHEALPIX + + key: `bool` + TODOHEALPIX If True, return alongside the field + construct the key identifying the auxiliary coordinate + of the field with the newly-computed vertical + coordinates, as a 2-tuple of field and then key. If + False, the default, then only return the field + construct. + + If no coordinates were computed, `None` will be + returned in the key (second) position of the 2-tuple. + + {{inplace: `bool`, optional}} + + :Returns: + + `{{class}}` or `None` + TODOHEALPIX + """ - crs = self.coordinate_reference('grid_mapping_name:healpix', - default=None) + f = _inplace_enabled_define_and_cleanup(self) + + keys = () + + crs = f.coordinate_reference("grid_mapping_name:healpix", default=None) if crs is None: - return + if key: + return f, keys - key, healpix_indices = self.coordinate(???, item=True, default=(None, None)) - if key is None: - return - - axis = self.get_domain_axes(key)[0] - dx = healpix_indices.to_dask_array( + return f + + # ------------------------------------------------------------ + # Get the HEALPix indices as a Dask array + # ------------------------------------------------------------ + c_key, healpix_index = f.coordinate( + "healpix_index", item=True, default=(None, None) + ) + if c_key is None: + raise ValueError("TODOHEALPIX") + + dx = healpix_index.to_dask_array( _force_mask_hardness=False, _force_to_memory=False ) - nside = 2**crs.coordinate_conversion.get_property('refinement_level') - order = crs.coordinate_conversion.get_property('healpix_index_method') - hp = astropy_healpix.HEALPix(nside=nside, order=order) + # ------------------------------------------------------------ + # Define functions to create latitudes and longitudes from + # HEALPix indices + # ------------------------------------------------------------ + refinement_level = crs.coordinate_conversion.get_property( + "refinement_level", None + ) + if refinement_level is None: + raise ValueError("TODOHEALPIX") + + healpix_index_method = crs.coordinate_conversion.get_property( + "healpix_index_method", None + ) + if healpix_index_method is None: + raise ValueError("TODOHEALPIX") + + if healpix_index_method == "nuniq": + raise ValueError("TODOHEALPIX") + + from astropy_healpix import HEALPix - def lon_coordinates(a): + hp = HEALPix(nside=2**refinement_level, order=healpix_index_method) + + def HEALPix_lon_coordinates(a): return hp.healpix_to_lonlat(a)[0].degree - - def lat_coordinates(a): + + def HEALPix_lat_coordinates(a): return hp.healpix_to_lonlat(a)[1].degree - - def lon_bounds(a): + + def HEALPix_lon_bounds(a): return hp.boundaries_lonlat(a)[0].degree - - def lat_bounds(a): + + def HEALPix_lat_bounds(a): return hp.boundaries_lonlat(a)[1].degree - - meta = np.array((), dtype=np.dtype('float64')) + + # ------------------------------------------------------------ + # Create new latitude and longitude coordinates with bounds + # ------------------------------------------------------------ + meta = np.array((), dtype=np.dtype("float64")) # Latitude coordinates - dx = dx.map_blocks(lat_coordinates, meta=meta) - lat = self._AuxiliaryCoordinate( - data=self._Data(dx, 'degrees_north'), - properties={'standard_name': 'latitude'} - copy=False + dx = dx.map_blocks(HEALPix_lat_coordinates, meta=meta) + lat = f._AuxiliaryCoordinate( + data=f._Data(dx, "degrees_north", copy=False), + properties={"standard_name": "latitude"}, + copy=False, ) # Longitude coordinates - dx = dx.map_blocks(lon_coordinates, meta=meta) - lon = self._AuxiliaryCoordinate( - data=self._Data(dx, 'degrees_east'),, - properties={'standard_name': 'longitude'} - copy=False + dx = dx.map_blocks(HEALPix_lon_coordinates, meta=meta) + lon = f._AuxiliaryCoordinate( + data=f._Data(dx, "degrees_east", copy=False), + properties={"standard_name": "longitude"}, + copy=False, ) # Latitude bounds - dx = da.blockwise(lat_bounds, "ij", dx, "i", new_axes={'j': 4}, - meta=meta) - bounds = self._Bounds(data=dx) + dx = da.blockwise( + HEALPix_lat_bounds, "ij", dx, "i", new_axes={"j": 4}, meta=meta + ) + bounds = f._Bounds(data=dx) lat.set_bounds(bounds) - + # Longitude bounds - dx = da.blockwise(lon_bounds, "ij", dx, "i", new_axes={'j': 4}, - meta=meta) - bounds = self._Bounds(data=dx) + dx = da.blockwise( + HEALPix_lon_bounds, "ij", dx, "i", new_axes={"j": 4}, meta=meta + ) + bounds = f._Bounds(data=dx) lon.set_bounds(bounds) - self.set_construct(lat, axes=axis, copy=False) - self.set_construct(lon, axes=axis, copy=False) - + if persist: + lat = lat.persist(inplace=True) + lon = lon.persist(inplace=True) + + # ------------------------------------------------------------ + # Set the new latitude and longitude coordinates + # ------------------------------------------------------------ + axis = f.get_domain_axes(c_key)[0] + lat_key = f.set_construct(lat, axes=axis, copy=False) + lon_key = f.set_construct(lon, axes=axis, copy=False) + keys = (lat_key, lon_key) + + if key: + return f, keys + + return f + def del_construct(self, *identity, default=ValueError(), **filter_kwargs): """Remove a metadata construct. From 491cb8f583acbd1f76933b627cd1602f3bb88e6f Mon Sep 17 00:00:00 2001 From: David Hassell Date: Wed, 18 Jun 2025 14:38:13 +0100 Subject: [PATCH 03/24] dev --- cfdm/examplefield.py | 92 +++++++++++- cfdm/mixin/fielddomain.py | 296 +++++++++++++++++++------------------- 2 files changed, 240 insertions(+), 148 deletions(-) diff --git a/cfdm/examplefield.py b/cfdm/examplefield.py index ffcf7f26d..9f06cc182 100644 --- a/cfdm/examplefield.py +++ b/cfdm/examplefield.py @@ -1,10 +1,12 @@ +import numpy as np + from .cfdmimplementation import implementation from .functions import CF _implementation = implementation() # The number of example fields -_n_example_fields = 12 +_n_example_fields = 13 def example_field(n, _implementation=_implementation): @@ -244,7 +246,9 @@ def example_field(n, _implementation=_implementation): CellConnectivity = _implementation.get_class("CellConnectivity") CellMeasure = _implementation.get_class("CellMeasure") CellMethod = _implementation.get_class("CellMethod") + CoordinateConversion = _implementation.get_class("CoordinateConversion") CoordinateReference = _implementation.get_class("CoordinateReference") + Datum = _implementation.get_class("Datum") DimensionCoordinate = _implementation.get_class("DimensionCoordinate") DomainAncillary = _implementation.get_class("DomainAncillary") DomainAxis = _implementation.get_class("DomainAxis") @@ -259,6 +263,8 @@ def example_field(n, _implementation=_implementation): mesh_id = "f51e5aa5e2b0439f9fae4f04e51556f7" + field = None + if n == 0: f = Field() @@ -5290,12 +5296,96 @@ def example_field(n, _implementation=_implementation): # # field data axes f.set_data_axes(("domainaxis0", "domainaxis1")) + elif n == 12: + # field: air_temperature + field = Field() + field.set_properties({'Conventions': 'CF-1.12', 'standard_name': 'air_temperature', 'units': 'K', 'units_metadata': 'temperature: on_scale'}) + field.nc_set_variable('tas') + data = Data([[291.5, 293.5, 285.3, 286.3, 286.2, 289.6, 285.6, 285.5, 287.1, 285.5, 291.5, 285.2, 285.0, 291.1, 287.9, 290.9, 288.6, 291.6, 289.6, 289.0, 294.0, 293.1, 291.5, 288.3, 289.6, 285.3, 285.4, 286.9, 294.3, 289.0, 294.5, 286.3, 288.3, 287.2, 285.7, 291.7, 290.1, 291.1, 286.8, 286.0, 291.1, 292.2, 285.7, 288.8, 285.8, 290.8, 287.0, 290.0], [294.0, 287.8, 294.6, 289.9, 289.2, 293.0, 286.8, 287.8, 285.2, 294.0, 288.1, 293.5, 289.6, 292.5, 290.0, 290.5, 292.9, 290.7, 293.6, 288.3, 293.5, 294.0, 288.7, 292.0, 292.9, 289.5, 286.8, 288.3, 292.6, 290.3, 290.8, 290.4, 287.7, 289.9, 288.4, 294.7, 291.4, 294.7, 287.6, 286.5, 291.4, 293.0, 288.8, 288.8, 292.3, 293.7, 290.1, 285.9]], units='K', dtype='f8') + field.set_data(data) + # + # domain_axis: ncdim%time + c = DomainAxis() + c.set_size(2) + c.nc_set_dimension('time') + field.set_construct(c, key='domainaxis0', copy=False) + # + # domain_axis: ncdim%cell + c = DomainAxis() + c.set_size(48) + c.nc_set_dimension('cell') + field.set_construct(c, key='domainaxis1', copy=False) + # + # domain_axis: ncdim%height + c = DomainAxis() + c.set_size(1) + c.nc_set_dimension('height') + field.set_construct(c, key='domainaxis2', copy=False) + # + # auxiliary_coordinate: healpix_index + c = AuxiliaryCoordinate() + c.set_properties({'standard_name': 'healpix_index', 'units': '1'}) + c.nc_set_variable('altitude') + data = Data([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47], units='1', dtype='i8') + c.set_data(data) + field.set_construct(c, axes=('domainaxis1',), key='auxiliarycoordinate0', copy=False) + # + # dimension_coordinate: time + c = DimensionCoordinate() + c.set_properties({'standard_name': 'time', 'calendar': 'proleptic_gregorian', 'units': 'days since 2025-06-01'}) + c.nc_set_variable('time') + data = Data([15.0, 45.5], units='days since 2025-06-01', calendar='proleptic_gregorian', dtype='f4') + c.set_data(data) + b = Bounds() + b.nc_set_variable('time_bounds') + data = Data([[0.0, 30.0], [30.0, 61.0]], units='days since 2025-06-01', calendar='proleptic_gregorian', dtype='f4') + b.set_data(data) + c.set_bounds(b) + field.set_construct(c, axes=('domainaxis0',), key='dimensioncoordinate0', copy=False) + # + # dimension_coordinate: height + c = DimensionCoordinate() + c.set_properties({'standard_name': 'height', 'units': 'm'}) + c.nc_set_variable('height') + data = Data([1.5], units='m', dtype='f4') + c.set_data(data) + field.set_construct(c, axes=('domainaxis2',), key='dimensioncoordinate1', copy=False) + # + # coordinate_reference: grid_mapping_name:healpix + c = CoordinateReference() + c.nc_set_variable('healpix') + c.set_coordinates({'auxiliarycoordinate0'}) + d = Datum() + d.set_parameters({'earth_radius': 6371000}) + c.set_datum(d) + f = CoordinateConversion() + f.set_parameters({'healpix_order': 'nested', 'refinement_level': 1, 'grid_mapping_name': 'healpix'}) + c.set_coordinate_conversion(f) + field.set_construct(c) + # + # cell_method: mean + c = CellMethod() + c.set_method('mean') + c.set_axes(('time',)) + field.set_construct(c) + # + # cell_method: mean + c = CellMethod() + c.set_method('mean') + c.set_axes(('area',)) + field.set_construct(c) + # + # field data axes + field.set_data_axes(('domainaxis0', 'domainaxis1')) else: raise ValueError( "Must select an example construct with an integer argument " f"between 0 and {_n_example_fields - 1} inclusive. Got {n!r}" ) + if field is not None: + f = field + return f diff --git a/cfdm/mixin/fielddomain.py b/cfdm/mixin/fielddomain.py index 5f05319a5..b609cf5a9 100644 --- a/cfdm/mixin/fielddomain.py +++ b/cfdm/mixin/fielddomain.py @@ -1,14 +1,16 @@ import logging import re -import dask.array as da -import numpy as np +from ..decorators import _manage_log_level_via_verbosity -from ..decorators import ( - _inplace_enabled, - _inplace_enabled_define_and_cleanup, - _manage_log_level_via_verbosity, -) +#import dask.array as da +#import numpy as np +# +#from ..decorators import ( +# _inplace_enabled, +# _inplace_enabled_define_and_cleanup, +# _manage_log_level_via_verbosity, +#) logger = logging.getLogger(__name__) @@ -1092,146 +1094,146 @@ def coordinate_references(self, *identities, **filter_kwargs): **filter_kwargs, ) - @_inplace_enabled(default=False) - def create_1d_coordinates(self, persist=False, key=False, inplace=False): - """TODOHEALPIX. - - .. versionadded:: NEXTVERSION - - :Parameters: - - persist: `bool`, optional - TODOHEALPIX - - key: `bool` - TODOHEALPIX If True, return alongside the field - construct the key identifying the auxiliary coordinate - of the field with the newly-computed vertical - coordinates, as a 2-tuple of field and then key. If - False, the default, then only return the field - construct. - - If no coordinates were computed, `None` will be - returned in the key (second) position of the 2-tuple. - - {{inplace: `bool`, optional}} - - :Returns: - - `{{class}}` or `None` - TODOHEALPIX - - """ - f = _inplace_enabled_define_and_cleanup(self) - - keys = () - - crs = f.coordinate_reference("grid_mapping_name:healpix", default=None) - if crs is None: - if key: - return f, keys - - return f - - # ------------------------------------------------------------ - # Get the HEALPix indices as a Dask array - # ------------------------------------------------------------ - c_key, healpix_index = f.coordinate( - "healpix_index", item=True, default=(None, None) - ) - if c_key is None: - raise ValueError("TODOHEALPIX") - - dx = healpix_index.to_dask_array( - _force_mask_hardness=False, _force_to_memory=False - ) - - # ------------------------------------------------------------ - # Define functions to create latitudes and longitudes from - # HEALPix indices - # ------------------------------------------------------------ - refinement_level = crs.coordinate_conversion.get_property( - "refinement_level", None - ) - if refinement_level is None: - raise ValueError("TODOHEALPIX") - - healpix_index_method = crs.coordinate_conversion.get_property( - "healpix_index_method", None - ) - if healpix_index_method is None: - raise ValueError("TODOHEALPIX") - - if healpix_index_method == "nuniq": - raise ValueError("TODOHEALPIX") - - from astropy_healpix import HEALPix - - hp = HEALPix(nside=2**refinement_level, order=healpix_index_method) - - def HEALPix_lon_coordinates(a): - return hp.healpix_to_lonlat(a)[0].degree - - def HEALPix_lat_coordinates(a): - return hp.healpix_to_lonlat(a)[1].degree - - def HEALPix_lon_bounds(a): - return hp.boundaries_lonlat(a)[0].degree - - def HEALPix_lat_bounds(a): - return hp.boundaries_lonlat(a)[1].degree - - # ------------------------------------------------------------ - # Create new latitude and longitude coordinates with bounds - # ------------------------------------------------------------ - meta = np.array((), dtype=np.dtype("float64")) - - # Latitude coordinates - dx = dx.map_blocks(HEALPix_lat_coordinates, meta=meta) - lat = f._AuxiliaryCoordinate( - data=f._Data(dx, "degrees_north", copy=False), - properties={"standard_name": "latitude"}, - copy=False, - ) - - # Longitude coordinates - dx = dx.map_blocks(HEALPix_lon_coordinates, meta=meta) - lon = f._AuxiliaryCoordinate( - data=f._Data(dx, "degrees_east", copy=False), - properties={"standard_name": "longitude"}, - copy=False, - ) - - # Latitude bounds - dx = da.blockwise( - HEALPix_lat_bounds, "ij", dx, "i", new_axes={"j": 4}, meta=meta - ) - bounds = f._Bounds(data=dx) - lat.set_bounds(bounds) - - # Longitude bounds - dx = da.blockwise( - HEALPix_lon_bounds, "ij", dx, "i", new_axes={"j": 4}, meta=meta - ) - bounds = f._Bounds(data=dx) - lon.set_bounds(bounds) - - if persist: - lat = lat.persist(inplace=True) - lon = lon.persist(inplace=True) - - # ------------------------------------------------------------ - # Set the new latitude and longitude coordinates - # ------------------------------------------------------------ - axis = f.get_domain_axes(c_key)[0] - lat_key = f.set_construct(lat, axes=axis, copy=False) - lon_key = f.set_construct(lon, axes=axis, copy=False) - keys = (lat_key, lon_key) - - if key: - return f, keys - - return f +# @_inplace_enabled(default=False) +# def create_1d_coordinates(self, persist=False, key=False, inplace=False): +# """TODOHEALPIX. +# +# .. versionadded:: NEXTVERSION +# +# :Parameters: +# +# persist: `bool`, optional +# TODOHEALPIX +# +# key: `bool` +# TODOHEALPIX If True, return alongside the field +# construct the key identifying the auxiliary coordinate +# of the field with the newly-computed vertical +# coordinates, as a 2-tuple of field and then key. If +# False, the default, then only return the field +# construct. +# +# If no coordinates were computed, `None` will be +# returned in the key (second) position of the 2-tuple. +# +# {{inplace: `bool`, optional}} +# +# :Returns: +# +# `{{class}}` or `None` +# TODOHEALPIX +# +# """ +# f = _inplace_enabled_define_and_cleanup(self) +# +# keys = () +# +# crs = f.coordinate_reference("grid_mapping_name:healpix", default=None) +# if crs is None: +# if key: +# return f, keys +# +# return f +# +# # ------------------------------------------------------------ +# # Get the HEALPix indices as a Dask array +# # ------------------------------------------------------------ +# c_key, healpix_index = f.coordinate( +# "healpix_index", item=True, default=(None, None) +# ) +# if c_key is None: +# raise ValueError("TODOHEALPIX") +# +# dx = healpix_index.to_dask_array( +# _force_mask_hardness=False, _force_to_memory=False +# ) +# +# # ------------------------------------------------------------ +# # Define functions to create latitudes and longitudes from +# # HEALPix indices +# # ------------------------------------------------------------ +# refinement_level = crs.coordinate_conversion.get_property( +# "refinement_level", None +# ) +# if refinement_level is None: +# raise ValueError("TODOHEALPIX") +# +# healpix_index_method = crs.coordinate_conversion.get_property( +# "healpix_index_method", None +# ) +# if healpix_index_method is None: +# raise ValueError("TODOHEALPIX") +# +# if healpix_index_method == "nuniq": +# raise ValueError("TODOHEALPIX") +# +# from astropy_healpix import HEALPix +# +# hp = HEALPix(nside=2**refinement_level, order=healpix_index_method) +# +# def HEALPix_lon_coordinates(a): +# return hp.healpix_to_lonlat(a)[0].degree +# +# def HEALPix_lat_coordinates(a): +# return hp.healpix_to_lonlat(a)[1].degree +# +# def HEALPix_lon_bounds(a): +# return hp.boundaries_lonlat(a)[0].degree +# +# def HEALPix_lat_bounds(a): +# return hp.boundaries_lonlat(a)[1].degree +# +# # ------------------------------------------------------------ +# # Create new latitude and longitude coordinates with bounds +# # ------------------------------------------------------------ +# meta = np.array((), dtype=np.dtype("float64")) +# +# # Latitude coordinates +# dx = dx.map_blocks(HEALPix_lat_coordinates, meta=meta) +# lat = f._AuxiliaryCoordinate( +# data=f._Data(dx, "degrees_north", copy=False), +# properties={"standard_name": "latitude"}, +# copy=False, +# ) +# +# # Longitude coordinates +# dx = dx.map_blocks(HEALPix_lon_coordinates, meta=meta) +# lon = f._AuxiliaryCoordinate( +# data=f._Data(dx, "degrees_east", copy=False), +# properties={"standard_name": "longitude"}, +# copy=False, +# ) +# +# # Latitude bounds +# dx = da.blockwise( +# HEALPix_lat_bounds, "ij", dx, "i", new_axes={"j": 4}, meta=meta +# ) +# bounds = f._Bounds(data=dx) +# lat.set_bounds(bounds) +# +# # Longitude bounds +# dx = da.blockwise( +# HEALPix_lon_bounds, "ij", dx, "i", new_axes={"j": 4}, meta=meta +# ) +# bounds = f._Bounds(data=dx) +# lon.set_bounds(bounds) +# +# if persist: +# lat = lat.persist(inplace=True) +# lon = lon.persist(inplace=True) +# +# # ------------------------------------------------------------ +# # Set the new latitude and longitude coordinates +# # ------------------------------------------------------------ +# axis = f.get_domain_axes(c_key)[0] +# lat_key = f.set_construct(lat, axes=axis, copy=False) +# lon_key = f.set_construct(lon, axes=axis, copy=False) +# keys = (lat_key, lon_key) +# +# if key: +# return f, keys +# +# return f def del_construct(self, *identity, default=ValueError(), **filter_kwargs): """Remove a metadata construct. From b91764575ddd5b195111bb5c60c5ebe59c103ec6 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Wed, 18 Jun 2025 22:37:11 +0100 Subject: [PATCH 04/24] dev --- cfdm/examplefield.py | 503 +++++++++++++++++++++++++-- cfdm/mixin/fielddomain.py | 288 +++++++-------- cfdm/read_write/netcdf/netcdfread.py | 2 + 3 files changed, 613 insertions(+), 180 deletions(-) diff --git a/cfdm/examplefield.py b/cfdm/examplefield.py index 9f06cc182..337090a09 100644 --- a/cfdm/examplefield.py +++ b/cfdm/examplefield.py @@ -6,7 +6,7 @@ _implementation = implementation() # The number of example fields -_n_example_fields = 13 +_n_example_fields = 14 def example_field(n, _implementation=_implementation): @@ -59,6 +59,10 @@ def example_field(n, _implementation=_implementation): ``11`` Discrete sampling geometry (DSG) "trajectory" features. + + ``12`` TODOHEALPIX + + ``13`` TODOHEALPIX ====== ================================================== See the examples for details. @@ -230,6 +234,8 @@ def example_field(n, _implementation=_implementation): : longitude(cf_role=trajectory_id(1), ncdim%trajectory(4)) = [[0.0, ..., 0.31]] degree_east : cf_role=trajectory_id(cf_role=trajectory_id(1)) = [flight1] + TODOHEALPIX + """ # For safety given the private second argument which we might not # document, otherwise a user gets an obscure error if they tried, say: @@ -264,7 +270,7 @@ def example_field(n, _implementation=_implementation): mesh_id = "f51e5aa5e2b0439f9fae4f04e51556f7" field = None - + if n == 0: f = Field() @@ -5299,84 +5305,505 @@ def example_field(n, _implementation=_implementation): elif n == 12: # field: air_temperature field = Field() - field.set_properties({'Conventions': 'CF-1.12', 'standard_name': 'air_temperature', 'units': 'K', 'units_metadata': 'temperature: on_scale'}) - field.nc_set_variable('tas') - data = Data([[291.5, 293.5, 285.3, 286.3, 286.2, 289.6, 285.6, 285.5, 287.1, 285.5, 291.5, 285.2, 285.0, 291.1, 287.9, 290.9, 288.6, 291.6, 289.6, 289.0, 294.0, 293.1, 291.5, 288.3, 289.6, 285.3, 285.4, 286.9, 294.3, 289.0, 294.5, 286.3, 288.3, 287.2, 285.7, 291.7, 290.1, 291.1, 286.8, 286.0, 291.1, 292.2, 285.7, 288.8, 285.8, 290.8, 287.0, 290.0], [294.0, 287.8, 294.6, 289.9, 289.2, 293.0, 286.8, 287.8, 285.2, 294.0, 288.1, 293.5, 289.6, 292.5, 290.0, 290.5, 292.9, 290.7, 293.6, 288.3, 293.5, 294.0, 288.7, 292.0, 292.9, 289.5, 286.8, 288.3, 292.6, 290.3, 290.8, 290.4, 287.7, 289.9, 288.4, 294.7, 291.4, 294.7, 287.6, 286.5, 291.4, 293.0, 288.8, 288.8, 292.3, 293.7, 290.1, 285.9]], units='K', dtype='f8') + field.set_properties( + { + "Conventions": "CF-1.12", + "standard_name": "air_temperature", + "units": "K", + "units_metadata": "temperature: on_scale", + } + ) + field.nc_set_variable("tas") + data = Data( + [ + [ + 291.5, + 293.5, + 285.3, + 286.3, + 286.2, + 289.6, + 285.6, + 285.5, + 287.1, + 285.5, + 291.5, + 285.2, + 285.0, + 291.1, + 287.9, + 290.9, + 288.6, + 291.6, + 289.6, + 289.0, + 294.0, + 293.1, + 291.5, + 288.3, + 289.6, + 285.3, + 285.4, + 286.9, + 294.3, + 289.0, + 294.5, + 286.3, + 288.3, + 287.2, + 285.7, + 291.7, + 290.1, + 291.1, + 286.8, + 286.0, + 291.1, + 292.2, + 285.7, + 288.8, + 285.8, + 290.8, + 287.0, + 290.0, + ], + [ + 294.0, + 287.8, + 294.6, + 289.9, + 289.2, + 293.0, + 286.8, + 287.8, + 285.2, + 294.0, + 288.1, + 293.5, + 289.6, + 292.5, + 290.0, + 290.5, + 292.9, + 290.7, + 293.6, + 288.3, + 293.5, + 294.0, + 288.7, + 292.0, + 292.9, + 289.5, + 286.8, + 288.3, + 292.6, + 290.3, + 290.8, + 290.4, + 287.7, + 289.9, + 288.4, + 294.7, + 291.4, + 294.7, + 287.6, + 286.5, + 291.4, + 293.0, + 288.8, + 288.8, + 292.3, + 293.7, + 290.1, + 285.9, + ], + ], + units="K", + dtype="f8", + ) field.set_data(data) # # domain_axis: ncdim%time c = DomainAxis() c.set_size(2) - c.nc_set_dimension('time') - field.set_construct(c, key='domainaxis0', copy=False) + c.nc_set_dimension("time") + field.set_construct(c, key="domainaxis0", copy=False) # # domain_axis: ncdim%cell c = DomainAxis() c.set_size(48) - c.nc_set_dimension('cell') - field.set_construct(c, key='domainaxis1', copy=False) + c.nc_set_dimension("cell") + field.set_construct(c, key="domainaxis1", copy=False) + # + # domain_axis: ncdim%height + c = DomainAxis() + c.set_size(1) + c.nc_set_dimension("height") + field.set_construct(c, key="domainaxis2", copy=False) + # + # auxiliary_coordinate: healpix_index + c = AuxiliaryCoordinate() + c.set_properties({"standard_name": "healpix_index", "units": "1"}) + c.nc_set_variable("healpix_index") + data = Data( + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + ], + units="1", + dtype="i4", + ) + c.set_data(data) + field.set_construct( + c, axes=("domainaxis1",), key="auxiliarycoordinate0", copy=False + ) + # + # dimension_coordinate: time + c = DimensionCoordinate() + c.set_properties( + { + "standard_name": "time", + "calendar": "proleptic_gregorian", + "units": "days since 2025-06-01", + } + ) + c.nc_set_variable("time") + data = Data( + [15.0, 45.5], + units="days since 2025-06-01", + calendar="proleptic_gregorian", + dtype="f4", + ) + c.set_data(data) + b = Bounds() + b.nc_set_variable("time_bounds") + data = Data( + [[0.0, 30.0], [30.0, 61.0]], + units="days since 2025-06-01", + calendar="proleptic_gregorian", + dtype="f4", + ) + b.set_data(data) + c.set_bounds(b) + field.set_construct( + c, axes=("domainaxis0",), key="dimensioncoordinate0", copy=False + ) + # + # dimension_coordinate: height + c = DimensionCoordinate() + c.set_properties({"standard_name": "height", "units": "m"}) + c.nc_set_variable("height") + data = Data([1.5], units="m", dtype="f4") + c.set_data(data) + field.set_construct( + c, axes=("domainaxis2",), key="dimensioncoordinate1", copy=False + ) + # + # coordinate_reference: grid_mapping_name:healpix + c = CoordinateReference() + c.nc_set_variable("healpix") + c.set_coordinates({"auxiliarycoordinate0"}) + d = Datum() + d.set_parameters({"earth_radius": 6371000}) + c.set_datum(d) + f = CoordinateConversion() + f.set_parameters( + { + "healpix_order": "nested", + "refinement_level": 1, + "grid_mapping_name": "healpix", + } + ) + c.set_coordinate_conversion(f) + field.set_construct(c) + # + # cell_method: mean + c = CellMethod() + c.set_method("mean") + c.set_axes(("time",)) + field.set_construct(c) + # + # cell_method: mean + c = CellMethod() + c.set_method("mean") + c.set_axes(("area",)) + field.set_construct(c) + # + # field data axes + field.set_data_axes(("domainaxis0", "domainaxis1")) + elif n == 13: + # field: air_temperature + field = Field() + field.set_properties( + { + "Conventions": "CF-1.12", + "standard_name": "air_temperature", + "units": "K", + "units_metadata": "temperature: on_scale", + } + ) + field.nc_set_variable("tas") + data = Data( + [ + [ + 291.5,291.5,291.5,291.5, + 293.5, 293.5, 293.5,293.5, + 285.3, 285.3, 285.3,285.3, + 286.3, 286.3, 286.3,286.3, + 286.2, + 289.6, + 285.6, + 285.5, + 287.1, + 285.5, + 291.5, + 285.2, + 285.0, + 291.1, + 287.9, + 290.9, + 288.6, + 291.6, + 289.6, + 289.0, + 294.0, + 293.1, + 291.5, + 288.3, + 289.6, + 285.3, + 285.4, + 286.9, + 294.3, + 289.0, + 294.5, + 286.3, + 288.3, + 287.2, + 285.7, + 291.7, + 290.1, + 291.1, + 286.8, + 286.0, + 291.1, + 292.2, + 285.7, + 288.8, + 285.8, + 290.8, + 287.0, + 290.0, + ], + [ + 294.0,294.0,294.0,294.0, + 287.8, 287.8, 287.8, 287.8, + 294.6,294.6,294.6,294.6, + 289.9,2289.9,89.9,289.9, + 289.2, + 293.0, + 286.8, + 287.8, + 285.2, + 294.0, + 288.1, + 293.5, + 289.6, + 292.5, + 290.0, + 290.5, + 292.9, + 290.7, + 293.6, + 288.3, + 293.5, + 294.0, + 288.7, + 292.0, + 292.9, + 289.5, + 286.8, + 288.3, + 292.6, + 290.3, + 290.8, + 290.4, + 287.7, + 289.9, + 288.4, + 294.7, + 291.4, + 294.7, + 287.6, + 286.5, + 291.4, + 293.0, + 288.8, + 288.8, + 292.3, + 293.7, + 290.1, + 285.9, + ], + ], + units="K", + dtype="f8", + ) + field.set_data(data) + # + # domain_axis: ncdim%time + c = DomainAxis() + c.set_size(2) + c.nc_set_dimension("time") + field.set_construct(c, key="domainaxis0", copy=False) + # + # domain_axis: ncdim%cell + c = DomainAxis() + c.set_size(60) + c.nc_set_dimension("cell") + field.set_construct(c, key="domainaxis1", copy=False) # # domain_axis: ncdim%height c = DomainAxis() c.set_size(1) - c.nc_set_dimension('height') - field.set_construct(c, key='domainaxis2', copy=False) + c.nc_set_dimension("height") + field.set_construct(c, key="domainaxis2", copy=False) # # auxiliary_coordinate: healpix_index c = AuxiliaryCoordinate() - c.set_properties({'standard_name': 'healpix_index', 'units': '1'}) - c.nc_set_variable('altitude') - data = Data([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47], units='1', dtype='i8') + c.set_properties({"standard_name": "healpix_index", "units": "1"}) + c.nc_set_variable("healpix_index") + data = Data( + [64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, + 76, 77, 78, 79, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, + 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, + 59, 60, 61, 62, 63], + units="1", + dtype="i4", + ) c.set_data(data) - field.set_construct(c, axes=('domainaxis1',), key='auxiliarycoordinate0', copy=False) + field.set_construct( + c, axes=("domainaxis1",), key="auxiliarycoordinate0", copy=False + ) # # dimension_coordinate: time c = DimensionCoordinate() - c.set_properties({'standard_name': 'time', 'calendar': 'proleptic_gregorian', 'units': 'days since 2025-06-01'}) - c.nc_set_variable('time') - data = Data([15.0, 45.5], units='days since 2025-06-01', calendar='proleptic_gregorian', dtype='f4') + c.set_properties( + { + "standard_name": "time", + "calendar": "proleptic_gregorian", + "units": "days since 2025-06-01", + } + ) + c.nc_set_variable("time") + data = Data( + [15.0, 45.5], + units="days since 2025-06-01", + calendar="proleptic_gregorian", + dtype="f4", + ) c.set_data(data) b = Bounds() - b.nc_set_variable('time_bounds') - data = Data([[0.0, 30.0], [30.0, 61.0]], units='days since 2025-06-01', calendar='proleptic_gregorian', dtype='f4') + b.nc_set_variable("time_bounds") + data = Data( + [[0.0, 30.0], [30.0, 61.0]], + units="days since 2025-06-01", + calendar="proleptic_gregorian", + dtype="f4", + ) b.set_data(data) c.set_bounds(b) - field.set_construct(c, axes=('domainaxis0',), key='dimensioncoordinate0', copy=False) + field.set_construct( + c, axes=("domainaxis0",), key="dimensioncoordinate0", copy=False + ) # # dimension_coordinate: height c = DimensionCoordinate() - c.set_properties({'standard_name': 'height', 'units': 'm'}) - c.nc_set_variable('height') - data = Data([1.5], units='m', dtype='f4') + c.set_properties({"standard_name": "height", "units": "m"}) + c.nc_set_variable("height") + data = Data([1.5], units="m", dtype="f4") c.set_data(data) - field.set_construct(c, axes=('domainaxis2',), key='dimensioncoordinate1', copy=False) + field.set_construct( + c, axes=("domainaxis2",), key="dimensioncoordinate1", copy=False + ) # # coordinate_reference: grid_mapping_name:healpix c = CoordinateReference() - c.nc_set_variable('healpix') - c.set_coordinates({'auxiliarycoordinate0'}) + c.nc_set_variable("healpix") + c.set_coordinates({"auxiliarycoordinate0"}) d = Datum() - d.set_parameters({'earth_radius': 6371000}) + d.set_parameters({"earth_radius": 6371000}) c.set_datum(d) f = CoordinateConversion() - f.set_parameters({'healpix_order': 'nested', 'refinement_level': 1, 'grid_mapping_name': 'healpix'}) + f.set_parameters( + { + "healpix_order": "nuniq", + "grid_mapping_name": "healpix", + } + ) c.set_coordinate_conversion(f) field.set_construct(c) # # cell_method: mean c = CellMethod() - c.set_method('mean') - c.set_axes(('time',)) + c.set_method("mean") + c.set_axes(("time",)) field.set_construct(c) # # cell_method: mean c = CellMethod() - c.set_method('mean') - c.set_axes(('area',)) + c.set_method("mean") + c.set_axes(("area",)) field.set_construct(c) # # field data axes - field.set_data_axes(('domainaxis0', 'domainaxis1')) + field.set_data_axes(("domainaxis0", "domainaxis1")) else: raise ValueError( "Must select an example construct with an integer argument " @@ -5385,7 +5812,7 @@ def example_field(n, _implementation=_implementation): if field is not None: f = field - + return f @@ -5440,6 +5867,10 @@ def example_fields(*n, _func=example_field): ``11`` Discrete sampling geometry (DSG) "trajectory" features. + + ``12`` TODOHEALPIX + + ``13`` TODOHEALPIX ====== ================================================== If no individual field constructs are selected then all @@ -5472,7 +5903,7 @@ def example_fields(*n, _func=example_field): , , , - ] + ] TODOHEALPIX >>> cfdm.example_fields(7, 1) [, diff --git a/cfdm/mixin/fielddomain.py b/cfdm/mixin/fielddomain.py index b609cf5a9..a782b1000 100644 --- a/cfdm/mixin/fielddomain.py +++ b/cfdm/mixin/fielddomain.py @@ -3,14 +3,14 @@ from ..decorators import _manage_log_level_via_verbosity -#import dask.array as da -#import numpy as np +# import dask.array as da +# import numpy as np # -#from ..decorators import ( +# from ..decorators import ( # _inplace_enabled, # _inplace_enabled_define_and_cleanup, # _manage_log_level_via_verbosity, -#) +# ) logger = logging.getLogger(__name__) @@ -1094,146 +1094,146 @@ def coordinate_references(self, *identities, **filter_kwargs): **filter_kwargs, ) -# @_inplace_enabled(default=False) -# def create_1d_coordinates(self, persist=False, key=False, inplace=False): -# """TODOHEALPIX. -# -# .. versionadded:: NEXTVERSION -# -# :Parameters: -# -# persist: `bool`, optional -# TODOHEALPIX -# -# key: `bool` -# TODOHEALPIX If True, return alongside the field -# construct the key identifying the auxiliary coordinate -# of the field with the newly-computed vertical -# coordinates, as a 2-tuple of field and then key. If -# False, the default, then only return the field -# construct. -# -# If no coordinates were computed, `None` will be -# returned in the key (second) position of the 2-tuple. -# -# {{inplace: `bool`, optional}} -# -# :Returns: -# -# `{{class}}` or `None` -# TODOHEALPIX -# -# """ -# f = _inplace_enabled_define_and_cleanup(self) -# -# keys = () -# -# crs = f.coordinate_reference("grid_mapping_name:healpix", default=None) -# if crs is None: -# if key: -# return f, keys -# -# return f -# -# # ------------------------------------------------------------ -# # Get the HEALPix indices as a Dask array -# # ------------------------------------------------------------ -# c_key, healpix_index = f.coordinate( -# "healpix_index", item=True, default=(None, None) -# ) -# if c_key is None: -# raise ValueError("TODOHEALPIX") -# -# dx = healpix_index.to_dask_array( -# _force_mask_hardness=False, _force_to_memory=False -# ) -# -# # ------------------------------------------------------------ -# # Define functions to create latitudes and longitudes from -# # HEALPix indices -# # ------------------------------------------------------------ -# refinement_level = crs.coordinate_conversion.get_property( -# "refinement_level", None -# ) -# if refinement_level is None: -# raise ValueError("TODOHEALPIX") -# -# healpix_index_method = crs.coordinate_conversion.get_property( -# "healpix_index_method", None -# ) -# if healpix_index_method is None: -# raise ValueError("TODOHEALPIX") -# -# if healpix_index_method == "nuniq": -# raise ValueError("TODOHEALPIX") -# -# from astropy_healpix import HEALPix -# -# hp = HEALPix(nside=2**refinement_level, order=healpix_index_method) -# -# def HEALPix_lon_coordinates(a): -# return hp.healpix_to_lonlat(a)[0].degree -# -# def HEALPix_lat_coordinates(a): -# return hp.healpix_to_lonlat(a)[1].degree -# -# def HEALPix_lon_bounds(a): -# return hp.boundaries_lonlat(a)[0].degree -# -# def HEALPix_lat_bounds(a): -# return hp.boundaries_lonlat(a)[1].degree -# -# # ------------------------------------------------------------ -# # Create new latitude and longitude coordinates with bounds -# # ------------------------------------------------------------ -# meta = np.array((), dtype=np.dtype("float64")) -# -# # Latitude coordinates -# dx = dx.map_blocks(HEALPix_lat_coordinates, meta=meta) -# lat = f._AuxiliaryCoordinate( -# data=f._Data(dx, "degrees_north", copy=False), -# properties={"standard_name": "latitude"}, -# copy=False, -# ) -# -# # Longitude coordinates -# dx = dx.map_blocks(HEALPix_lon_coordinates, meta=meta) -# lon = f._AuxiliaryCoordinate( -# data=f._Data(dx, "degrees_east", copy=False), -# properties={"standard_name": "longitude"}, -# copy=False, -# ) -# -# # Latitude bounds -# dx = da.blockwise( -# HEALPix_lat_bounds, "ij", dx, "i", new_axes={"j": 4}, meta=meta -# ) -# bounds = f._Bounds(data=dx) -# lat.set_bounds(bounds) -# -# # Longitude bounds -# dx = da.blockwise( -# HEALPix_lon_bounds, "ij", dx, "i", new_axes={"j": 4}, meta=meta -# ) -# bounds = f._Bounds(data=dx) -# lon.set_bounds(bounds) -# -# if persist: -# lat = lat.persist(inplace=True) -# lon = lon.persist(inplace=True) -# -# # ------------------------------------------------------------ -# # Set the new latitude and longitude coordinates -# # ------------------------------------------------------------ -# axis = f.get_domain_axes(c_key)[0] -# lat_key = f.set_construct(lat, axes=axis, copy=False) -# lon_key = f.set_construct(lon, axes=axis, copy=False) -# keys = (lat_key, lon_key) -# -# if key: -# return f, keys -# -# return f + # @_inplace_enabled(default=False) + # def create_1d_coordinates(self, persist=False, key=False, inplace=False): + # """TODOHEALPIX. + # + # .. versionadded:: NEXTVERSION + # + # :Parameters: + # + # persist: `bool`, optional + # TODOHEALPIX + # + # key: `bool` + # TODOHEALPIX If True, return alongside the field + # construct the key identifying the auxiliary coordinate + # of the field with the newly-computed vertical + # coordinates, as a 2-tuple of field and then key. If + # False, the default, then only return the field + # construct. + # + # If no coordinates were computed, `None` will be + # returned in the key (second) position of the 2-tuple. + # + # {{inplace: `bool`, optional}} + # + # :Returns: + # + # `{{class}}` or `None` + # TODOHEALPIX + # + # """ + # f = _inplace_enabled_define_and_cleanup(self) + # + # keys = () + # + # crs = f.coordinate_reference("grid_mapping_name:healpix", default=None) + # if crs is None: + # if key: + # return f, keys + # + # return f + # + # # ------------------------------------------------------------ + # # Get the HEALPix indices as a Dask array + # # ------------------------------------------------------------ + # c_key, healpix_index = f.coordinate( + # "healpix_index", item=True, default=(None, None) + # ) + # if c_key is None: + # raise ValueError("TODOHEALPIX") + # + # dx = healpix_index.to_dask_array( + # _force_mask_hardness=False, _force_to_memory=False + # ) + # + # # ------------------------------------------------------------ + # # Define functions to create latitudes and longitudes from + # # HEALPix indices + # # ------------------------------------------------------------ + # refinement_level = crs.coordinate_conversion.get_property( + # "refinement_level", None + # ) + # if refinement_level is None: + # raise ValueError("TODOHEALPIX") + # + # healpix_index_method = crs.coordinate_conversion.get_property( + # "healpix_index_method", None + # ) + # if healpix_index_method is None: + # raise ValueError("TODOHEALPIX") + # + # if healpix_index_method == "nuniq": + # raise ValueError("TODOHEALPIX") + # + # from astropy_healpix import HEALPix + # + # hp = HEALPix(nside=2**refinement_level, order=healpix_index_method) + # + # def HEALPix_lon_coordinates(a): + # return hp.healpix_to_lonlat(a)[0].degree + # + # def HEALPix_lat_coordinates(a): + # return hp.healpix_to_lonlat(a)[1].degree + # + # def HEALPix_lon_bounds(a): + # return hp.boundaries_lonlat(a)[0].degree + # + # def HEALPix_lat_bounds(a): + # return hp.boundaries_lonlat(a)[1].degree + # + # # ------------------------------------------------------------ + # # Create new latitude and longitude coordinates with bounds + # # ------------------------------------------------------------ + # meta = np.array((), dtype=np.dtype("float64")) + # + # # Latitude coordinates + # dx = dx.map_blocks(HEALPix_lat_coordinates, meta=meta) + # lat = f._AuxiliaryCoordinate( + # data=f._Data(dx, "degrees_north", copy=False), + # properties={"standard_name": "latitude"}, + # copy=False, + # ) + # + # # Longitude coordinates + # dx = dx.map_blocks(HEALPix_lon_coordinates, meta=meta) + # lon = f._AuxiliaryCoordinate( + # data=f._Data(dx, "degrees_east", copy=False), + # properties={"standard_name": "longitude"}, + # copy=False, + # ) + # + # # Latitude bounds + # dx = da.blockwise( + # HEALPix_lat_bounds, "ij", dx, "i", new_axes={"j": 4}, meta=meta + # ) + # bounds = f._Bounds(data=dx) + # lat.set_bounds(bounds) + # + # # Longitude bounds + # dx = da.blockwise( + # HEALPix_lon_bounds, "ij", dx, "i", new_axes={"j": 4}, meta=meta + # ) + # bounds = f._Bounds(data=dx) + # lon.set_bounds(bounds) + # + # if persist: + # lat = lat.persist(inplace=True) + # lon = lon.persist(inplace=True) + # + # # ------------------------------------------------------------ + # # Set the new latitude and longitude coordinates + # # ------------------------------------------------------------ + # axis = f.get_domain_axes(c_key)[0] + # lat_key = f.set_construct(lat, axes=axis, copy=False) + # lon_key = f.set_construct(lon, axes=axis, copy=False) + # keys = (lat_key, lon_key) + # + # if key: + # return f, keys + # + # return f def del_construct(self, *identity, default=ValueError(), **filter_kwargs): """Remove a metadata construct. diff --git a/cfdm/read_write/netcdf/netcdfread.py b/cfdm/read_write/netcdf/netcdfread.py index d277be493..e5b80e7ab 100644 --- a/cfdm/read_write/netcdf/netcdfread.py +++ b/cfdm/read_write/netcdf/netcdfread.py @@ -286,6 +286,8 @@ def cf_coordinate_reference_coordinates(self): "ocean_double_sigma_coordinate": ( "ocean_double_sigma_coordinate", ), + "healpix": ( "healpix_index", "latitude", "longitude" ), + } def cf_interpolation_names(self): From 3076f2714f8cae3425168cc151e75374b8e5d194 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Thu, 19 Jun 2025 17:09:44 +0100 Subject: [PATCH 05/24] dev --- cfdm/examplefield.py | 119 ++++++++++++++++++++++----- cfdm/read_write/netcdf/netcdfread.py | 3 +- 2 files changed, 100 insertions(+), 22 deletions(-) diff --git a/cfdm/examplefield.py b/cfdm/examplefield.py index 337090a09..3254d27ab 100644 --- a/cfdm/examplefield.py +++ b/cfdm/examplefield.py @@ -1,5 +1,3 @@ -import numpy as np - from .cfdmimplementation import implementation from .functions import CF @@ -235,7 +233,7 @@ def example_field(n, _implementation=_implementation): : cf_role=trajectory_id(cf_role=trajectory_id(1)) = [flight1] TODOHEALPIX - + """ # For safety given the private second argument which we might not # document, otherwise a user gets an obscure error if they tried, say: @@ -5367,10 +5365,10 @@ def example_field(n, _implementation=_implementation): 290.0, ], [ - 294.0, - 287.8, + 294.2, + 287.7, 294.6, - 289.9, + 289.5, 289.2, 293.0, 286.8, @@ -5591,10 +5589,22 @@ def example_field(n, _implementation=_implementation): data = Data( [ [ - 291.5,291.5,291.5,291.5, - 293.5, 293.5, 293.5,293.5, - 285.3, 285.3, 285.3,285.3, - 286.3, 286.3, 286.3,286.3, + 291.6, + 291.7, + 291.4, + 291.3, + 293.6, + 293.7, + 293.4, + 293.3, + 285.4, + 285.5, + 285.2, + 285.1, + 286.4, + 286.5, + 286.2, + 286.1, 286.2, 289.6, 285.6, @@ -5641,10 +5651,22 @@ def example_field(n, _implementation=_implementation): 290.0, ], [ - 294.0,294.0,294.0,294.0, - 287.8, 287.8, 287.8, 287.8, - 294.6,294.6,294.6,294.6, - 289.9,2289.9,89.9,289.9, + 294.3, + 294.4, + 294.1, + 294.0, + 287.8, + 287.9, + 287.6, + 287.5, + 294.7, + 294.8, + 294.5, + 294.4, + 289.6, + 289.7, + 289.4, + 289.3, 289.2, 293.0, 286.8, @@ -5719,11 +5741,68 @@ def example_field(n, _implementation=_implementation): c.set_properties({"standard_name": "healpix_index", "units": "1"}) c.nc_set_variable("healpix_index") data = Data( - [64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, - 76, 77, 78, 79, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, - 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, - 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, - 59, 60, 61, 62, 63], + [ + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + ], units="1", dtype="i4", ) @@ -5803,7 +5882,7 @@ def example_field(n, _implementation=_implementation): field.set_construct(c) # # field data axes - field.set_data_axes(("domainaxis0", "domainaxis1")) + field.set_data_axes(("domainaxis0", "domainaxis1")) else: raise ValueError( "Must select an example construct with an integer argument " diff --git a/cfdm/read_write/netcdf/netcdfread.py b/cfdm/read_write/netcdf/netcdfread.py index e5b80e7ab..e0dedc3d7 100644 --- a/cfdm/read_write/netcdf/netcdfread.py +++ b/cfdm/read_write/netcdf/netcdfread.py @@ -286,8 +286,7 @@ def cf_coordinate_reference_coordinates(self): "ocean_double_sigma_coordinate": ( "ocean_double_sigma_coordinate", ), - "healpix": ( "healpix_index", "latitude", "longitude" ), - + "healpix": ("healpix_index", "latitude", "longitude"), } def cf_interpolation_names(self): From 5126094fd0682ccac0ed7932f6c051be7401336a Mon Sep 17 00:00:00 2001 From: David Hassell Date: Thu, 19 Jun 2025 17:11:14 +0100 Subject: [PATCH 06/24] dev --- cfdm/examplefield.py | 1 - cfdm/mixin/fielddomain.py | 150 -------------------------------------- 2 files changed, 151 deletions(-) diff --git a/cfdm/examplefield.py b/cfdm/examplefield.py index 3254d27ab..88ede414a 100644 --- a/cfdm/examplefield.py +++ b/cfdm/examplefield.py @@ -5913,7 +5913,6 @@ def example_fields(*n, _func=example_field): ====== ================================================== ``0`` Cell method and dimension coordinate metadata constructs. - ``1`` Cell method, dimension coordinate, auxiliary coordinate, cell measure, coordinate reference, domain ancillary and field ancillary metadata diff --git a/cfdm/mixin/fielddomain.py b/cfdm/mixin/fielddomain.py index a782b1000..06b84bd5d 100644 --- a/cfdm/mixin/fielddomain.py +++ b/cfdm/mixin/fielddomain.py @@ -3,15 +3,6 @@ from ..decorators import _manage_log_level_via_verbosity -# import dask.array as da -# import numpy as np -# -# from ..decorators import ( -# _inplace_enabled, -# _inplace_enabled_define_and_cleanup, -# _manage_log_level_via_verbosity, -# ) - logger = logging.getLogger(__name__) @@ -1094,147 +1085,6 @@ def coordinate_references(self, *identities, **filter_kwargs): **filter_kwargs, ) - # @_inplace_enabled(default=False) - # def create_1d_coordinates(self, persist=False, key=False, inplace=False): - # """TODOHEALPIX. - # - # .. versionadded:: NEXTVERSION - # - # :Parameters: - # - # persist: `bool`, optional - # TODOHEALPIX - # - # key: `bool` - # TODOHEALPIX If True, return alongside the field - # construct the key identifying the auxiliary coordinate - # of the field with the newly-computed vertical - # coordinates, as a 2-tuple of field and then key. If - # False, the default, then only return the field - # construct. - # - # If no coordinates were computed, `None` will be - # returned in the key (second) position of the 2-tuple. - # - # {{inplace: `bool`, optional}} - # - # :Returns: - # - # `{{class}}` or `None` - # TODOHEALPIX - # - # """ - # f = _inplace_enabled_define_and_cleanup(self) - # - # keys = () - # - # crs = f.coordinate_reference("grid_mapping_name:healpix", default=None) - # if crs is None: - # if key: - # return f, keys - # - # return f - # - # # ------------------------------------------------------------ - # # Get the HEALPix indices as a Dask array - # # ------------------------------------------------------------ - # c_key, healpix_index = f.coordinate( - # "healpix_index", item=True, default=(None, None) - # ) - # if c_key is None: - # raise ValueError("TODOHEALPIX") - # - # dx = healpix_index.to_dask_array( - # _force_mask_hardness=False, _force_to_memory=False - # ) - # - # # ------------------------------------------------------------ - # # Define functions to create latitudes and longitudes from - # # HEALPix indices - # # ------------------------------------------------------------ - # refinement_level = crs.coordinate_conversion.get_property( - # "refinement_level", None - # ) - # if refinement_level is None: - # raise ValueError("TODOHEALPIX") - # - # healpix_index_method = crs.coordinate_conversion.get_property( - # "healpix_index_method", None - # ) - # if healpix_index_method is None: - # raise ValueError("TODOHEALPIX") - # - # if healpix_index_method == "nuniq": - # raise ValueError("TODOHEALPIX") - # - # from astropy_healpix import HEALPix - # - # hp = HEALPix(nside=2**refinement_level, order=healpix_index_method) - # - # def HEALPix_lon_coordinates(a): - # return hp.healpix_to_lonlat(a)[0].degree - # - # def HEALPix_lat_coordinates(a): - # return hp.healpix_to_lonlat(a)[1].degree - # - # def HEALPix_lon_bounds(a): - # return hp.boundaries_lonlat(a)[0].degree - # - # def HEALPix_lat_bounds(a): - # return hp.boundaries_lonlat(a)[1].degree - # - # # ------------------------------------------------------------ - # # Create new latitude and longitude coordinates with bounds - # # ------------------------------------------------------------ - # meta = np.array((), dtype=np.dtype("float64")) - # - # # Latitude coordinates - # dx = dx.map_blocks(HEALPix_lat_coordinates, meta=meta) - # lat = f._AuxiliaryCoordinate( - # data=f._Data(dx, "degrees_north", copy=False), - # properties={"standard_name": "latitude"}, - # copy=False, - # ) - # - # # Longitude coordinates - # dx = dx.map_blocks(HEALPix_lon_coordinates, meta=meta) - # lon = f._AuxiliaryCoordinate( - # data=f._Data(dx, "degrees_east", copy=False), - # properties={"standard_name": "longitude"}, - # copy=False, - # ) - # - # # Latitude bounds - # dx = da.blockwise( - # HEALPix_lat_bounds, "ij", dx, "i", new_axes={"j": 4}, meta=meta - # ) - # bounds = f._Bounds(data=dx) - # lat.set_bounds(bounds) - # - # # Longitude bounds - # dx = da.blockwise( - # HEALPix_lon_bounds, "ij", dx, "i", new_axes={"j": 4}, meta=meta - # ) - # bounds = f._Bounds(data=dx) - # lon.set_bounds(bounds) - # - # if persist: - # lat = lat.persist(inplace=True) - # lon = lon.persist(inplace=True) - # - # # ------------------------------------------------------------ - # # Set the new latitude and longitude coordinates - # # ------------------------------------------------------------ - # axis = f.get_domain_axes(c_key)[0] - # lat_key = f.set_construct(lat, axes=axis, copy=False) - # lon_key = f.set_construct(lon, axes=axis, copy=False) - # keys = (lat_key, lon_key) - # - # if key: - # return f, keys - # - # return f - def del_construct(self, *identity, default=ValueError(), **filter_kwargs): """Remove a metadata construct. From 68446a709f157c83c5068ec18a3da0e475005437 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Mon, 30 Jun 2025 17:47:05 +0100 Subject: [PATCH 07/24] dev --- cfdm/examplefield.py | 66 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/cfdm/examplefield.py b/cfdm/examplefield.py index 88ede414a..453cc8420 100644 --- a/cfdm/examplefield.py +++ b/cfdm/examplefield.py @@ -58,9 +58,14 @@ def example_field(n, _implementation=_implementation): ``11`` Discrete sampling geometry (DSG) "trajectory" features. - ``12`` TODOHEALPIX - - ``13`` TODOHEALPIX + ``12`` A global HEALPix grid with "nested" indices at + refinement level 1. The field area-weighted global + means are equal to those of example field ``13``. + + ``13`` A global HEALPix grid with "nuniq" indices + representing refinement levels 1 and 2. The + area-weighted global means are equal to those of + example field ``12``. ====== ================================================== See the examples for details. @@ -232,7 +237,25 @@ def example_field(n, _implementation=_implementation): : longitude(cf_role=trajectory_id(1), ncdim%trajectory(4)) = [[0.0, ..., 0.31]] degree_east : cf_role=trajectory_id(cf_role=trajectory_id(1)) = [flight1] - TODOHEALPIX + >>> print(cfdm.example_field(12)) + Field: air_temperature (ncvar%tas) + ---------------------------------- + Data : air_temperature(time(2), healpix_index(48)) K + Cell methods : time(2): mean area: mean + Dimension coords: time(2) = [2025-06-16 00:00:00, 2025-07-16 12:00:00] proleptic_gregorian + : height(1) = [1.5] m + Auxiliary coords: healpix_index(healpix_index(48)) = [0, ..., 47] 1 + Coord references: grid_mapping_name:healpix + + >>> print(cfdm.example_field(13)) + Field: air_temperature (ncvar%tas) + ---------------------------------- + Data : air_temperature(time(2), healpix_index(60)) K + Cell methods : time(2): mean area: mean + Dimension coords: time(2) = [2025-06-16 00:00:00, 2025-07-16 12:00:00] proleptic_gregorian + : height(1) = [1.5] m + Auxiliary coords: healpix_index(healpix_index(60)) = [64, ..., 63] 1 + Coord references: grid_mapping_name:healpix """ # For safety given the private second argument which we might not @@ -5563,7 +5586,7 @@ def example_field(n, _implementation=_implementation): # cell_method: mean c = CellMethod() c.set_method("mean") - c.set_axes(("time",)) + c.set_axes(("domainaxis0",)) field.set_construct(c) # # cell_method: mean @@ -5872,7 +5895,7 @@ def example_field(n, _implementation=_implementation): # cell_method: mean c = CellMethod() c.set_method("mean") - c.set_axes(("time",)) + c.set_axes(("domainaxis0",)) field.set_construct(c) # # cell_method: mean @@ -5946,9 +5969,14 @@ def example_fields(*n, _func=example_field): ``11`` Discrete sampling geometry (DSG) "trajectory" features. - ``12`` TODOHEALPIX + ``12`` A global HEALPix grid with "nested" indices at + refinement level 1. The field area-weighted global + means are equal to those of example field ``13``. - ``13`` TODOHEALPIX + ``13`` A global HEALPix grid with "nuniq" indices + representing refinement levels 1 and 2. The + area-weighted global means are equal to those of + example field ``12``. ====== ================================================== If no individual field constructs are selected then all @@ -5981,7 +6009,9 @@ def example_fields(*n, _func=example_field): , , , - ] TODOHEALPIX + + , + ] >>> cfdm.example_fields(7, 1) [, @@ -6062,6 +6092,12 @@ def example_domain(n, _func=example_field): ``11`` Discrete sampling geometry (DSG) "trajectory" features. + + ``12`` A global HEALPix grid with "nested" indices at + refinement level 1. + + ``13`` A global HEALPix grid with "nuniq" indices + representing refinement levels 1 and 2. ====== ================================================== See the examples for details. @@ -6180,5 +6216,17 @@ def example_domain(n, _func=example_field): : longitude(cf_role=trajectory_id(1), ncdim%trajectory(4)) = [[0.0, ..., 0.31]] degree_east : cf_role=trajectory_id(cf_role=trajectory_id(1)) = [flight1] + >>> print(cfdm.example_domain(12)) + Dimension coords: time(2) = [2025-06-16 00:00:00, 2025-07-16 12:00:00] proleptic_gregorian + : height(1) = [1.5] m + Auxiliary coords: healpix_index(healpix_index(48)) = [0, ..., 47] 1 + Coord references: grid_mapping_name:healpix + + >>> print(cfdm.example_domain(13)) + Dimension coords: time(2) = [2025-06-16 00:00:00, 2025-07-16 12:00:00] proleptic_gregorian + : height(1) = [1.5] m + Auxiliary coords: healpix_index(healpix_index(60)) = [64, ..., 63] 1 + Coord references: grid_mapping_name:healpix + """ return _func(n).get_domain() From 61162781b5c0361a6468852c16ca937bbada6fad Mon Sep 17 00:00:00 2001 From: David Hassell Date: Mon, 30 Jun 2025 18:20:16 +0100 Subject: [PATCH 08/24] dev --- README.md | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index f6bcfff77..17ca8905f 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ The ``cfdm`` package can: * create new field and domain constructs in memory, * write and append field and domain constructs to netCDF datasets on disk, * read, write, and manipulate UGRID mesh topologies, +* read, write, and manipulate HEALPix grids, * read, write, and create coordinates defined by geometry cells, * read and write netCDF4 string data-type variables, * read, write, and create netCDF and CDL datasets containing diff --git a/setup.py b/setup.py index 34b99f9f6..33fea52cb 100755 --- a/setup.py +++ b/setup.py @@ -60,6 +60,7 @@ def _get_version(): * create new field and domain constructs in memory, * write and append field and domain constructs to netCDF datasets on disk, * read, write, and manipulate UGRID mesh topologies, +* read, write, and manipulate HEALPix grids, * read, write, and create coordinates defined by geometry cells, * read and write netCDF4 string data-type variables, * read, write, and create netCDF and CDL datasets containing hierarchical groups, From eece5385083f42aaf4b0b739c961b835bc26e7d9 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Wed, 2 Jul 2025 17:39:22 +0100 Subject: [PATCH 09/24] dev --- cfdm/examplefield.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cfdm/examplefield.py b/cfdm/examplefield.py index 453cc8420..8040f86c0 100644 --- a/cfdm/examplefield.py +++ b/cfdm/examplefield.py @@ -5575,7 +5575,7 @@ def example_field(n, _implementation=_implementation): f = CoordinateConversion() f.set_parameters( { - "healpix_order": "nested", + "index_scheme": "nested", "refinement_level": 1, "grid_mapping_name": "healpix", } @@ -5885,7 +5885,7 @@ def example_field(n, _implementation=_implementation): f = CoordinateConversion() f.set_parameters( { - "healpix_order": "nuniq", + "index_scheme": "nuniq", "grid_mapping_name": "healpix", } ) From 9b8a44f8bc142d11a30158a07249f1de3eb35be9 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Thu, 3 Jul 2025 18:37:55 +0100 Subject: [PATCH 10/24] dev --- cfdm/examplefield.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cfdm/examplefield.py b/cfdm/examplefield.py index 8040f86c0..4e09c1f77 100644 --- a/cfdm/examplefield.py +++ b/cfdm/examplefield.py @@ -62,7 +62,7 @@ def example_field(n, _implementation=_implementation): refinement level 1. The field area-weighted global means are equal to those of example field ``13``. - ``13`` A global HEALPix grid with "nuniq" indices + ``13`` A global HEALPix grid with "nested_unique" indices representing refinement levels 1 and 2. The area-weighted global means are equal to those of example field ``12``. @@ -5575,7 +5575,7 @@ def example_field(n, _implementation=_implementation): f = CoordinateConversion() f.set_parameters( { - "index_scheme": "nested", + "indexing_scheme": "nested", "refinement_level": 1, "grid_mapping_name": "healpix", } @@ -5885,7 +5885,7 @@ def example_field(n, _implementation=_implementation): f = CoordinateConversion() f.set_parameters( { - "index_scheme": "nuniq", + "indexing_scheme": "nested_unique", "grid_mapping_name": "healpix", } ) @@ -5973,7 +5973,7 @@ def example_fields(*n, _func=example_field): refinement level 1. The field area-weighted global means are equal to those of example field ``13``. - ``13`` A global HEALPix grid with "nuniq" indices + ``13`` A global HEALPix grid with "nested_unique" indices representing refinement levels 1 and 2. The area-weighted global means are equal to those of example field ``12``. @@ -6096,7 +6096,7 @@ def example_domain(n, _func=example_field): ``12`` A global HEALPix grid with "nested" indices at refinement level 1. - ``13`` A global HEALPix grid with "nuniq" indices + ``13`` A global HEALPix grid with "nested_unique" indices representing refinement levels 1 and 2. ====== ================================================== From 0347aa8044544b820a4cc4e75f99875ccf9da219 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Fri, 4 Jul 2025 15:08:17 +0100 Subject: [PATCH 11/24] dev --- cfdm/data/data.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/cfdm/data/data.py b/cfdm/data/data.py index 03cc155ac..26624212e 100644 --- a/cfdm/data/data.py +++ b/cfdm/data/data.py @@ -559,12 +559,6 @@ def __float__(self): x.__float__() <==> float(x) - **Performance** - - `__float__` causes all delayed operations to be executed, - unless the dask array size is already known to be greater than - 1. - """ if self.size != 1: raise TypeError( @@ -572,7 +566,7 @@ def __float__(self): f"Python scalars. Got {self}" ) - return float(self.array[(0,) * self.ndim]) + return float(self.first_element()) def __format__(self, format_spec): """Interpret format specifiers for size 1 arrays. @@ -770,11 +764,6 @@ def __int__(self): x.__int__() <==> int(x) - **Performance** - - `__int__` causes all delayed operations to be executed, unless - the dask array size is already known to be greater than 1. - """ if self.size != 1: raise TypeError( @@ -782,7 +771,7 @@ def __int__(self): f"Python scalars. Got {self}" ) - return int(self.array[(0,) * self.ndim]) + return int(self.first_element()) def __iter__(self): """Called when an iterator is required. From b1cc583ff3e020e70fc0ab86d0cb4580421e10f0 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Sun, 6 Jul 2025 12:59:44 +0100 Subject: [PATCH 12/24] dev --- cfdm/data/data.py | 8 +++----- cfdm/examplefield.py | 25 +++++++++++++++---------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/cfdm/data/data.py b/cfdm/data/data.py index 26624212e..568a55f53 100644 --- a/cfdm/data/data.py +++ b/cfdm/data/data.py @@ -6484,8 +6484,8 @@ def reshape(self, *shape, merge_chunks=True, limit=None, inplace=False): """Change the shape of the data without changing its values. It assumes that the array is stored in row-major order, and - only allows for reshapings that collapse or merge dimensions - like ``(1, 2, 3, 4) -> (1, 6, 4)`` or ``(64,) -> (4, 4, 4)``. + only allows for reshapings that collapse or merge dimensions, + e.g. ``(1, 2, 3, 4) -> (1, 6, 4)`` or ``(64,) -> (4, 4, 4)``. .. versionadded:: (cfdm) 1.11.2.0 @@ -6549,9 +6549,7 @@ def reshape(self, *shape, merge_chunks=True, limit=None, inplace=False): original_shape = self.shape original_ndim = len(original_shape) - dx = d.to_dask_array( - _force_mask_hardness=False, _force_to_memory=False - ) + dx = d.to_dask_array(_force_mask_hardness=False) dx = dx.reshape(*shape, merge_chunks=merge_chunks, limit=limit) d._set_dask(dx, in_memory=None) diff --git a/cfdm/examplefield.py b/cfdm/examplefield.py index 4e09c1f77..0cc93368d 100644 --- a/cfdm/examplefield.py +++ b/cfdm/examplefield.py @@ -60,11 +60,13 @@ def example_field(n, _implementation=_implementation): ``12`` A global HEALPix grid with "nested" indices at refinement level 1. The field area-weighted global - means are equal to those of example field ``13``. + latitude-longitude means are equal to those of + example field ``13``. - ``13`` A global HEALPix grid with "nested_unique" indices - representing refinement levels 1 and 2. The - area-weighted global means are equal to those of + ``13`` A global HEALPix Multi-Order Coverage grid with + "nested_unique" indices representing refinement + levels 1 and 2. The area-weighted global + latitude-longitude means are equal to those of example field ``12``. ====== ================================================== @@ -5971,11 +5973,13 @@ def example_fields(*n, _func=example_field): ``12`` A global HEALPix grid with "nested" indices at refinement level 1. The field area-weighted global - means are equal to those of example field ``13``. + latitude-longitude means are equal to those of + example field ``13``. - ``13`` A global HEALPix grid with "nested_unique" indices - representing refinement levels 1 and 2. The - area-weighted global means are equal to those of + ``13`` A global HEALPix Multi-Order Coverage grid with + "nested_unique" indices representing refinement + levels 1 and 2. The area-weighted global + latitude-longitude means are equal to those of example field ``12``. ====== ================================================== @@ -6096,8 +6100,9 @@ def example_domain(n, _func=example_field): ``12`` A global HEALPix grid with "nested" indices at refinement level 1. - ``13`` A global HEALPix grid with "nested_unique" indices - representing refinement levels 1 and 2. + ``13`` A global HEALPix Multi-Order Coverage grid with + "nested_unique" indices representing refinement + levels 1 and 2. ====== ================================================== See the examples for details. From 095fe916565de09e1cdf16d5546f41734e7b863e Mon Sep 17 00:00:00 2001 From: David Hassell Date: Wed, 23 Jul 2025 08:31:01 +0100 Subject: [PATCH 13/24] dev --- cfdm/data/data.py | 3 +++ cfdm/examplefield.py | 17 +++++++++-------- cfdm/read_write/netcdf/netcdfread.py | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/cfdm/data/data.py b/cfdm/data/data.py index 568a55f53..5cb5b26c7 100644 --- a/cfdm/data/data.py +++ b/cfdm/data/data.py @@ -560,6 +560,7 @@ def __float__(self): x.__float__() <==> float(x) """ + # REVIEW: By using 'first_element', delayed operation might not be executed. if self.size != 1: raise TypeError( "only length-1 arrays can be converted to " @@ -765,6 +766,7 @@ def __int__(self): x.__int__() <==> int(x) """ + # REVIEW: By using 'first_element', delayed operation might not be executed. if self.size != 1: raise TypeError( "only length-1 arrays can be converted to " @@ -6549,6 +6551,7 @@ def reshape(self, *shape, merge_chunks=True, limit=None, inplace=False): original_shape = self.shape original_ndim = len(original_shape) + # REVIEW: Can't set '_force_to_memory=False' because FileAraray object can't cope with 'dx.reshape' dx = d.to_dask_array(_force_mask_hardness=False) dx = dx.reshape(*shape, merge_chunks=merge_chunks, limit=limit) d._set_dask(dx, in_memory=None) diff --git a/cfdm/examplefield.py b/cfdm/examplefield.py index 0cc93368d..c74ae1477 100644 --- a/cfdm/examplefield.py +++ b/cfdm/examplefield.py @@ -59,15 +59,16 @@ def example_field(n, _implementation=_implementation): features. ``12`` A global HEALPix grid with "nested" indices at - refinement level 1. The field area-weighted global - latitude-longitude means are equal to those of - example field ``13``. + refinement level 1. The field's area-weighted + global latitude-longitude means are equal to those + of example field ``13``. ``13`` A global HEALPix Multi-Order Coverage grid with "nested_unique" indices representing refinement - levels 1 and 2. The area-weighted global + levels 1 and 2. The field's area-weighted global latitude-longitude means are equal to those of example field ``12``. + ====== ================================================== See the examples for details. @@ -5972,13 +5973,13 @@ def example_fields(*n, _func=example_field): features. ``12`` A global HEALPix grid with "nested" indices at - refinement level 1. The field area-weighted global - latitude-longitude means are equal to those of - example field ``13``. + refinement level 1. The field's area-weighted + global latitude-longitude means are equal to those + of example field ``13``. ``13`` A global HEALPix Multi-Order Coverage grid with "nested_unique" indices representing refinement - levels 1 and 2. The area-weighted global + levels 1 and 2. The field's area-weighted global latitude-longitude means are equal to those of example field ``12``. ====== ================================================== diff --git a/cfdm/read_write/netcdf/netcdfread.py b/cfdm/read_write/netcdf/netcdfread.py index e0dedc3d7..f7cac67d0 100644 --- a/cfdm/read_write/netcdf/netcdfread.py +++ b/cfdm/read_write/netcdf/netcdfread.py @@ -269,6 +269,7 @@ def cf_coordinate_reference_coordinates(self): "latitude", "longitude", ), + "healpix": ("healpix_index", "latitude", "longitude"), "atmosphere_ln_pressure_coordinate": ( "atmosphere_ln_pressure_coordinate", ), @@ -286,7 +287,6 @@ def cf_coordinate_reference_coordinates(self): "ocean_double_sigma_coordinate": ( "ocean_double_sigma_coordinate", ), - "healpix": ("healpix_index", "latitude", "longitude"), } def cf_interpolation_names(self): From a1461f96cf2dab40043d9a3eab8a7fc661854a1e Mon Sep 17 00:00:00 2001 From: David Hassell Date: Wed, 30 Jul 2025 16:28:24 +0100 Subject: [PATCH 14/24] dev --- cfdm/data/data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cfdm/data/data.py b/cfdm/data/data.py index 5cb5b26c7..85a54fb6b 100644 --- a/cfdm/data/data.py +++ b/cfdm/data/data.py @@ -6276,6 +6276,8 @@ def rechunk( {{balance: `bool`, optional}} + {{inplace: `bool`, optional}} + :Returns: `Data` or `None` From 80abbdcdefc66e2e4895cddcd9a8a41ff4e3c0dc Mon Sep 17 00:00:00 2001 From: David Hassell Date: Mon, 4 Aug 2025 11:29:03 +0100 Subject: [PATCH 15/24] dev --- cfdm/examplefield.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cfdm/examplefield.py b/cfdm/examplefield.py index c74ae1477..66968c0e4 100644 --- a/cfdm/examplefield.py +++ b/cfdm/examplefield.py @@ -247,7 +247,7 @@ def example_field(n, _implementation=_implementation): Cell methods : time(2): mean area: mean Dimension coords: time(2) = [2025-06-16 00:00:00, 2025-07-16 12:00:00] proleptic_gregorian : height(1) = [1.5] m - Auxiliary coords: healpix_index(healpix_index(48)) = [0, ..., 47] 1 + : healpix_index(healpix_index(48)) = [0, ..., 47] 1 Coord references: grid_mapping_name:healpix >>> print(cfdm.example_field(13)) @@ -5464,8 +5464,8 @@ def example_field(n, _implementation=_implementation): c.nc_set_dimension("height") field.set_construct(c, key="domainaxis2", copy=False) # - # auxiliary_coordinate: healpix_index - c = AuxiliaryCoordinate() + # dimension_coordinate: healpix_index + c = DimensionCoordinate() c.set_properties({"standard_name": "healpix_index", "units": "1"}) c.nc_set_variable("healpix_index") data = Data( @@ -5524,7 +5524,7 @@ def example_field(n, _implementation=_implementation): ) c.set_data(data) field.set_construct( - c, axes=("domainaxis1",), key="auxiliarycoordinate0", copy=False + c, axes=("domainaxis1",), key="dimensioncoordinate2", copy=False ) # # dimension_coordinate: time @@ -5571,7 +5571,7 @@ def example_field(n, _implementation=_implementation): # coordinate_reference: grid_mapping_name:healpix c = CoordinateReference() c.nc_set_variable("healpix") - c.set_coordinates({"auxiliarycoordinate0"}) + c.set_coordinates({"dimensioncoordinate2"}) d = Datum() d.set_parameters({"earth_radius": 6371000}) c.set_datum(d) @@ -6225,7 +6225,7 @@ def example_domain(n, _func=example_field): >>> print(cfdm.example_domain(12)) Dimension coords: time(2) = [2025-06-16 00:00:00, 2025-07-16 12:00:00] proleptic_gregorian : height(1) = [1.5] m - Auxiliary coords: healpix_index(healpix_index(48)) = [0, ..., 47] 1 + : healpix_index(healpix_index(48)) = [0, ..., 47] 1 Coord references: grid_mapping_name:healpix >>> print(cfdm.example_domain(13)) From 9ad9b99623ba8fd4581cb15583844ad228001e8b Mon Sep 17 00:00:00 2001 From: David Hassell Date: Wed, 6 Aug 2025 09:28:29 +0100 Subject: [PATCH 16/24] dev --- cfdm/data/data.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cfdm/data/data.py b/cfdm/data/data.py index 85a54fb6b..9756a0779 100644 --- a/cfdm/data/data.py +++ b/cfdm/data/data.py @@ -6553,10 +6553,9 @@ def reshape(self, *shape, merge_chunks=True, limit=None, inplace=False): original_shape = self.shape original_ndim = len(original_shape) - # REVIEW: Can't set '_force_to_memory=False' because FileAraray object can't cope with 'dx.reshape' dx = d.to_dask_array(_force_mask_hardness=False) dx = dx.reshape(*shape, merge_chunks=merge_chunks, limit=limit) - d._set_dask(dx, in_memory=None) + d._set_dask(dx, in_memory=True) # Set axis names for the reshaped data if dx.ndim != original_ndim: From e981c47c8269e3b813bcb880f297da01d109e706 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Thu, 14 Aug 2025 09:48:06 +0100 Subject: [PATCH 17/24] fix typos --- cfdm/constructs.py | 4 ++-- cfdm/read_write/netcdf/netcdfwrite.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cfdm/constructs.py b/cfdm/constructs.py index ddd272fc2..38fd11177 100644 --- a/cfdm/constructs.py +++ b/cfdm/constructs.py @@ -296,7 +296,7 @@ def _del_construct(self, key, default=ValueError()): # -------------------------------------------------------- # Since a cell method construct was deleted, check to see # if it was for climatological time, and if so reset the - # climatology status of approriate coordinate constructs. + # climatology status of appropriate coordinate constructs. # -------------------------------------------------------- qualifiers = out.qualifiers() if "within" in qualifiers or "over" in qualifiers: @@ -552,7 +552,7 @@ def _equals_domain_axis( return True def _set_climatology(self, cell_methods=None, coordinates=None): - """Set the climatology flag on approriate coordinate constructs. + """Set the climatology flag on appropriate coordinate constructs. The setting is based on the cell method constructs. diff --git a/cfdm/read_write/netcdf/netcdfwrite.py b/cfdm/read_write/netcdf/netcdfwrite.py index ddc27e5be..bac23b6c8 100644 --- a/cfdm/read_write/netcdf/netcdfwrite.py +++ b/cfdm/read_write/netcdf/netcdfwrite.py @@ -690,7 +690,7 @@ def _write_dimension_coordinate(self, f, key, coord, ncdim, coordinates): create = True # If the dimension coordinate is already in the file but not - # in an approriate group then we have to create a new netCDF + # in an appropriate group then we have to create a new netCDF # variable. This is to prevent a downstream error ocurring # when the parent data variable tries to reference one of its # netCDF dimensions that is not in the same group nor a parent From b0975b9c8e9753d490de74db809635619a7389a0 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Thu, 21 Aug 2025 08:23:01 +0100 Subject: [PATCH 18/24] dev --- cfdm/examplefield.py | 70 ++++++-------------------------------------- 1 file changed, 9 insertions(+), 61 deletions(-) diff --git a/cfdm/examplefield.py b/cfdm/examplefield.py index 66968c0e4..85a0cf1a1 100644 --- a/cfdm/examplefield.py +++ b/cfdm/examplefield.py @@ -1,3 +1,5 @@ +import numpy as np + from .cfdmimplementation import implementation from .functions import CF @@ -247,7 +249,7 @@ def example_field(n, _implementation=_implementation): Cell methods : time(2): mean area: mean Dimension coords: time(2) = [2025-06-16 00:00:00, 2025-07-16 12:00:00] proleptic_gregorian : height(1) = [1.5] m - : healpix_index(healpix_index(48)) = [0, ..., 47] 1 + : healpix_index(healpix_index(48)) = [0, ..., 47] Coord references: grid_mapping_name:healpix >>> print(cfdm.example_field(13)) @@ -257,7 +259,7 @@ def example_field(n, _implementation=_implementation): Cell methods : time(2): mean area: mean Dimension coords: time(2) = [2025-06-16 00:00:00, 2025-07-16 12:00:00] proleptic_gregorian : height(1) = [1.5] m - Auxiliary coords: healpix_index(healpix_index(60)) = [64, ..., 63] 1 + Auxiliary coords: healpix_index(healpix_index(60)) = [64, ..., 63] Coord references: grid_mapping_name:healpix """ @@ -5466,62 +5468,9 @@ def example_field(n, _implementation=_implementation): # # dimension_coordinate: healpix_index c = DimensionCoordinate() - c.set_properties({"standard_name": "healpix_index", "units": "1"}) + c.set_properties({"standard_name": "healpix_index"}) c.nc_set_variable("healpix_index") - data = Data( - [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 45, - 46, - 47, - ], - units="1", - dtype="i4", - ) + data = Data(np.arange(48, dtype="int32")) c.set_data(data) field.set_construct( c, axes=("domainaxis1",), key="dimensioncoordinate2", copy=False @@ -5764,7 +5713,7 @@ def example_field(n, _implementation=_implementation): # # auxiliary_coordinate: healpix_index c = AuxiliaryCoordinate() - c.set_properties({"standard_name": "healpix_index", "units": "1"}) + c.set_properties({"standard_name": "healpix_index"}) c.nc_set_variable("healpix_index") data = Data( [ @@ -5829,7 +5778,6 @@ def example_field(n, _implementation=_implementation): 62, 63, ], - units="1", dtype="i4", ) c.set_data(data) @@ -6225,13 +6173,13 @@ def example_domain(n, _func=example_field): >>> print(cfdm.example_domain(12)) Dimension coords: time(2) = [2025-06-16 00:00:00, 2025-07-16 12:00:00] proleptic_gregorian : height(1) = [1.5] m - : healpix_index(healpix_index(48)) = [0, ..., 47] 1 + : healpix_index(healpix_index(48)) = [0, ..., 47] Coord references: grid_mapping_name:healpix >>> print(cfdm.example_domain(13)) Dimension coords: time(2) = [2025-06-16 00:00:00, 2025-07-16 12:00:00] proleptic_gregorian : height(1) = [1.5] m - Auxiliary coords: healpix_index(healpix_index(60)) = [64, ..., 63] 1 + Auxiliary coords: healpix_index(healpix_index(60)) = [64, ..., 63] Coord references: grid_mapping_name:healpix """ From 8c4ed010ed674f3eeb463d8fe987f2438930c4ee Mon Sep 17 00:00:00 2001 From: David Hassell Date: Thu, 21 Aug 2025 17:38:16 +0100 Subject: [PATCH 19/24] dev --- cfdm/examplefield.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/cfdm/examplefield.py b/cfdm/examplefield.py index 85a0cf1a1..7e2e1eb5d 100644 --- a/cfdm/examplefield.py +++ b/cfdm/examplefield.py @@ -60,16 +60,16 @@ def example_field(n, _implementation=_implementation): ``11`` Discrete sampling geometry (DSG) "trajectory" features. - ``12`` A global HEALPix grid with "nested" indices at - refinement level 1. The field's area-weighted - global latitude-longitude means are equal to those - of example field ``13``. + ``12`` A global HEALPix grid with "nested" indexing + scheme at refinement level 1. The field's + area-weighted global latitude-longitude means are + equal to those of example field ``13``. ``13`` A global HEALPix Multi-Order Coverage grid with - "nested_unique" indices representing refinement - levels 1 and 2. The field's area-weighted global - latitude-longitude means are equal to those of - example field ``12``. + "nested_unique" indexing scheme representing + refinement levels 1 and 2. The field's + area-weighted global latitude-longitude means are + equal to those of example field ``12``. ====== ================================================== @@ -6046,12 +6046,12 @@ def example_domain(n, _func=example_field): ``11`` Discrete sampling geometry (DSG) "trajectory" features. - ``12`` A global HEALPix grid with "nested" indices at - refinement level 1. + ``12`` A global HEALPix grid with "nested" indexing + scheme at refinement level 1. ``13`` A global HEALPix Multi-Order Coverage grid with - "nested_unique" indices representing refinement - levels 1 and 2. + "nested_unique" indexing scheme representing + refinement levels 1 and 2. ====== ================================================== See the examples for details. From ef7e8631ff6d56e1971dcaf8d8791b240a251e8a Mon Sep 17 00:00:00 2001 From: David Hassell Date: Thu, 30 Oct 2025 00:26:37 +0000 Subject: [PATCH 20/24] dev --- Changelog.rst | 10 ++++++++++ cfdm/constructs.py | 3 ++- cfdm/examplefield.py | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Changelog.rst b/Changelog.rst index 3ab3931c6..13a1a3300 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -1,3 +1,13 @@ +Version NEXTVERSION +---------------- + +**????-??-??** + +* Support for HEALPix grids + (https://github.com/NCAS-CMS/cfdm/issues/???) + +---- + Version 1.12.3.1 ---------------- diff --git a/cfdm/constructs.py b/cfdm/constructs.py index 38fd11177..aa6ced303 100644 --- a/cfdm/constructs.py +++ b/cfdm/constructs.py @@ -552,7 +552,8 @@ def _equals_domain_axis( return True def _set_climatology(self, cell_methods=None, coordinates=None): - """Set the climatology flag on appropriate coordinate constructs. + """Set the climatology flag on appropriate coordinate + constructs. The setting is based on the cell method constructs. diff --git a/cfdm/examplefield.py b/cfdm/examplefield.py index 7e2e1eb5d..ad385cbd9 100644 --- a/cfdm/examplefield.py +++ b/cfdm/examplefield.py @@ -6051,7 +6051,7 @@ def example_domain(n, _func=example_field): ``13`` A global HEALPix Multi-Order Coverage grid with "nested_unique" indexing scheme representing - refinement levels 1 and 2. + refinement levels 1 and 2. ====== ================================================== See the examples for details. From 13db6040cd6ccb1a6a2cf008b76db5642296ac7b Mon Sep 17 00:00:00 2001 From: David Hassell Date: Mon, 17 Nov 2025 17:49:38 +0000 Subject: [PATCH 21/24] dev --- cfdm/examplefield.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/cfdm/examplefield.py b/cfdm/examplefield.py index ad385cbd9..3bed78d0a 100644 --- a/cfdm/examplefield.py +++ b/cfdm/examplefield.py @@ -66,11 +66,10 @@ def example_field(n, _implementation=_implementation): equal to those of example field ``13``. ``13`` A global HEALPix Multi-Order Coverage grid with - "nested_unique" indexing scheme representing - refinement levels 1 and 2. The field's - area-weighted global latitude-longitude means are - equal to those of example field ``12``. - + "nuniq" indexing scheme representing refinement + levels 1 and 2. The field's area-weighted global + latitude-longitude means are equal to those of + example field ``12``. ====== ================================================== See the examples for details. @@ -5836,7 +5835,7 @@ def example_field(n, _implementation=_implementation): f = CoordinateConversion() f.set_parameters( { - "indexing_scheme": "nested_unique", + "indexing_scheme": "nuniq", "grid_mapping_name": "healpix", } ) @@ -5926,8 +5925,8 @@ def example_fields(*n, _func=example_field): of example field ``13``. ``13`` A global HEALPix Multi-Order Coverage grid with - "nested_unique" indices representing refinement - levels 1 and 2. The field's area-weighted global + "nuniq" indices representing refinement levels 1 + and 2. The field's area-weighted global latitude-longitude means are equal to those of example field ``12``. ====== ================================================== @@ -6050,8 +6049,8 @@ def example_domain(n, _func=example_field): scheme at refinement level 1. ``13`` A global HEALPix Multi-Order Coverage grid with - "nested_unique" indexing scheme representing - refinement levels 1 and 2. + "nuniq" indexing scheme representing refinement + levels 1 and 2. ====== ================================================== See the examples for details. From 72288818e50b1b916150b44d0dd7b6246e3762e9 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Tue, 25 Nov 2025 15:09:32 +0000 Subject: [PATCH 22/24] dev --- cfdm/data/data.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/cfdm/data/data.py b/cfdm/data/data.py index 8a946396d..4e09db635 100644 --- a/cfdm/data/data.py +++ b/cfdm/data/data.py @@ -558,15 +558,20 @@ def __float__(self): x.__float__() <==> float(x) + **Performance** + + `__float__` causes all delayed operations to be executed, + unless the dask array size is already known to be greater than + 1. + """ - # REVIEW: By using 'first_element', delayed operation might not be executed. if self.size != 1: raise TypeError( "only length-1 arrays can be converted to " f"Python scalars. Got {self}" ) - return float(self.first_element()) + return float(self.array[(0,) * self.ndim]) def __format__(self, format_spec): """Interpret format specifiers for size 1 arrays. @@ -766,15 +771,19 @@ def __int__(self): x.__int__() <==> int(x) + **Performance** + + `__int__` causes all delayed operations to be executed, unless + the dask array size is already known to be greater than 1. + """ - # REVIEW: By using 'first_element', delayed operation might not be executed. if self.size != 1: raise TypeError( "only length-1 arrays can be converted to " f"Python scalars. Got {self}" ) - return int(self.first_element()) + return int(self.array[(0,) * self.ndim]) def __iter__(self): """Called when an iterator is required. From 7913523bff0cf25854a735cfd6b86ddb9c6ab781 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Thu, 27 Nov 2025 16:53:02 +0000 Subject: [PATCH 23/24] dev --- Changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changelog.rst b/Changelog.rst index 97fc89aee..a0a29c00e 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -4,7 +4,7 @@ Version NEXTVERSION **2026-01-??** * Support for HEALPix grids - (https://github.com/NCAS-CMS/cfdm/issues/???) + (https://github.com/NCAS-CMS/cfdm/issues/370) * Reduce the time taken to import `cfdm` (https://github.com/NCAS-CMS/cfdm/issues/361) From 2cbf2abcc536b1cf97abcb77082c4c676e5379ed Mon Sep 17 00:00:00 2001 From: David Hassell Date: Fri, 9 Jan 2026 11:45:02 +0000 Subject: [PATCH 24/24] typo --- Changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.rst b/Changelog.rst index a2b0c0a52..52335bec1 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -21,6 +21,7 @@ Version 1.12.4.0 * New function `cfdm.dataset_flatten` that replaces the deprecated `cfdm.netcdf_flatten` (https://github.com/NCAS-CMS/cfdm/issues/355) * Reduce the time taken to import `cfdm` + (https://github.com/NCAS-CMS/cfdm/issues/361) * New optional dependency: ``zarr>=3.1.3`` * Removed dependency (now optional): ``zarr>=3.0.8``