Skip to content
13 changes: 13 additions & 0 deletions Changelog.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
Version ???
-----------
* 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 optional dependency: ``xarray>=2026.7.0``

----

Version 3.20.1
--------------

Expand Down
37 changes: 37 additions & 0 deletions cf/mixin/fielddomain.py
Original file line number Diff line number Diff line change
Expand Up @@ -3884,6 +3884,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 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 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)
178 changes: 178 additions & 0 deletions cf/test/test_xarray.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import datetime
import faulthandler
import unittest

faulthandler.enable() # to debug seg faults and timeouts

import xarray as xr

import cf


class xarrayTest(unittest.TestCase):
"""Unit test for converting to xarray."""

def setUp(self):
"""Preparations called immediately before each test method."""
# Disable log messages to silence expected warnings
cf.log_level("DISABLE")
# Note: to enable all messages for given methods, lines or
# calls (those without a 'verbose' option to do the same)
# e.g. to debug them, wrap them (for methods, start-to-end
# internally) as follows:
#
# cf.LOG_LEVEL('DEBUG')
# < ... test code ... >
# cf.log_level('DISABLE')

def test_Field_to_xarray(self):
"""Test Field.to_xarray."""
fields = cf.example_fields()

# Write each field to a different xarray dataset
for f in fields:
ds = f.to_xarray()
self.assertIsInstance(ds, xr.Dataset)
str(ds)
self.assertIn("Conventions", ds.attrs)

# Write all fields to one xarray dataset
ds = cf.write(fields, fmt="XARRAY")
self.assertIsInstance(ds, xr.Dataset)
str(ds)

def test_Domain_to_xarray(self):
"""Test Domain.to_xarray."""
domains = [f.domain for f in cf.example_fields()]

# Write each domain to a different xarray dataset
for d in domains:
ds = d.to_xarray()
self.assertIsInstance(ds, xr.Dataset)
str(ds)

# Write all domains to one xarray dataset
ds = cf.write(domains, fmt="XARRAY")
self.assertIsInstance(ds, xr.Dataset)
str(ds)

def test_FieldList_to_xarray(self):
"""Test FieldList.to_xarray."""
fields = cf.example_fields()
ds = fields.to_xarray()
self.assertIsInstance(ds, xr.Dataset)
str(ds)

def test_DomainList_to_xarray(self):
"""Test DomainList.to_xarray."""
domains = cf.DomainList([f.domain for f in cf.example_fields()])
ds = domains.to_xarray()
self.assertIsInstance(ds, xr.Dataset)
str(ds)

def test_FieldList_to_xarray_from_disk(self):
"""Test FieldList.to_xarray from datasets read from disk."""
for dataset in (
"example_field_0.nc",
"example_field_0.zarr2",
"example_field_0.zarr3",
"gathered.nc",
"DSG_timeSeries_contiguous.nc",
"DSG_timeSeries_indexed.nc",
"DSG_timeSeriesProfile_indexed_contiguous.nc",
"parent.nc",
"external.nc",
"external_missing.nc",
"combined.nc",
"geometry_1.nc",
"geometry_2.nc",
"geometry_3.nc",
"geometry_4.nc",
"geometry_interior_ring.nc",
"geometry_interior_ring_2.nc",
"string_char.nc",
"subsampled_2.nc",
"ugrid_1.nc",
"ugrid_2.nc",
"ugrid_3.nc",
"test_file.nc",
"extra_data.pp",
"file1.pp",
"umfile.pp",
"wgdos_packed.pp",
):
f = cf.read(dataset)
ds = f.to_xarray()
self.assertIsInstance(ds, xr.Dataset)
str(ds)

def test_Field_to_xarray_groups(self):
"""Test Field.to_xarray with groups."""
f = cf.example_field(0)
g = f.copy()

ds = f.to_xarray()
self.assertIsInstance(ds, xr.Dataset)

f.nc_set_variable("/forecast/model/q2")
ds = f.to_xarray()
self.assertIsInstance(ds, xr.DataTree)
self.assertIn("q2", ds["/forecast/model"])
str(ds)

# group=True
ds = cf.write([f, g], fmt="XARRAY")
self.assertIsInstance(ds, xr.DataTree)
str(ds)

self.assertIn("q", ds)
self.assertIn("q2", ds["/forecast/model"])

# group=False
ds = f.to_xarray(group=False)
self.assertIsInstance(ds, xr.Dataset)
self.assertIn("q2", ds)
str(ds)

ds = cf.write([f, g], fmt="XARRAY", group=False)
self.assertIsInstance(ds, xr.Dataset)
str(ds)

self.assertIn("q", ds)
self.assertIn("q2", ds)

def test_FieldList_to_xarray_groups(self):
"""Test FieldList.to_xarray with groups."""
f = cf.example_fields(0)

ds = f.to_xarray()
self.assertIsInstance(ds, xr.Dataset)

f[0].nc_set_variable("/forecast/model/q2")
ds = f.to_xarray()
self.assertIsInstance(ds, xr.DataTree)
self.assertIn("q2", ds["/forecast/model"])
str(ds)

ds = f.to_xarray(group=False)
self.assertIsInstance(ds, xr.Dataset)
self.assertIn("q2", ds)
str(ds)

def test_Field_to_xarray_aggregation(self):
"""Test Field.to_xarray with aggregated data."""
f = cf.read("example_field_0.nc")[0]
dsf = f.to_xarray()

self.assertEqual(f.shape[0], 5)
g = cf.aggregate([f[:3], f[3:]])[0]
dsg = g.to_xarray()

self.assertTrue(dsg.equals(dsf))


if __name__ == "__main__":
print("Run date:", datetime.datetime.now())
cf.environment()
print("")
unittest.main(verbosity=2)
14 changes: 14 additions & 0 deletions docs/source/class/cf.Domain.rst
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,20 @@ NetCDF
~cf.Domain.nc_set_global_attribute
~cf.Domain.nc_set_global_attributes

.. _Domain-xarray:

xarray
------

.. rubric:: Methods

.. autosummary::
:nosignatures:
:toctree: ../method/
:template: method.rst

~cf.Domain.to_xarray

Groups
^^^^^^

Expand Down
14 changes: 14 additions & 0 deletions docs/source/class/cf.Field.rst
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,20 @@ NetCDF
~cf.Field.nc_dataset_chunksizes
~cf.Field.nc_set_dataset_chunksizes

.. _Field-xarray:

xarray
------

.. rubric:: Methods

.. autosummary::
:nosignatures:
:toctree: ../method/
:template: method.rst

~cf.Field.to_xarray

Groups
^^^^^^

Expand Down
14 changes: 14 additions & 0 deletions docs/source/class/cf.FieldList.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ Comparison

~cf.FieldList.equals

.. _FieldList-xarray:

xarray
------

.. rubric:: Methods

.. autosummary::
:nosignatures:
:toctree: ../method/
:template: method.rst

~cf.FieldList.to_xarray

Miscellaneous
-------------

Expand Down
6 changes: 6 additions & 0 deletions docs/source/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,12 @@ environments for which these features are not required.

For reading and writing Zarr datasets.

.. rubric:: xarray

* `xarray <https://pypi.org/project/xarray>`_, version 2026.7.0 or newer.

For converting fields and domains to `xarray` datasets in memory.

.. rubric:: Regridding

* `esmpy <https://earthsystemmodeling.org/esmpy/>`_, version 8.7.0 or
Expand Down
Loading