Coverage for src / CSET / operators / regrid.py: 59%
175 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-13 07:38 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-13 07:38 +0000
1# © Crown copyright, Met Office (2022-2024) and CSET contributors.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
15"""Operators to regrid cubes."""
17import logging
18import re
19import warnings
20from multiprocessing import Pool
22import iris
23import iris.coord_systems
24import iris.coords as icoords
25import iris.cube
26import numpy as np
27from scipy.interpolate import LinearNDInterpolator
29from CSET._common import iter_maybe
30from CSET.operators._utils import get_cube_yxcoordname
33class BoundaryWarning(UserWarning):
34 """Selected gridpoint is close to the domain edge.
36 In many cases gridpoints near the domain boundary contain non-physical
37 values, so caution is advised when interpreting them.
38 """
41def regrid_onto_cube(
42 toregrid: iris.cube.Cube | iris.cube.CubeList,
43 target: iris.cube.Cube,
44 method: str,
45 **kwargs,
46) -> iris.cube.Cube | iris.cube.CubeList:
47 """Regrid a cube or CubeList, projecting onto a target cube.
49 All cubes must have at least 2 spatial (map) dimensions.
51 Arguments
52 ----------
53 toregrid: iris.cube | iris.cube.CubeList
54 An iris Cube of data to regrid, or multiple cubes to regrid in a
55 CubeList. A minimum requirement is that the cube(s) need to be 2D with a
56 latitude, longitude coordinates.
57 target: Cube
58 An iris cube of the data to regrid onto. It needs to be 2D with a
59 latitude, longitude coordinate.
60 method: str
61 Method used to regrid onto, etc. Linear will use iris.analysis.Linear()
63 Returns
64 -------
65 iris.cube | iris.cube.CubeList
66 An iris cube of the data that has been regridded, or a CubeList of the
67 cubes that have been regridded in the same order they were passed in
68 toregrid.
70 Raises
71 ------
72 ValueError
73 If a unique x/y coordinate cannot be found
74 NotImplementedError
75 If the cubes grid, or the method for regridding, is not yet supported.
77 Notes
78 -----
79 Currently rectlinear grids (uniform) are supported.
80 """
81 # To store regridded cubes.
82 regridded_cubes = iris.cube.CubeList()
84 # Iterate over all cubes and regrid.
85 for cube in iter_maybe(toregrid):
86 # Get y,x coord names
87 y_coord, x_coord = get_cube_yxcoordname(cube)
89 # List of supported grids - check if it is compatible
90 supported_grids = (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS)
91 if not isinstance(cube.coord(x_coord).coord_system, supported_grids):
92 raise NotImplementedError(
93 f"Does not currently support {cube.coord(x_coord).coord_system} coordinate system"
94 )
95 if not isinstance(cube.coord(y_coord).coord_system, supported_grids):
96 raise NotImplementedError(
97 f"Does not currently support {cube.coord(y_coord).coord_system} coordinate system"
98 )
100 regrid_method = getattr(iris.analysis, method, None)
101 if callable(regrid_method):
102 regridded_cubes.append(cube.regrid(target, regrid_method()))
103 else:
104 raise NotImplementedError(
105 f"Does not currently support {method} regrid method"
106 )
108 # Preserve returning a cube if only a cube has been supplied to regrid.
109 if len(regridded_cubes) == 1:
110 return regridded_cubes[0]
111 else:
112 return regridded_cubes
115def regrid_onto_xyspacing(
116 toregrid: iris.cube.Cube | iris.cube.CubeList,
117 xspacing: float,
118 yspacing: float,
119 method: str,
120 **kwargs,
121) -> iris.cube.Cube | iris.cube.CubeList:
122 """Regrid cube or cubelist onto a set x,y spacing.
124 Regrid cube(s) using specified x,y spacing, which is performed linearly.
126 Parameters
127 ----------
128 toregrid: iris.cube | iris.cube.CubeList
129 An iris cube of the data to regrid, or multiple cubes to regrid in a
130 cubelist. A minimum requirement is that the cube(s) need to be 2D with a
131 latitude, longitude coordinates.
132 xspacing: float
133 Spacing of points in longitude direction (could be degrees, meters etc.)
134 yspacing: float
135 Spacing of points in latitude direction (could be degrees, meters etc.)
136 method: str
137 Method used to regrid onto, etc. Linear will use iris.analysis.Linear()
139 Returns
140 -------
141 iris.cube | iris.cube.CubeList
142 An iris cube of the data that has been regridded, or a cubelist of the
143 cubes that have been regridded in the same order they were passed in
144 toregrid.
146 Raises
147 ------
148 ValueError
149 If a unique x/y coordinate cannot be found
150 NotImplementedError
151 If the cubes grid, or the method for regridding, is not yet supported.
153 Notes
154 -----
155 Currently rectlinear grids (uniform) are supported.
157 """
158 # To store regridded cubes.
159 regridded_cubes = iris.cube.CubeList()
161 # Iterate over all cubes and regrid.
162 for cube in iter_maybe(toregrid):
163 # Get x,y coord names
164 y_coord, x_coord = get_cube_yxcoordname(cube)
166 # List of supported grids - check if it is compatible
167 supported_grids = (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS)
168 if not isinstance(cube.coord(x_coord).coord_system, supported_grids):
169 raise NotImplementedError(
170 f"Does not currently support {cube.coord(x_coord).coord_system} regrid method"
171 )
172 if not isinstance(cube.coord(y_coord).coord_system, supported_grids):
173 raise NotImplementedError(
174 f"Does not currently support {cube.coord(y_coord).coord_system} regrid method"
175 )
177 # Get axis
178 lat, lon = cube.coord(y_coord), cube.coord(x_coord)
180 # Get bounds
181 lat_min, lon_min = lat.points.min(), lon.points.min()
182 lat_max, lon_max = lat.points.max(), lon.points.max()
184 # Generate new mesh
185 latout = np.arange(lat_min, lat_max, yspacing)
186 lonout = np.arange(lon_min, lon_max, xspacing)
188 regrid_method = getattr(iris.analysis, method, None)
189 if callable(regrid_method):
190 regridded_cubes.append(
191 cube.interpolate(
192 [(y_coord, latout), (x_coord, lonout)], regrid_method()
193 )
194 )
195 else:
196 raise NotImplementedError(
197 f"Does not currently support {method} regrid method"
198 )
200 # Preserve returning a cube if only a cube has been supplied to regrid.
201 if len(regridded_cubes) == 1:
202 return regridded_cubes[0]
203 else:
204 return regridded_cubes
207def regrid_to_single_point(
208 cubes: iris.cube.Cube | iris.cube.CubeList,
209 lat_pt: float,
210 lon_pt: float,
211 latlon_in_type: str = "rotated",
212 method: str = "Nearest",
213 boundary_margin: int = 8,
214 **kwargs,
215) -> iris.cube.Cube:
216 """Select data at a single point by longitude and latitude.
218 Selection of model grid point is performed by a regrid function, either
219 selecting the nearest gridpoint to the selected longitude and latitude
220 values or using linear interpolation across the surrounding points.
222 Parameters
223 ----------
224 cubes: Cube | CubeList
225 An iris cube or CubeList of the data to regrid. As a minimum, it needs
226 to be 2D with latitude, longitude coordinates.
227 lon_pt: float
228 Selected value of longitude: this should be in the range -180 degrees to
229 180 degrees.
230 lat_pt: float
231 Selected value of latitude: this should be in the range -90 degrees to
232 90 degrees.
233 latlon_in_type: str, optional
234 Specify whether the input longitude and latitude point is in standard
235 geographic realworld coordinates ("realworld") or on the rotated grid
236 of the cube ("rotated"). Default is "rotated".
237 method: str
238 Method used to determine the values at the selected longitude and
239 latitude. The recommended approach is to use iris.analysis.Nearest(),
240 which selects the nearest gridpoint. An alternative is
241 iris.analysis.Linear(), which obtains the values at the selected
242 longitude and latitude by linear interpolation.
243 boundary_margin: int, optional
244 Number of grid points from the domain boundary considered "unreliable".
245 Defaults to 8.
247 Returns
248 -------
249 regridded_cubes: Cube | CubeList
250 An iris cube or CubeList of the data at the specified point (this may
251 have time and/or height dimensions).
253 Raises
254 ------
255 ValueError
256 If a unique x/y coordinate cannot be found; also if, for selecting a
257 single gridpoint, the chosen longitude and latitude point is outside the
258 domain; also (currently) if the difference between the actual and target
259 points exceed 0.1 degrees.
260 NotImplementedError
261 If the cubes grid, or the method for regridding, is not yet supported.
263 Notes
264 -----
265 The acceptable coordinate names for X and Y coordinates are currently
266 described in X_COORD_NAMES and Y_COORD_NAMES. These cover commonly used
267 coordinate types, though a user can append new ones. Currently rectilinear
268 grids (uniform) are supported. Warnings are raised if the selected gridpoint
269 is within boundary_margin grid lengths of the domain boundary as data here
270 is potentially unreliable.
271 """
272 # To store regridded cubes.
273 regridded_cubes = iris.cube.CubeList()
275 # Iterate over all cubes and regrid.
276 for cube in iter_maybe(cubes):
277 # Get x and y coordinate names.
278 y_coord, x_coord = get_cube_yxcoordname(cube)
280 # List of supported grids - check if it is compatible
281 # NOTE: The "RotatedGeogCS" option below seems to be required for rotated grids --
282 # this may need to be added in other places in these Operators.
283 supported_grids = (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS)
284 if not isinstance(cube.coord(x_coord).coord_system, supported_grids):
285 raise NotImplementedError(
286 f"Does not currently support {cube.coord(x_coord).coord_system} regrid method"
287 )
288 if not isinstance(cube.coord(y_coord).coord_system, supported_grids):
289 raise NotImplementedError(
290 f"Does not currently support {cube.coord(y_coord).coord_system} regrid method"
291 )
293 # Transform input coordinates onto rotated grid if requested
294 if latlon_in_type == "realworld":
295 lon_tr, lat_tr = transform_lat_long_points(lon_pt, lat_pt, cube)
296 elif latlon_in_type == "rotated": 296 ↛ 300line 296 didn't jump to line 300 because the condition on line 296 was always true
297 lon_tr, lat_tr = lon_pt, lat_pt
299 # Get axis
300 lat, lon = cube.coord(y_coord), cube.coord(x_coord)
302 # Get bounds
303 lat_min, lon_min = lat.points.min(), lon.points.min()
304 lat_max, lon_max = lat.points.max(), lon.points.max()
306 # Use different logic for single point obs data.
307 if len(cube.coord(x_coord).points) > 1:
308 # Get boundaries of frame to avoid selecting gridpoint close to domain edge
309 lat_min_bound, lon_min_bound = (
310 lat.points[boundary_margin - 1],
311 lon.points[boundary_margin - 1],
312 )
313 lat_max_bound, lon_max_bound = (
314 lat.points[-boundary_margin],
315 lon.points[-boundary_margin],
316 )
318 # Check to see if selected point is outside the domain
319 if (lat_tr < lat_min) or (lat_tr > lat_max):
320 raise ValueError("Selected point is outside the domain.")
321 else:
322 if (lon_tr < lon_min) or (lon_tr > lon_max):
323 if (lon_tr + 360.0 >= lon_min) and (lon_tr + 360.0 <= lon_max):
324 lon_tr += 360.0
325 elif (lon_tr - 360.0 >= lon_min) and (lon_tr - 360.0 <= lon_max):
326 lon_tr -= 360.0
327 else:
328 raise ValueError("Selected point is outside the domain.")
330 # Check to see if selected point is near the domain boundaries
331 if (
332 (lat_tr < lat_min_bound)
333 or (lat_tr > lat_max_bound)
334 or (lon_tr < lon_min_bound)
335 or (lon_tr > lon_max_bound)
336 ):
337 warnings.warn(
338 f"Selected point is within {boundary_margin} gridlengths of the domain edge, data may be unreliable.",
339 category=BoundaryWarning,
340 stacklevel=2,
341 )
343 regrid_method = getattr(iris.analysis, method, None)
344 if not callable(regrid_method):
345 raise NotImplementedError(
346 f"Does not currently support {method} regrid method"
347 )
349 cube_rgd = cube.interpolate(((lat, lat_tr), (lon, lon_tr)), regrid_method())
350 regridded_cubes.append(cube_rgd)
351 else:
352 if (
353 np.abs((lat_tr - lat.points[0])) > 0.1
354 or np.abs((lon_tr - lon.points[0])) > 0.1
355 ):
356 raise ValueError(
357 "Selected point is too far from the specified coordinates. It should be within 0.1 degrees."
358 )
359 else:
360 print(
361 "*** lat/long diffs",
362 np.abs(lat_tr - lat_pt),
363 np.abs(lon_tr - lon_pt),
364 )
365 regridded_cubes.append(cube)
367 # Preserve returning a cube if only a cube has been supplied to regrid.
368 if len(regridded_cubes) == 1:
369 return regridded_cubes[0]
370 else:
371 return regridded_cubes
374def transform_lat_long_points(lon, lat, cube):
375 """Transform a selected point in longitude and latitude.
377 Transform the coordinates of a point from the real world
378 grid to the corresponding point on the rotated grid of a cube.
380 Parameters
381 ----------
382 cube: Cube
383 An iris cube of data defining the rotated grid to be used in
384 the longitude-latitude transformation.
385 lon: float
386 Selected value of longitude: this should be in the range -180 degrees to
387 180 degrees.
388 lat: float
389 Selected value of latitude: this should be in the range -90 degrees to
390 90 degrees.
392 Returns
393 -------
394 lon_rot, lat_rot: float
395 Coordinates of the selected point on the rotated grid specified within
396 the selected cube.
398 """
399 import cartopy.crs as ccrs
401 rot_pole = cube.coord_system().as_cartopy_crs()
402 true_grid = ccrs.Geodetic()
403 rot_coords = rot_pole.transform_point(lon, lat, true_grid)
404 lon_rot = rot_coords[0]
405 lat_rot = rot_coords[1]
407 return lon_rot, lat_rot
410def interpolate_to_point_cube(
411 fld: iris.cube.Cube | iris.cube.CubeList, point_cube: iris.cube.Cube, **kwargs
412) -> iris.cube.Cube | iris.cube.CubeList:
413 """Interpolate from a 2D field to a set of points.
415 Interpolate the 2D field in fld to the set of points
416 specified in point_cube.
418 Parameters
419 ----------
420 fld: Cube
421 An iris cube containing a two-dimensional field.
422 point_cube: Cube
423 An iris cube specifying the point to which the data
424 will be interpolated.
426 Returns
427 -------
428 fld_point_cube: Cube
429 An iris cube containing interpolated values at the points
430 specified by the point cube.
432 """
433 #
434 # As a basis, create a copy of the point cube.
435 fld_point_cube = point_cube.copy()
436 # Get indices of positional coordinates. We assume that the
437 # point cube is unrotated.
438 klon = None
439 klat = None
440 for kc in range(len(fld_point_cube.aux_coords)):
441 if fld_point_cube.aux_coords[kc].standard_name == "latitude":
442 klat = kc
443 elif fld_point_cube.aux_coords[kc].standard_name == "longitude":
444 klon = kc
445 #
446 # The input may have a rotated coordinate system.
447 if len(fld.coords("grid_latitude")) > 0:
448 # Interpolate in rotated coordinates.
449 rot_csyst = fld.coords("grid_latitude")[0].coord_system
450 rotpt = iris.analysis.cartography.rotate_pole(
451 fld_point_cube.aux_coords[klon].points,
452 fld_point_cube.aux_coords[klat].points,
453 rot_csyst.grid_north_pole_longitude,
454 rot_csyst.grid_north_pole_latitude,
455 )
456 # Add other interpolation options later.
457 fld_interpolator = iris.analysis.Linear(extrapolation_mode="mask").interpolator(
458 fld, ["time", "grid_latitude", "grid_longitude"]
459 )
460 for jt in range(len(fld_point_cube.coords("time")[0].points)):
461 fld_point_cube.data[jt, :] = np.ma.masked_invalid(
462 [
463 fld_interpolator(
464 [
465 fld_point_cube.coord("time").points[jt],
466 rotpt[1][k],
467 rotpt[0][k],
468 ]
469 ).data
470 if ~point_cube.data.mask[jt][k]
471 else np.nan
472 for k in range(len(rotpt[0]))
473 ]
474 )
475 else:
476 # Add other interpolation options later.
477 fld_interpolator = iris.analysis.Linear(extrapolation_mode="mask").interpolator(
478 fld, ["time", "latitude", "longitude"]
479 )
480 for jt in range(len(fld_point_cube.coords("time")[0].points)):
481 fld_point_cube.data[jt, :] = np.ma.masked_invalid(
482 [
483 fld_interpolator(
484 [
485 fld_point_cube.coords("time")[0].points[jt],
486 fld_point_cube.coord("latitude").points[k],
487 fld_point_cube.coord("longitude").points[k],
488 ]
489 ).data
490 if ~point_cube.data.mask[jt][k]
491 else np.nan
492 for k in range(fld_point_cube.coord("latitude").points)
493 ]
494 )
495 return fld_point_cube
498UGRID_VAR_LOOKUP = {
499 "t": {
500 "standard_name": "air_temperature",
501 "long_name": "temperature_at_pressure_levels",
502 "units": "K",
503 },
504 "u": {
505 "standard_name": "eastward_wind",
506 "long_name": "zonal_wind_at_pressure_levels",
507 "units": "m s-1",
508 },
509 "v": {
510 "standard_name": "northward_wind",
511 "long_name": "meridional_wind_at_pressure_levels",
512 "units": "m s-1",
513 },
514 "w": {
515 "standard_name": "upward_air_velocity",
516 "long_name": "vertical_wind_at_pressure_levels",
517 "units": "m s-1",
518 },
519 "q": {
520 "standard_name": "specific_humidity",
521 "long_name": "vapour_specific_humidity_at_pressure_levels_for_climate_averaging",
522 "units": "kg kg-1",
523 },
524 "z": {
525 "standard_name": "geopotential_height",
526 "long_name": "geopotential_height_at_pressure_levels",
527 "units": "m",
528 },
529 "sp": {
530 "standard_name": "surface_air_pressure",
531 "long_name": "surface_air_pressure",
532 "units": "Pa",
533 },
534 "10u": {
535 "long_name": "eastward_wind_at_10m",
536 "units": "m s-1",
537 },
538 "10v": {
539 "long_name": "northward_wind_at_10m",
540 "units": "m s-1",
541 },
542 "lsm": {
543 "long_name": "land_binary_mask",
544 },
545 "2t": {
546 "long_name": "temperature_at_screen_level",
547 "units": "K",
548 },
549 "2d": {
550 "long_name": "dew_point_temperature_at_screen_level",
551 "units": "K",
552 },
553 "skt": {
554 "long_name": "grid_surface_temperature",
555 "units": "K",
556 },
557 "tp": {
558 "long_name": "surface_microphysical_rainfall_rate",
559 "units": "mm hr-1",
560 },
561}
564def _rebuild_ugrid_meta(data, origcube, lat, lon, var_lookup=UGRID_VAR_LOOKUP):
565 """
566 Build a structured Iris cube (time, lat, lon) from regridded data,
567 preserving metadata and adding a pressure auxiliary coordinate
568 inferred from the cube name if present.
570 Parameters
571 ----------
572 out : np.ndarray
573 Regridded data array with shape (time, lat, lon)
574 cube : iris.cube.Cube
575 Original unstructured source cube
576 lat_grid : np.ndarray
577 1D latitude coordinate values
578 lon_grid : np.ndarray
579 1D longitude coordinate values
581 Returns
582 -------
583 iris.cube.Cube
584 New structured Iris cube
585 """
586 # --- Dimension coordinates ---
587 time_coord = origcube.coord("time")
589 lat_coord = icoords.DimCoord(
590 lat,
591 standard_name="latitude",
592 units="degrees",
593 )
595 lon_coord = icoords.DimCoord(
596 lon,
597 standard_name="longitude",
598 units="degrees",
599 )
601 # ---- Parse cube name ----
602 # Matches e.g. t_50, u_850, q_1000
603 m = re.match(r"^([a-zA-Z][a-zA-Z0-9]*|\d+[a-zA-Z]+)(?:_(\d+))?$", origcube.name())
605 if m:
606 var_key, pressure_hpa = m.group(1), m.group(2)
608 # ---- Add pressure auxiliary coordinate ----
609 if pressure_hpa is not None:
610 pressure_coord = icoords.DimCoord(
611 [int(pressure_hpa)],
612 long_name="pressure",
613 units="hPa",
614 )
616 # --- Build the cube ---
617 out_cube = iris.cube.Cube(
618 data[:, np.newaxis, :, :],
619 dim_coords_and_dims=[
620 (time_coord, 0),
621 (pressure_coord, 1),
622 (lat_coord, 2),
623 (lon_coord, 3),
624 ],
625 )
627 else:
628 # --- Build the cube ---
629 out_cube = iris.cube.Cube(
630 data,
631 dim_coords_and_dims=[
632 (time_coord, 0),
633 (lat_coord, 1),
634 (lon_coord, 2),
635 ],
636 )
638 # ---- Rename cube using lookup dictionary ----
639 meta = var_lookup.get(var_key)
641 if meta is not None:
642 if "standard_name" in meta:
643 out_cube.standard_name = meta["standard_name"]
645 if "long_name" in meta:
646 out_cube.long_name = meta["long_name"]
648 if "units" in meta:
649 out_cube.units = meta["units"]
651 # Optional: set short name to CF standard_name
652 out_cube.rename(meta.get("long_name", origcube.name()))
653 else:
654 # Fallback: keep original name but drop pressure suffix
655 out_cube.rename(var_key)
657 # Add forecast reference time as 'time_origin' to mimic lfric where it will reconstruct forecast_period
658 # Extract the origin string from the units
659 time_origin = time_coord.units.origin
661 # Strip the "seconds since " part
662 time_origin = time_origin.split("since ")[1]
664 # Add to cube attributes as str
665 out_cube.coord("time").attributes["time_origin"] = time_origin
667 return out_cube
670def _restructure_ugrid_regrid(cube, tri, lat_grid, lon_grid, xy):
672 # Don't regrid lat/lon coord!
673 if cube.ndim > 1:
674 out = np.empty((cube.shape[0], lat_grid.size, lon_grid.size))
676 print("Interpolating", cube.name())
678 src_vals = cube.data.T
680 interp = LinearNDInterpolator(tri, src_vals)
682 out_flat = interp(xy)
684 out = out_flat.T.reshape(cube.shape[0], lat_grid.size, lon_grid.size)
686 out_cube = _rebuild_ugrid_meta(
687 out, cube, lat_grid, lon_grid, var_lookup=UGRID_VAR_LOOKUP
688 )
690 # Change units, geopot in m2 s-2
691 if out_cube.long_name == "geopotential_height":
692 out_cube.data = out_cube.data / 9.81
694 # Raw data in units of 6h accum in meters.
695 if out_cube.long_name == "surface_microphysical_rainfall_rate":
696 out_cube.data = (out_cube.data * 1000.0) / 6
698 if out_cube is not None:
699 out_cube.data = np.asarray(out_cube.data, dtype=np.float32)
701 return out_cube
704def restructure_ugrid(cubes):
705 """
706 TODO - file locking a cache?
707 """
708 logging.info("Restructuring UGRID...")
710 # First, extract latitude and longitude coordinates
711 lat = cubes.extract("latitude")[0].data
712 lon = cubes.extract("longitude")[0].data
713 points = np.column_stack((lon, lat))
715 # Create output mesh, using standard grid ~2km resolution
716 # TODO: is there a way we can intelligently guess grid res?
717 lon_grid = np.arange(lon.data.min(), lon.data.max(), 0.02)
718 lat_grid = np.arange(lat.data.min(), lat.data.max(), 0.02)
719 Lon2d, Lat2d = np.meshgrid(lon_grid, lat_grid)
721 # Flatten target points
722 xy = np.column_stack((Lon2d.ravel(), Lat2d.ravel()))
724 # Build triangulation via a dummy interpolator
725 tri_interp = LinearNDInterpolator(points, np.zeros(points.shape[0]))
726 tri = tri_interp.tri
728 # Actually scales poorly if cpu = os.cpu_no. think some of it is parallised already.
729 # int(os.cpu_count()/2)
730 # current implementation of CSET uses one allocation for lots of jobs
731 with Pool(processes=1) as pool:
732 results = pool.starmap(
733 _restructure_ugrid_regrid,
734 [(cube, tri, lat_grid, lon_grid, xy) for cube in cubes],
735 )
737 fixed_cubes = iris.cube.CubeList(c for c in results if c is not None)
739 return fixed_cubes.concatenate()