Skip to content

Commit fe2013a

Browse files
authored
Merge pull request #6801 from stephenworsley/v3.14.x.mergeback
V3.14.x.mergeback
2 parents c794e8e + 77a62b4 commit fe2013a

File tree

5 files changed

+192
-32
lines changed

5 files changed

+192
-32
lines changed

docs/src/userguide/plotting_examples/masking_brazil_plot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import iris
77
import iris.quickplot as qplt
8-
from iris.util import mask_cube_from_shapefile
8+
from iris.util import mask_cube_from_shape
99

1010
country_shp_reader = shpreader.Reader(
1111
shpreader.natural_earth(
@@ -19,7 +19,7 @@
1919
][0]
2020

2121
cube = iris.load_cube(iris.sample_data_path("air_temp.pp"))
22-
brazil_cube = mask_cube_from_shapefile(cube, brazil_shp)
22+
brazil_cube = mask_cube_from_shape(cube=cube, shape=brazil_shp)
2323

2424
qplt.pcolormesh(brazil_cube)
2525
plt.show()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Masking data with a stereographic projection and plotted with quickplot."""
2+
3+
import cartopy.crs as ccrs
4+
import cartopy.io.shapereader as shpreader
5+
import matplotlib.pyplot as plt
6+
7+
import iris
8+
import iris.quickplot as qplt
9+
from iris.util import mask_cube_from_shape
10+
11+
# Define WGS84 coordinate reference system
12+
wgs84 = ccrs.PlateCarree(globe=ccrs.Globe(ellipse="WGS84"))
13+
14+
country_shp_reader = shpreader.Reader(
15+
shpreader.natural_earth(
16+
resolution="10m", category="cultural", name="admin_0_countries"
17+
)
18+
)
19+
uk_shp = [
20+
country.geometry
21+
for country in country_shp_reader.records()
22+
if "United Kingdom" in country.attributes["NAME_LONG"]
23+
][0]
24+
25+
cube = iris.load_cube(iris.sample_data_path("toa_brightness_stereographic.nc"))
26+
uk_cube = mask_cube_from_shape(cube=cube, shape=uk_shp, shape_crs=wgs84)
27+
28+
plt.figure(figsize=(12, 5))
29+
# Plot #1: original data
30+
ax = plt.subplot(131)
31+
qplt.pcolormesh(cube, vmin=210, vmax=330)
32+
plt.gca().coastlines()
33+
34+
# Plot #2: UK geometry
35+
ax = plt.subplot(132, title="Mask Geometry", projection=ccrs.Orthographic(-5, 45))
36+
ax.set_extent([-12, 5, 49, 61])
37+
ax.add_geometries(
38+
[
39+
uk_shp,
40+
],
41+
crs=wgs84,
42+
edgecolor="None",
43+
facecolor="orange",
44+
)
45+
plt.gca().coastlines()
46+
47+
# Plot #3 masked data
48+
ax = plt.subplot(133)
49+
qplt.pcolormesh(uk_cube, vmin=210, vmax=330)
50+
plt.gca().coastlines()
51+
52+
plt.tight_layout()
53+
plt.show()

docs/src/userguide/subsetting_a_cube.rst

Lines changed: 114 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -332,9 +332,55 @@ on bounds can be done in the following way::
332332
The above example constrains to cells where either the upper or lower bound occur
333333
after 1st January 2008.
334334

335+
.. _cube_masking:
336+
335337
Cube Masking
336338
--------------
337339

340+
Masking a cube allows you to hide unwanted data points without changing the
341+
shape or size of the cube. This can be achieved by two methods:
342+
343+
1. Masking a cube using a boolean mask array via :func:`iris.util.mask_cube`.
344+
2. Masking a cube using a shapefile via :func:`iris.util.mask_cube_from_shape`.
345+
346+
.. _masking-from-boolean:
347+
348+
Masking a cube using a boolean mask array
349+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
350+
351+
The :func:`iris.util.mask_cube` function allows you to mask unwanted data points
352+
in a cube using a boolean mask array. The mask array must have the same shape as
353+
the data array in the cube, with ``True`` values indicating points to be masked.
354+
355+
For example, the mask could be based on a threshold value. In the following
356+
example we mask all points in a cube where the air potential temperature is
357+
greater than 290 K.
358+
359+
>>> filename = iris.sample_data_path('uk_hires.pp')
360+
>>> cube_temp = iris.load_cube(filename, 'air_potential_temperature')
361+
>>> print(cube_temp.summary(shorten=True))
362+
air_potential_temperature / (K) (time: 3; model_level_number: 7; grid_latitude: 204; grid_longitude: 187)
363+
>>> type(cube_temp.data)
364+
<class 'numpy.ndarray'>
365+
366+
Note that this example cube has 4 dimensions: time and model level number in
367+
addition to grid latitude, and grid longitude. The data array associated with
368+
the cube is a regular :py:class:`numpy.ndarray`.
369+
370+
We can build a boolean mask array by applying a condition to the cube's data
371+
array:
372+
373+
>>> mask = cube_temp.data > 290
374+
>>> cube_masked = iris.util.mask_cube(cube_temp, mask)
375+
>>> print(cube_masked.summary(shorten=True))
376+
air_potential_temperature / (K) (time: 3; model_level_number: 7; grid_latitude: 204; grid_longitude: 187)
377+
>>> type(cube_masked.data)
378+
<class 'numpy.ma.MaskedArray'>
379+
380+
The masked cube will have the same shape and coordinates as the original cube,
381+
but the data array now includes an associated boolean mask, and the cube's
382+
`data` property is now a :py:class:`numpy.ma.MaskedArray`.
383+
338384
.. _masking-from-shapefile:
339385

340386
Masking from a shapefile
@@ -346,26 +392,77 @@ Often we want to perform some kind of analysis over a complex geographical featu
346392
- over a continent, country, or list of countries
347393
- over a river watershed or lake basin
348394
- over states or administrative regions of a country
395+
- extract data along the trajectory of a storm track
396+
- extract data at specific points of interest such as cities or weather stations
397+
398+
These geographical features can often be described by `ESRI Shapefiles`_.
399+
Shapefiles are a file format first developed for GIS software in the 1990s, and
400+
`Natural Earth`_ maintain a large freely usable database of shapefiles of many
401+
geographical and political divisions, accessible via `cartopy`_. Users may also
402+
provide or create their own custom shapefiles for `cartopy`_ to load, or or any
403+
other source that can be interpreted as a `shapely.Geometry`_ object, such as
404+
shapes encoded in a geoJSON or KML file.
405+
406+
The :func:`iris.util.mask_cube_from_shape` function facilitates cube masking
407+
from shapefiles. Once a shape is loaded as a `shapely.Geometry`, and passed to
408+
the function, any data outside the bounds of the shape geometry is masked.
409+
410+
.. important::
411+
For best masking results, both the cube **and** masking shape (geometry)
412+
should have a coordinate reference system (CRS) defined. Note that the CRS of
413+
the masking geometry must be provided explicitly to :func:`iris.util.mask_cube_from_shape`
414+
(via the ``shape_crs`` keyword argument), whereas the :class:`iris.cube.Cube`
415+
CRS is read from the cube itself.
416+
417+
The cube **must** have a :attr:`iris.coords.Coord.coord_system` defined
418+
otherwise an error will be raised.
349419

350-
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,
351-
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.
420+
.. note::
421+
Because shape vectors are inherently Cartesian in nature, they contain no
422+
inherent understanding of the spherical geometry underpinning geographic
423+
coordinate systems. For this reason, **shapefiles or shape vectors that
424+
cross the antimeridian or poles are not supported by this function** to
425+
avoid unexpected masking behaviour.
426+
427+
For shapes that do cross these boundaries, this function expects the user
428+
to undertake fixes upstream of Iris, using tools like `GDAL`_ or
429+
`antimeridian`_ to ensure correct geometry wrapping.
430+
431+
As an introductory example, we load a shapefile of country borders for Brazil
432+
from `Natural Earth`_ via the `Cartopy_shapereader`_. The `.geometry` attribute
433+
of the records in the reader contain the `Shapely`_ polygon we're interested in.
434+
They contain the coordinates that define the polygon being masked under the
435+
WGS84 coordinate system. We pass this to the :class:`iris.util.mask_cube_from_shape`
436+
function and this returns a copy of the cube with a :py:class:`numpy.masked_array`
437+
as the data payload, where the data outside the shape is hidden by the masked
438+
array.
352439

353-
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.
440+
.. plot:: userguide/plotting_examples/masking_brazil_plot.py
441+
:include-source:
354442

355-
First, we load the correct shapefile from NaturalEarth via the `Cartopy_shapereader`_ instructions. Here we get one for Brazil.
356-
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
357-
and once we have those we just need to provide them to the :class:`iris.util.mask_cube_from_shapefile` function.
358-
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.
443+
We can see that the dimensions of the cube haven't changed - the plot still has
444+
a global extent. But only the data over Brazil is plotted - the rest has been
445+
masked out.
359446

447+
.. important::
448+
Because we do not explicitly pass a CRS for the shape geometry to
449+
:func:`iris.util.mask_cube_from_shape`, the function assumes the geometry
450+
has the same CRS as the cube.
360451

361-
.. plot:: userguide/plotting_examples/masking_brazil_plot.py
362-
:include-source:
452+
However, a :class:`iris.cube.Cube` and `Shapely`_ geometry do not need to have
453+
the same CRS, as long as both have a CRS defined. Where the CRS of the
454+
:class:`iris.cube.Cube` and geometry differ, :func:`iris.util.mask_cube_from_shape`
455+
will reproject the geometry (via `GDAL`_) onto the cube's CRS prior to masking.
456+
The masked cube will be returned in the same CRS as the input cube.
363457

364-
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.
458+
In the following example, we load a cube containing satellite derived temperature
459+
data in a stereographic projection (with projected coordinates with units of
460+
metres), and mask it to only show data over the United Kingdom, based on a
461+
shapefile of the UK boundary defined in WGS84 lat-lon coordinates.
462+
463+
.. plot:: userguide/plotting_examples/masking_stereographic_plot.py
464+
:include-source:
365465

366-
.. note::
367-
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°
368-
Converting your Cube's coordinate system may help if you get a fully masked cube as the output from this function unexpectedly.
369466

370467

371468
Cube Iteration
@@ -481,8 +578,11 @@ Similarly, Iris cubes have indexing capability::
481578
print(cube[1, ::-2])
482579

483580

581+
.. _antimeridian: https://www.gadom.ski/antimeridian/latest/
484582
.. _Cartopy_shapereader: https://cartopy.readthedocs.io/stable/tutorials/using_the_shapereader.html#id1
485-
.. _Natural Earth: https://www.naturalearthdata.com/
486583
.. _ESRI Shapefiles: https://support.esri.com/en-us/technical-paper/esri-shapefile-technical-description-279
584+
.. _GDAL: https://gdal.org/en/stable/programs/ogr2ogr.html
585+
.. _Natural Earth: https://www.naturalearthdata.com/
586+
.. _shapely.Geometry: https://shapely.readthedocs.io/en/stable/geometry.html
487587

488588

docs/src/whatsnew/3.14.rst

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
.. include:: ../common_links.inc
22

3-
v3.14 (31 Oct 2025 [release candidate])
4-
***************************************
3+
v3.14 (14 Nov 2025)
4+
*******************
55

66
This document explains the changes made to Iris for this release
77
(:doc:`View all changes <index>`.)
88

99

10-
.. dropdown:: 3.14 Release Highlights
10+
.. dropdown:: v3.14 Release Highlights
1111
:color: primary
1212
:icon: info
1313
:animate: fade-in
@@ -166,8 +166,9 @@ This document explains the changes made to Iris for this release
166166
#. `@rcomer`_ updated all Cartopy references to point to the new location at
167167
https://cartopy.readthedocs.io (:pull:`6636`)
168168

169-
#. `@hsteptoe`_ added additional worked examples to the :func:`iris.util.mask_cube_from_shape`
170-
documentation, to demonstrate how to use the function with different types of shapefiles.
169+
#. `@hsteptoe`_ added additional worked examples to :ref:`cube_masking` in the user guide,
170+
and :func:`iris.util.mask_cube_from_shape` documentation, to demonstrate how to use the
171+
function with different types of shapefiles.
171172
(:pull:`6129`)
172173

173174

lib/iris/util.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2262,7 +2262,8 @@ def mask_cube_from_shapefile(
22622262
Parameters
22632263
----------
22642264
cube : :class:`~iris.cube.Cube` object
2265-
The ``Cube`` object to masked. Must be singular, rather than a ``CubeList``.
2265+
The :class:`~iris.cube.Cube` object to masked. Must be singular,
2266+
rather than a :class:`~iris.cube.CubeList`.
22662267
shape : Shapely.Geometry object
22672268
A single `shape` of the area to remain unmasked on the ``cube``.
22682269
If it a line object of some kind then minimum_weight will be ignored,
@@ -2284,7 +2285,7 @@ def mask_cube_from_shapefile(
22842285
:func:`~iris.util.mask_cube`
22852286
Mask any cells in the cube’s data array.
22862287
:func:`~iris.util.mask_cube_from_shape`
2287-
Mask any cells in the cube’s data array.
2288+
Mask all points in a cube that do not intersect a shape object.
22882289
22892290
Notes
22902291
-----
@@ -2296,7 +2297,9 @@ def mask_cube_from_shapefile(
22962297
22972298
Warnings
22982299
--------
2299-
This function requires additional dependencies: ``rasterio`` and ``affine``.
2300+
This function requires additional dependencies:
2301+
`rasterio <https://rasterio.readthedocs.io/en/stable/>`_
2302+
and `affine <https://affine.readthedocs.io/en/latest/>`_.
23002303
"""
23012304
message = (
23022305
"iris.util.mask_cube_from_shapefile has been deprecated, and will be removed in a "
@@ -2349,13 +2352,14 @@ def mask_cube_from_shape(
23492352
Parameters
23502353
----------
23512354
cube : :class:`~iris.cube.Cube` object
2352-
The ``Cube`` object to masked. Must be singular, rather than a ``CubeList``.
2355+
The :class:`~iris.cube.Cube` object to masked. Must be singular,
2356+
rather than a :class:`~iris.cube.CubeList`.
23532357
shape : shapely.Geometry object
23542358
A single ``shape`` of the area to remain unmasked on the ``cube``.
23552359
If it a line object of some kind then minimum_weight will be ignored,
23562360
because you cannot compare the area of a 1D line and 2D Cell.
23572361
shape_crs : cartopy.crs.CRS, default=None
2358-
The coordinate reference system of the shape object.
2362+
The coordinate reference system of the ``shape`` object.
23592363
in_place : bool, default=False
23602364
Whether to mask the ``cube`` in-place or return a newly masked ``cube``.
23612365
Defaults to ``False``.
@@ -2440,22 +2444,24 @@ def mask_cube_from_shape(
24402444
Notes
24412445
-----
24422446
Iris does not handle the shape loading so it is agnostic to the source type of the shape.
2443-
The shape can be loaded from an Esri shapefile, created using the ``shapely`` library, or
2444-
any other source that can be interpreted as a ``shapely.Geometry`` object, such as shapes
2445-
encoded in a geoJSON or KML file.
2447+
The shape can be loaded from an Esri shapefile, created using the
2448+
`shapely <https://shapely.readthedocs.io/en/stable/>`_ library, or any other source that
2449+
can be interpreted as a `shapely.Geometry <https://shapely.readthedocs.io/en/stable/geometry.html>`_
2450+
object, such as shapes encoded in a geoJSON or KML file.
24462451
24472452
Warnings
24482453
--------
2449-
For best masking results, both the cube _and_ masking geometry should have a
2454+
For best masking results, both the cube **and** masking geometry should have a
24502455
coordinate reference system (CRS) defined. Note that CRS of the masking geometry
24512456
must be provided explicitly to this function (via ``shape_crs``), whereas the
24522457
cube CRS is read from the cube itself. The cube **must** have a coord_system defined.
24532458
24542459
Masking results will be most consistent when the cube and masking geometry have the same CRS.
24552460
2456-
If a CRS is _not_ provided for the the masking geometry, the CRS of the cube is assumed.
2461+
If a CRS is **not** provided for the the masking geometry, the CRS of the cube is assumed.
24572462
2458-
This function requires additional dependencies: ``rasterio`` and ``affine``.
2463+
This function requires additional dependencies: `rasterio <https://rasterio.readthedocs.io/en/stable/>`_
2464+
and `affine <https://affine.readthedocs.io/en/latest/>`_.
24592465
24602466
Because shape vectors are inherently Cartesian in nature, they contain no inherent
24612467
understanding of the spherical geometry underpinning geographic coordinate systems.

0 commit comments

Comments
 (0)