diff --git a/.github/workflows/ci-manifest.yml b/.github/workflows/ci-manifest.yml index a7785a1c50..2e0fa85a91 100644 --- a/.github/workflows/ci-manifest.yml +++ b/.github/workflows/ci-manifest.yml @@ -23,4 +23,4 @@ concurrency: jobs: manifest: name: "check-manifest" - uses: scitools/workflows/.github/workflows/ci-manifest.yml@2024.04.0 + uses: scitools/workflows/.github/workflows/ci-manifest.yml@2024.04.1 diff --git a/.github/workflows/refresh-lockfiles.yml b/.github/workflows/refresh-lockfiles.yml index b16bd72690..1c3bd74e13 100644 --- a/.github/workflows/refresh-lockfiles.yml +++ b/.github/workflows/refresh-lockfiles.yml @@ -14,5 +14,5 @@ on: jobs: refresh_lockfiles: - uses: scitools/workflows/.github/workflows/refresh-lockfiles.yml@2024.04.0 + uses: scitools/workflows/.github/workflows/refresh-lockfiles.yml@2024.04.1 secrets: inherit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7735f193db..57a08127a1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: no-commit-to-branch - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.3.5" + rev: "v0.3.7" hooks: - id: ruff types: [file, python] diff --git a/docs/src/common_links.inc b/docs/src/common_links.inc index 476796396f..3a329fe40b 100644 --- a/docs/src/common_links.inc +++ b/docs/src/common_links.inc @@ -35,6 +35,7 @@ .. _ruff: https://github.com/astral-sh/ruff .. _SciTools: https://github.com/SciTools .. _scitools-iris: https://pypi.org/project/scitools-iris/ +.. _Shapely: https://shapely.readthedocs.io/en/stable/index.html .. _sphinx: https://www.sphinx-doc.org/en/master/ .. _sphinx-apidoc: https://github.com/sphinx-contrib/apidoc .. _test-iris-imagehash: https://github.com/SciTools/test-iris-imagehash diff --git a/docs/src/userguide/subsetting_a_cube.rst b/docs/src/userguide/subsetting_a_cube.rst index 27a223042e..7440d22adc 100644 --- a/docs/src/userguide/subsetting_a_cube.rst +++ b/docs/src/userguide/subsetting_a_cube.rst @@ -1,3 +1,5 @@ +.. include:: ../common_links.inc + .. _subsetting_a_cube: ================= @@ -338,26 +340,32 @@ Cube Masking Masking from a shapefile ^^^^^^^^^^^^^^^^^^^^^^^^ -Often we want to perform so kind of analysis over a complex geographical feature - only over land points or sea points: -or over a continent, a country, a river watershed or administrative region. These geographical features can often be described by shapefiles. -Shapefiles are a file format first developed for GIS software in the 1990s, and now `Natural Earth`_ maintain a large freely usable database of shapefiles of many geographical and poltical divisions, -accessible via cartopy. Users may also provide their own custom shapefiles. +Often we want to perform some kind of analysis over a complex geographical feature e.g., + +- over only land/sea points +- over a continent, country, or list of countries +- over a river watershed or lake basin +- over states or administrative regions of a country + +These geographical features can often be described by `ESRI Shapefiles`_. Shapefiles are a file format first developed for GIS software in the 1990s, and `Natural Earth`_ maintain a large freely usable database of shapefiles of many geographical and political divisions, +accessible via `cartopy`_. Users may also provide their own custom shapefiles for `cartopy`_ to load, or their own underlying geometry in the same format as a shapefile geometry. These shapefiles can be used to mask an iris cube, so that any data outside the bounds of the shapefile is hidden from further analysis or plotting. -First, we load the correct shapefile from NaturalEarth via the `Cartopy`_ instructions. Here we get one for Brazil. -The `.geometry` attribute of the records in the reader contain the shapely polygon we're interested in - once we have those we just need to provide them to -the :class:`iris.util.mask_cube_from_shapefile` function. Once plotted, we can see that only our area of interest remains in the data. +First, we load the correct shapefile from NaturalEarth via the `Cartopy_shapereader`_ instructions. Here we get one for Brazil. +The `.geometry` attribute of the records in the reader contain the `Shapely`_ polygon we're interested in. They contain the coordinates that define the polygon (or set of lines) being masked +and once we have those we just need to provide them to the :class:`iris.util.mask_cube_from_shapefile` function. +This returns a copy of the cube with a :class:`numpy.masked_array` as the data payload, where the data outside the shape is hidden by the masked array. We can see this in the following example. .. plot:: userguide/plotting_examples/masking_brazil_plot.py :include-source: -We can see that the dimensions of the cube haven't changed - the plot is still global. But only the data over Brazil is plotted - the rest is masked. +We can see that the dimensions of the cube haven't changed - the plot is still global. But only the data over Brazil is plotted - the rest has been masked out. .. note:: While Iris will try to dynamically adjust the shapefile to mask cubes of different projections, it can struggle with rotated pole projections and cubes with Meridians not at 0° - Converting your Cube's coordinate system may help if you get a fully masked cube from this function. + Converting your Cube's coordinate system may help if you get a fully masked cube as the output from this function unexpectedly. Cube Iteration @@ -473,5 +481,8 @@ Similarly, Iris cubes have indexing capability:: print(cube[1, ::-2]) -.. _Cartopy: https://scitools.org.uk/cartopy/docs/latest/tutorials/using_the_shapereader.html#id1 +.. _Cartopy_shapereader: https://scitools.org.uk/cartopy/docs/latest/tutorials/using_the_shapereader.html#id1 .. _Natural Earth: https://www.naturalearthdata.com/ +.. _ESRI Shapefiles: https://support.esri.com/en-us/technical-paper/esri-shapefile-technical-description-279 + + diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index fc189a4495..29818ded77 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -77,7 +77,7 @@ This document explains the changes made to Iris for this release 📚 Documentation ================ -#. N/A +#. `@hsteptoe`_ added more detailed examples to :class:`~iris.cube.Cube` functions :func:`~iris.cube.Cube.slices` and :func:`~iris.cube.Cube.slices_over`. (:pull:`5735`) 💼 Internal @@ -90,7 +90,7 @@ This document explains the changes made to Iris for this release Whatsnew author names (@github name) in alphabetical order. Note that, core dev names are automatically included by the common_links.inc: - +.. _@hsteptoe: https://github.com/hsteptoe .. comment diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 6a7c68267c..ef9d707d64 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -3403,10 +3403,37 @@ def slices_over(self, ref_to_slice): Examples -------- - For example, to get all subcubes along the time dimension:: - - for sub_cube in cube.slices_over('time'): - print(sub_cube) + For example, for a cube with dimensions `realization`, `time`, `latitude` and + `longitude`: + + >>> fname = iris.sample_data_path('GloSea4', 'ensemble_01[01].pp') + >>> cube = iris.load_cube(fname, 'surface_temperature') + >>> print(cube.summary(shorten=True)) + surface_temperature / (K) (realization: 2; time: 6; latitude: 145; longitude: 192) + + To get all 12x2D longitude/latitude subcubes: + + >>> for sub_cube in cube.slices_over(['realization', 'time']): + ... print(sub_cube.summary(shorten=True)) + surface_temperature / (K) (latitude: 145; longitude: 192) + surface_temperature / (K) (latitude: 145; longitude: 192) + surface_temperature / (K) (latitude: 145; longitude: 192) + surface_temperature / (K) (latitude: 145; longitude: 192) + surface_temperature / (K) (latitude: 145; longitude: 192) + surface_temperature / (K) (latitude: 145; longitude: 192) + surface_temperature / (K) (latitude: 145; longitude: 192) + surface_temperature / (K) (latitude: 145; longitude: 192) + surface_temperature / (K) (latitude: 145; longitude: 192) + surface_temperature / (K) (latitude: 145; longitude: 192) + surface_temperature / (K) (latitude: 145; longitude: 192) + surface_temperature / (K) (latitude: 145; longitude: 192) + + To get realizations as 2x3D separate subcubes, using the `realization` dimension index: + + >>> for sub_cube in cube.slices_over(0): + ... print(sub_cube.summary(shorten=True)) + surface_temperature / (K) (time: 6; latitude: 145; longitude: 192) + surface_temperature / (K) (time: 6; latitude: 145; longitude: 192) Notes ----- @@ -3421,7 +3448,7 @@ def slices_over(self, ref_to_slice): iris.cube.Cube.slices : Return an iterator of all subcubes given the coordinates or dimension indices. - """ + """ # noqa: D214, D406, D407, D410, D411 # Required to handle a mix between types. if _is_single_item(ref_to_slice): ref_to_slice = [ref_to_slice] @@ -3463,10 +3490,9 @@ def slices(self, ref_to_slice, ordered=True): A mix of input types can also be provided. They must all be orthogonal (i.e. point to different dimensions). ordered : bool, default=True - If True, the order which the coords to slice or data_dims - are given will be the order in which they represent the data in - the resulting cube slices. If False, the order will follow that of - the source cube. Default is True. + If True, subcube dimensions are ordered to match the dimension order + in `ref_to_slice`. If False, the order will follow that of + the source cube. Returns ------- @@ -3474,18 +3500,51 @@ def slices(self, ref_to_slice, ordered=True): Examples -------- - For example, to get all 2d longitude/latitude subcubes from a - multi-dimensional cube:: - - for sub_cube in cube.slices(['longitude', 'latitude']): - print(sub_cube) + For example, for a cube with dimensions `realization`, `time`, `latitude` and + `longitude`: + + >>> fname = iris.sample_data_path('GloSea4', 'ensemble_01[01].pp') + >>> cube = iris.load_cube(fname, 'surface_temperature') + >>> print(cube.summary(shorten=True)) + surface_temperature / (K) (realization: 2; time: 6; latitude: 145; longitude: 192) + + To get all 12x2D longitude/latitude subcubes: + + >>> for sub_cube in cube.slices(['longitude', 'latitude']): + ... print(sub_cube.summary(shorten=True)) + surface_temperature / (K) (longitude: 192; latitude: 145) + surface_temperature / (K) (longitude: 192; latitude: 145) + surface_temperature / (K) (longitude: 192; latitude: 145) + surface_temperature / (K) (longitude: 192; latitude: 145) + surface_temperature / (K) (longitude: 192; latitude: 145) + surface_temperature / (K) (longitude: 192; latitude: 145) + surface_temperature / (K) (longitude: 192; latitude: 145) + surface_temperature / (K) (longitude: 192; latitude: 145) + surface_temperature / (K) (longitude: 192; latitude: 145) + surface_temperature / (K) (longitude: 192; latitude: 145) + surface_temperature / (K) (longitude: 192; latitude: 145) + surface_temperature / (K) (longitude: 192; latitude: 145) + + + .. warning:: + Note that the dimension order returned in the sub_cubes matches the order specified + in the ``cube.slices`` call, *not* the order of the dimensions in the original cube. + + To get all realizations as 2x3D separate subcubes, using the `time`, `latitude` + and `longitude` dimensions' indices: + + >>> for sub_cube in cube.slices([1, 2, 3]): + ... print(sub_cube.summary(shorten=True)) + surface_temperature / (K) (time: 6; latitude: 145; longitude: 192) + surface_temperature / (K) (time: 6; latitude: 145; longitude: 192) See Also -------- - iris.cube.Cube.slices : - Return an iterator of all subcubes given the coordinates or dimension indices. + iris.cube.Cube.slices_over : + Return an iterator of all subcubes along a given coordinate or + dimension index. - """ + """ # noqa: D214, D406, D407, D410, D411 if not isinstance(ordered, bool): raise TypeError("'ordered' argument to slices must be boolean.") @@ -4512,8 +4571,8 @@ def rolling_window(self, coord, aggregator, window, **kwargs): -------- >>> import iris, iris.analysis >>> fname = iris.sample_data_path('GloSea4', 'ensemble_010.pp') - >>> air_press = iris.load_cube(fname, 'surface_temperature') - >>> print(air_press) + >>> cube = iris.load_cube(fname, 'surface_temperature') + >>> print(cube) surface_temperature / (K) \ (time: 6; latitude: 145; longitude: 192) Dimension coordinates: @@ -4537,7 +4596,7 @@ def rolling_window(self, coord, aggregator, window, **kwargs): 'Data from Met Office Unified Model' um_version '7.6' - >>> print(air_press.rolling_window('time', iris.analysis.MEAN, 3)) + >>> print(cube.rolling_window('time', iris.analysis.MEAN, 3)) surface_temperature / (K) \ (time: 4; latitude: 145; longitude: 192) Dimension coordinates: