Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
54a5049
Plan PR 6, stage 1: framing, constraints and file structure
trexfeathers Oct 15, 2026
c9c191a
Plan PR 6, stage 2: the two changes to generic code
trexfeathers Oct 15, 2026
ead323b
Plan PR 6, stage 3: four more findings, and the encoding module
trexfeathers Oct 15, 2026
a04f7c3
Plan PR 6, stage 4: opening for writing, and creating arrays
trexfeathers Oct 15, 2026
308d4af
Plan PR 6, stage 5: writing values, and the saver itself
trexfeathers Oct 15, 2026
06232f2
Plan PR 6, stage 6: deferred writes, and reaching iris.save
trexfeathers Oct 15, 2026
421779c
Plan PR 6, stage 7: round trips, cross-reader conformance and docs
trexfeathers Oct 15, 2026
9ba7c26
Plan PR 6, stage 8: review focus, the record, and self-review
trexfeathers Oct 15, 2026
3097f91
Plan PR 6: create the cf/saver test package in Task 2
trexfeathers Oct 15, 2026
825c0ee
Move the CF attribute handlers into the CF package
trexfeathers Oct 15, 2026
dfe388b
Let grid-mapping parameters reach a store that is not netCDF
trexfeathers Oct 15, 2026
49f7dce
Add the Zarr write-side value conversions
trexfeathers Oct 15, 2026
a01b52a
Open a Zarr store for writing
trexfeathers Oct 15, 2026
c4d67ef
Close the code fence that swallowed half the plan
trexfeathers Oct 15, 2026
1110e8f
Declare Zarr dimensions, and create Zarr arrays
trexfeathers Oct 15, 2026
c27331f
Let a string variable default its own fill value
trexfeathers Oct 15, 2026
cf83c8d
Write Zarr values, attributes and consolidated metadata
trexfeathers Oct 15, 2026
758de26
Add the Zarr saver, and iris.fileformats.zarr.save
trexfeathers Oct 15, 2026
7b109dc
Fix four review findings in the Zarr saver
trexfeathers Oct 15, 2026
648ffde
Defer Zarr writes, aligned to the write grid
trexfeathers Oct 15, 2026
895522c
Fix review findings in the Zarr saver's write alignment and masking
trexfeathers Oct 15, 2026
d6fa6bf
Reach the Zarr saver from iris.save
trexfeathers Oct 15, 2026
2e67f10
Check the zarr version floor in the save path too
trexfeathers Oct 15, 2026
c03f064
Test the Zarr save round trip, and what the format does not promise
trexfeathers Oct 15, 2026
fbc9bf6
Apply CF packing before writing to a Zarr array
trexfeathers Oct 15, 2026
e3f464d
Split the mesh round trip into what saving proves and what loading ca…
trexfeathers Oct 15, 2026
5c909fe
Refuse non-integer packing, and make pack() exactly netCDF4's transform
trexfeathers Oct 15, 2026
101ede1
Make three round-trip tests able to fail for what they claim to test
trexfeathers Oct 15, 2026
3d6e544
Check that xarray reads what Iris writes, and Iris reads xarray's
trexfeathers Oct 15, 2026
48fdd33
Prove the missing_value half of the same encoding decision
trexfeathers Oct 15, 2026
e141c2b
Document saving to Zarr
trexfeathers Oct 15, 2026
0971838
Fix Zarr saving docs: a working mode example, scoped collision
trexfeathers Oct 15, 2026
fb44d17
Mention native Zarr saving in iris.save's own docstring
trexfeathers Oct 15, 2026
fd80ff1
Record Zarr saving in the design document
trexfeathers Oct 15, 2026
f75429d
Fix three errors in the Zarr saving record
trexfeathers Oct 15, 2026
244ace9
Stop mode="a" truncating a cube whose dimension name collides
trexfeathers Oct 15, 2026
712f40a
Share the cube-attribute marshalling with the netCDF saver
trexfeathers Oct 15, 2026
5d4248a
Accept the attribute and dtype forms netCDF already accepts
trexfeathers Oct 15, 2026
c1bd03b
Refuse the keywords that did nothing, and say what is true
trexfeathers Oct 15, 2026
8f04b73
Record the whole-branch review fixes in the design document
trexfeathers Oct 15, 2026
dcb0b8f
Say why a masked string cannot be stored, and record it
trexfeathers Oct 15, 2026
d8c39b4
Default the Zarr version to None, and say what each mode does
trexfeathers Oct 21, 2026
b07d831
Drop the parameter count from the grid-mapping prose
trexfeathers Oct 21, 2026
335ced7
Write grid-mapping parameters through a function, not a proxy
trexfeathers Oct 22, 2026
6a321da
Reach the Zarr saver through an iris.io wrapper, like every other format
trexfeathers Oct 22, 2026
2ec7040
Recognise the Zarr saver when a caller supplies it directly
trexfeathers Oct 23, 2026
977b405
Clearer _declared_dimensions docstring.
trexfeathers Oct 23, 2026
0461319
Appropriate use of currently.
trexfeathers Oct 23, 2026
f703791
Trim justification from the Zarr docs, and box the two hazards.
trexfeathers Oct 23, 2026
6deaa46
Pin the three Zarr saving behaviours review found untested.
trexfeathers Oct 23, 2026
bed8ea1
Better error message for writing version 2.
trexfeathers Oct 23, 2026
b946433
Move the Zarr interrupted-save warning to where saving is introduced.
trexfeathers Oct 23, 2026
2363c45
Mesh todo comment.
trexfeathers Oct 23, 2026
4ed606a
Fix test.
trexfeathers Oct 23, 2026
29f3cf3
Fix docs build.
trexfeathers Oct 23, 2026
9fd0c0a
Follow brownfield's names, and drop the mesh xfail it fixed
trexfeathers Oct 23, 2026
ef9a2e4
Record PR 6 as raised, and what the rebase met
trexfeathers Oct 23, 2026
e43b982
Include Xarray test dependency in lock files.
trexfeathers Oct 23, 2026
e81e0a5
Record that the lock files now carry xarray
trexfeathers Oct 23, 2026
119595b
Merge brownfield, for the cartopy 0.26 fixes
trexfeathers Oct 23, 2026
f8e039b
Parenthesise a string the newer ruff refuses
trexfeathers Oct 23, 2026
3a78ea2
Guard the Zarr import in a netCDF test module
trexfeathers Oct 23, 2026
d5d5688
Catch a dimension size an existing array already holds
trexfeathers Oct 23, 2026
5028ef3
Make create_variable's refusals match what it can do
trexfeathers Oct 23, 2026
f9cc6c1
Drop a stale consolidated document when consolidation is declined
trexfeathers Oct 23, 2026
c964fc9
Run the shared write contract against Zarr
trexfeathers Oct 23, 2026
5610711
Reconcile the design document with itself
trexfeathers Oct 23, 2026
c0c0868
Answer supports_dtype from what Zarr has, not what it lacks
trexfeathers Oct 23, 2026
12fef38
Correct the header, and say in it that it tracks §12.1
trexfeathers Oct 23, 2026
df4f443
Create Zarr arrays in the platform's own byte order
trexfeathers Oct 23, 2026
82c6783
Move the Zarr dtype facts out of the comments
trexfeathers Oct 23, 2026
73bf860
Stop the design document citing rounds of work
trexfeathers Oct 23, 2026
f594416
Reconcile three Zarr details with their reviews
trexfeathers Oct 23, 2026
1bfd0bf
Spell the dtype-kind sets as tuples, not strings
trexfeathers Oct 26, 2026
43597bf
Cut the argument out of two keyword comments
trexfeathers Oct 26, 2026
fd26535
Raise the Zarr version floor to 3.1.0
trexfeathers Oct 26, 2026
1008ab8
Give CountingStore the read-only copy zarr 3.1.6 supplies
trexfeathers Oct 26, 2026
7c3827a
Answer supports_dtype with True, and delete the dtype table
trexfeathers Oct 26, 2026
b02b342
Pin where the Zarr version floor sits
trexfeathers Oct 26, 2026
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
3 changes: 2 additions & 1 deletion .github/WRITING.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ never fixed at all.
- **Never cite what decays silently**: a line number in another file, a count
of call sites, a time relative to writing ("currently", "this PR", "the new
behaviour"). Nothing checks any of them, and your "now" is never the
reader's.
reader's. However, user-facing messages such as warnings/errors remain
appropriate places for phrases such as "currently" or "in future".
- **Never cite an artefact of how the work was done**: a review finding, a
task or plan number, a phase, a checklist item. These resolve to nothing for
a later reader. Cite an issue, a pull request, a design document, or a
Expand Down
6 changes: 4 additions & 2 deletions .hooks/check_docs_page_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,10 @@ def check_docstring(text: str, expected_type: str) -> list[str]:

if docstring is None:
return [
"Missing a module docstring, so the page generated from it has no "
"sphinx-needs item."
(
"Missing a module docstring, so the page generated from it has "
"no sphinx-needs item."
)
]

return check_item(docstring, expected_type)
Expand Down
148 changes: 139 additions & 9 deletions docs/src/user_manual/explanation/zarr_io.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.. explanation:: Zarr I/O Handling in Iris
:tags: topic_load_save

Read about how Iris loads Zarr stores.
Read about how Iris loads and saves Zarr stores.

.. include:: ../../common_links.inc

Expand All @@ -19,9 +19,9 @@ version 3 stores natively, through
`zarr-python <https://zarr.readthedocs.io/>`_.

``zarr`` is an **optional dependency**, and Iris requires at least version
3.0.8. Install it to load Zarr stores::
3.1.0. Install it to load Zarr stores::

conda install -c conda-forge "zarr>=3.0.8"
conda install -c conda-forge "zarr>=3.1.0"

Iris without it installs and runs normally, raising an ``ImportError`` that
names the package and its minimum required version if you ask it to load a
Expand All @@ -44,16 +44,122 @@ A store holding UGRID mesh data loads as a mesh cube, the same as the
equivalent netCDF file would. :func:`iris.mesh.load_meshes` reads a store
too.

.. note::

A Zarr attribute may be any JSON value, so a cube loaded from a store
may carry one that netCDF cannot hold. Saving that cube as netCDF fails.
See :ref:`zarr_io_attributes`.

Saving
------

Iris does not save to a native Zarr store. ``iris.save(cube, "out.zarr")``
raises ``ValueError: Cannot save; no saver``, naming neither Zarr nor an
alternative. Save to netCDF instead, or write Zarr through netCDF-c's NCZarr
driver, covered below in :ref:`zarr_io_nczarr`.
A store is a directory, or a URL, so pass it where you would pass a file::

iris.save(cubes, "/data/forecast.zarr")

Iris writes Zarr **version 3** stores. A whole :class:`~iris.cube.CubeList`
goes into one store, which is opened once. Saving through netCDF-c's NCZarr
driver writes **version 2** instead, covered below in :ref:`zarr_io_nczarr`.

.. warning::

A save that is interrupted leaves a partial store behind, and Iris cannot
roll it back. The damage is quiet -- see
:ref:`zarr_io_interrupted_save`.

Unlike the netCDF saver, this one does **not** overwrite. ``iris.save`` raises
if the target already exists, because replacing a store means deleting many
objects one at a time, and an interrupted replacement destroys what was there
without completing what replaces it. Say so explicitly to opt in::

iris.save(cubes, "/data/forecast.zarr", mode="w") # replace
iris.save(more_cubes, "/data/forecast.zarr", mode="a") # add what is not there

``mode="a"`` adds a *new* array or group to a store that already holds others.
It never extends, rewrites, resizes or re-chunks an array that is already
there. Saving a cube whose own data variable already exists in the target
group is an error rather than an overwrite, so saving the same cube twice with
``mode="a"`` raises rather than duplicating it. Appending to data is not
supported, for Zarr or for netCDF: deciding whether incoming data is
compatible with what a store already holds is a data-model problem, and the
storage format does not solve it.

Everything else a cube carries -- its coordinates, bounds and cell measures --
is not matched against what the store already holds. Adding a cube to a store
that already carries a ``latitude`` array writes a second one, named
``latitude_0``, rather than reusing the first. Deciding whether two
coordinates are "the same" across unequal values, differing units and
different calendars is its own piece of work, so Iris does not attempt it.

Storage keywords
^^^^^^^^^^^^^^^^

The Zarr saver takes **zarr-python's** vocabulary, not netCDF's::

iris.save(
cube,
"out.zarr",
chunks=(1, 50, 50),
shards=(4, 100, 100),
compressors=[zarr.codecs.BloscCodec(cname="zstd")],
)

``chunks``, ``shards``, ``compressors``, ``filters`` and ``serializer`` are
passed through as zarr-python defines them. A netCDF keyword such as
``zlib=True`` raises :class:`TypeError` naming the Zarr replacement where there
is one, and saying there is none where there is not. ``order`` raises too: it
is zarr-python's own keyword, but it has no effect on a version 3 array.

For a cube holding **lazy** data, ``chunks`` defaults to the cube's own lazy
chunking. Real data has no chunking of its own to inherit, and gets
zarr-python's automatic chunking instead. Passing ``chunks`` explicitly
overrides either case. The default is also dropped when you pass ``shards``: a
shard holds a whole number of inner chunks, and zarr-python picks the inner
chunk in that case.

Deferred saving
^^^^^^^^^^^^^^^

``compute=False`` returns a :class:`dask.delayed.Delayed` that writes the lazy
data when computed, and consolidates the store's metadata after the last chunk::

result = iris.save(cubes, "deferred.zarr", compute=False)
result.compute()

A cube loaded from Zarr may carry an attribute netCDF cannot hold, which
fails the save. See :ref:`zarr_io_attributes`.
Consolidated metadata
^^^^^^^^^^^^^^^^^^^^^

Iris writes a consolidated metadata document at the store's root when the save
completes, which makes a remote store usable: a reader fetches one object
instead of one per array. Pass ``consolidate_metadata=False`` to decline.

When Iris adds to a store that already carries one, it rewrites it -- or, if
you declined consolidation for that save, removes it. A consolidated document
that does not mention what was just written is what a default reader believes,
so a stale one hides data; having none at all is slower to read but tells the
truth.

.. _zarr_io_interrupted_save:

What an interrupted save leaves behind
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

An interrupted save leaves a partial store, and Iris offers no rollback. Zarr
has no multi-object transaction: a store's metadata and its chunks are separate
objects, so a save interrupted between them leaves the two disagreeing, and a
missing chunk is not an error -- it reads back as the fill value, with no
warning. The usual mitigation, writing to a temporary name and renaming on
success, is not adopted: a directory rename is atomic on a local filesystem but
object stores have no equivalent, so the guarantee would evaporate exactly
where Zarr is most used. Callers who need transactional writes want
`Icechunk <https://icechunk.io/>`_.

Meshes
^^^^^^

A cube with an unstructured :class:`~iris.mesh.MeshXY` saves correctly: the
store carries the full CF-UGRID structure, the same mesh, topology and
connectivity variables a netCDF save would write.

Remote stores
-------------
Expand Down Expand Up @@ -87,6 +193,16 @@ To read another group, call the Zarr loader directly::
)
)

