Repository: SciTools/iris
Pull request #6524 (2025-06-20 12:23:19)

More informative stack trace for benchmark data generation
## 🚀 Pull Request

### Description
<!-- Provide a clear description about your awesome pull request -->
<!-- Tell us all about your new feature, improvement, or bug fix -->

The current setup produces a long and confusing trace. Due to our use of `subprocess` to run a _separate_ Python instance, the information is provides is close to meaningless - a big block of text with the function definition, but no information about what went wrong when running the function.

<details><summary>Example from the current setup</summary>

```
               For parameters: 2
               Traceback (most recent call last):
                 File "/home/runner/work/iris/iris/benchmarks/.asv/env/e35ec55c4ba4ad9d53b3b315a88cda8c/delegated_env/lib/python3.13/site-packages/asv_runner/server.py", line 179, in _run_server
                   _run(run_args)
                   ~~~~^^^^^^^^^^
                 File "/home/runner/work/iris/iris/benchmarks/.asv/env/e35ec55c4ba4ad9d53b3b315a88cda8c/delegated_env/lib/python3.13/site-packages/asv_runner/run.py", line 65, in _run
                   skip = benchmark.do_setup()
                 File "/home/runner/work/iris/iris/benchmarks/.asv/env/e35ec55c4ba4ad9d53b3b315a88cda8c/delegated_env/lib/python3.13/site-packages/asv_runner/benchmarks/time.py", line 80, in do_setup
                   result = Benchmark.do_setup(self)
                 File "/home/runner/work/iris/iris/benchmarks/.asv/env/e35ec55c4ba4ad9d53b3b315a88cda8c/delegated_env/lib/python3.13/site-packages/asv_runner/benchmarks/_base.py", line 632, in do_setup
                   setup(*self._current_params)
                   ~~~~~^^^^^^^^^^^^^^^^^^^^^^^
                 File "/home/runner/work/iris/iris/benchmarks/benchmarks/__init__.py", line 68, in setup
                   (self.file_path,) = create_um_files(
                                       ~~~~~~~~~~~~~~~^
                       param, param, param, param, False, ["NetCDF"]
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                   ).values()
                   ^
                 File "/home/runner/work/iris/iris/benchmarks/benchmarks/generate_data/um_files.py", line 184, in create_um_files
                   _ = run_function_elsewhere(
                       _create_um_files, len_x, len_y, len_z, len_t, compress, save_paths
                   )
                 File "/home/runner/work/iris/iris/benchmarks/benchmarks/generate_data/__init__.py", line 95, in run_function_elsewhere
                   result = run(
                       [DATA_GEN_PYTHON, "-c", python_string], capture_output=True, check=True
                   )
                 File "/home/runner/work/iris/iris/benchmarks/.asv/env/e35ec55c4ba4ad9d53b3b315a88cda8c/delegated_env/lib/python3.13/subprocess.py", line 577, in run
                   raise CalledProcessError(retcode, process.args,
                                            output=stdout, stderr=stderr)
               subprocess.CalledProcessError: Command '['/home/runner/work/iris/iris/.nox/tests-3-13/bin/python3.13', '-c', 'def _create_um_files(\n    len_x: int, len_y: int, len_z: int, len_t: int, compress, save_paths: dict\n) -> None:\n    """Generate an FF object of given shape and compression, save to FF/PP/NetCDF.\n\n    This is run externally\n    (:func:`benchmarks.generate_data.run_function_elsewhere`), so all imports\n    are self-contained and input parameters are simple types.\n    """\n    from copy import deepcopy\n    from datetime import datetime\n    from tempfile import NamedTemporaryFile\n\n    from mule import ArrayDataProvider, Field3, FieldsFile\n    from mule.pp import fields_to_pp_file\n    import numpy as np\n\n    from iris import load_cube\n    from iris import save as save_cube\n\n    template = {\n        "fixed_length_header": {"dataset_type": 3, "grid_staggering": 3},\n        "integer_constants": {\n            "num_p_levels": len_z,\n            "num_cols": len_x,\n            "num_rows": len_y,\n        },\n        "real_constants": {},\n        "level_dependent_constants": {"dims": (len_z + 1, None)},\n    }\n    new_ff = FieldsFile.from_template(deepcopy(template))\n\n    data_array = np.arange(len_x * len_y).reshape(len_x, len_y)\n    array_provider = ArrayDataProvider(data_array)\n\n    def add_field(level_: int, time_step_: int) -> None:\n        """Add a minimal field to the new :class:`~mule.FieldsFile`.\n\n        Includes the minimum information to allow Mule saving and Iris\n        loading, as well as incrementation for vertical levels and time\n        steps to allow generation of z and t dimensions.\n        """\n        new_field = Field3.empty()\n        # To correspond to the header-release 3 class used.\n        new_field.lbrel = 3\n        # Mule uses the first element of the lookup to test for\n        #  unpopulated fields (and skips them), so the first element should\n        #  be set to something. The year will do.\n        new_field.raw[1] = datetime.now().year\n\n        # Horizontal.\n        new_field.lbcode = 1\n        new_field.lbnpt = len_x\n        new_field.lbrow = len_y\n        new_field.bdx = new_ff.real_constants.col_spacing\n        new_field.bdy = new_ff.real_constants.row_spacing\n        new_field.bzx = new_ff.real_constants.start_lon - 0.5 * new_field.bdx\n        new_field.bzy = new_ff.real_constants.start_lat - 0.5 * new_field.bdy\n\n        # Hemisphere.\n        new_field.lbhem = 32\n        # Processing.\n        new_field.lbproc = 0\n\n        # Vertical.\n        # Hybrid height values by simulating sequences similar to those in a\n        #  theta file.\n        new_field.lbvc = 65\n        if level_ == 0:\n            new_field.lblev = 9999\n        else:\n            new_field.lblev = level_\n\n        level_1 = level_ + 1\n        six_rec = 20 / 3\n        three_rec = six_rec / 2\n\n        new_field.blev = level_1**2 * six_rec - six_rec\n        new_field.brsvd1 = level_1**2 * six_rec + (six_rec * level_1) - three_rec\n\n        brsvd2_simulated = np.linspace(0.995, 0, len_z)\n        shift = min(len_z, 2)\n        bhrlev_simulated = np.concatenate([np.ones(shift), brsvd2_simulated[:-shift]])\n        new_field.brsvd2 = brsvd2_simulated[level_]\n        new_field.bhrlev = bhrlev_simulated[level_]\n\n        # Time.\n        new_field.lbtim = 11\n\n        new_field.lbyr = time_step_\n        for attr_name in ["lbmon", "lbdat", "lbhr", "lbmin", "lbsec"]:\n            setattr(new_field, attr_name, 0)\n\n        new_field.lbyrd = time_step_ + 1\n        for attr_name in ["lbmond", "lbdatd", "lbhrd", "lbmind", "lbsecd"]:\n            setattr(new_field, attr_name, 0)\n\n        # Data and packing.\n        new_field.lbuser1 = 1\n        new_field.lbpack = int(compress)\n        new_field.bacc = 0\n        new_field.bmdi = -1\n        new_field.lbext = 0\n        new_field.set_data_provider(array_provider)\n\n        new_ff.fields.append(new_field)\n\n    for time_step in range(len_t):\n        for level in range(len_z):\n            add_field(level, time_step + 1)\n\n    ff_path = save_paths.get("FF", None)\n    pp_path = save_paths.get("PP", None)\n    nc_path = save_paths.get("NetCDF", None)\n\n    if ff_path:\n        new_ff.to_file(ff_path)\n    if pp_path:\n        fields_to_pp_file(str(pp_path), new_ff.fields)\n    if nc_path:\n        temp_ff_path = None\n        # Need an Iris Cube from the FF content.\n        if ff_path:\n            # Use the existing file.\n            ff_cube = load_cube(ff_path)\n        else:\n            # Make a temporary file.\n            temp_ff_path = NamedTemporaryFile()\n            new_ff.to_file(temp_ff_path.name)\n            ff_cube = load_cube(temp_ff_path.name)\n\n        save_cube(ff_cube, nc_path, zlib=compress)\n        if temp_ff_path:\n            temp_ff_path.close()\n\n_create_um_files(2,2,2,2,False,{\'NetCDF\': \'/home/runner/work/iris/iris/benchmarks/.data/UM_2_2_2_2/False.nc\'})']' returned non-zero exit status 1.
```

</details>

The proposed setup suppresses the original trace, instead raising its own deliberately constructed error derived from the `stderr` of the `subprocess` call.

<details><summary>Example from the proposed setup</summary>

```
              For parameters: 2
              Traceback (most recent call last):
                File "/home/users/martin.yeo/repos/iris/.nox/tests-3-13/lib/python3.13/site-packages/asv_runner/server.py", line 179, in _run_server
                  _run(run_args)
                  ~~~~^^^^^^^^^^
                File "/home/users/martin.yeo/repos/iris/.nox/tests-3-13/lib/python3.13/site-packages/asv_runner/run.py", line 65, in _run
                  skip = benchmark.do_setup()
                File "/home/users/martin.yeo/repos/iris/.nox/tests-3-13/lib/python3.13/site-packages/asv_runner/benchmarks/time.py", line 80, in do_setup
                  result = Benchmark.do_setup(self)
                File "/home/users/martin.yeo/repos/iris/.nox/tests-3-13/lib/python3.13/site-packages/asv_runner/benchmarks/_base.py", line 632, in do_setup
                  setup(*self._current_params)
                  ~~~~~^^^^^^^^^^^^^^^^^^^^^^^
                File "/home/users/martin.yeo/repos/iris/benchmarks/benchmarks/__init__.py", line 68, in setup
                  (self.file_path,) = create_um_files(
                                      ~~~~~~~~~~~~~~~^
                      param, param, param, param, False, ["NetCDF"]
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                  ).values()
                  ^
                File "/home/users/martin.yeo/repos/iris/benchmarks/benchmarks/generate_data/um_files.py", line 184, in create_um_files
                  _ = run_function_elsewhere(
                      _create_um_files, len_x, len_y, len_z, len_t, compress, save_paths
                  )
                File "/home/users/martin.yeo/repos/iris/benchmarks/benchmarks/generate_data/__init__.py", line 112, in run_function_elsewhere
                  raise DataGenerationError(error_.stderr) from None
              benchmarks.generate_data.DataGenerationError: Traceback (most recent call last):
                File "<string>", line 136, in <module>
                  _create_um_files(2,2,2,2,False,{'NetCDF': '/home/users/martin.yeo/repos/iris/benchmarks/.data/UM_2_2_2_2/False.nc'})
                  ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                File "<string>", line 129, in _create_um_files
                  new_ff.to_file(temp_ff_path.name)
                  ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
                File "/home/users/martin.yeo/repos/iris/.nox/tests-3-13/lib/python3.13/site-packages/mule/__init__.py", line 1412, in to_file
                  self._write_to_file(output_file)
                  ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
                File "/home/users/martin.yeo/repos/iris/.nox/tests-3-13/lib/python3.13/site-packages/mule/ff.py", line 496, in _write_to_file
                  super(FieldsFile, self)._write_to_file(output_file)
                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
                File "/home/users/martin.yeo/repos/iris/.nox/tests-3-13/lib/python3.13/site-packages/mule/__init__.py", line 1738, in _write_to_file
                  data_bytes, data_size = write_operator.to_bytes(field)
                                          ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
                File "/home/users/martin.yeo/repos/iris/.nox/tests-3-13/lib/python3.13/site-packages/mule/ff.py", line 315, in to_bytes
                  return data.tostring(), data.size
                         ^^^^^^^^^^^^^
              AttributeError: 'numpy.ndarray' object has no attribute 'tostring'
              asv: benchmark failed (exit status 1)

```

</details>

---
[Consult Iris pull request check list]( https://scitools-iris.readthedocs.io/en/latest/developers_guide/contributing_pull_request_checklist.html)

---
Add any of the below labels to trigger actions on this PR:

- https://github.com/SciTools/iris/labels/benchmark_this


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/SciTools/iris/pull/6524?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=SciTools) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 89.87%. Comparing base [(`99cbe83`)](https://app.codecov.io/gh/SciTools/iris/commit/99cbe8378c1609ff984912762b22bb5703923b9c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=SciTools) to head [(`dd35ad7`)](https://app.codecov.io/gh/SciTools/iris/commit/dd35ad70b5c4f032b950fa9d41510eb4a95cdff0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=SciTools).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #6524   +/-   ##
=======================================
  Coverage   89.87%   89.87%           
=======================================
  Files          90       90           
  Lines       23908    23908           
  Branches     4459     4459           
=======================================
  Hits        21487    21487           
  Misses       1669     1669           
  Partials      752      752           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/SciTools/iris/pull/6524?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=SciTools).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=SciTools).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/benchmarks/benchmarks/generate_data/__init__.py b/benchmarks/benchmarks/generate_data/__init__.py
index 8b089c9fb..9a3671389 100644
--- a/benchmarks/benchmarks/generate_data/__init__.py
+++ b/benchmarks/benchmarks/generate_data/__init__.py
@@ -59,6 +59,12 @@ elif not BENCHMARK_DATA.is_dir():
 REUSE_DATA = True
 
 
