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

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. 

14 

15"""Operators to regrid cubes.""" 

16 

17import logging 

18import re 

19import warnings 

20from multiprocessing import Pool 

21 

22import iris 

23import iris.coord_systems 

24import iris.coords as icoords 

25import iris.cube 

26import numpy as np 

27from scipy.interpolate import LinearNDInterpolator 

28 

29from CSET._common import iter_maybe 

30from CSET.operators._utils import get_cube_yxcoordname 

31 

32 

33class BoundaryWarning(UserWarning): 

34 """Selected gridpoint is close to the domain edge. 

35 

36 In many cases gridpoints near the domain boundary contain non-physical 

37 values, so caution is advised when interpreting them. 

38 """ 

39 

40 

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. 

48 

49 All cubes must have at least 2 spatial (map) dimensions. 

50 

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() 

62 

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. 

69 

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. 

76 

77 Notes 

78 ----- 

79 Currently rectlinear grids (uniform) are supported. 

80 """ 

81 # To store regridded cubes. 

82 regridded_cubes = iris.cube.CubeList() 

83 

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) 

88 

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 ) 

99 

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 ) 

107 

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 

113 

114 

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. 

123 

124 Regrid cube(s) using specified x,y spacing, which is performed linearly. 

125 

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() 

138 

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. 

145 

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. 

152 

153 Notes 

154 ----- 

155 Currently rectlinear grids (uniform) are supported. 

156 

157 """ 

158 # To store regridded cubes. 

159 regridded_cubes = iris.cube.CubeList() 

160 

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) 

165 

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 ) 

176 

177 # Get axis 

178 lat, lon = cube.coord(y_coord), cube.coord(x_coord) 

179 

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() 

183 

184 # Generate new mesh 

185 latout = np.arange(lat_min, lat_max, yspacing) 

186 lonout = np.arange(lon_min, lon_max, xspacing) 

187 

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 ) 

199 

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 

205 

206 

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. 

217 

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. 

221 

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. 

246 

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). 

252 

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. 

262 

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() 

274 

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) 

279 

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 ) 

292 

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 

298 

299 # Get axis 

300 lat, lon = cube.coord(y_coord), cube.coord(x_coord) 

301 

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() 

305 

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 ) 

317 

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.") 

329 

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 ) 

342 

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 ) 

348 

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) 

366 

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 

372 

373 

374def transform_lat_long_points(lon, lat, cube): 

375 """Transform a selected point in longitude and latitude. 

376 

377 Transform the coordinates of a point from the real world 

378 grid to the corresponding point on the rotated grid of a cube. 

379 

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. 

391 

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. 

397 

398 """ 

399 import cartopy.crs as ccrs 

400 

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] 

406 

407 return lon_rot, lat_rot 

408 

409 

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. 

414 

415 Interpolate the 2D field in fld to the set of points 

416 specified in point_cube. 

417 

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. 

425 

426 Returns 

427 ------- 

428 fld_point_cube: Cube 

429 An iris cube containing interpolated values at the points 

430 specified by the point cube. 

431 

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 

496 

497 

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} 

562 

563 

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. 

569 

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 

580 

581 Returns 

582 ------- 

583 iris.cube.Cube 

584 New structured Iris cube 

585 """ 

586 # --- Dimension coordinates --- 

587 time_coord = origcube.coord("time") 

588 

589 lat_coord = icoords.DimCoord( 

590 lat, 

591 standard_name="latitude", 

592 units="degrees", 

593 ) 

594 

595 lon_coord = icoords.DimCoord( 

596 lon, 

597 standard_name="longitude", 

598 units="degrees", 

599 ) 

600 

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()) 

604 

605 if m: 

606 var_key, pressure_hpa = m.group(1), m.group(2) 

607 

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 ) 

615 

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 ) 

626 

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 ) 

637 

638 # ---- Rename cube using lookup dictionary ---- 

639 meta = var_lookup.get(var_key) 

640 

641 if meta is not None: 

642 if "standard_name" in meta: 

643 out_cube.standard_name = meta["standard_name"] 

644 

645 if "long_name" in meta: 

646 out_cube.long_name = meta["long_name"] 

647 

648 if "units" in meta: 

649 out_cube.units = meta["units"] 

650 

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) 

656 

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 

660 

661 # Strip the "seconds since " part 

662 time_origin = time_origin.split("since ")[1] 

663 

664 # Add to cube attributes as str 

665 out_cube.coord("time").attributes["time_origin"] = time_origin 

666 

667 return out_cube 

668 

669 

670def _restructure_ugrid_regrid(cube, tri, lat_grid, lon_grid, xy): 

671 

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)) 

675 

676 print("Interpolating", cube.name()) 

677 

678 src_vals = cube.data.T 

679 

680 interp = LinearNDInterpolator(tri, src_vals) 

681 

682 out_flat = interp(xy) 

683 

684 out = out_flat.T.reshape(cube.shape[0], lat_grid.size, lon_grid.size) 

685 

686 out_cube = _rebuild_ugrid_meta( 

687 out, cube, lat_grid, lon_grid, var_lookup=UGRID_VAR_LOOKUP 

688 ) 

689 

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 

693 

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 

697 

698 if out_cube is not None: 

699 out_cube.data = np.asarray(out_cube.data, dtype=np.float32) 

700 

701 return out_cube 

702 

703 

704def restructure_ugrid(cubes): 

705 """ 

706 TODO - file locking a cache? 

707 """ 

708 logging.info("Restructuring UGRID...") 

709 

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)) 

714 

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) 

720 

721 # Flatten target points 

722 xy = np.column_stack((Lon2d.ravel(), Lat2d.ravel())) 

723 

724 # Build triangulation via a dummy interpolator 

725 tri_interp = LinearNDInterpolator(points, np.zeros(points.shape[0])) 

726 tri = tri_interp.tri 

727 

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 ) 

736 

737 fixed_cubes = iris.cube.CubeList(c for c in results if c is not None) 

738 

739 return fixed_cubes.concatenate()