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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ Version NEXTVERSION

**2026-??-??**

* New methods to convert to `xarray`: `cf.Field.to_xarray`,
`cf.FieldList.to_xarray`, `cf.Domain.to_xarray`, and
`cf.DomainList.to_xarray`
(https://github.com/NCAS-CMS/cf-python/issues/933)
* New output format for `cf.write` that creates an `xarray` dataset in
memory: ``'XARRAY'``
(https://github.com/NCAS-CMS/cf-python/issues/933)
* New keyword to `cf.read`: ``filesystem``
(https://github.com/NCAS-CMS/cf-python/issues/931)
* New keyword parameter to `cf.Data.compute`: ``persist``
Expand All @@ -24,8 +31,11 @@ Version NEXTVERSION
(https://github.com/NCAS-CMS/cfdm/issues/391)
* Fix for subspacing with cyclic `cf.wi` and `cf.wo` arguments
(https://github.com/NCAS-CMS/cf-python/issues/887)
* New optional dependency: ``xarray>=2026.2.0``
* Changed dependency: ``cfdm>=1.13.1.0, <1.13.2.0``

----

Version 3.19.0
--------------

Expand Down
15 changes: 11 additions & 4 deletions cf/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -6903,18 +6903,25 @@ def collapse(

# ---------------------------------------------------------
# Update dimension coordinates, auxiliary coordinates,
# cell measures and domain ancillaries
# cell measures, domain ancillaries, domain_topologies,
# and cell connectivities.
# ---------------------------------------------------------
for axis, domain_axis in collapse_axes.items():
# Ignore axes which are already size 1
size = domain_axis.get_size()
if size == 1:
continue

# REMOVE all cell measures and domain ancillaries
# which span this axis
# REMOVE all cell measures, domain ancillaries,
# domain_topologies, and cell connectivities which
# span this axis
c = f.constructs.filter(
filter_by_type=("cell_measure", "domain_ancillary"),
filter_by_type=(
"cell_measure",
"domain_ancillary",
"domain_topology",
"cell_connectivity",
),
filter_by_axis=(axis,),
axis_mode="or",
todict=True,
Expand Down
37 changes: 37 additions & 0 deletions cf/mixin/fielddomain.py
Original file line number Diff line number Diff line change
Expand Up @@ -3044,6 +3044,43 @@ def set_coordinate_reference(

return self.set_construct(ref, key=key, copy=False)

def to_xarray(self, group=True):
"""Convert the {{class}} to an `xarray` dataset.

{{cf_xarray description}}

Note that ``ds = f.to_xarray()`` is identical to ``ds =
cf.write(f, fmt='XARRAY')``; and multiple {{class_lower}}s may
be written to the same `xarray` dataset with
`cf.{{class}}List.to_xarray`, or with `cf.write` (e.g. ``ds =
cf.write([f, g], fmt='XARRAY')``). Also, `cf.write` allows a
mixture a mixture of fields and domains to be written to the
same `xarray` dataset.

.. versionadded:: NEXTVERSION

.. seealso:: `cf.{{class}}List.to_xarray`, `cf.write`

:Parameter:

group: `bool`, optional

If False then create a "flat" dataset, i.e. one with
only the root group, regardless of any group structure
specified by the netCDF interfaces of the
{{class_lower}} and its components. If True (the
default) then any sub-groups will be created and
populated.

:Returns:

{{Returns xarray}}

"""
from cf.read_write import write

return write(self, fmt="XARRAY", group=group)

# ----------------------------------------------------------------
# Aliases
# ----------------------------------------------------------------
Expand Down
32 changes: 32 additions & 0 deletions cf/mixin/fielddomainlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,3 +331,35 @@ def select_by_rank(self, *ranks):
"""

return type(self)(f for f in self if f.match_by_rank(*ranks))

def to_xarray(self, group=True):
"""Convert the list elements to an `xarray` Dataset.

{{cf_xarray description}}

Note that ``ds = fl.to_xarray()`` is identical to ``ds =
cf.write(fl, fmt='XARRAY')``. Also, `cfdm.write` allows a
mixture a mixture of fields and domains to be written to the
same `xarray` dataset.

.. versionadded:: NEXTVERSION

.. seealso:: `cf.write`

:Parameter:

group: `bool`, optional
If False then create a "flat" dataset, i.e. one with
only the root group, regardless of any group structure
specified by the netCDF interfaces of the list
elements and their components. If True (the default)
then any sub-groups will be created and populated.

:Returns:

{{Returns xarray}}

"""
from cf.read_write import write

return write(self, fmt="XARRAY", group=group)
145 changes: 145 additions & 0 deletions cf/test/create_test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -2228,6 +2228,150 @@ def _make_ugrid_2(filename):
return filename


def _make_ugrid_3(filename):
"""Create a UGRID mesh topology and no fields/domains."""
n = netCDF4.Dataset(filename, "w")

n.Conventions = f"CF-{VN}"

n.createDimension("nMesh3_node", 7)
n.createDimension("nMesh3_edge", 9)
n.createDimension("nMesh3_face", 3)
n.createDimension("connectivity2", 2)
n.createDimension("connectivity4", 4)
n.createDimension("connectivity5", 5)

Mesh3 = n.createVariable("Mesh3", "i4", ())
Mesh3.cf_role = "mesh_topology"
Mesh3.topology_dimension = 2
Mesh3.node_coordinates = "Mesh3_node_x Mesh3_node_y"
Mesh3.face_node_connectivity = "Mesh3_face_nodes"
Mesh3.edge_node_connectivity = "Mesh3_edge_nodes"
Mesh3.face_dimension = "nMesh3_face"
Mesh3.edge_dimension = "nMesh3_edge"
Mesh3.face_face_connectivity = "Mesh3_face_links"
Mesh3.edge_edge_connectivity = "Mesh3_edge_links"

# Node
Mesh3_node_x = n.createVariable("Mesh3_node_x", "f4", ("nMesh3_node",))
Mesh3_node_x.standard_name = "longitude"
Mesh3_node_x.units = "degrees_east"
Mesh3_node_x[...] = [-45, -43, -45, -43, -45, -43, -40]

Mesh3_node_y = n.createVariable("Mesh3_node_y", "f4", ("nMesh3_node",))
Mesh3_node_y.standard_name = "latitude"
Mesh3_node_y.units = "degrees_north"
Mesh3_node_y[...] = [35, 35, 33, 33, 31, 31, 34]

Mesh3_edge_nodes = n.createVariable(
"Mesh3_edge_nodes", "i4", ("nMesh3_edge", "connectivity2")
)
Mesh3_edge_nodes.long_name = "Maps every edge to its two nodes"
Mesh3_edge_nodes[...] = [
[1, 6],
[3, 6],
[3, 1],
[0, 1],
[2, 0],
[2, 3],
[2, 4],
[5, 4],
[3, 5],
]

# Face
Mesh3_face_x = n.createVariable(
"Mesh3_face_x", "f8", ("nMesh3_face",), fill_value=-99
)
Mesh3_face_x.standard_name = "longitude"
Mesh3_face_x.units = "degrees_east"
Mesh3_face_x[...] = [-44, -44, -42]

Mesh3_face_y = n.createVariable(
"Mesh3_face_y", "f8", ("nMesh3_face",), fill_value=-99
)
Mesh3_face_y.standard_name = "latitude"
Mesh3_face_y.units = "degrees_north"
Mesh3_face_y[...] = [34, 32, 34]

Mesh3_face_nodes = n.createVariable(
"Mesh3_face_nodes",
"i4",
("nMesh3_face", "connectivity4"),
fill_value=-99,
)
Mesh3_face_nodes.long_name = "Maps every face to its corner nodes"
Mesh3_face_nodes[...] = [[2, 3, 1, 0], [4, 5, 3, 2], [6, 1, 3, -99]]

Mesh3_face_links = n.createVariable(
"Mesh3_face_links",
"i4",
("nMesh3_face", "connectivity4"),
fill_value=-99,
)
Mesh3_face_links.long_name = "neighbour faces for faces"
Mesh3_face_links[...] = [
[1, 2, -99, -99],
[0, -99, -99, -99],
[0, -99, -99, -99],
]

# Edge
Mesh3_edge_x = n.createVariable(
"Mesh3_edge_x", "f8", ("nMesh3_edge",), fill_value=-99
)
Mesh3_edge_x.standard_name = "longitude"
Mesh3_edge_x.units = "degrees_east"
Mesh3_edge_x[...] = [-41.5, -41.5, -43, -44, -45, -44, -45, -44, -43]

Mesh3_edge_y = n.createVariable(
"Mesh3_edge_y", "f8", ("nMesh3_edge",), fill_value=-99
)
Mesh3_edge_y.standard_name = "latitude"
Mesh3_edge_y.units = "degrees_north"
Mesh3_edge_y[...] = [34.5, 33.5, 34, 35, 34, 33, 32, 31, 32]

Mesh3_edge_links = n.createVariable(
"Mesh3_edge_links",
"i4",
("nMesh3_edge", "connectivity5"),
fill_value=-99,
)
Mesh3_edge_links.long_name = "neighbour edges for edges"
Mesh3_edge_links[...] = [
[1, 2, 3, -99, -99],
[0, 2, 5, 8, -99],
[3, 0, 1, 5, 8],
[4, 2, 0, -99, -99],
[
3,
5,
6,
-99,
-99,
],
[4, 6, 2, 1, 8],
[
4,
5,
7,
-99,
-99,
],
[
6,
8,
-99,
-99,
-99,
],
[7, 5, 2, 1, -99],
]

n.close()
return filename


def _make_aggregation_value(filename):
"""Create an aggregation variable with 'unique_values'."""
n = netCDF4.Dataset(filename, "w")
Expand Down Expand Up @@ -2341,6 +2485,7 @@ def _make_aggregation_value(filename):

ugrid_1 = _make_ugrid_1("ugrid_1.nc")
ugrid_2 = _make_ugrid_2("ugrid_2.nc")
ugrid_3 = _make_ugrid_3("ugrid_3.nc")

aggregation_value = _make_aggregation_value("aggregation_value.nc")

Expand Down
Loading