+class DataGenerationError(Exception):
+    """Exception raised for errors during data generation."""
+
+    pass
+
+
 def run_function_elsewhere(func_to_run, *args, **kwargs):
     """Run a given function using the :const:`DATA_GEN_PYTHON` executable.
 
@@ -92,9 +98,19 @@ def run_function_elsewhere(func_to_run, *args, **kwargs):
         f"{func_to_run.__name__}(" + ",".join(func_call_term_strings) + ")"
     )
     python_string = "\n".join([func_string, func_call_string])
-    result = run(
-        [DATA_GEN_PYTHON, "-c", python_string], capture_output=True, check=True
-    )
+
+    try:
+        result = run(
+            [DATA_GEN_PYTHON, "-c", python_string],
+            capture_output=True,
+            check=True,
+            text=True,
+        )
+    except CalledProcessError as error_:
+        # From None 'breaks' the error chain - we don't want the original
+        #  traceback since it is long and confusing.
+        raise DataGenerationError(error_.stderr) from None
+
     return result.stdout
 
 
diff --git a/benchmarks/benchmarks/generate_data/stock.py b/benchmarks/benchmarks/generate_data/stock.py
index 47014078e..63970cd34 100644
--- a/benchmarks/benchmarks/generate_data/stock.py
+++ b/benchmarks/benchmarks/generate_data/stock.py
@@ -45,7 +45,7 @@ def _create_file__xios_common(func_name, **kwargs):
             temp_file_dir=str(BENCHMARK_DATA),
             **kwargs,
         )
-        Path(actual_path.decode()).replace(save_path)
+        Path(actual_path).replace(save_path)
     return save_path
 
 
diff --git a/benchmarks/benchmarks/generate_data/um_files.py b/benchmarks/benchmarks/generate_data/um_files.py
index 40bf83e79..e2bab6b27 100644
--- a/benchmarks/benchmarks/generate_data/um_files.py
+++ b/benchmarks/benchmarks/generate_data/um_files.py
@@ -22,12 +22,22 @@ def _create_um_files(
     from tempfile import NamedTemporaryFile
 
     from mule import ArrayDataProvider, Field3, FieldsFile
+    import mule.ff
     from mule.pp import fields_to_pp_file
     import numpy as np
 
     from iris import load_cube
     from iris import save as save_cube
 
+    def to_bytes_patch(self, field):
+        data = field.get_data()
+        dtype = mule.ff._DATA_DTYPES[self.WORD_SIZE][field.lbuser1]
+        data = data.astype(dtype)
+        return data.tobytes(), data.size
+
+    # TODO: remove this patch when fixed in mule, see https://github.com/MetOffice/simulation-systems/discussions/389
+    mule.ff._WriteFFOperatorUnpacked.to_bytes = to_bytes_patch
+
     template = {
         "fixed_length_header": {"dataset_type": 3, "grid_staggering": 3},
         "integer_constants": {
diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py
index 8b2df3d1a..be113bc91 100644
--- a/lib/iris/__init__.py
+++ b/lib/iris/__init__.py
@@ -182,6 +182,7 @@ class Future(threading.local):
         save_split_attrs=False,
         date_microseconds=False,
         derived_bounds=False,
+        lam_pole_offset=False,
     ):
         """Container for run-time options controls.
 
@@ -217,6 +218,11 @@ class Future(threading.local):
             you didn't need to before.
         derived_bounds : bool, default=False
             When True, uses the new form for deriving bounds with the load.
