Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,18 @@ jobs:
fail-fast: false
matrix:
os: ["ubuntu-latest"]
python-version: ["3.13"]
python-version: ["3.14"]
session: ["doctest", "gallery"]
include:
- os: "ubuntu-latest"
python-version: "3.13"
python-version: "3.14"
session: "tests"
coverage: "--coverage"
- os: "ubuntu-latest"
python-version: "3.12"
python-version: "3.13"
session: "tests"
- os: "ubuntu-latest"
python-version: "3.11"
python-version: "3.12"
session: "tests"

env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]
python-version: ["3.12", "3.13", "3.14"]
session: ["wheel"]
env:
ENV_NAME: "ci-wheels"
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/bm_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def _check_requirements(package: str) -> None:

def _prep_data_gen_env() -> None:
"""Create or access a separate, unchanging environment for generating test data."""
python_version = "3.13"
python_version = "3.14"
data_gen_var = "DATA_GEN_PYTHON"
if data_gen_var in environ:
echo("Using existing data generation environment.")
Expand Down
16 changes: 15 additions & 1 deletion docs/src/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"""Config for sphinx."""

import datetime
import importlib
from importlib.metadata import version as get_version
from inspect import getsource
import ntpath
Expand Down Expand Up @@ -219,6 +220,11 @@ def _dotv(version):
autoclass_content = "both"
modindex_common_prefix = ["iris"]

# if geovista is not installed we need to mock the imports so the autodoc build works:
if importlib.util.find_spec("geovista") is None:
autodoc_mock_imports = ["geovista", "pyvista"]


# -- apidoc extension ---------------------------------------------------------
# See https://github.com/sphinx-contrib/apidoc
source_code_root = (Path(__file__).parents[2]).absolute()
Expand Down Expand Up @@ -283,7 +289,15 @@ def _dotv(version):

# -- Doctest ("make doctest")--------------------------------------------------

doctest_global_setup = "import iris"
doctest_global_setup = """
import iris

# To handle conditional doctest skipping if geovista is not installed:
try:
import geovista as gv
except ImportError:
gv = None
"""

# -- Options for HTML output --------------------------------------------------

Expand Down
1 change: 1 addition & 0 deletions docs/src/further_topics/ugrid/operations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,7 @@ below:
:icon: code
.. doctest:: ugrid_operations
:skipif: gv is None
>>> from geovista.geodesic import BBox
>>> from iris import load_cube, sample_data_path
Expand Down
8 changes: 7 additions & 1 deletion docs/src/whatsnew/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,19 @@ This document explains the changes made to Iris for this release

#. `@tkknight`_ removed flake8, we have ruff now instead. (:pull:`6889`)

#. `@trexfeathers`_ and `@ukmo-ccbunney`_ updated CI to support Python 3.14
inline with `SPEC0 Minimum Supported Dependencies`_. Note: `pyvista` (and
hence `geovista`) is not yet compatible with Python 3.14, so
`:module:~iris.experimental.geovista` is currently only available for
Python \<3.14. (:pull:`6816`, :issue:`6775`)

.. comment
Whatsnew author names (@github name) in alphabetical order. Note that,
core dev names are automatically included by the common_links.inc:

.. _@hdyson: https://github.com/hdyson


.. _SPEC0 Minimum Supported Dependencies: https://scientific-python.org/specs/spec-0000/

.. comment
Whatsnew resources in alphabetical order:
193 changes: 110 additions & 83 deletions lib/iris/experimental/geovista.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,64 +64,79 @@ def cube_to_polydata(cube, **kwargs):
cube_w_time = load_cube(sample_data_path("A1B_north_america.nc"))
cube_mesh = load_cube(sample_data_path("mesh_C4_synthetic_float.nc"))

>>> from iris.experimental.geovista import cube_to_polydata
.. doctest::
:skipif: gv is None

>>> from iris.experimental.geovista import cube_to_polydata

Converting a standard 2-dimensional :class:`~iris.cube.Cube` with
1-dimensional coordinates:

