diff --git a/docs/src/common_links.inc b/docs/src/common_links.inc index f84b8bf1ba..8c1e846531 100644 --- a/docs/src/common_links.inc +++ b/docs/src/common_links.inc @@ -49,6 +49,7 @@ .. _SciTools Contributor's License Agreement (CLA): https://cla-assistant.io/SciTools/ .. _extlinks: https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html .. _Diataxis: https://diataxis.fr/ +.. _CF Conventions: https://cfconventions.org/ .. comment diff --git a/docs/src/index.rst b/docs/src/index.rst index 5059bcd062..2098d2f2cb 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -8,7 +8,7 @@ Iris **A powerful, format-agnostic, community-driven Python package for analysing and visualising Earth science data.** -Iris implements a data model based on the `CF conventions `_ +Iris implements a data model based on the `CF conventions`_ giving you a powerful, format-agnostic interface for working with your data. It excels when working with multi-dimensional Earth Science data, where tabular representations become unwieldy and inefficient. diff --git a/docs/src/user_manual/explanation/um_files_loading.rst b/docs/src/user_manual/explanation/um_files_loading.rst index 8c6718805a..2b1b6706f3 100644 --- a/docs/src/user_manual/explanation/um_files_loading.rst +++ b/docs/src/user_manual/explanation/um_files_loading.rst @@ -344,6 +344,8 @@ Time Information **Details** +More about units: :doc:`../explanation/units`. + In Iris (as in CF) times and time intervals are both expressed as simple numbers, following the approach of the `UDUNITS project `_. diff --git a/docs/src/user_manual/explanation/units.rst b/docs/src/user_manual/explanation/units.rst new file mode 100644 index 0000000000..ba5f35f9be --- /dev/null +++ b/docs/src/user_manual/explanation/units.rst @@ -0,0 +1,150 @@ +.. explanation:: Units + :tags: topic_data_model + + Read about how Iris objects such as Cubes and Coordinates are assigned scientific units. + +.. include:: /common_links.inc + +===== +Units +===== + +.. todo:: cross reference in navigating_a_cube, why_iris, cube_maths, um_files_loading, glossary + +A measure such as 'temperature' cannot just be described by a number - e.g. +273.15 - it must also be associated with a unit - e.g. 'Kelvin' - to give it +meaning. This is a core element of the :term:`CF Conventions`, and so is +fundamental to the design of Iris: + +- All data model objects - e.g. :term:`Cube`, :term:`Coordinate`; anything + based on :class:`~iris.common.mixin.CFVariableMixin` - have a + :attr:`~iris.common.mixin.CFVariableMixin.units` attribute. +- Operations on Iris objects are 'recorded' in their units attribute. E.g. + a :class:`~iris.cube.Cube` with units of ``m/s`` multiplied by a + :class:`~iris.cube.Cube` with units of ``s`` will have units of ``m`` after + the operation. Read more: :ref:`cube_maths_combining_units`. +- Operations between :class:`~iris.cube.Cube` objects - e.g. arithmetic or + merging - are only permitted if the units are compatible. Read more: + :doc:`/user_manual/section_indexes/metadata_arithmetic`, + :ref:`merge_and_concat`. + +In addition to the CF Conventions, the Iris ecosystem defines two further units: + +- ``no-unit`` - the associated data is not suitable for describing with a unit. +- ``unknown`` - the unit describing the associated data cannot be determined. + If a calculation is prevented because it would result in inappropriate units, + it may be forced by setting the units of the original cubes to be + ``"unknown"``. + +The Units API +------------- + +.. testsetup:: + + from iris.cube import Cube + from iris.experimental.units import USE_CFPINT + my_cube = Cube([0, 1, 2]) + +Setting the :attr:`~iris.common.mixin.CFVariableMixin.units` on an object is +simple - you provide a string and Iris will parse and store it appropriately: + + >>> my_cube.units = 'm/s' + >>> print(my_cube.units) + m/s + >>> print(repr(my_cube.units)) + Unit('m/s') + +As well as a string, you can assign objects directly from the unit-handling +library and these will also be parsed. If you want to test this: the function +:func:`iris.common.units.make_unit` outputs the same object that would be +created when assigning an input to the +:attr:`~iris.common.mixin.CFVariableMixin.units` attribute. + +You can choose which library you want Iris to use for unit parsing and operations: + +.. list-table:: Choice of Cf-units or Pint in Iris + :header-rows: 1 + :stub-columns: 1 + + * - Library + - Iris Class + - Parent Class + - Status @ ``2026-04-13`` + * - `Cf-units`_ + - :class:`iris.common.units.CfUnit` + - :class:`cf_units.Unit` + - Default + * - `Pint`_ + - :class:`iris.common.units.PintUnit` + - :class:`pint.Unit` + - Experimental + +You make this choice using the :data:`iris.experimental.units.USE_CFPINT` flag: + + >>> with USE_CFPINT.context(): + ... my_cube.units = "m/s" + >>> print(my_cube.units) + m s-1 + >>> print(repr(my_cube.units)) + Unit('meter / second') + +In both cases Iris internals ensure the unit is CF-compliant. CF-compliance is +standard behaviour for Cf-units; while the Pint case currently (``2026-04-13``) +uses the `Cfpint`_ library with some Iris-specific modifications. The intent is a +**seamless user experience** +regardless of the underlying library, hopefully allowing a +**gradual transition to `Pint`_**, +bringing Iris users closer to the wider scientific Python ecosystem. + +To aid the above transition, any compatibility features have been marked as +deprecated, with advice on how to update your code. Example: +:attr:`iris.common.units.PintUnit.is_dimensionless`. + +The Libraries Underneath +------------------------ + +The :term:`CF Conventions` officially define unit behaviour as follows: + +- Reference time units: e.g. ``days since 2000-12-01``, behaviour + described directly in the `CF Conventions`_ pages; section 4.4. +- All other units: behaviour provided by the `UDUNITS2`_ package, with a small + number of modifications described in the `CF Conventions`_ pages; section 3.1. + +Since reference time units are not based on existing software, the rules given +by the CF Conventions are implemented by the `Cftime`_ Python package. + +A full software implementation of CF Conventions units must therefore combine: + +- cftime +- UDUNITS2 +- The specific UDUNITS2 modifications + +The two most established packages for this are: `Cf-units`_ and `Cfunits`_. + +Both of these packages have +struggled to find ways of combining Python with UDUNITS2, especially when it +comes to installation. These struggles have inspired attempts to implement +UDUNITS2 in Python or via the `Pint`_ Python package, all of which are experimental +at time of writing (``2026-04-13``). + +Iris' hope for the future is for a mature and well maintained implementation of +**CF-compliant Pint**. The CF Conventions are the de facto standard for storing +atmospheric/oceanographic data, while Pint is the most widely accepted Python +package for units. Being Pint-based (rather than UDUNITS2-based), should +improve the Iris user experience: + +- Better interoperability/collaboration with the wider scientific Python ecosystem. +- A better maintained units library - more features, more support. +- Simpler installation. Specifically: this would allow Iris to be fully + installed via Pip (UDUNITS2 needs Conda for installation). + +This is why we are future-proofing Iris to support both Cf-units and Pint, and +why we are working with international collaborators to establish a +CF-compliant Pint implementation. + +.. _Cf-units: https://github.com/SciTools/cf-units +.. _Cfunits: https://github.com/NCAS-CMS/cfunits +.. _Pint: https://github.com/hgrecco/pint +.. _Cfpint: https://github.com/SciTools/cfpint +.. _UDUNITS2: https://www.unidata.ucar.edu/software/udunits +.. _Cftime: https://github.com/unidata/cftime diff --git a/docs/src/user_manual/explanation/why_iris.rst b/docs/src/user_manual/explanation/why_iris.rst index d7df72d8ad..865427591e 100644 --- a/docs/src/user_manual/explanation/why_iris.rst +++ b/docs/src/user_manual/explanation/why_iris.rst @@ -17,7 +17,7 @@ It excels when working with multi-dimensional Earth Science data, where tabular representations become unwieldy and inefficient. `CF Standard names `_, -`units `_, and coordinate metadata +units, and coordinate metadata are built into Iris, giving you a rich and expressive interface for maintaining an accurate representation of your data. Its treatment of data and associated metadata as first-class objects includes: diff --git a/docs/src/user_manual/how_to/navigating_a_cube.rst b/docs/src/user_manual/how_to/navigating_a_cube.rst index 2e4f3c0ca9..1b85f3a59f 100644 --- a/docs/src/user_manual/how_to/navigating_a_cube.rst +++ b/docs/src/user_manual/how_to/navigating_a_cube.rst @@ -70,8 +70,8 @@ and :attr:`Cube.units ` respectively:: print(cube.units) Interrogating these with the standard :func:`type` function will tell you that ``standard_name`` and ``long_name`` -are either a string or ``None``, and ``units`` is an instance of :class:`iris.unit.Unit`. A more in depth discussion on -the cube units and their functional effects can be found at the end of :doc:`../tutorial/cube_maths`. +are either a string or ``None``, and ``units`` is a specialised unit-handling +object. Read more about units here: :doc:`../explanation/units`. You can access a string representing the "name" of a cube with the :meth:`Cube.name() ` method:: diff --git a/docs/src/user_manual/reference/glossary.rst b/docs/src/user_manual/reference/glossary.rst index 3c04b1756b..7ebe9601c6 100644 --- a/docs/src/user_manual/reference/glossary.rst +++ b/docs/src/user_manual/reference/glossary.rst @@ -203,7 +203,7 @@ Glossary The unit with which the :term:`phenomenon` is measured e.g. m / sec. | **Related:** :term:`Cube` - | **More information:** :doc:`../explanation/iris_cubes` + | **More information:** :doc:`../explanation/units` :doc:`../explanation/iris_cubes` | Xarray diff --git a/docs/src/user_manual/section_indexes/general.rst b/docs/src/user_manual/section_indexes/general.rst index f3d28824a1..43d235f481 100644 --- a/docs/src/user_manual/section_indexes/general.rst +++ b/docs/src/user_manual/section_indexes/general.rst @@ -26,3 +26,4 @@ Below are any pages not belonging to any other User Manual section. ../explanation/um_files_loading ../explanation/ux_guide ../explanation/which_regridder_to_use + ../explanation/units diff --git a/docs/src/user_manual/tutorial/cube_maths.rst b/docs/src/user_manual/tutorial/cube_maths.rst index 817b496686..f9e38b9a35 100644 --- a/docs/src/user_manual/tutorial/cube_maths.rst +++ b/docs/src/user_manual/tutorial/cube_maths.rst @@ -17,7 +17,7 @@ this attribute can then be manipulated directly:: cube.data -= 273.15 The problem with manipulating the data directly is that other metadata may -become inconsistent; in this case the units of the cube are no longer what was +become inconsistent; in this case the :term:`unit` of the cube are no longer what was intended. This example could be rectified by changing the units attribute:: cube.units = 'celsius' @@ -237,6 +237,8 @@ The result could now be plotted using the guidance provided in the Combining Units --------------- +More about units: :doc:`../explanation/units`. + It should be noted that when combining cubes by multiplication, division or power operations, the resulting cube will have a unit which is an appropriate combination of the constituent units. In the above example, since ``pressure`` diff --git a/docs/src/user_manual/tutorial/merge_and_concat.rst b/docs/src/user_manual/tutorial/merge_and_concat.rst index 3f717f064e..ddd008fed1 100644 --- a/docs/src/user_manual/tutorial/merge_and_concat.rst +++ b/docs/src/user_manual/tutorial/merge_and_concat.rst @@ -593,7 +593,7 @@ Concatenate **Time Units** -Differences in the units of the time coordinates of the input cubes probably cause +Differences in the :term:`unit` of the time coordinates of the input cubes probably cause the greatest amount of concatenate-related difficulties. In recognition of this, Iris has a helper function, :func:`~iris.util.unify_time_units`, to apply a common time unit to all the input cubes. diff --git a/lib/iris/common/mixin.py b/lib/iris/common/mixin.py index 856019a704..ffea5d43a2 100644 --- a/lib/iris/common/mixin.py +++ b/lib/iris/common/mixin.py @@ -14,7 +14,7 @@ from collections.abc import Mapping from functools import wraps -from typing import Any +from typing import TYPE_CHECKING, Any # Optional imports : actually only needed for type hints try: @@ -34,6 +34,9 @@ from .metadata import BaseMetadata from .units import make_unit +if TYPE_CHECKING: + from units import CfUnit, PintUnit + __all__ = ["CFVariableMixin", "LimitedAttributeDict"] @@ -229,11 +232,12 @@ def var_name(self, name: str | None) -> None: self._metadata_manager.var_name = name @property - def units(self) -> cf_units.Unit | cfpint.Unit: + def units(self) -> CfUnit | PintUnit: """The S.I. unit of the object. - If not ``None``, this is always an Iris Unit type - either :class:`CfUnit` or - :class:`cfpint.Unit`. See :func:`iris.common.units.make_unit`. + If not ``None``, this is always an Iris Unit type - either + :class:`~iris.common.units.CfUnit` or :class:`~iris.common.units.PintUnit`. + See :func:`iris.common.units.make_unit`. """ return self._metadata_manager.units diff --git a/lib/iris/common/units/__init__.py b/lib/iris/common/units/__init__.py index b408ba4c9e..46cf61c449 100644 --- a/lib/iris/common/units/__init__.py +++ b/lib/iris/common/units/__init__.py @@ -2,7 +2,13 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Generic definition of units as used in Iris.""" +"""Generic definition of units as used in Iris. + +.. z_reference:: iris.common.units + :tags: topic_data_model + + API reference +""" from typing import Any diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 44be3a63d7..819f3da157 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1293,7 +1293,7 @@ def __init__( #: The "standard name" for the Cube's phenomenon. self.standard_name = standard_name - #: An instance of :class:`cf_units.Unit` describing the Cube's data. + #: An instance of :class:`~iris.common.units.CfUnit` or :class:`~iris.common.units.PintUnit` describing the Cube's data. self.units = units #: The "long name" for the Cube's phenomenon. diff --git a/lib/iris/experimental/units.py b/lib/iris/experimental/units.py index 952840e696..6487d73991 100644 --- a/lib/iris/experimental/units.py +++ b/lib/iris/experimental/units.py @@ -3,7 +3,13 @@ # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Control for unit types.""" +"""Control for unit types. + +.. z_reference:: iris.experimental.units + :tags: topic_experimental;topic_data_model + + API reference +""" from contextlib import contextmanager import threading