+        lam_pole_offset : bool, default=False
+            When True, saving a cube on a "Limited Area Model" (LAM) domain
+            to a PP file will set the pole longitude (PP field ``bplon``) to
+            180.0 degrees if the grid is defined on a standard pole. Does not
+            affect global or rotated-pole domains.
 
         """
         # The flag 'example_future_flag' is provided as a reference for the
@@ -231,6 +237,7 @@ class Future(threading.local):
         self.__dict__["save_split_attrs"] = save_split_attrs
         self.__dict__["date_microseconds"] = date_microseconds
         self.__dict__["derived_bounds"] = derived_bounds
+        self.__dict__["lam_pole_offset"] = lam_pole_offset
 
         # TODO: next major release: set IrisDeprecation to subclass
         #  DeprecationWarning instead of UserWarning.
diff --git a/lib/iris/common/mixin.py b/lib/iris/common/mixin.py
index dae10abc9..cc4aecf77 100644
--- a/lib/iris/common/mixin.py
+++ b/lib/iris/common/mixin.py
@@ -203,7 +203,7 @@ class Unit(cf_units.Unit):
             warnings.warn(message, category=FutureWarning)
 
             if hasattr(result, "shape"):
-                vfunc = np.vectorize(_round)
+                vfunc = np.frompyfunc(_round, 1, 1)
                 result = vfunc(result)
             else:
                 result = _round(result)
diff --git a/lib/iris/common/resolve.py b/lib/iris/common/resolve.py
index c4bc18309..49df0e66e 100644
--- a/lib/iris/common/resolve.py
+++ b/lib/iris/common/resolve.py
@@ -2358,7 +2358,7 @@ class Resolve:
             >>> resolver.map_rhs_to_lhs
             True
             >>> cube1.data.sum()
-            np.float32(124652160.0)
+            np.float32(1.2465214e+08)
             >>> zeros.shape
             (240, 37, 49)
             >>> zeros.sum()
diff --git a/lib/iris/cube.py b/lib/iris/cube.py
index be8ddcfc4..d75b43b6c 100644
--- a/lib/iris/cube.py
+++ b/lib/iris/cube.py
@@ -162,18 +162,19 @@ class CubeList(list):
 
     def xml(self, checksum=False, order=True, byteorder=True):
         """Return a string of the XML that this list of cubes represents."""
-        doc = Document()
-        cubes_xml_element = doc.createElement("cubes")
-        cubes_xml_element.setAttribute("xmlns", XML_NAMESPACE_URI)
-
-        for cube_obj in self:
-            cubes_xml_element.appendChild(
-                cube_obj._xml_element(
-                    doc, checksum=checksum, order=order, byteorder=byteorder
+        with np.printoptions(legacy="2.2"):
+            doc = Document()
+            cubes_xml_element = doc.createElement("cubes")
+            cubes_xml_element.setAttribute("xmlns", XML_NAMESPACE_URI)
+
+            for cube_obj in self:
+                cubes_xml_element.appendChild(
+                    cube_obj._xml_element(
+                        doc, checksum=checksum, order=order, byteorder=byteorder
+                    )
                 )
-            )
 
-        doc.appendChild(cubes_xml_element)
+            doc.appendChild(cubes_xml_element)
 
         # return our newly created XML string
         doc = Cube._sort_xml_attrs(doc)
@@ -3864,17 +3865,18 @@ class Cube(CFVariableMixin):
         byteorder: bool = True,
     ) -> str:
         """Return a fully valid CubeML string representation of the Cube."""
-        doc = Document()
+        with np.printoptions(legacy="2.2"):
+            doc = Document()
 
-        cube_xml_element = self._xml_element(
-            doc, checksum=checksum, order=order, byteorder=byteorder
-        )
-        cube_xml_element.setAttribute("xmlns", XML_NAMESPACE_URI)
-        doc.appendChild(cube_xml_element)
+            cube_xml_element = self._xml_element(
+                doc, checksum=checksum, order=order, byteorder=byteorder
+            )
+            cube_xml_element.setAttribute("xmlns", XML_NAMESPACE_URI)
+            doc.appendChild(cube_xml_element)
 
-        # Print our newly created XML
-        doc = self._sort_xml_attrs(doc)
-        return iris.util._print_xml(doc)
+            # Print our newly created XML
+            doc = self._sort_xml_attrs(doc)
+            return iris.util._print_xml(doc)
 
     def _xml_element(self, doc, checksum=False, order=True, byteorder=True):
         cube_xml_element = doc.createElement("cube")
diff --git a/lib/iris/fileformats/pp_save_rules.py b/lib/iris/fileformats/pp_save_rules.py
index 5a0f4eabf..a6a72393a 100644
--- a/lib/iris/fileformats/pp_save_rules.py
+++ b/lib/iris/fileformats/pp_save_rules.py
@@ -44,6 +44,27 @@ def _basic_coord_system_rules(cube, pp):
     if cube.coord_system("GeogCS") is not None or cube.coord_system(None) is None:
         pp.bplat = 90
         pp.bplon = 0
+
+        try:
+            # LAMs should have bplon of 180
+            x_coord = cube.coord(axis="x", dim_coords=True)
+            y_coord = cube.coord(axis="y", dim_coords=True)
+            if not iris.util._is_circular(
+                x_coord.points, 360.0, x_coord.bounds
+            ) or not iris.util._is_circular(y_coord.points, 180.0, y_coord.bounds):
+                if iris.FUTURE.lam_pole_offset:
+                    pp.bplon = 180.0
+                else:
+                    msg = (
+                        "Saving a cube defined on a Limited Area Model (LAM) grid to PP format "
+                        "will set the 'pole longitude' field (`bplon`) to 180.0 degrees in "
+                        "future iris releases. Enable this behaviour now with the Future flag "
+                        "`iris.FUTURE.lam_pole_offset = True`"
+                    )
+                    warnings.warn(msg, category=FutureWarning)
+
+        except iris.exceptions.CoordinateNotFoundError:
+            pass
     elif cube.coord_system("RotatedGeogCS") is not None:
         pp.bplat = cube.coord_system("RotatedGeogCS").grid_north_pole_latitude
         pp.bplon = cube.coord_system("RotatedGeogCS").grid_north_pole_longitude
diff --git a/lib/iris/util.py b/lib/iris/util.py
index e0d2fa518..1795c8d87 100644
--- a/lib/iris/util.py
+++ b/lib/iris/util.py
@@ -1603,10 +1603,12 @@ def _is_circular(points, modulus, bounds=None):
                 circular_value = (points[-1] + diff) % modulus
                 try:
                     np.testing.assert_approx_equal(
-                        points[0], circular_value, significant=4
+                        points[0] % modulus, circular_value, significant=4
                     )
                     circular = True
                 except AssertionError:
+                    # TODO: I consider the following to be not needed with the modulus being used in the
+                    # above check, but there are some tests that seem to rely on this behavior...
                     if points[0] == 0:
                         try:
                             np.testing.assert_approx_equal(
diff --git a/requirements/locks/py311-linux-64.lock b/requirements/locks/py311-linux-64.lock
index 3fca5730c..2675413e7 100644
--- a/requirements/locks/py311-linux-64.lock
+++ b/requirements/locks/py311-linux-64.lock
@@ -1,6 +1,6 @@
 # Generated by conda-lock.
 # platform: linux-64
-# input_hash: 2b6dcb79d042b6d443c993e2efcab54170e75cb982e572542a1a289168710623
+# input_hash: 2ab37bb9a0b7ddd216650e61026560917ec230278b2d71d0671ffed30830e199
 @EXPLICIT
 https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81
 https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45
@@ -12,10 +12,10 @@ https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h3f2d84a_0.
 https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-7_cp311.conda#6320dac78b3b215ceac35858b2cfdb70
 https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a
 https://conda.anaconda.org/conda-forge/linux-64/utfcpp-4.0.6-h005c6e1_0.conda#9464e297fa2bf08030c65a54342b48c3
-https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.44-hd8ed1ab_0.conda#4c33d6dd91f49a0efcc0748fd4d7348b
+https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.45-hd8ed1ab_0.conda#6db9be3b67190229479780eeeee1b35b
 https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.4.26-hbd8a1cb_0.conda#95db94f75ba080a22eb623590993167b
 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29
-https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda#01f8d123c96816249efd255a31ad7712
+https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_5.conda#acd9213a63cb62521290e581ef82de80
 https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_2.conda#434ca7e50e40f4918ab701e3facd59a0
 https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_2.conda#fbe7d535ff9d3a168c148e07358cd5b1
 https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d
@@ -54,7 +54,6 @@ https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62e
 https://conda.anaconda.org/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda#418c6ca5929a611cbd69204907a83995
 https://conda.anaconda.org/conda-forge/linux-64/double-conversion-3.3.1-h5888daf_0.conda#bfd56492d8346d669010eccafe0ba058
 https://conda.anaconda.org/conda-forge/linux-64/epoxy-1.5.10-h166bdaf_1.tar.bz2#a089d06164afd2d511347d3f87214e0b
-https://conda.anaconda.org/conda-forge/linux-64/expat-2.7.0-h5888daf_0.conda#d6845ae4dea52a2f90178bf1829a21f8
 https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.10-h36c2ea0_0.tar.bz2#ac7bc6a654f8f41b352b38f4051135f8
 https://conda.anaconda.org/conda-forge/linux-64/geos-3.13.1-h97f6797_0.conda#5bc18c66111bc94532b0d2df00731c66
 https://conda.anaconda.org/conda-forge/linux-64/jsoncpp-1.9.6-hf42df4d_1.conda#7bdc5e2cc11cb0a0f795bdad9732b0f2
@@ -79,16 +78,16 @@ https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.1-hee588c1_0.cond
 https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda#eecce068c7e4eddeb169591baac20ac4
 https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_2.conda#9d2072af184b5caa29492bf2344597bb
 https://conda.anaconda.org/conda-forge/linux-64/libudunits2-2.2.28-h40f5838_3.conda#4bdace082e911a3e1f1f0b721bed5b56
-https://conda.anaconda.org/conda-forge/linux-64/liburing-2.9-h84d6215_0.conda#ecd409e7bfcf4ee73f74d7a2cc91a4c3
+https://conda.anaconda.org/conda-forge/linux-64/liburing-2.10-h84d6215_0.conda#1a11973f25f6168f4f6a65883cf7bb2a
 https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b
 https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda#92ed62436b625154323d40d5f2f11dd7
 https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc
 https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda#9de5350a85c4a20c685259b889aa6393
 https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.9-hc50e24c_0.conda#c7f302fd11eeb0987a6a5e1f3aed6a21
-https://conda.anaconda.org/conda-forge/linux-64/mysql-common-9.2.0-h266115a_0.conda#db22a0962c953e81a2a679ecb1fc6027
-https://conda.anaconda.org/conda-forge/linux-64/opencl-headers-2024.10.24-h5888daf_0.conda#3ba02cce423fdac1a8582bd6bb189359
+https://conda.anaconda.org/conda-forge/linux-64/mysql-common-9.3.0-h266115a_0.conda#9693774d2822c39a9541f2a80c346e30
+https://conda.anaconda.org/conda-forge/linux-64/opencl-headers-2025.06.13-h5888daf_0.conda#45c3d2c224002d6d0d7769142b29f986
 https://conda.anaconda.org/conda-forge/linux-64/openh264-2.6.0-hc22cd8d_0.conda#b28cf020fd2dead0ca6d113608683842
-https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.0-h29eaf8c_0.conda#d2f1c87d4416d1e7344cf92b1aaee1c4
+https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.2-h29eaf8c_0.conda#39b4228a867772d610c02e06f939a5b8
 https://conda.anaconda.org/conda-forge/linux-64/pugixml-1.15-h3f63f65_0.conda#b11a4c6bf6f6f44e5e143f759ffa2087
 https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446
 https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.1-h8bd8927_1.conda#3b3e64af585eadfb52bb90b553db5edf
@@ -123,9 +122,9 @@ https://conda.anaconda.org/conda-forge/linux-64/libunwind-1.6.2-h9c3ff4c_0.tar.b
 https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h9c3ff4c_0.tar.bz2#309dec04b70a3cc0f1e84a4013683bc0
 https://conda.anaconda.org/conda-forge/linux-64/libvpx-1.14.1-hac33072_0.conda#cde393f461e0c169d9ffb2fc70f81c33
 https://conda.anaconda.org/conda-forge/linux-64/libzip-1.11.2-h6991a6a_0.conda#a7b27c075c9b7f459f1c022090697cba
-https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-9.2.0-he0572af_0.conda#93340b072c393d23c4700a1d40565dca
+https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-9.3.0-he0572af_0.conda#5eb3dd8ccc96213c0961a2bba1f611d1
 https://conda.anaconda.org/conda-forge/linux-64/ocl-icd-2.3.3-hb9d3cd8_0.conda#56f8947aa9d5cf37b0b3d43b83f34192
-https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.44-hc749103_2.conda#31614c73d7b103ef76faa4d83d261d34
+https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.45-hc749103_0.conda#b90bece58b4c2bf25969b70f3be42d25
 https://conda.anaconda.org/conda-forge/linux-64/python-3.11.13-h9e4cc4f_0_cpython.conda#8c399445b6dc73eab839659e6c7b5ad1
 https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda#353823361b1d27eb3960efb076dfcaf6
 https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.50.1-h9eae976_0.conda#e12789602c09a875957c1c78ff47cdf6
@@ -151,7 +150,7 @@ https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#
 https://conda.anaconda.org/conda-forge/noarch/colorcet-3.1.0-pyhd8ed1ab_1.conda#91d7152c744dc0f18ef8beb3cbc9980a
 https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_1.conda#44600c4667a319d67dbe0681fc0bc833
 https://conda.anaconda.org/conda-forge/linux-64/cyrus-sasl-2.1.27-h54b06d7_7.conda#dce22f70b4e5a407ce88f2be046f4ceb
-https://conda.anaconda.org/conda-forge/linux-64/cython-3.1.1-py311ha3e34f5_1.conda#40cd0c2e1c0ff8bb647b77b12d375ba0
+https://conda.anaconda.org/conda-forge/linux-64/cython-3.1.2-py311ha3e34f5_2.conda#f56da6e1e1f310f27cca558e58882f40
 https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.9-pyhd8ed1ab_1.conda#8d88f4a2242e6b96f9ecff9a6a05b2f1
 https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda#24c1ca34138ee57de72a943237cde4cc
 https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda#a71efeae2c160f6789900ba2631a2c90
@@ -171,7 +170,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-31_h59b9bed_openbl
 https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h4637d8d_4.conda#d4529f4dff3057982a7617c7ac58fde3
 https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda#45f6713cb00f124af300342512219182
 https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.13.3-ha770c72_1.conda#51f5be229d83ecd401fb369ab96ae669
-https://conda.anaconda.org/conda-forge/linux-64/libglib-2.84.1-h2ff4ddf_0.conda#0305434da649d4fb48a425e588b79ea6
+https://conda.anaconda.org/conda-forge/linux-64/libglib-2.84.2-h3618099_0.conda#072ab14a02164b7c0c089055368ff776
 https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda#c8013e438185f33b13814c5c488acd5c
 https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-257.6-h4e0b6ca_0.conda#071409970083d0f99ab7b569352771c9
 https://conda.anaconda.org/conda-forge/linux-64/libtheora-1.1.1-h4ab18f5_1006.conda#553281a034e9cf8693c9df49f6c78ea1
@@ -180,7 +179,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h4bc477f_0.conda#
 https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2#91e27ef3d05cc772ce627e51cff111c4
 https://conda.anaconda.org/conda-forge/noarch/loguru-0.7.3-pyh707e725_0.conda#49647ac1de4d1e4b49124aedf3934e02
 https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py311h2dc5d0c_1.conda#6565a715337ae279e351d0abd8ffe88a
-https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.1.0-py311hd18a35c_0.conda#682f76920687f7d9283039eb542fdacf
+https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.1.1-py311hd18a35c_0.conda#d0898973440adc2ad25917028669126d
 https://conda.anaconda.org/conda-forge/linux-64/multidict-6.4.4-py311h2dc5d0c_0.conda#cf1ae2989d73b0e6d86f2ee7c08d7a9b
 https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19
 https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.3-h5fbd93e_0.conda#9e5816bc95d285c115a3ebc2f8563564
@@ -214,28 +213,28 @@ https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.0-pyhe01879
 https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-16.0.0-py311h9ecbd09_0.conda#51a12678b609f5794985fda8372b1a49
 https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986
 https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda#a0901183f08b6c7107aab109733a3c91
-https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.44-hb9d3cd8_0.conda#7c91bfc90672888259675ad2ad28af9c
+https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.45-hb9d3cd8_0.conda#397a013c2dc5145a70737871aaa87e98
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda#febbab7d15033c913d53c7a2c102309d
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.1-hb9d3cd8_0.conda#4bdb303603e9821baf5fe5fdff1dc8f8
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda#96d57aba173e878a2089d5638016dc5e
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxt-1.3.1-hb9d3cd8_0.conda#279b0de5f6ba95457190a1c459a64e31
 https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda#e52c2ef711ccf31bb7f70ca87d144b9e
-https://conda.anaconda.org/conda-forge/noarch/zipp-3.22.0-pyhd8ed1ab_0.conda#234be740b00b8e41567e5b0ed95aaba9
+https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda#df5e78d904988eb55042c0c97446079f
 https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda#74ac5069774cdbc53910ec4d631a3999
 https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.3.2-pyhd8ed1ab_0.conda#1a3981115a398535dbe3f6d5faae3d36
 https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda#f730d54ba9cd543666d7220c9f7ed563
 https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda#0a01c169f0ab0f91b26e77a3301fbfe4
 https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py311hf29c0ef_0.conda#55553ecd5328336368db611f350b7039
 https://conda.anaconda.org/conda-forge/noarch/click-default-group-1.2.4-pyhd8ed1ab_1.conda#7cd83dd6831b61ad9624a694e4afd7dc
-https://conda.anaconda.org/conda-forge/linux-64/coverage-7.8.2-py311h2dc5d0c_0.conda#21c1ef48cc2bf485e6d38c5611e91da2
+https://conda.anaconda.org/conda-forge/linux-64/coverage-7.9.1-py311h2dc5d0c_0.conda#f524bd18889f169f2fa8dc70df1c53c3
 https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.0.1-py311h9ecbd09_0.conda#69a0a85acdcc5e6d0f1cc915c067ad4c
-https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2#ecfff944ba3960ecb334b9a2663d708d
+https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda#679616eb5ad4e521c83da4650860aba7
 https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda#72e42d28960d875c7654614f8b50939a
-https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.58.2-py311h2dc5d0c_0.conda#c58ce3ab3833471964d7ee6b83203425
+https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.58.4-py311h2dc5d0c_0.conda#dd3ef31d2bf022668f30859d5e11c83e
 https://conda.anaconda.org/conda-forge/linux-64/freetype-2.13.3-ha770c72_1.conda#9ccd736d31e0c6e41f54e704e5312811
 https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.42.12-hb9ae30d_0.conda#201db6c2d9a3c5e46573ac4cb2e92f4f
 https://conda.anaconda.org/conda-forge/linux-64/glew-2.1.0-h9c3ff4c_2.tar.bz2#fb05eb5c47590b247658243d27fc32f1
-https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.84.1-h4833e2c_0.conda#ddc06964296eee2b4070e65415b332fd
+https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.84.2-h4833e2c_0.conda#f2ec1facec64147850b7674633978050
 https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda#4d8df0b0db060d33c9a702ada998a8fe
 https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda#b4754fb1bdcb70c8fd54f918301582c6
 https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.3-nompi_h2d575fe_109.conda#e7a7a6e6f70553a31e6e79c65768d089
@@ -246,7 +245,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda#e
 https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_2.conda#928b8be80851f5d8ffb016f9c81dae7a
 https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.11.2-default_h0d58e46_1001.conda#804ca9e91bcaea0824a341d55b1684f2
 https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-31_h7ac8fdf_openblas.conda#452b98eafe050ecff932f0ec832dd03f
-https://conda.anaconda.org/conda-forge/linux-64/libllvm20-20.1.6-he9d0ab4_0.conda#bf8ccdd2c1c1a54a3fa25bb61f26460e
+https://conda.anaconda.org/conda-forge/linux-64/libllvm20-20.1.7-he9d0ab4_0.conda#63f1accca4913e6b66a2d546c30ff4db
 https://conda.anaconda.org/conda-forge/linux-64/libusb-1.0.29-h73b1eb8_0.conda#d17e3fb595a9f24fa9e149239a33475d
 https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.10.0-h65c71a3_0.conda#fedf6bfe5d21d21d2b1785ec00a8889a
 https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.9.1-pyhd8ed1ab_1.conda#7ba3f09fceae6a120d664217e58fe686
@@ -267,8 +266,8 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-libxinerama-1.1.5-h5888daf_
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.4-hb9d3cd8_0.conda#2de7f99d6581a4a7adbff607b5c278ca
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxscrnsaver-1.2.4-hb9d3cd8_0.conda#303f7a0e9e0cd7d250bb6b952cecda90
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxxf86vm-1.1.6-hb9d3cd8_0.conda#5efa5fa6243a622445fdfd72aee15efa
-https://conda.anaconda.org/conda-forge/linux-64/yarl-1.20.0-py311h2dc5d0c_0.conda#f937346d50212388f8c954e52133356f
-https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.12.8-py311h2dc5d0c_0.conda#575f5c8cba66e0b332238be31fc8fe2b
+https://conda.anaconda.org/conda-forge/linux-64/yarl-1.20.1-py311h2dc5d0c_0.conda#18c288aa6aae90e2fd8d1cf01d655e4f
+https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.12.12-py311h2dc5d0c_0.conda#0f41db54546a986ff8ee792e02325754
 https://conda.anaconda.org/conda-forge/noarch/asv_runner-0.2.1-pyhd8ed1ab_0.conda#fdcbeb072c80c805a2ededaa5f91cd79
 https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.4-pyha770c72_0.conda#9f07c4fc992adb2d6c30da7fab3959a7
 https://conda.anaconda.org/conda-forge/noarch/dask-core-2025.5.1-pyhd8ed1ab_0.conda#8f0ef561cd615a17df3256742a3457c4
@@ -280,7 +279,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.9.2-nompi_h00e09a9_1
 https://conda.anaconda.org/conda-forge/linux-64/libpq-17.5-h27ae623_0.conda#6458be24f09e1b034902ab44fe9de908
 https://conda.anaconda.org/conda-forge/linux-64/libsndfile-1.2.2-hc60ed4a_1.conda#ef1910918dd895516a769ed36b5b3a4e
 https://conda.anaconda.org/conda-forge/linux-64/libva-2.22.0-h4f16b4b_2.conda#2c65566e79dc11318ce689c656fb551c
-https://conda.anaconda.org/conda-forge/linux-64/numpy-2.2.6-py311h5d046bc_0.conda#babce4d9841ebfcee64249d98eb4e0d4
+https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.0-py311h519dc76_0.conda#002e600fcc82f415bfaad7d05a44c016
 https://conda.anaconda.org/conda-forge/noarch/pbr-6.1.1-pyhd8ed1ab_1.conda#af09bcffe77b7aee6dfe3365e481e284
 https://conda.anaconda.org/conda-forge/linux-64/pyproj-3.7.1-py311h0f98d5a_0.conda#2c17ffd168aafaa12be759e300133e1c
 https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.0-pyhd8ed1ab_0.conda#516d31f063ce7e49ced17f105b63a1f1
@@ -303,7 +302,7 @@ https://conda.anaconda.org/conda-forge/linux-64/netcdf-fortran-4.6.1-nompi_h22f9
 https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.0-py311h7db5c69_0.conda#805040d254f51cb15df55eff6e213d09
 https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hac146a9_1.conda#66b1fa9608d8836e25f9919159adc9c6
 https://conda.anaconda.org/conda-forge/linux-64/pykdtree-1.4.1-py311h9f3472d_0.conda#71a1a3016f6caabac25f77b742460248
-https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.1.1-pyhd8ed1ab_0.conda#1e35d8f975bc0e984a19819aa91c440a
+https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.2.1-pyhd8ed1ab_0.conda#ce978e1b9ed8b8d49164e90a5cdc94cd
 https://conda.anaconda.org/conda-forge/noarch/pytest-mock-3.14.1-pyhd8ed1ab_0.conda#11b313328806f1dfbab0eb1d219388c4
 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.7.0-pyhd8ed1ab_0.conda#15353a2a0ea6dfefaa52fc5ab5b98f41
 https://conda.anaconda.org/conda-forge/linux-64/python-stratify-0.3.0-py311h9f3472d_3.conda#a7c4169b1c920361597ddacb461350fd
@@ -334,8 +333,8 @@ https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-lite-fron
 https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.10.3-py311h2b939e6_0.conda#0d1b7dfe2bbf983cb7907f6cbd6613e6
 https://conda.anaconda.org/conda-forge/linux-64/netcdf4-1.7.2-nompi_py311hae66bec_101.conda#776c96dd51168ef9da0d63d5e49785f5
 https://conda.anaconda.org/conda-forge/noarch/pre-commit-4.2.0-pyha770c72_0.conda#17e487cc8b5507cd3abc09398cf27949
-https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda#a9b9368f3701a417eac9edbcae7cb737
-https://conda.anaconda.org/conda-forge/linux-64/sdl3-3.2.14-he3e324a_0.conda#a750ab1e94750185033ea96eadfc925d
+https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda#f6082eae112814f1447b56a5e1f6ed05
+https://conda.anaconda.org/conda-forge/linux-64/sdl3-3.2.16-he3e324a_0.conda#c3ab38fdbcf36625620c9a4df786320a
 https://conda.anaconda.org/conda-forge/linux-64/cartopy-0.24.0-py311h7db5c69_0.conda#20ba399d57a2b5de789a5b24341481a1
 https://conda.anaconda.org/conda-forge/noarch/cmocean-4.0.3-pyhd8ed1ab_1.conda#dd71e4ec2fbffe38c0359976505f816e
 https://conda.anaconda.org/conda-forge/noarch/esmpy-8.8.1-pyhecae5ae_0.conda#fe06d00073c01b5a741122e64df7ad9f
@@ -343,7 +342,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libass-0.17.3-h52826cd_2.conda#0
 https://conda.anaconda.org/conda-forge/noarch/nc-time-axis-1.4.1-pyhd8ed1ab_1.conda#9a2be7d0089f5934b550933ca0d9fe85
 https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.3-h9ac818e_1.conda#21899b96828014270bd24fd266096612
 https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.2-pyhd8ed1ab_1.conda#b3e783e8e8ed7577cf0b6dee37d1fbac
-https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.8.3-h6441bc3_2.conda#648bb58758f49aea99a48dbe69dcdea2
+https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.8.3-h75f3359_3.conda#1badcbafab8cefdf2f90432dccbd7b2c
 https://conda.anaconda.org/conda-forge/linux-64/sdl2-2.32.54-h3f2d84a_0.conda#91f8537d64c4d52cbbb2910e8bd61bd2
 https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.43-h0c6a113_5.conda#67d00e9cfe751cfe581726c5eff7c184
 https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.4-he92a37e_3.conda#d27665b20bc4d074b86e628b3ba5ab8b
diff --git a/requirements/locks/py312-linux-64.lock b/requirements/locks/py312-linux-64.lock
index 193c8fd04..d7192bce3 100644
--- a/requirements/locks/py312-linux-64.lock
+++ b/requirements/locks/py312-linux-64.lock
@@ -1,6 +1,6 @@
 # Generated by conda-lock.
 # platform: linux-64
-# input_hash: d681b241630803f697d5e9b6fa9676acada3ca6dddc5fceae1c3862e485f519e
+# input_hash: bf2a25a17b2c07e25119f33dada708044990c97795a068075294e902de374798
 @EXPLICIT
 https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81
 https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45
@@ -12,10 +12,10 @@ https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h3f2d84a_0.
 https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda#0dfcdc155cf23812a0c9deada86fb723
 https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a
 https://conda.anaconda.org/conda-forge/linux-64/utfcpp-4.0.6-h005c6e1_0.conda#9464e297fa2bf08030c65a54342b48c3
-https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.44-hd8ed1ab_0.conda#4c33d6dd91f49a0efcc0748fd4d7348b
+https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.45-hd8ed1ab_0.conda#6db9be3b67190229479780eeeee1b35b
 https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.4.26-hbd8a1cb_0.conda#95db94f75ba080a22eb623590993167b
 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29
-https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda#01f8d123c96816249efd255a31ad7712
+https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_5.conda#acd9213a63cb62521290e581ef82de80
 https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_2.conda#434ca7e50e40f4918ab701e3facd59a0
 https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_2.conda#fbe7d535ff9d3a168c148e07358cd5b1
 https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d
@@ -54,7 +54,6 @@ https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62e
 https://conda.anaconda.org/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda#418c6ca5929a611cbd69204907a83995
 https://conda.anaconda.org/conda-forge/linux-64/double-conversion-3.3.1-h5888daf_0.conda#bfd56492d8346d669010eccafe0ba058
 https://conda.anaconda.org/conda-forge/linux-64/epoxy-1.5.10-h166bdaf_1.tar.bz2#a089d06164afd2d511347d3f87214e0b
-https://conda.anaconda.org/conda-forge/linux-64/expat-2.7.0-h5888daf_0.conda#d6845ae4dea52a2f90178bf1829a21f8
 https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.10-h36c2ea0_0.tar.bz2#ac7bc6a654f8f41b352b38f4051135f8
 https://conda.anaconda.org/conda-forge/linux-64/geos-3.13.1-h97f6797_0.conda#5bc18c66111bc94532b0d2df00731c66
 https://conda.anaconda.org/conda-forge/linux-64/jsoncpp-1.9.6-hf42df4d_1.conda#7bdc5e2cc11cb0a0f795bdad9732b0f2
@@ -79,16 +78,16 @@ https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.1-hee588c1_0.cond
 https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda#eecce068c7e4eddeb169591baac20ac4
 https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_2.conda#9d2072af184b5caa29492bf2344597bb
 https://conda.anaconda.org/conda-forge/linux-64/libudunits2-2.2.28-h40f5838_3.conda#4bdace082e911a3e1f1f0b721bed5b56
-https://conda.anaconda.org/conda-forge/linux-64/liburing-2.9-h84d6215_0.conda#ecd409e7bfcf4ee73f74d7a2cc91a4c3
+https://conda.anaconda.org/conda-forge/linux-64/liburing-2.10-h84d6215_0.conda#1a11973f25f6168f4f6a65883cf7bb2a
 https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b
 https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda#92ed62436b625154323d40d5f2f11dd7
 https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc
 https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda#9de5350a85c4a20c685259b889aa6393
 https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.9-hc50e24c_0.conda#c7f302fd11eeb0987a6a5e1f3aed6a21
-https://conda.anaconda.org/conda-forge/linux-64/mysql-common-9.2.0-h266115a_0.conda#db22a0962c953e81a2a679ecb1fc6027
-https://conda.anaconda.org/conda-forge/linux-64/opencl-headers-2024.10.24-h5888daf_0.conda#3ba02cce423fdac1a8582bd6bb189359
+https://conda.anaconda.org/conda-forge/linux-64/mysql-common-9.3.0-h266115a_0.conda#9693774d2822c39a9541f2a80c346e30
+https://conda.anaconda.org/conda-forge/linux-64/opencl-headers-2025.06.13-h5888daf_0.conda#45c3d2c224002d6d0d7769142b29f986
 https://conda.anaconda.org/conda-forge/linux-64/openh264-2.6.0-hc22cd8d_0.conda#b28cf020fd2dead0ca6d113608683842
-https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.0-h29eaf8c_0.conda#d2f1c87d4416d1e7344cf92b1aaee1c4
+https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.2-h29eaf8c_0.conda#39b4228a867772d610c02e06f939a5b8
 https://conda.anaconda.org/conda-forge/linux-64/pugixml-1.15-h3f63f65_0.conda#b11a4c6bf6f6f44e5e143f759ffa2087
 https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446
 https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.1-h8bd8927_1.conda#3b3e64af585eadfb52bb90b553db5edf
@@ -123,9 +122,9 @@ https://conda.anaconda.org/conda-forge/linux-64/libunwind-1.6.2-h9c3ff4c_0.tar.b
 https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h9c3ff4c_0.tar.bz2#309dec04b70a3cc0f1e84a4013683bc0
 https://conda.anaconda.org/conda-forge/linux-64/libvpx-1.14.1-hac33072_0.conda#cde393f461e0c169d9ffb2fc70f81c33
 https://conda.anaconda.org/conda-forge/linux-64/libzip-1.11.2-h6991a6a_0.conda#a7b27c075c9b7f459f1c022090697cba
-https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-9.2.0-he0572af_0.conda#93340b072c393d23c4700a1d40565dca
+https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-9.3.0-he0572af_0.conda#5eb3dd8ccc96213c0961a2bba1f611d1
 https://conda.anaconda.org/conda-forge/linux-64/ocl-icd-2.3.3-hb9d3cd8_0.conda#56f8947aa9d5cf37b0b3d43b83f34192
-https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.44-hc749103_2.conda#31614c73d7b103ef76faa4d83d261d34
+https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.45-hc749103_0.conda#b90bece58b4c2bf25969b70f3be42d25
 https://conda.anaconda.org/conda-forge/linux-64/python-3.12.11-h9e4cc4f_0_cpython.conda#94206474a5608243a10c92cefbe0908f
 https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda#353823361b1d27eb3960efb076dfcaf6
 https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.50.1-h9eae976_0.conda#e12789602c09a875957c1c78ff47cdf6
@@ -151,7 +150,7 @@ https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#
 https://conda.anaconda.org/conda-forge/noarch/colorcet-3.1.0-pyhd8ed1ab_1.conda#91d7152c744dc0f18ef8beb3cbc9980a
 https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_1.conda#44600c4667a319d67dbe0681fc0bc833
 https://conda.anaconda.org/conda-forge/linux-64/cyrus-sasl-2.1.27-h54b06d7_7.conda#dce22f70b4e5a407ce88f2be046f4ceb
-https://conda.anaconda.org/conda-forge/linux-64/cython-3.1.1-py312h2614dfc_1.conda#90713c494a3ba0f517304841df2875d8
+https://conda.anaconda.org/conda-forge/linux-64/cython-3.1.2-py312h2614dfc_2.conda#a43c07059dee14cea7f2b1d025ec9440
 https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.9-pyhd8ed1ab_1.conda#8d88f4a2242e6b96f9ecff9a6a05b2f1
 https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda#24c1ca34138ee57de72a943237cde4cc
 https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda#a71efeae2c160f6789900ba2631a2c90
@@ -171,7 +170,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-31_h59b9bed_openbl
 https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h4637d8d_4.conda#d4529f4dff3057982a7617c7ac58fde3
 https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda#45f6713cb00f124af300342512219182
 https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.13.3-ha770c72_1.conda#51f5be229d83ecd401fb369ab96ae669
-https://conda.anaconda.org/conda-forge/linux-64/libglib-2.84.1-h2ff4ddf_0.conda#0305434da649d4fb48a425e588b79ea6
+https://conda.anaconda.org/conda-forge/linux-64/libglib-2.84.2-h3618099_0.conda#072ab14a02164b7c0c089055368ff776
 https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda#c8013e438185f33b13814c5c488acd5c
 https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-257.6-h4e0b6ca_0.conda#071409970083d0f99ab7b569352771c9
 https://conda.anaconda.org/conda-forge/linux-64/libtheora-1.1.1-h4ab18f5_1006.conda#553281a034e9cf8693c9df49f6c78ea1
@@ -180,7 +179,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h4bc477f_0.conda#
 https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2#91e27ef3d05cc772ce627e51cff111c4
 https://conda.anaconda.org/conda-forge/noarch/loguru-0.7.3-pyh707e725_0.conda#49647ac1de4d1e4b49124aedf3934e02
 https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py312h178313f_1.conda#eb227c3e0bf58f5bd69c0532b157975b
-https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.1.0-py312h68727a3_0.conda#5c9b020a3f86799cdc6115e55df06146
+https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.1.1-py312h68727a3_0.conda#6998b34027ecc577efe4e42f4b022a98
 https://conda.anaconda.org/conda-forge/linux-64/multidict-6.4.4-py312h178313f_0.conda#93bba0d9fb1c83e7642363c9f8062bd2
 https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19
 https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.3-h5fbd93e_0.conda#9e5816bc95d285c115a3ebc2f8563564
@@ -214,28 +213,28 @@ https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.0-pyhe01879
 https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-16.0.0-py312h66e93f0_0.conda#617f5d608ff8c28ad546e5d9671cbb95
 https://conda.anaconda.org/conda-forge/noarch/wheel-0.45.1-pyhd8ed1ab_1.conda#75cb7132eb58d97896e173ef12ac9986
 https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda#a0901183f08b6c7107aab109733a3c91
-https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.44-hb9d3cd8_0.conda#7c91bfc90672888259675ad2ad28af9c
+https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.45-hb9d3cd8_0.conda#397a013c2dc5145a70737871aaa87e98
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda#febbab7d15033c913d53c7a2c102309d
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.1-hb9d3cd8_0.conda#4bdb303603e9821baf5fe5fdff1dc8f8
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda#96d57aba173e878a2089d5638016dc5e
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxt-1.3.1-hb9d3cd8_0.conda#279b0de5f6ba95457190a1c459a64e31
 https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda#e52c2ef711ccf31bb7f70ca87d144b9e
-https://conda.anaconda.org/conda-forge/noarch/zipp-3.22.0-pyhd8ed1ab_0.conda#234be740b00b8e41567e5b0ed95aaba9
+https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda#df5e78d904988eb55042c0c97446079f
 https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda#74ac5069774cdbc53910ec4d631a3999
 https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.3.2-pyhd8ed1ab_0.conda#1a3981115a398535dbe3f6d5faae3d36
 https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda#f730d54ba9cd543666d7220c9f7ed563
 https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda#0a01c169f0ab0f91b26e77a3301fbfe4
 https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda#a861504bbea4161a9170b85d4d2be840
 https://conda.anaconda.org/conda-forge/noarch/click-default-group-1.2.4-pyhd8ed1ab_1.conda#7cd83dd6831b61ad9624a694e4afd7dc
-https://conda.anaconda.org/conda-forge/linux-64/coverage-7.8.2-py312h178313f_0.conda#141e4480d38281c3988f3a9aa917b07d
+https://conda.anaconda.org/conda-forge/linux-64/coverage-7.9.1-py312h178313f_0.conda#4c18b79fa2a3371557ed3663876e5dcc
 https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.0.1-py312h66e93f0_0.conda#6198b134b1c08173f33653896974d477
-https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2#ecfff944ba3960ecb334b9a2663d708d
+https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda#679616eb5ad4e521c83da4650860aba7
 https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda#72e42d28960d875c7654614f8b50939a
-https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.58.2-py312h178313f_0.conda#286068e5706fa6eacce413a594cf0d4b
+https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.58.4-py312h178313f_0.conda#223a4616e3db7336569eafefac04ebbf
 https://conda.anaconda.org/conda-forge/linux-64/freetype-2.13.3-ha770c72_1.conda#9ccd736d31e0c6e41f54e704e5312811
 https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.42.12-hb9ae30d_0.conda#201db6c2d9a3c5e46573ac4cb2e92f4f
 https://conda.anaconda.org/conda-forge/linux-64/glew-2.1.0-h9c3ff4c_2.tar.bz2#fb05eb5c47590b247658243d27fc32f1
-https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.84.1-h4833e2c_0.conda#ddc06964296eee2b4070e65415b332fd
+https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.84.2-h4833e2c_0.conda#f2ec1facec64147850b7674633978050
 https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda#4d8df0b0db060d33c9a702ada998a8fe
 https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda#b4754fb1bdcb70c8fd54f918301582c6
 https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.3-nompi_h2d575fe_109.conda#e7a7a6e6f70553a31e6e79c65768d089
@@ -246,7 +245,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda#e
 https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_2.conda#928b8be80851f5d8ffb016f9c81dae7a
 https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.11.2-default_h0d58e46_1001.conda#804ca9e91bcaea0824a341d55b1684f2
 https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-31_h7ac8fdf_openblas.conda#452b98eafe050ecff932f0ec832dd03f
-https://conda.anaconda.org/conda-forge/linux-64/libllvm20-20.1.6-he9d0ab4_0.conda#bf8ccdd2c1c1a54a3fa25bb61f26460e
+https://conda.anaconda.org/conda-forge/linux-64/libllvm20-20.1.7-he9d0ab4_0.conda#63f1accca4913e6b66a2d546c30ff4db
 https://conda.anaconda.org/conda-forge/linux-64/libusb-1.0.29-h73b1eb8_0.conda#d17e3fb595a9f24fa9e149239a33475d
 https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.10.0-h65c71a3_0.conda#fedf6bfe5d21d21d2b1785ec00a8889a
 https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.9.1-pyhd8ed1ab_1.conda#7ba3f09fceae6a120d664217e58fe686
@@ -267,8 +266,8 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-libxinerama-1.1.5-h5888daf_
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.4-hb9d3cd8_0.conda#2de7f99d6581a4a7adbff607b5c278ca
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxscrnsaver-1.2.4-hb9d3cd8_0.conda#303f7a0e9e0cd7d250bb6b952cecda90
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxxf86vm-1.1.6-hb9d3cd8_0.conda#5efa5fa6243a622445fdfd72aee15efa
-https://conda.anaconda.org/conda-forge/linux-64/yarl-1.20.0-py312h178313f_0.conda#d5294c1a9a77309a5ac10b0f8d483b18
-https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.12.8-py312h178313f_0.conda#cc34f35d5f1e4ff404a221935dd2f3d5
+https://conda.anaconda.org/conda-forge/linux-64/yarl-1.20.1-py312h178313f_0.conda#3b3fa80c71d6a8d0380e9e790f5a4a8a
+https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.12.12-py312h178313f_0.conda#db7f3bd6ae00b2a00732b1c6228d34fd
 https://conda.anaconda.org/conda-forge/noarch/asv_runner-0.2.1-pyhd8ed1ab_0.conda#fdcbeb072c80c805a2ededaa5f91cd79
 https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.4-pyha770c72_0.conda#9f07c4fc992adb2d6c30da7fab3959a7
 https://conda.anaconda.org/conda-forge/noarch/dask-core-2025.5.1-pyhd8ed1ab_0.conda#8f0ef561cd615a17df3256742a3457c4
@@ -280,7 +279,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.9.2-nompi_h00e09a9_1
 https://conda.anaconda.org/conda-forge/linux-64/libpq-17.5-h27ae623_0.conda#6458be24f09e1b034902ab44fe9de908
 https://conda.anaconda.org/conda-forge/linux-64/libsndfile-1.2.2-hc60ed4a_1.conda#ef1910918dd895516a769ed36b5b3a4e
 https://conda.anaconda.org/conda-forge/linux-64/libva-2.22.0-h4f16b4b_2.conda#2c65566e79dc11318ce689c656fb551c
-https://conda.anaconda.org/conda-forge/linux-64/numpy-2.2.6-py312h72c5963_0.conda#17fac9db62daa5c810091c2882b28f45
+https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.0-py312h6cf2f7f_0.conda#8b4095ed29d1072f7e4badfbaf9e5851
 https://conda.anaconda.org/conda-forge/noarch/pbr-6.1.1-pyhd8ed1ab_1.conda#af09bcffe77b7aee6dfe3365e481e284
 https://conda.anaconda.org/conda-forge/linux-64/pyproj-3.7.1-py312he630544_0.conda#ed6a6bbd26559986ecd90b9a1797cbf0
 https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.0-pyhd8ed1ab_0.conda#516d31f063ce7e49ced17f105b63a1f1
@@ -303,7 +302,7 @@ https://conda.anaconda.org/conda-forge/linux-64/netcdf-fortran-4.6.1-nompi_h22f9
 https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.0-py312hf9745cd_0.conda#ac82ac336dbe61106e21fb2e11704459
 https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hac146a9_1.conda#66b1fa9608d8836e25f9919159adc9c6
 https://conda.anaconda.org/conda-forge/linux-64/pykdtree-1.4.1-py312hc0a28a1_0.conda#6a0691f8e533d92b14a6d5eee07b6964
-https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.1.1-pyhd8ed1ab_0.conda#1e35d8f975bc0e984a19819aa91c440a
+https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.2.1-pyhd8ed1ab_0.conda#ce978e1b9ed8b8d49164e90a5cdc94cd
 https://conda.anaconda.org/conda-forge/noarch/pytest-mock-3.14.1-pyhd8ed1ab_0.conda#11b313328806f1dfbab0eb1d219388c4
 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.7.0-pyhd8ed1ab_0.conda#15353a2a0ea6dfefaa52fc5ab5b98f41
 https://conda.anaconda.org/conda-forge/linux-64/python-stratify-0.3.0-py312hc0a28a1_3.conda#81bbcb20ea4a53b05a8cf51f31496038
@@ -334,8 +333,8 @@ https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-lite-fron
 https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.10.3-py312hd3ec401_0.conda#2d69618b52d70970c81cc598e4b51118
 https://conda.anaconda.org/conda-forge/linux-64/netcdf4-1.7.2-nompi_py312h21d6d8e_101.conda#0cabc6745882859ec7be35433c7b8f24
 https://conda.anaconda.org/conda-forge/noarch/pre-commit-4.2.0-pyha770c72_0.conda#17e487cc8b5507cd3abc09398cf27949
-https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda#a9b9368f3701a417eac9edbcae7cb737
-https://conda.anaconda.org/conda-forge/linux-64/sdl3-3.2.14-he3e324a_0.conda#a750ab1e94750185033ea96eadfc925d
+https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda#f6082eae112814f1447b56a5e1f6ed05
+https://conda.anaconda.org/conda-forge/linux-64/sdl3-3.2.16-he3e324a_0.conda#c3ab38fdbcf36625620c9a4df786320a
 https://conda.anaconda.org/conda-forge/linux-64/cartopy-0.24.0-py312hf9745cd_0.conda#ea213e31805199cb7d0da457b879ceed
 https://conda.anaconda.org/conda-forge/noarch/cmocean-4.0.3-pyhd8ed1ab_1.conda#dd71e4ec2fbffe38c0359976505f816e
 https://conda.anaconda.org/conda-forge/noarch/esmpy-8.8.1-pyhecae5ae_0.conda#fe06d00073c01b5a741122e64df7ad9f
@@ -343,7 +342,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libass-0.17.3-h52826cd_2.conda#0
 https://conda.anaconda.org/conda-forge/noarch/nc-time-axis-1.4.1-pyhd8ed1ab_1.conda#9a2be7d0089f5934b550933ca0d9fe85
 https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.3-h9ac818e_1.conda#21899b96828014270bd24fd266096612
 https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.2-pyhd8ed1ab_1.conda#b3e783e8e8ed7577cf0b6dee37d1fbac
-https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.8.3-h6441bc3_2.conda#648bb58758f49aea99a48dbe69dcdea2
+https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.8.3-h75f3359_3.conda#1badcbafab8cefdf2f90432dccbd7b2c
 https://conda.anaconda.org/conda-forge/linux-64/sdl2-2.32.54-h3f2d84a_0.conda#91f8537d64c4d52cbbb2910e8bd61bd2
 https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.43-h0c6a113_5.conda#67d00e9cfe751cfe581726c5eff7c184
 https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.4-he92a37e_3.conda#d27665b20bc4d074b86e628b3ba5ab8b
diff --git a/requirements/locks/py313-linux-64.lock b/requirements/locks/py313-linux-64.lock
index 7ebd0a1a4..deece1d00 100644
--- a/requirements/locks/py313-linux-64.lock
+++ b/requirements/locks/py313-linux-64.lock
@@ -1,6 +1,6 @@
 # Generated by conda-lock.
 # platform: linux-64
-# input_hash: 9647ac284f93ff42b1239561c944e8342bdb4d367be1bbe78c168ae4751c3588
+# input_hash: 1c3b094e421df3076cd379373be66793051a8f4eb4ab3f4f5c7545ac740d1367
 @EXPLICIT
 https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81
 https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45
@@ -12,10 +12,10 @@ https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h3f2d84a_0.
 https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-7_cp313.conda#e84b44e6300f1703cb25d29120c5b1d8
 https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda#4222072737ccff51314b5ece9c7d6f5a
 https://conda.anaconda.org/conda-forge/linux-64/utfcpp-4.0.6-h005c6e1_0.conda#9464e297fa2bf08030c65a54342b48c3
-https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.44-hd8ed1ab_0.conda#4c33d6dd91f49a0efcc0748fd4d7348b
+https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.45-hd8ed1ab_0.conda#6db9be3b67190229479780eeeee1b35b
 https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.4.26-hbd8a1cb_0.conda#95db94f75ba080a22eb623590993167b
 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29
-https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda#01f8d123c96816249efd255a31ad7712
+https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_5.conda#acd9213a63cb62521290e581ef82de80
 https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_2.conda#434ca7e50e40f4918ab701e3facd59a0
 https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_2.conda#fbe7d535ff9d3a168c148e07358cd5b1
 https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d
@@ -55,7 +55,6 @@ https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda#62e
 https://conda.anaconda.org/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda#418c6ca5929a611cbd69204907a83995
 https://conda.anaconda.org/conda-forge/linux-64/double-conversion-3.3.1-h5888daf_0.conda#bfd56492d8346d669010eccafe0ba058
 https://conda.anaconda.org/conda-forge/linux-64/epoxy-1.5.10-h166bdaf_1.tar.bz2#a089d06164afd2d511347d3f87214e0b
-https://conda.anaconda.org/conda-forge/linux-64/expat-2.7.0-h5888daf_0.conda#d6845ae4dea52a2f90178bf1829a21f8
 https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.10-h36c2ea0_0.tar.bz2#ac7bc6a654f8f41b352b38f4051135f8
 https://conda.anaconda.org/conda-forge/linux-64/geos-3.13.1-h97f6797_0.conda#5bc18c66111bc94532b0d2df00731c66
 https://conda.anaconda.org/conda-forge/linux-64/jsoncpp-1.9.6-hf42df4d_1.conda#7bdc5e2cc11cb0a0f795bdad9732b0f2
@@ -79,15 +78,15 @@ https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.1-hee588c1_0.cond
 https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda#eecce068c7e4eddeb169591baac20ac4
 https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_2.conda#9d2072af184b5caa29492bf2344597bb
 https://conda.anaconda.org/conda-forge/linux-64/libudunits2-2.2.28-h40f5838_3.conda#4bdace082e911a3e1f1f0b721bed5b56
-https://conda.anaconda.org/conda-forge/linux-64/liburing-2.9-h84d6215_0.conda#ecd409e7bfcf4ee73f74d7a2cc91a4c3
+https://conda.anaconda.org/conda-forge/linux-64/liburing-2.10-h84d6215_0.conda#1a11973f25f6168f4f6a65883cf7bb2a
 https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b
 https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda#92ed62436b625154323d40d5f2f11dd7
 https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda#9de5350a85c4a20c685259b889aa6393
 https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.9-hc50e24c_0.conda#c7f302fd11eeb0987a6a5e1f3aed6a21
-https://conda.anaconda.org/conda-forge/linux-64/mysql-common-9.2.0-h266115a_0.conda#db22a0962c953e81a2a679ecb1fc6027
-https://conda.anaconda.org/conda-forge/linux-64/opencl-headers-2024.10.24-h5888daf_0.conda#3ba02cce423fdac1a8582bd6bb189359
+https://conda.anaconda.org/conda-forge/linux-64/mysql-common-9.3.0-h266115a_0.conda#9693774d2822c39a9541f2a80c346e30
+https://conda.anaconda.org/conda-forge/linux-64/opencl-headers-2025.06.13-h5888daf_0.conda#45c3d2c224002d6d0d7769142b29f986
 https://conda.anaconda.org/conda-forge/linux-64/openh264-2.6.0-hc22cd8d_0.conda#b28cf020fd2dead0ca6d113608683842
-https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.0-h29eaf8c_0.conda#d2f1c87d4416d1e7344cf92b1aaee1c4
+https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.2-h29eaf8c_0.conda#39b4228a867772d610c02e06f939a5b8
 https://conda.anaconda.org/conda-forge/linux-64/pugixml-1.15-h3f63f65_0.conda#b11a4c6bf6f6f44e5e143f759ffa2087
 https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda#283b96675859b20a825f8fa30f311446
 https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.1-h8bd8927_1.conda#3b3e64af585eadfb52bb90b553db5edf
@@ -122,10 +121,10 @@ https://conda.anaconda.org/conda-forge/linux-64/libunwind-1.6.2-h9c3ff4c_0.tar.b
 https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h9c3ff4c_0.tar.bz2#309dec04b70a3cc0f1e84a4013683bc0
 https://conda.anaconda.org/conda-forge/linux-64/libvpx-1.14.1-hac33072_0.conda#cde393f461e0c169d9ffb2fc70f81c33
 https://conda.anaconda.org/conda-forge/linux-64/libzip-1.11.2-h6991a6a_0.conda#a7b27c075c9b7f459f1c022090697cba
-https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-9.2.0-he0572af_0.conda#93340b072c393d23c4700a1d40565dca
+https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-9.3.0-he0572af_0.conda#5eb3dd8ccc96213c0961a2bba1f611d1
 https://conda.anaconda.org/conda-forge/linux-64/ocl-icd-2.3.3-hb9d3cd8_0.conda#56f8947aa9d5cf37b0b3d43b83f34192
-https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.44-hc749103_2.conda#31614c73d7b103ef76faa4d83d261d34
-https://conda.anaconda.org/conda-forge/linux-64/python-3.13.3-hf636f53_101_cp313.conda#10622e12d649154af0bd76bcf33a7c5c
+https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.45-hc749103_0.conda#b90bece58b4c2bf25969b70f3be42d25
+https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hf636f53_101_cp313.conda#f3fa8f5ca181e0bacf92a09114fc4f31
 https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda#353823361b1d27eb3960efb076dfcaf6
 https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.50.1-h9eae976_0.conda#e12789602c09a875957c1c78ff47cdf6
 https://conda.anaconda.org/conda-forge/linux-64/udunits2-2.2.28-h40f5838_3.conda#6bb8deb138f87c9d48320ac21b87e7a1
@@ -150,7 +149,7 @@ https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#
 https://conda.anaconda.org/conda-forge/noarch/colorcet-3.1.0-pyhd8ed1ab_1.conda#91d7152c744dc0f18ef8beb3cbc9980a
 https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_1.conda#44600c4667a319d67dbe0681fc0bc833
 https://conda.anaconda.org/conda-forge/linux-64/cyrus-sasl-2.1.27-h54b06d7_7.conda#dce22f70b4e5a407ce88f2be046f4ceb
-https://conda.anaconda.org/conda-forge/linux-64/cython-3.1.1-py313h5dec8f5_1.conda#f114755cdd37627732b1884b7b15d4b5
+https://conda.anaconda.org/conda-forge/linux-64/cython-3.1.2-py313h5dec8f5_2.conda#790ba9e115dfa69fde25212a51fe3d30
 https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.9-pyhd8ed1ab_1.conda#8d88f4a2242e6b96f9ecff9a6a05b2f1
 https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda#24c1ca34138ee57de72a943237cde4cc
 https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda#a71efeae2c160f6789900ba2631a2c90
@@ -170,7 +169,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-31_h59b9bed_openbl
 https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h4637d8d_4.conda#d4529f4dff3057982a7617c7ac58fde3
 https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda#45f6713cb00f124af300342512219182
 https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.13.3-ha770c72_1.conda#51f5be229d83ecd401fb369ab96ae669
-https://conda.anaconda.org/conda-forge/linux-64/libglib-2.84.1-h2ff4ddf_0.conda#0305434da649d4fb48a425e588b79ea6
+https://conda.anaconda.org/conda-forge/linux-64/libglib-2.84.2-h3618099_0.conda#072ab14a02164b7c0c089055368ff776
 https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda#c8013e438185f33b13814c5c488acd5c
 https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-257.6-h4e0b6ca_0.conda#071409970083d0f99ab7b569352771c9
 https://conda.anaconda.org/conda-forge/linux-64/libtheora-1.1.1-h4ab18f5_1006.conda#553281a034e9cf8693c9df49f6c78ea1
@@ -179,7 +178,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h4bc477f_0.conda#
 https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2#91e27ef3d05cc772ce627e51cff111c4
 https://conda.anaconda.org/conda-forge/noarch/loguru-0.7.3-pyh707e725_0.conda#49647ac1de4d1e4b49124aedf3934e02
 https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py313h8060acc_1.conda#21b62c55924f01b6eef6827167b46acb
-https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.1.0-py313h33d0bda_0.conda#7f907b1065247efa419bb70d3a3341b5
+https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.1.1-py313h33d0bda_0.conda#009fb5ad03d4506be5f1e5c2f875f1c2
 https://conda.anaconda.org/conda-forge/linux-64/multidict-6.4.4-py313h8060acc_0.conda#058b6c31f33ca77d48f11167af868a48
 https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19
 https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.3-h5fbd93e_0.conda#9e5816bc95d285c115a3ebc2f8563564
@@ -212,28 +211,28 @@ https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda#40d
 https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.1-py313h536fd9c_0.conda#e9434a5155db25c38ade26f71a2f5a48
 https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.0-pyhe01879c_0.conda#2adcd9bb86f656d3d43bf84af59a1faf
 https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda#a0901183f08b6c7107aab109733a3c91
-https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.44-hb9d3cd8_0.conda#7c91bfc90672888259675ad2ad28af9c
+https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.45-hb9d3cd8_0.conda#397a013c2dc5145a70737871aaa87e98
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda#febbab7d15033c913d53c7a2c102309d
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.1-hb9d3cd8_0.conda#4bdb303603e9821baf5fe5fdff1dc8f8
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda#96d57aba173e878a2089d5638016dc5e
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxt-1.3.1-hb9d3cd8_0.conda#279b0de5f6ba95457190a1c459a64e31
 https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda#e52c2ef711ccf31bb7f70ca87d144b9e
-https://conda.anaconda.org/conda-forge/noarch/zipp-3.22.0-pyhd8ed1ab_0.conda#234be740b00b8e41567e5b0ed95aaba9
+https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda#df5e78d904988eb55042c0c97446079f
 https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda#74ac5069774cdbc53910ec4d631a3999
 https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.3.2-pyhd8ed1ab_0.conda#1a3981115a398535dbe3f6d5faae3d36
 https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda#f730d54ba9cd543666d7220c9f7ed563
 https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda#0a01c169f0ab0f91b26e77a3301fbfe4
 https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py313hfab6e84_0.conda#ce6386a5892ef686d6d680c345c40ad1
 https://conda.anaconda.org/conda-forge/noarch/click-default-group-1.2.4-pyhd8ed1ab_1.conda#7cd83dd6831b61ad9624a694e4afd7dc
-https://conda.anaconda.org/conda-forge/linux-64/coverage-7.8.2-py313h8060acc_0.conda#b278629953bd3424060870fca744de4a
+https://conda.anaconda.org/conda-forge/linux-64/coverage-7.9.1-py313h8060acc_0.conda#5e959c405af6d6b603810fdf12b6f191
 https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.0.1-py313h536fd9c_0.conda#e886bb6a3c24f8b9dd4fcd1d617a1f64
-https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2#ecfff944ba3960ecb334b9a2663d708d
+https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda#679616eb5ad4e521c83da4650860aba7
 https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda#72e42d28960d875c7654614f8b50939a
-https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.58.2-py313h8060acc_0.conda#e651d100ab0c032d68923868653fe00a
+https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.58.4-py313h8060acc_0.conda#1a5eb37c590d8adeb64145990f70c50b
 https://conda.anaconda.org/conda-forge/linux-64/freetype-2.13.3-ha770c72_1.conda#9ccd736d31e0c6e41f54e704e5312811
 https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.42.12-hb9ae30d_0.conda#201db6c2d9a3c5e46573ac4cb2e92f4f
 https://conda.anaconda.org/conda-forge/linux-64/glew-2.1.0-h9c3ff4c_2.tar.bz2#fb05eb5c47590b247658243d27fc32f1
-https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.84.1-h4833e2c_0.conda#ddc06964296eee2b4070e65415b332fd
+https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.84.2-h4833e2c_0.conda#f2ec1facec64147850b7674633978050
 https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda#4d8df0b0db060d33c9a702ada998a8fe
 https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda#b4754fb1bdcb70c8fd54f918301582c6
 https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.3-nompi_h2d575fe_109.conda#e7a7a6e6f70553a31e6e79c65768d089
@@ -244,7 +243,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda#e
 https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_2.conda#928b8be80851f5d8ffb016f9c81dae7a
 https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.11.2-default_h0d58e46_1001.conda#804ca9e91bcaea0824a341d55b1684f2
 https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-31_h7ac8fdf_openblas.conda#452b98eafe050ecff932f0ec832dd03f
-https://conda.anaconda.org/conda-forge/linux-64/libllvm20-20.1.6-he9d0ab4_0.conda#bf8ccdd2c1c1a54a3fa25bb61f26460e
+https://conda.anaconda.org/conda-forge/linux-64/libllvm20-20.1.7-he9d0ab4_0.conda#63f1accca4913e6b66a2d546c30ff4db
 https://conda.anaconda.org/conda-forge/linux-64/libusb-1.0.29-h73b1eb8_0.conda#d17e3fb595a9f24fa9e149239a33475d
 https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.10.0-h65c71a3_0.conda#fedf6bfe5d21d21d2b1785ec00a8889a
 https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.9.1-pyhd8ed1ab_1.conda#7ba3f09fceae6a120d664217e58fe686
@@ -265,8 +264,8 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-libxinerama-1.1.5-h5888daf_
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.4-hb9d3cd8_0.conda#2de7f99d6581a4a7adbff607b5c278ca
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxscrnsaver-1.2.4-hb9d3cd8_0.conda#303f7a0e9e0cd7d250bb6b952cecda90
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxxf86vm-1.1.6-hb9d3cd8_0.conda#5efa5fa6243a622445fdfd72aee15efa
-https://conda.anaconda.org/conda-forge/linux-64/yarl-1.20.0-py313h8060acc_0.conda#b65428b96edcf98334d8a5e2cdc3d336
-https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.12.8-py313h8060acc_0.conda#45b58ef17ff9ae3b77ec1c88beb736f4
+https://conda.anaconda.org/conda-forge/linux-64/yarl-1.20.1-py313h8060acc_0.conda#b3659ec61a97eb6f64aeca04effb999d
+https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.12.12-py313h8060acc_0.conda#df2bac23c7ef0fee997f2cc6f13b8483
 https://conda.anaconda.org/conda-forge/noarch/asv_runner-0.2.1-pyhd8ed1ab_0.conda#fdcbeb072c80c805a2ededaa5f91cd79
 https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.4-pyha770c72_0.conda#9f07c4fc992adb2d6c30da7fab3959a7
 https://conda.anaconda.org/conda-forge/noarch/dask-core-2025.5.1-pyhd8ed1ab_0.conda#8f0ef561cd615a17df3256742a3457c4
@@ -278,7 +277,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.9.2-nompi_h00e09a9_1
 https://conda.anaconda.org/conda-forge/linux-64/libpq-17.5-h27ae623_0.conda#6458be24f09e1b034902ab44fe9de908
 https://conda.anaconda.org/conda-forge/linux-64/libsndfile-1.2.2-hc60ed4a_1.conda#ef1910918dd895516a769ed36b5b3a4e
 https://conda.anaconda.org/conda-forge/linux-64/libva-2.22.0-h4f16b4b_2.conda#2c65566e79dc11318ce689c656fb551c
-https://conda.anaconda.org/conda-forge/linux-64/numpy-2.2.6-py313h17eae1a_0.conda#7a2d2f9adecd86ed5c29c2115354f615
+https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.0-py313h17eae1a_0.conda#db18a34466bef0863e9301b518a75e8f
 https://conda.anaconda.org/conda-forge/linux-64/pyproj-3.7.1-py313hdb96ca5_0.conda#82955f9ccbfc0ecfd9c34dbf8a0767be
 https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.0-pyhd8ed1ab_0.conda#516d31f063ce7e49ced17f105b63a1f1
 https://conda.anaconda.org/conda-forge/noarch/setuptools-scm-8.3.1-pyhd8ed1ab_0.conda#996376098e3648237b3efb0e0ad460c1
@@ -301,7 +300,7 @@ https://conda.anaconda.org/conda-forge/linux-64/netcdf-fortran-4.6.1-nompi_h22f9
 https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.0-py313ha87cce1_0.conda#8664b4fa9b5b23b0d1cdc55c7195fcfe
 https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hac146a9_1.conda#66b1fa9608d8836e25f9919159adc9c6
 https://conda.anaconda.org/conda-forge/linux-64/pykdtree-1.4.1-py313ha014f3b_0.conda#a6d57e9737d2a713e41d6252e0dd5439
-https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.1.1-pyhd8ed1ab_0.conda#1e35d8f975bc0e984a19819aa91c440a
+https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.2.1-pyhd8ed1ab_0.conda#ce978e1b9ed8b8d49164e90a5cdc94cd
 https://conda.anaconda.org/conda-forge/noarch/pytest-mock-3.14.1-pyhd8ed1ab_0.conda#11b313328806f1dfbab0eb1d219388c4
 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.7.0-pyhd8ed1ab_0.conda#15353a2a0ea6dfefaa52fc5ab5b98f41
 https://conda.anaconda.org/conda-forge/linux-64/python-stratify-0.3.0-py313ha014f3b_3.conda#041b8326743c64bd02b8c0f34f05e1ef
@@ -331,8 +330,8 @@ https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-lite-fron
 https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.10.3-py313h129903b_0.conda#4f8816d006b1c155ec416bcf7ff6cee2
 https://conda.anaconda.org/conda-forge/linux-64/netcdf4-1.7.2-nompi_py313h2a70696_101.conda#fe03a55f80aef5f47b65320cd10025b4
 https://conda.anaconda.org/conda-forge/noarch/pre-commit-4.2.0-pyha770c72_0.conda#17e487cc8b5507cd3abc09398cf27949
-https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda#a9b9368f3701a417eac9edbcae7cb737
-https://conda.anaconda.org/conda-forge/linux-64/sdl3-3.2.14-he3e324a_0.conda#a750ab1e94750185033ea96eadfc925d
+https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda#f6082eae112814f1447b56a5e1f6ed05
+https://conda.anaconda.org/conda-forge/linux-64/sdl3-3.2.16-he3e324a_0.conda#c3ab38fdbcf36625620c9a4df786320a
 https://conda.anaconda.org/conda-forge/linux-64/cartopy-0.24.0-py313ha87cce1_0.conda#44c2091019480603a885aa01e7b710e7
 https://conda.anaconda.org/conda-forge/noarch/cmocean-4.0.3-pyhd8ed1ab_1.conda#dd71e4ec2fbffe38c0359976505f816e
 https://conda.anaconda.org/conda-forge/noarch/esmpy-8.8.1-pyhecae5ae_0.conda#fe06d00073c01b5a741122e64df7ad9f
@@ -340,7 +339,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libass-0.17.3-h52826cd_2.conda#0
 https://conda.anaconda.org/conda-forge/noarch/nc-time-axis-1.4.1-pyhd8ed1ab_1.conda#9a2be7d0089f5934b550933ca0d9fe85
 https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.3-h9ac818e_1.conda#21899b96828014270bd24fd266096612
 https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.2-pyhd8ed1ab_1.conda#b3e783e8e8ed7577cf0b6dee37d1fbac
-https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.8.3-h6441bc3_2.conda#648bb58758f49aea99a48dbe69dcdea2
+https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.8.3-h75f3359_3.conda#1badcbafab8cefdf2f90432dccbd7b2c
 https://conda.anaconda.org/conda-forge/linux-64/sdl2-2.32.54-h3f2d84a_0.conda#91f8537d64c4d52cbbb2910e8bd61bd2
 https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.43-h0c6a113_5.conda#67d00e9cfe751cfe581726c5eff7c184
 https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.4-he92a37e_3.conda#d27665b20bc4d074b86e628b3ba5ab8b


Test patch:
diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst
index 909ef18bc..6923db717 100644
--- a/docs/src/whatsnew/latest.rst
+++ b/docs/src/whatsnew/latest.rst
@@ -59,6 +59,15 @@ This document explains the changes made to Iris for this release
 #. `@schlunma`_ fixed loading of netCDF files with coordinates that have
    non-string units. (:issue:`6505`, :pull:`6506`)
 
+#. `@ukmo-ccbunney`_ correctly set the ``bplon`` PP field parameter when saving
+   a cube defined on Limited Area Model (LAM) grid to PP format. Activate this
+   behaviour with the new Futures flag ``iris.FUTURE.lam_pole_offset=True``.
+   (:issue:`3560`, :pull:`6520`)
+
+#. `@stephenworsley`_ fixed incompatibilities with numpy v2.3 affecting arrays of dates and
+   array printing. (:pull:`6518`)
+
+
 💣 Incompatible Changes
 =======================
 
@@ -98,6 +107,10 @@ This document explains the changes made to Iris for this release
 #. `@trexfeathers`_ added a CI workflow to quickly validate that the
    benchmarking setup is still working. (:pull:`6496`)
 
+#. `@trexfeathers`_ improved the stack trace for errors that occur during
+   benchmark data generation, showing developers the root problem at-a-glance
+   without needing local replication. (:pull:`6524`)
+
 
 .. comment
     Whatsnew author names (@github name) in alphabetical order. Note that,
diff --git a/lib/iris/tests/_shared_utils.py b/lib/iris/tests/_shared_utils.py
index 992f70d1e..052eef2cd 100644
--- a/lib/iris/tests/_shared_utils.py
+++ b/lib/iris/tests/_shared_utils.py
@@ -451,6 +451,15 @@ def assert_text_file(source_filename, reference_filename, desc="text file"):
 
 def assert_data_almost_equal(data, reference_filename, **kwargs):
     reference_path = get_result_path(reference_filename)
+
+    def fixed_std(data):
+        # When data is constant, std() is too sensitive.
+        if data.max() == data.min():
+            data_std = 0
+        else:
+            data_std = data.std()
+        return data_std
+
     if _check_reference_file(reference_path):
         kwargs.setdefault("err_msg", "Reference file %s" % reference_path)
         with open(reference_path, "r") as reference_file:
@@ -470,7 +479,7 @@ def assert_data_almost_equal(data, reference_filename, **kwargs):
                 assert math.isnan(data.mean())
             else:
                 data_stats = np.array(
-                    (data.mean(), data.std(), data.max(), data.min()),
+                    (data.mean(), fixed_std(data), data.max(), data.min()),
                     dtype=np.float64,
                 )
                 assert_array_all_close(nstats, data_stats, **kwargs)
@@ -478,7 +487,7 @@ def assert_data_almost_equal(data, reference_filename, **kwargs):
         _ensure_folder(reference_path)
         stats = collections.OrderedDict(
             [
-                ("std", np.float64(data.std())),
+                ("std", np.float64(fixed_std(data))),
                 ("min", np.float64(data.min())),
                 ("max", np.float64(data.max())),
                 ("shape", data.shape),
diff --git a/lib/iris/tests/results/analysis/calculus/handmade2_wrt_lon.data.0.json b/lib/iris/tests/results/analysis/calculus/handmade2_wrt_lon.data.0.json
index 325c7d9d0..c84f7adbe 100644
--- a/lib/iris/tests/results/analysis/calculus/handmade2_wrt_lon.data.0.json
+++ b/lib/iris/tests/results/analysis/calculus/handmade2_wrt_lon.data.0.json
@@ -1,1 +1,1 @@
-{"std": 9.5367431640625e-07, "min": 11.111110687255859, "max": 11.111110687255859, "shape": [49, 50], "masked": false, "mean": 11.111111640930176}
\ No newline at end of file
+{"std": 0.0, "min": 11.111110687255859, "max": 11.111110687255859, "shape": [49, 50], "masked": false, "mean": 11.111111640930176}
\ No newline at end of file
diff --git a/lib/iris/tests/results/analysis/hmean_latitude_longitude.data.0.json b/lib/iris/tests/results/analysis/hmean_latitude_longitude.data.0.json
index 9007f1fdf..417af0a47 100644
--- a/lib/iris/tests/results/analysis/hmean_latitude_longitude.data.0.json
+++ b/lib/iris/tests/results/analysis/hmean_latitude_longitude.data.0.json
@@ -1,1 +1,1 @@
-{"std": 1.9073486328125e-06, "min": 10288680426.032187, "max": 10288680426.032187, "shape": [10], "masked": false, "mean": 10288680426.032185}
\ No newline at end of file
+{"std": 0.0, "min": 10288680426.032187, "max": 10288680426.032187, "shape": [10], "masked": false, "mean": 10288680426.032185}
\ No newline at end of file
diff --git a/lib/iris/tests/results/analysis/variance_latitude_longitude.data.0.json b/lib/iris/tests/results/analysis/variance_latitude_longitude.data.0.json
index 176e8d427..85bdeeaa5 100644
--- a/lib/iris/tests/results/analysis/variance_latitude_longitude.data.0.json
+++ b/lib/iris/tests/results/analysis/variance_latitude_longitude.data.0.json
@@ -1,1 +1,1 @@
-{"std": 3.0517578125e-05, "min": 214667549910.37509, "max": 214667549910.37509, "shape": [10], "masked": false, "mean": 214667549910.37506}
\ No newline at end of file
+{"std": 0.0, "min": 214667549910.37509, "max": 214667549910.37509, "shape": [10], "masked": false, "mean": 214667549910.37506}
\ No newline at end of file
diff --git a/lib/iris/tests/test_pp_module.py b/lib/iris/tests/test_pp_module.py
index 72ed85127..5db8c7069 100644
--- a/lib/iris/tests/test_pp_module.py
+++ b/lib/iris/tests/test_pp_module.py
@@ -8,6 +8,7 @@ import os
 from types import GeneratorType
 
 import cftime
+import numpy as np
 from numpy.testing import assert_array_equal
 import pytest
 
@@ -63,7 +64,8 @@ class IrisPPTest:
         for pp_field in pp_fields:
             pp_field.data
 
-        test_string = str(pp_fields)
+        with np.printoptions(legacy="2.2"):
+            test_string = str(pp_fields)
         reference_path = _shared_utils.get_result_path(reference_filename)
         if os.path.isfile(reference_path):
             with open(reference_path, "r") as reference_fh:
diff --git a/lib/iris/tests/unit/fileformats/pp/test_save.py b/lib/iris/tests/unit/fileformats/pp/test_save.py
index 90e4e15a1..bd48ade80 100644
--- a/lib/iris/tests/unit/fileformats/pp/test_save.py
+++ b/lib/iris/tests/unit/fileformats/pp/test_save.py
@@ -4,10 +4,6 @@
 # See LICENSE in the root of the repository for full licensing details.
 """Unit tests for the `iris.fileformats.pp.save` function."""
 
-# Import iris.tests first so that some things can be initialised before
-# importing anything else.
-import iris.tests as tests  # isort:skip
-
 from unittest import mock
 
 import cf_units
@@ -15,7 +11,9 @@ import cftime
 import numpy as np
 import pytest
 
+from iris import FUTURE
 from iris.coords import CellMethod, DimCoord
+from iris.cube import Cube
 from iris.fileformats._ff_cross_references import STASH_TRANS
 import iris.fileformats.pp as pp
 from iris.fileformats.pp_save_rules import _lbproc_rules, verify
@@ -43,37 +41,35 @@ def test_grid_and_pole__scalar_dim_longitude(unit, modulus):
     assert field.lbnpt == lon.points.size
 
 
-def test_realization():
+def test_realization(mocker):
     cube = stock.lat_lon_cube()
     real_coord = DimCoord(42, standard_name="realization", units=1)
     cube.add_aux_coord(real_coord)
-    with mock.patch("iris.fileformats.pp.PPField3", autospec=True) as pp_field:
-        pp_field.lbrsvd = list(range(4))
-        verify(cube, pp_field)
-        member_number = pp_field.lbrsvd[3]
+    pp_field = mocker.patch("iris.fileformats.pp.PPField3", autospec=True)
+    pp_field.lbrsvd = list(range(4))
+    verify(cube, pp_field)
+    member_number = pp_field.lbrsvd[3]
 
     assert member_number == 42
 
 
-def test_stash_string():
+def test_stash_string(mocker):
     cube = stock.lat_lon_cube()
     cube.attributes["STASH"] = "m01s34i001"
-    with mock.patch("iris.fileformats.pp.PPField3", autospec=True) as pp_field:
-        pp_field.lbuser = list(range(7))
-        verify(cube, pp_field)
-        stash_num = pp_field.lbuser[3]
+    pp_field = mocker.patch("iris.fileformats.pp.PPField3", autospec=True)
+    pp_field.lbuser = list(range(7))
+    verify(cube, pp_field)
+    stash_num = pp_field.lbuser[3]
 
     assert stash_num == 34001
 
 
-def test_bad_stash_string():
+def test_bad_stash_string(mocker):
     cube = stock.lat_lon_cube()
     cube.attributes["STASH"] = "ooovarvoo"
-    with mock.patch("iris.fileformats.pp.PPField3", autospec=True) as pp_field:
-        with pytest.raises(
-            ValueError, match='Expected STASH code MSI string "mXXsXXiXXX"'
-        ):
-            verify(cube, pp_field)
+    pp_field = mocker.patch("iris.fileformats.pp.PPField3", autospec=True)
+    with pytest.raises(ValueError, match='Expected STASH code MSI string "mXXsXXiXXX"'):
+        verify(cube, pp_field)
 
 
 def _pp_save_ppfield_values(cube):
@@ -91,8 +87,8 @@ def _pp_save_ppfield_values(cube):
     return pp_field
 
 
-class TestVertical(tests.IrisTest):
-    def setUp(self):
+class TestVertical:
+    def setup_method(self):
         self.cube = stock.lat_lon_cube()
 
     def test_pseudo_level(self):
@@ -100,7 +96,7 @@ class TestVertical(tests.IrisTest):
         coord = DimCoord(pseudo_level, long_name="pseudo_level", units="1")
         self.cube.add_aux_coord(coord)
         lbuser5_produced = _pp_save_ppfield_values(self.cube).lbuser[4]
-        self.assertEqual(pseudo_level, lbuser5_produced)
+        assert pseudo_level == lbuser5_produced
 
     def test_soil_level(self):
         soil_level = 314
@@ -108,11 +104,11 @@ class TestVertical(tests.IrisTest):
         self.cube.add_aux_coord(coord)
         self.cube.standard_name = "moisture_content_of_soil_layer"
         field = _pp_save_ppfield_values(self.cube)
-        self.assertEqual(field.lbvc, 6)
-        self.assertEqual(field.lblev, soil_level)
-        self.assertEqual(field.blev, soil_level)
-        self.assertEqual(field.brsvd[0], 0)
-        self.assertEqual(field.brlev, 0)
+        assert field.lbvc == 6
+        assert field.lblev == soil_level
+        assert field.blev == soil_level
+        assert field.brsvd[0] == 0
+        assert field.brlev == 0
 
     def test_soil_depth(self):
         lower, point, upper = 1, 2, 3
@@ -120,47 +116,45 @@ class TestVertical(tests.IrisTest):
         self.cube.add_aux_coord(coord)
         self.cube.standard_name = "moisture_content_of_soil_layer"
         field = _pp_save_ppfield_values(self.cube)
-        self.assertEqual(field.lbvc, 6)
-        self.assertEqual(field.lblev, 0)
-        self.assertEqual(field.blev, point)
-        self.assertEqual(field.brsvd[0], lower)
-        self.assertEqual(field.brlev, upper)
+        assert field.lbvc == 6
+        assert field.lblev == 0
+        assert field.blev == point
+        assert field.brsvd[0] == lower
+        assert field.brlev == upper
 
 
-class TestLbfcProduction(tests.IrisTest):
-    def setUp(self):
+class TestLbfcProduction:
+    def setup_method(self):
         self.cube = stock.lat_lon_cube()
 
     def check_cube_stash_yields_lbfc(self, stash, lbfc_expected):
         if stash:
             self.cube.attributes["STASH"] = stash
         lbfc_produced = _pp_save_ppfield_values(self.cube).lbfc
-        self.assertEqual(lbfc_produced, lbfc_expected)
+        assert lbfc_produced == lbfc_expected
 
     def test_known_stash(self):
         stashcode_str = "m04s07i002"
-        self.assertIn(stashcode_str, STASH_TRANS)
+        assert stashcode_str in STASH_TRANS
         self.check_cube_stash_yields_lbfc(stashcode_str, 359)
 
     def test_unknown_stash(self):
         stashcode_str = "m99s99i999"
-        self.assertNotIn(stashcode_str, STASH_TRANS)
+        assert stashcode_str not in STASH_TRANS
         self.check_cube_stash_yields_lbfc(stashcode_str, 0)
 
     def test_no_stash(self):
-        self.assertNotIn("STASH", self.cube.attributes)
+        assert "STASH" not in self.cube.attributes
         self.check_cube_stash_yields_lbfc(None, 0)
 
     def check_cube_name_units_yields_lbfc(self, name, units, lbfc_expected):
         self.cube.rename(name)
         self.cube.units = units
         lbfc_produced = _pp_save_ppfield_values(self.cube).lbfc
-        self.assertEqual(
-            lbfc_produced,
-            lbfc_expected,
+        assert lbfc_produced == lbfc_expected, (
             "Lbfc for ({!r} / {!r}) should be {:d}, got {:d}".format(
                 name, units, lbfc_expected, lbfc_produced
-            ),
+            )
         )
 
     def test_name_units_to_lbfc(self):
@@ -173,8 +167,8 @@ class TestLbfcProduction(tests.IrisTest):
         self.check_cube_name_units_yields_lbfc("Junk_Name", "K", 0)
 
 
-class TestLbsrceProduction(tests.IrisTest):
-    def setUp(self):
+class TestLbsrceProduction:
+    def setup_method(self):
         self.cube = stock.lat_lon_cube()
 
     def check_cube_um_source_yields_lbsrce(
@@ -185,7 +179,7 @@ class TestLbsrceProduction(tests.IrisTest):
         if um_version_str is not None:
             self.cube.attributes["um_version"] = um_version_str
         lbsrce_produced = _pp_save_ppfield_values(self.cube).lbsrce
-        self.assertEqual(lbsrce_produced, lbsrce_expected)
+        assert lbsrce_produced == lbsrce_expected
 
     def test_none(self):
         self.check_cube_um_source_yields_lbsrce(None, None, 0)
@@ -206,7 +200,7 @@ class TestLbsrceProduction(tests.IrisTest):
         )
 
 
-class Test_Save__LbprocProduction(tests.IrisTest):
+class Test_Save__LbprocProduction:
     # This test class is a little different to the others.
     # If it called `pp.save` via `_pp_save_ppfield_values` it would run
     # `pp_save_rules.verify` and run all the save rules. As this class uses
@@ -215,33 +209,34 @@ class Test_Save__LbprocProduction(tests.IrisTest):
     # correctly (i.e. as a `SplittableInt` object).
     # To work around this we call the lbproc rules directly here.
 
-    def setUp(self):
+    @pytest.fixture(autouse=True)
+    def _setup(self, mocker):
         self.cube = stock.realistic_3d()
         self.pp_field = mock.MagicMock(spec=pp.PPField3)
         self.pp_field.HEADER_DEFN = pp.PPField3.HEADER_DEFN
-        self.patch("iris.fileformats.pp.PPField3", return_value=self.pp_field)
+        mocker.patch("iris.fileformats.pp.PPField3", return_value=self.pp_field)
 
     def test_no_cell_methods(self):
         lbproc = _lbproc_rules(self.cube, self.pp_field).lbproc
-        self.assertEqual(lbproc, 0)
+        assert lbproc == 0
 
     def test_mean(self):
         self.cube.cell_methods = (CellMethod("mean", "time", "1 hour"),)
         lbproc = _lbproc_rules(self.cube, self.pp_field).lbproc
-        self.assertEqual(lbproc, 128)
+        assert lbproc == 128
 
     def test_minimum(self):
         self.cube.cell_methods = (CellMethod("minimum", "time", "1 hour"),)
         lbproc = _lbproc_rules(self.cube, self.pp_field).lbproc
-        self.assertEqual(lbproc, 4096)
+        assert lbproc == 4096
 
     def test_maximum(self):
         self.cube.cell_methods = (CellMethod("maximum", "time", "1 hour"),)
         lbproc = _lbproc_rules(self.cube, self.pp_field).lbproc
-        self.assertEqual(lbproc, 8192)
+        assert lbproc == 8192
 
 
-class TestTimeMean(tests.IrisTest):
+class TestTimeMean:
     """Tests that time mean cell method is converted to pp appropriately.
 
     Pattern is pairs of tests - one with time mean method, and one without, to