Saving takes a group more directly: ``iris.save`` itself takes a ``group=``
keyword, naming a ``/``-separated path within the store::

iris.save(cubes, "/data/sentinel.zarr", group="measurements/reflectance/r10m")

Passing ``group=`` implies ``mode="a"``, because writing a group into a store
means the store may already hold others. ``iris.load`` and ``iris.load_cube``
have no equivalent keyword: reading a group other than the root means calling
the Zarr loader directly, as above.

Dimension names
---------------

Expand Down Expand Up @@ -129,6 +245,20 @@ That one case can mask a genuine zero, so Iris warns when it happens, naming
the variable. Setting a ``_FillValue`` attribute on the array prevents both
the masking and the warning.

On save, Iris writes the missing-value declaration as well as the storage fill:
a masked cube gets a ``_FillValue`` attribute even when it carried none of its
own, because on version 3 the attribute is what declares missing data and the
storage ``fill_value`` is not. Without it, every masked point would read back
unmasked. Pass ``fill_value=`` to choose the sentinel; the default is the same
one netCDF uses for that dtype.

A floating ``_FillValue`` is written as the base64 of a little-endian
``float64``, which is what xarray and the wider Zarr ecosystem read. This
deviates from CF section 2.5.1, which asks for a value of the variable's own
type. The deviation is deliberate and load-bearing: a plain JSON number there
makes the whole store unreadable by default xarray, which is a worse outcome
than a convention deviation that every reader understands.

Chunking
--------

Expand Down
12 changes: 11 additions & 1 deletion docs/src/user_manual/tutorial/s3_io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
:tags: topic_load_save

A lesson on using s3-fuse with Iris to load/save netCDF, PP and GRIB
data from/to S3 buckets, and on reading a Zarr store from S3 directly.
data from/to S3 buckets, and on loading from and saving to a Zarr store
on S3 directly.

.. _s3_io:

Expand Down Expand Up @@ -33,6 +34,15 @@ the objects it needs over the network::
That needs ``s3fs`` installed and configured, and no mount. If your data is in
Zarr, prefer this to the FUSE approach described below.

Saving works the same way::

iris.save(cubes, "s3://bucket/forecast.zarr")

The same store library and credentials apply, and the same defaults: an
existing store is not overwritten unless you pass ``mode="w"``, and the
metadata is consolidated when the save completes, which is what makes the store
cheap to open again over the network.

Other filetypes via a virtual file system
-----------------------------------------

Expand Down
Loading
Loading