>>> print(cube.summary(shorten=True))
air_temperature / (K) (latitude: 73; longitude: 96)
>>> print(cube_to_polydata(cube))
PolyData (...
N Cells: 7008
N Points: 7178
N Strips: 0
X Bounds: -9.992e-01, 9.992e-01
Y Bounds: -9.992e-01, 9.992e-01
Z Bounds: -1.000e+00, 1.000e+00
N Arrays: 4
.. doctest::
:skipif: gv is None

>>> print(cube.summary(shorten=True))
air_temperature / (K) (latitude: 73; longitude: 96)
>>> print(cube_to_polydata(cube))
PolyData (...
N Cells: 7008
N Points: 7178
N Strips: 0
X Bounds: -9.992e-01, 9.992e-01
Y Bounds: -9.992e-01, 9.992e-01
Z Bounds: -1.000e+00, 1.000e+00
N Arrays: 4

Configure the conversion by passing additional keyword arguments:

>>> print(cube_to_polydata(cube, radius=2))
PolyData (...
N Cells: 7008
N Points: 7178
N Strips: 0
X Bounds: -1.998e+00, 1.998e+00
Y Bounds: -1.998e+00, 1.998e+00
Z Bounds: -2.000e+00, 2.000e+00
N Arrays: 4
.. doctest::
:skipif: gv is None

>>> print(cube_to_polydata(cube, radius=2))
PolyData (...
N Cells: 7008
N Points: 7178
N Strips: 0
X Bounds: -1.998e+00, 1.998e+00
Y Bounds: -1.998e+00, 1.998e+00
Z Bounds: -2.000e+00, 2.000e+00
N Arrays: 4

Converting a :class:`~iris.cube.Cube` that has a
:attr:`~iris.cube.Cube.mesh` describing its horizontal space:

>>> print(cube_mesh.summary(shorten=True))
synthetic / (1) (-- : 96)
>>> print(cube_to_polydata(cube_mesh))
PolyData (...
N Cells: 96
N Points: 98
N Strips: 0
X Bounds: -1.000e+00, 1.000e+00
Y Bounds: -1.000e+00, 1.000e+00
Z Bounds: -1.000e+00, 1.000e+00
N Arrays: 4
.. doctest::
:skipif: gv is None

>>> print(cube_mesh.summary(shorten=True))
synthetic / (1) (-- : 96)
>>> print(cube_to_polydata(cube_mesh))
PolyData (...
N Cells: 96
N Points: 98
N Strips: 0
X Bounds: -1.000e+00, 1.000e+00
Y Bounds: -1.000e+00, 1.000e+00
Z Bounds: -1.000e+00, 1.000e+00
N Arrays: 4

Remember to reduce the dimensionality of your :class:`~iris.cube.Cube` to
just be the horizontal space:

>>> print(cube_w_time.summary(shorten=True))
air_temperature / (K) (time: 240; latitude: 37; longitude: 49)
>>> print(cube_to_polydata(cube_w_time[0, :, :]))
PolyData (...
N Cells: 1813
N Points: 1900
N Strips: 0
X Bounds: -6.961e-01, 6.961e-01
Y Bounds: -9.686e-01, -3.411e-01
Z Bounds: 2.483e-01, 8.714e-01
N Arrays: 4
.. doctest::
:skipif: gv is None

>>> print(cube_w_time.summary(shorten=True))
air_temperature / (K) (time: 240; latitude: 37; longitude: 49)
>>> print(cube_to_polydata(cube_w_time[0, :, :]))
PolyData (...
N Cells: 1813
N Points: 1900
N Strips: 0
X Bounds: -6.961e-01, 6.961e-01
Y Bounds: -9.686e-01, -3.411e-01
Z Bounds: 2.483e-01, 8.714e-01
N Arrays: 4

"""
if cube.mesh:
Expand Down Expand Up @@ -227,57 +242,69 @@ def extract_unstructured_region(cube, polydata, region, **kwargs):
The parameters of :func:`extract_unstructured_region` have been designed with
flexibility and reuse in mind. This is demonstrated below.

>>> from geovista.geodesic import BBox
>>> from iris.experimental.geovista import cube_to_polydata, extract_unstructured_region
>>> print(cube_w_mesh.shape)
(72, 96)
>>> # The mesh dimension represents the horizontal space of the cube.
>>> print(cube_w_mesh.shape[cube_w_mesh.mesh_dim()])
96
>>> cube_polydata = cube_to_polydata(cube_w_mesh[0, :])
>>> extracted_cube = extract_unstructured_region(
... cube=cube_w_mesh,
... polydata=cube_polydata,
... region=BBox(lons=[0, 70, 70, 0], lats=[-25, -25, 45, 45]),
... )
>>> print(extracted_cube.shape)
(72, 11)
.. doctest::
:skipif: gv is None

>>> from geovista.geodesic import BBox
>>> from iris.experimental.geovista import cube_to_polydata, extract_unstructured_region
>>> print(cube_w_mesh.shape)
(72, 96)
>>> # The mesh dimension represents the horizontal space of the cube.
>>> print(cube_w_mesh.shape[cube_w_mesh.mesh_dim()])
96
>>> cube_polydata = cube_to_polydata(cube_w_mesh[0, :])
>>> extracted_cube = extract_unstructured_region(
... cube=cube_w_mesh,
... polydata=cube_polydata,
... region=BBox(lons=[0, 70, 70, 0], lats=[-25, -25, 45, 45]),
... )
>>> print(extracted_cube.shape)
(72, 11)

Now reuse the same `cube` and `polydata` to extract a different region:

>>> new_region = BBox(lons=[0, 35, 35, 0], lats=[-25, -25, 45, 45])
>>> extracted_cube = extract_unstructured_region(
... cube=cube_w_mesh,
... polydata=cube_polydata,
... region=new_region,
... )
>>> print(extracted_cube.shape)
(72, 6)
.. doctest::
:skipif: gv is None

>>> new_region = BBox(lons=[0, 35, 35, 0], lats=[-25, -25, 45, 45])
>>> extracted_cube = extract_unstructured_region(
... cube=cube_w_mesh,
... polydata=cube_polydata,
... region=new_region,
... )
>>> print(extracted_cube.shape)
(72, 6)

Now apply the same region extraction to a different `cube` that has the
same horizontal shape:

>>> print(other_cube_w_mesh.shape)
(20, 96)
>>> extracted_cube = extract_unstructured_region(
... cube=other_cube_w_mesh,
... polydata=cube_polydata,
... region=new_region,
... )
>>> print(extracted_cube.shape)
(20, 6)
.. doctest::
:skipif: gv is None

>>> print(other_cube_w_mesh.shape)
(20, 96)
>>> extracted_cube = extract_unstructured_region(
... cube=other_cube_w_mesh,
... polydata=cube_polydata,
... region=new_region,
... )
>>> print(extracted_cube.shape)
(20, 6)

Arbitrary keywords can be passed down to
:meth:`geovista.geodesic.BBox.enclosed` (``outside`` in this example):

>>> extracted_cube = extract_unstructured_region(
... cube=other_cube_w_mesh,
... polydata=cube_polydata,
... region=new_region,
... outside=True,
... )
>>> print(extracted_cube.shape)
(20, 90)
.. doctest::
:skipif: gv is None

>>> extracted_cube = extract_unstructured_region(
... cube=other_cube_w_mesh,
... polydata=cube_polydata,
... region=new_region,
... outside=True,
... )
>>> print(extracted_cube.shape)
(20, 90)

"""
if cube.mesh:
Expand Down
8 changes: 8 additions & 0 deletions lib/iris/tests/integration/experimental/geovista/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@
# 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.
"""Integration tests for the :mod:`iris.experimental.geovista` module."""

import pytest

# Skip this whole package if geovista (and by extension pyvista) is not available:
pytest.importorskip(
"geovista",
reason="Skipping geovista integration tests as `geovista` is not installed",
)
6 changes: 4 additions & 2 deletions lib/iris/tests/test_coding_standards.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import subprocess
from typing import List, Tuple

from packaging.version import Version
import pytest

import iris
Expand Down Expand Up @@ -70,8 +71,9 @@ def test_python_versions():
Test is designed to fail whenever Iris' supported Python versions are
updated, insisting that versions are updated EVERYWHERE in-sync.
"""
latest_supported = "3.13"
all_supported = ["3.11", "3.12", latest_supported]
all_supported = ["3.12", "3.13", "3.14"]
_parsed = [Version(v) for v in all_supported]
latest_supported = str(max(_parsed))

root_dir = Path(__file__).parents[3]
workflows_dir = root_dir / ".github" / "workflows"
Expand Down
7 changes: 7 additions & 0 deletions lib/iris/tests/unit/experimental/geovista/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,10 @@
# 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.
"""Unit tests for the :mod:`iris.experimental.geovista` module."""

import pytest

# Skip this whole package if geovista (and by extension pyvista) is not available:
pytest.importorskip(
"geovista", reason="Skipping geovista unit tests as `geovista` is not installed"
)
2 changes: 1 addition & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
nox.options.reuse_existing_virtualenvs = True

#: Python versions we can run sessions under
_PY_VERSIONS_ALL = ["3.11", "3.12", "3.13"]
_PY_VERSIONS_ALL = ["3.12", "3.13", "3.14"]
_PY_VERSION_LATEST = _PY_VERSIONS_ALL[-1]

#: One specific python version for docs builds
Expand Down
Loading
Loading