@@ -249,109 +244,156 @@ class TestTimeMean(tests.IrisTest):
 
     """
 
-    def test_t1_time_mean(self):
-        cube = _get_single_time_cube(set_time_mean=True)
-        tc = cube.coord(axis="t")
+    def test_t1_time_mean(self, mocker, single_mean_time_cube):
+        tc = single_mean_time_cube.coord(axis="t")
         expected = tc.units.num2date(0)
 
-        with mock.patch("iris.fileformats.pp.PPField3", autospec=True) as pp_field:
-            verify(cube, pp_field)
+        pp_field = mocker.patch("iris.fileformats.pp.PPField3", autospec=True)
+        verify(single_mean_time_cube, pp_field)
         actual = pp_field.t1
 
-        self.assertEqual(expected, actual)
+        assert expected == actual
 
-    def test_t1_no_time_mean(self):
-        cube = _get_single_time_cube()
-        tc = cube.coord(axis="t")
+    def test_t1_no_time_mean(self, mocker, single_time_cube):
+        tc = single_time_cube.coord(axis="t")
         expected = tc.units.num2date(15)
 
-        with mock.patch("iris.fileformats.pp.PPField3", autospec=True) as pp_field:
-            verify(cube, pp_field)
+        pp_field = mocker.patch("iris.fileformats.pp.PPField3", autospec=True)
+        verify(single_time_cube, pp_field)
         actual = pp_field.t1
 
-        self.assertEqual(expected, actual)
+        assert expected == actual
 
-    def test_t2_time_mean(self):
-        cube = _get_single_time_cube(set_time_mean=True)
-        tc = cube.coord(axis="t")
+    def test_t2_time_mean(self, mocker, single_mean_time_cube):
+        tc = single_mean_time_cube.coord(axis="t")
         expected = tc.units.num2date(30)
 
-        with mock.patch("iris.fileformats.pp.PPField3", autospec=True) as pp_field:
-            verify(cube, pp_field)
+        pp_field = mocker.patch("iris.fileformats.pp.PPField3", autospec=True)
+        verify(single_mean_time_cube, pp_field)
         actual = pp_field.t2
 
-        self.assertEqual(expected, actual)
+        assert expected == actual
 
-    def test_t2_no_time_mean(self):
-        cube = _get_single_time_cube(set_time_mean=False)
+    def test_t2_no_time_mean(self, mocker, single_time_cube):
         expected = cftime.datetime(0, 0, 0, calendar=None, has_year_zero=True)
 
-        with mock.patch("iris.fileformats.pp.PPField3", autospec=True) as pp_field:
-            verify(cube, pp_field)
+        pp_field = mocker.patch("iris.fileformats.pp.PPField3", autospec=True)
+        verify(single_time_cube, pp_field)
         actual = pp_field.t2
-        self.assertEqual(expected, actual)
+        assert expected == actual
 
-    def test_lbft_no_forecast_time(self):
+    def test_lbft_no_forecast_time(self, mocker, single_time_cube):
         # Different pattern here: checking that lbft hasn't been changed from
         # the default value.
-        cube = _get_single_time_cube()
-        mock_lbft = mock.sentinel.lbft
+        mock_lbft = mocker.sentinel.lbft
 
-        with mock.patch("iris.fileformats.pp.PPField3", autospec=True) as pp_field:
-            pp_field.lbft = mock_lbft
-            verify(cube, pp_field)
+        pp_field = mocker.patch("iris.fileformats.pp.PPField3", autospec=True)
+        pp_field.lbft = mock_lbft
+        verify(single_time_cube, pp_field)
         actual = pp_field.lbft
 
         assert mock_lbft is actual
 
-    def test_lbtim_no_time_mean(self):
-        cube = _get_single_time_cube()
+    def test_lbtim_no_time_mean(self, mocker, single_time_cube):
         expected_ib = 0
         expected_ic = 2  # 360 day calendar
 
-        with mock.patch("iris.fileformats.pp.PPField3", autospec=True) as pp_field:
-            verify(cube, pp_field)
+        pp_field = mocker.patch("iris.fileformats.pp.PPField3", autospec=True)
+        verify(single_time_cube, pp_field)
         actual_ib = pp_field.lbtim.ib
         actual_ic = pp_field.lbtim.ic
 
-        self.assertEqual(expected_ib, actual_ib)
-        self.assertEqual(expected_ic, actual_ic)
+        assert expected_ib == actual_ib
+        assert expected_ic == actual_ic
 
-    def test_lbtim_time_mean(self):
-        cube = _get_single_time_cube(set_time_mean=True)
+    def test_lbtim_time_mean(self, mocker, single_mean_time_cube):
         expected_ib = 2  # Time mean
         expected_ic = 2  # 360 day calendar
 
-        with mock.patch("iris.fileformats.pp.PPField3", autospec=True) as pp_field:
-            verify(cube, pp_field)
+        pp_field = mocker.patch("iris.fileformats.pp.PPField3", autospec=True)
+        verify(single_mean_time_cube, pp_field)
         actual_ib = pp_field.lbtim.ib
         actual_ic = pp_field.lbtim.ic
 
-        self.assertEqual(expected_ib, actual_ib)
-        self.assertEqual(expected_ic, actual_ic)
+        assert expected_ib == actual_ib
+        assert expected_ic == actual_ic
 
-    def test_lbproc_no_time_mean(self):
-        cube = _get_single_time_cube()
+    def test_lbproc_no_time_mean(self, mocker, single_time_cube):
         expected = 0
 
-        with mock.patch("iris.fileformats.pp.PPField3", autospec=True) as pp_field:
-            verify(cube, pp_field)
+        pp_field = mocker.patch("iris.fileformats.pp.PPField3", autospec=True)
+        verify(single_time_cube, pp_field)
         actual = pp_field.lbproc
 
-        self.assertEqual(expected, actual)
+        assert expected == actual
 
-    def test_lbproc_time_mean(self):
-        cube = _get_single_time_cube(set_time_mean=True)
+    def test_lbproc_time_mean(self, mocker, single_mean_time_cube):
         expected = 128
 
-        with mock.patch("iris.fileformats.pp.PPField3", autospec=True) as pp_field:
-            verify(cube, pp_field)
+        pp_field = mocker.patch("iris.fileformats.pp.PPField3", autospec=True)
+        verify(single_mean_time_cube, pp_field)
         actual = pp_field.lbproc
 
-        self.assertEqual(expected, actual)
+        assert expected == actual
 
 
-def _get_single_time_cube(set_time_mean=False):
+class TestPoleLocation:
+    @pytest.mark.parametrize(
+        "future_enabled",
+        [
+            pytest.param(True, id="with_lam_pole_offset_future"),
+            pytest.param(False, id="default"),
+        ],
+    )
+    def test_lam_standard_pole(self, mocker, future_enabled):
+        cube = stock.lat_lon_cube()
+        pp_field = mocker.patch("iris.fileformats.pp.PPField3", autospec=True)
+        with FUTURE.context(lam_pole_offset=future_enabled):
+            if future_enabled:
+                verify(cube, pp_field)
+                assert pp_field.bplon == 180.0
+            else:
+                with pytest.warns(FutureWarning, match="iris.FUTURE.lam_pole_offset"):
+                    verify(cube, pp_field)
+                assert pp_field.bplon == 0.0
+
+        assert pp_field.bplat == 90.0
+
+    def test_global_standard_pole(self, mocker, global_cube):
+        pp_field = mocker.patch("iris.fileformats.pp.PPField3", autospec=True)
+        with FUTURE.context(lam_pole_offset=True):
+            verify(global_cube, pp_field)
+
+        # Global domains should have bplon set to 0.0
+        assert pp_field.bplon == 0.0
+        assert pp_field.bplat == 90.0
+
+    def test_zonal_slice(self, mocker, zonal_slice_cube):
+        pp_field = mocker.patch("iris.fileformats.pp.PPField3", autospec=True)
+        with FUTURE.context(lam_pole_offset=True):
+            verify(zonal_slice_cube, pp_field)
+
+        # Zonal slice doesn't have lat coord, so is not a LAM:
+        assert pp_field.bplon == 0.0
+        assert pp_field.bplat == 90.0
+
+    def test_lam_rotated_pole(self, mocker, single_time_cube):
+        coord_system = single_time_cube.coord_system()
+        bplon = coord_system.grid_north_pole_longitude
+        bplat = coord_system.grid_north_pole_latitude
+        pp_field = mocker.patch("iris.fileformats.pp.PPField3", autospec=True)
+
+        with FUTURE.context(lam_pole_offset=True):
+            verify(single_time_cube, pp_field)
+
+        assert pp_field.lbcode == 101
+        assert pp_field.bplon == bplon
+        assert pp_field.bplat == bplat
+
+
+@pytest.fixture
+def single_time_cube():
+    # stock.realistic_3d returns a cube on a rotated pole grid
     cube = stock.realistic_3d()[0:1, :, :]
     cube.remove_coord("time")
     cube.remove_coord("forecast_period")
@@ -362,10 +404,38 @@ def _get_single_time_cube(set_time_mean=False):
         bounds=[[0, 30]],
     )
     cube.add_dim_coord(tc, 0)
-    if set_time_mean:
-        cube.cell_methods = (CellMethod("mean", coords="time"),)
     return cube
 
 
-if __name__ == "__main__":
-    tests.main()
+@pytest.fixture
+def zonal_slice_cube():
+    x_coord = DimCoord(
+        np.arange(0, 360, 10), standard_name="longitude", units="degrees", circular=True
+    )
+    t_coord = DimCoord(
+        np.arange(5), standard_name="time", units="hours since 2020-01-01T00:00:00"
+    )
+    return Cube(
+        data=np.zeros((len(t_coord.points), len(x_coord.points))),
+        dim_coords_and_dims=[(t_coord, 0), (x_coord, 1)],
+    )
+
+
+@pytest.fixture
+def single_mean_time_cube(single_time_cube):
+    single_time_cube.cell_methods = (CellMethod("mean", coords="time"),)
+    return single_time_cube
+
+
+@pytest.fixture()
+def global_cube():
+    x_coord = DimCoord(
+        np.arange(0, 360, 10), standard_name="longitude", units="degrees", circular=True
+    )
+    y_coord = DimCoord(
+        np.arange(-90, 90, 10), standard_name="latitude", units="degrees", circular=True
+    )
+    return Cube(
+        data=np.zeros((len(x_coord.points), len(y_coord.points))),
+        dim_coords_and_dims=[(x_coord, 0), (y_coord, 1)],
+    )
diff --git a/lib/iris/tests/unit/test_Future.py b/lib/iris/tests/unit/test_Future.py
index b554509bc..cd923fd9e 100644
--- a/lib/iris/tests/unit/test_Future.py
+++ b/lib/iris/tests/unit/test_Future.py
@@ -124,7 +124,7 @@ class Test__str_repr:
     def _check_content(self, future, text):
         assert text == (
             "Future(datum_support=False, pandas_ndim=False, save_split_attrs=False, "
-            "date_microseconds=False, derived_bounds=False)"
+            "date_microseconds=False, derived_bounds=False, lam_pole_offset=False)"
         )
         # Also just check that all the property elements are included
         for propname in future.__dict__.keys():
diff --git a/lib/iris/tests/unit/util/test__is_circular.py b/lib/iris/tests/unit/util/test__is_circular.py
index 8dc7d6f2a..1f6688680 100644
--- a/lib/iris/tests/unit/util/test__is_circular.py
+++ b/lib/iris/tests/unit/util/test__is_circular.py
@@ -5,15 +5,45 @@
 """Test function :func:`iris.util._is_circular`."""
 
 import numpy as np
+import pytest
 
 from iris.util import _is_circular
 
 
+@pytest.mark.parametrize(
+    "with_bounds",
+    [
+        pytest.param(True, id="with_bounds"),
+        pytest.param(False, id="without_bounds"),
+    ],
+)
 class Test:
-    def test_simple(self):
-        data = np.arange(12) * 30
-        assert _is_circular(data, 360)
+    @staticmethod
+    def _calc_bounds(points):
+        diff = np.diff(points).mean()
+        return np.c_[points - 0.5 * diff, points + 0.5 * diff]
 
-    def test_negative_diff(self):
-        data = (np.arange(96) * -3.749998) + 3.56249908e02
-        assert _is_circular(data, 360)
+    def test_simple(self, with_bounds):
+        points = np.arange(12) * 30
+        bounds = self._calc_bounds(points) if with_bounds else None
+        assert _is_circular(points, 360, bounds)
+
+    def test_negative_diff(self, with_bounds):
+        points = (np.arange(96) * -3.749998) + 3.56249908e02
+        bounds = self._calc_bounds(points) if with_bounds else None
+        assert _is_circular(points, 360, bounds)
+
+    def test_negative_origin(self, with_bounds):
+        points = np.arange(-180, 180, 30)
+        bounds = self._calc_bounds(points) if with_bounds else None
+        assert _is_circular(points, 360, bounds)
+
+    def test_ciruclar_latitude(self, with_bounds):
+        points = np.arange(-90, 90, 30)
+        bounds = self._calc_bounds(points) if with_bounds else None
+        assert _is_circular(points, 180, bounds)  # note the modulus!
+
+    def test_not_circular(self, with_bounds):
+        points = np.arange(0, 300, 30)
+        bounds = self._calc_bounds(points) if with_bounds else None
+        assert not _is_circular(points, 360, bounds)