Coverage for src / CSET / operators / regrid.py: 99%

89 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-27 11:58 +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 warnings 

18 

19import iris 

20import iris.coord_systems 

21import iris.cube 

22import numpy as np 

23 

24from CSET._common import iter_maybe 

25from CSET.operators._utils import get_cube_yxcoordname 

26 

27 

28class BoundaryWarning(UserWarning): 

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

30 

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

32 values, so caution is advised when interpreting them. 

33 """ 

34 

35 

36def regrid_onto_cube( 

37 toregrid: iris.cube.Cube | iris.cube.CubeList, 

38 target: iris.cube.Cube, 

39 method: str, 

40 **kwargs, 

41) -> iris.cube.Cube | iris.cube.CubeList: 

42 """Regrid a cube or CubeList, projecting onto a target cube. 

43 

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

45 

46 Arguments 

47 ---------- 

48 toregrid: iris.cube | iris.cube.CubeList 

49 An iris Cube of data to regrid, or multiple cubes to regrid in a 

50 CubeList. A minimum requirement is that the cube(s) need to be 2D with a 

51 latitude, longitude coordinates. 

52 target: Cube 

53 An iris cube of the data to regrid onto. It needs to be 2D with a 

54 latitude, longitude coordinate. 

55 method: str 

56 Method used to regrid onto, etc. Linear will use iris.analysis.Linear() 

57 

58 Returns 

59 ------- 

60 iris.cube | iris.cube.CubeList 

61 An iris cube of the data that has been regridded, or a CubeList of the 

62 cubes that have been regridded in the same order they were passed in 

63 toregrid. 

64 

65 Raises 

66 ------ 

67 ValueError 

68 If a unique x/y coordinate cannot be found 

69 NotImplementedError 

70 If the cubes grid, or the method for regridding, is not yet supported. 

71 

72 Notes 

73 ----- 

74 Currently rectlinear grids (uniform) are supported. 

75 """ 

76 # To store regridded cubes. 

77 regridded_cubes = iris.cube.CubeList() 

78 

79 # Iterate over all cubes and regrid. 

80 for cube in iter_maybe(toregrid): 

81 # Get y,x coord names 

82 y_coord, x_coord = get_cube_yxcoordname(cube) 

83 

84 # List of supported grids - check if it is compatible 

85 supported_grids = (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS) 

86 if not isinstance(cube.coord(x_coord).coord_system, supported_grids): 

87 raise NotImplementedError( 

88 f"Does not currently support {cube.coord(x_coord).coord_system} coordinate system" 

89 ) 

90 if not isinstance(cube.coord(y_coord).coord_system, supported_grids): 

91 raise NotImplementedError( 

92 f"Does not currently support {cube.coord(y_coord).coord_system} coordinate system" 

93 ) 

94 

95 regrid_method = getattr(iris.analysis, method, None) 

96 if callable(regrid_method): 

97 regridded_cubes.append(cube.regrid(target, regrid_method())) 

98 else: 

99 raise NotImplementedError( 

100 f"Does not currently support {method} regrid method" 

101 ) 

102 

103 # Preserve returning a cube if only a cube has been supplied to regrid. 

104 if len(regridded_cubes) == 1: 

105 return regridded_cubes[0] 

106 else: 

107 return regridded_cubes 

108 

109 

110def regrid_onto_xyspacing( 

111 toregrid: iris.cube.Cube | iris.cube.CubeList, 

112 xspacing: float, 

113 yspacing: float, 

114 method: str, 

115 **kwargs, 

116) -> iris.cube.Cube | iris.cube.CubeList: 

117 """Regrid cube or cubelist onto a set x,y spacing. 

118 

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

120 

121 Parameters 

122 ---------- 

123 toregrid: iris.cube | iris.cube.CubeList 

124 An iris cube of the data to regrid, or multiple cubes to regrid in a 

125 cubelist. A minimum requirement is that the cube(s) need to be 2D with a 

126 latitude, longitude coordinates. 

127 xspacing: float 

128 Spacing of points in longitude direction (could be degrees, meters etc.) 

129 yspacing: float 

130 Spacing of points in latitude direction (could be degrees, meters etc.) 

131 method: str 

132 Method used to regrid onto, etc. Linear will use iris.analysis.Linear() 

133 

134 Returns 

135 ------- 

136 iris.cube | iris.cube.CubeList 

137 An iris cube of the data that has been regridded, or a cubelist of the 

138 cubes that have been regridded in the same order they were passed in 

139 toregrid. 

140 

141 Raises 

142 ------ 

143 ValueError 

144 If a unique x/y coordinate cannot be found 

145 NotImplementedError 

146 If the cubes grid, or the method for regridding, is not yet supported. 

147 

148 Notes 

149 ----- 

150 Currently rectlinear grids (uniform) are supported. 

151 

152 """ 

153 # To store regridded cubes. 

154 regridded_cubes = iris.cube.CubeList() 

155 

156 # Iterate over all cubes and regrid. 

157 for cube in iter_maybe(toregrid): 

158 # Get x,y coord names 

159 y_coord, x_coord = get_cube_yxcoordname(cube) 

160 

161 # List of supported grids - check if it is compatible 

162 supported_grids = (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS) 

163 if not isinstance(cube.coord(x_coord).coord_system, supported_grids): 

164 raise NotImplementedError( 

165 f"Does not currently support {cube.coord(x_coord).coord_system} regrid method" 

166 ) 

167 if not isinstance(cube.coord(y_coord).coord_system, supported_grids): 

168 raise NotImplementedError( 

169 f"Does not currently support {cube.coord(y_coord).coord_system} regrid method" 

170 ) 

171 

172 # Get axis 

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

174 

175 # Get bounds 

176 lat_min, lon_min = lat.points.min(), lon.points.min() 

177 lat_max, lon_max = lat.points.max(), lon.points.max() 

178 

179 # Generate new mesh 

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

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

182 

183 regrid_method = getattr(iris.analysis, method, None) 

184 if callable(regrid_method): 

185 regridded_cubes.append( 

186 cube.interpolate( 

187 [(y_coord, latout), (x_coord, lonout)], regrid_method() 

188 ) 

189 ) 

190 else: 

191 raise NotImplementedError( 

192 f"Does not currently support {method} regrid method" 

193 ) 

194 

195 # Preserve returning a cube if only a cube has been supplied to regrid. 

196 if len(regridded_cubes) == 1: 

197 return regridded_cubes[0] 

198 else: 

199 return regridded_cubes 

200 

201 

202def regrid_to_single_point( 

203 cubes: iris.cube.Cube | iris.cube.CubeList, 

204 lat_pt: float, 

205 lon_pt: float, 

206 latlon_in_type: str = "rotated", 

207 method: str = "Nearest", 

208 boundary_margin: int = 8, 

209 **kwargs, 

210) -> iris.cube.Cube: 

211 """Select data at a single point by longitude and latitude. 

212 

213 Selection of model grid point is performed by a regrid function, either 

214 selecting the nearest gridpoint to the selected longitude and latitude 

215 values or using linear interpolation across the surrounding points. 

216 

217 Parameters 

218 ---------- 

219 cubes: Cube | CubeList 

220 An iris cube or CubeList of the data to regrid. As a minimum, it needs 

221 to be 2D with latitude, longitude coordinates. 

222 lon_pt: float 

223 Selected value of longitude: this should be in the range -180 degrees to 

224 180 degrees. 

225 lat_pt: float 

226 Selected value of latitude: this should be in the range -90 degrees to 

227 90 degrees. 

228 latlon_in_type: str, optional 

229 Specify whether the input longitude and latitude point is in standard 

230 geographic realworld coordinates ("realworld") or on the rotated grid 

231 of the cube ("rotated"). Default is "rotated". 

232 method: str 

233 Method used to determine the values at the selected longitude and 

234 latitude. The recommended approach is to use iris.analysis.Nearest(), 

235 which selects the nearest gridpoint. An alternative is 

236 iris.analysis.Linear(), which obtains the values at the selected 

237 longitude and latitude by linear interpolation. 

238 boundary_margin: int, optional 

239 Number of grid points from the domain boundary considered "unreliable". 

240 Defaults to 8. 

241 

242 Returns 

243 ------- 

244 regridded_cubes: Cube | CubeList 

245 An iris cube or CubeList of the data at the specified point (this may 

246 have time and/or height dimensions). 

247 

248 Raises 

249 ------ 

250 ValueError 

251 If a unique x/y coordinate cannot be found; also if, for selecting a 

252 single gridpoint, the chosen longitude and latitude point is outside the 

253 domain. 

254 NotImplementedError 

255 If the cubes grid, or the method for regridding, is not yet supported. 

256 

257 Notes 

258 ----- 

259 The acceptable coordinate names for X and Y coordinates are currently 

260 described in X_COORD_NAMES and Y_COORD_NAMES. These cover commonly used 

261 coordinate types, though a user can append new ones. Currently rectilinear 

262 grids (uniform) are supported. Warnings are raised if the selected gridpoint 

263 is within boundary_margin grid lengths of the domain boundary as data here 

264 is potentially unreliable. 

265 

266 """ 

267 # To store regridded cubes. 

268 regridded_cubes = iris.cube.CubeList() 

269 

270 # Iterate over all cubes and regrid. 

271 for cube in iter_maybe(cubes): 

272 # Get x and y coordinate names. 

273 y_coord, x_coord = get_cube_yxcoordname(cube) 

274 

275 # List of supported grids - check if it is compatible 

276 # NOTE: The "RotatedGeogCS" option below seems to be required for rotated grids -- 

277 # this may need to be added in other places in these Operators. 

278 supported_grids = (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS) 

279 if not isinstance(cube.coord(x_coord).coord_system, supported_grids): 

280 raise NotImplementedError( 

281 f"Does not currently support {cube.coord(x_coord).coord_system} regrid method" 

282 ) 

283 if not isinstance(cube.coord(y_coord).coord_system, supported_grids): 

284 raise NotImplementedError( 

285 f"Does not currently support {cube.coord(y_coord).coord_system} regrid method" 

286 ) 

287 

288 # Transform input coordinates onto rotated grid if requested 

289 if latlon_in_type == "realworld": 

290 lon_tr, lat_tr = transform_lat_long_points(lon_pt, lat_pt, cube) 

291 elif latlon_in_type == "rotated": 291 ↛ 295line 291 didn't jump to line 295 because the condition on line 291 was always true

292 lon_tr, lat_tr = lon_pt, lat_pt 

293 

294 # Get axis 

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

296 

297 # Get bounds 

298 lat_min, lon_min = lat.points.min(), lon.points.min() 

299 lat_max, lon_max = lat.points.max(), lon.points.max() 

300 

301 # Get boundaries of frame to avoid selecting gridpoint close to domain edge 

302 lat_min_bound, lon_min_bound = ( 

303 lat.points[boundary_margin - 1], 

304 lon.points[boundary_margin - 1], 

305 ) 

306 lat_max_bound, lon_max_bound = ( 

307 lat.points[-boundary_margin], 

308 lon.points[-boundary_margin], 

309 ) 

310 

311 # Check to see if selected point is outside the domain 

312 if (lat_tr < lat_min) or (lat_tr > lat_max): 

313 raise ValueError("Selected point is outside the domain.") 

314 else: 

315 if (lon_tr < lon_min) or (lon_tr > lon_max): 

316 if (lon_tr + 360.0 >= lon_min) and (lon_tr + 360.0 <= lon_max): 

317 lon_tr += 360.0 

318 elif (lon_tr - 360.0 >= lon_min) and (lon_tr - 360.0 <= lon_max): 

319 lon_tr -= 360.0 

320 else: 

321 raise ValueError("Selected point is outside the domain.") 

322 

323 # Check to see if selected point is near the domain boundaries 

324 if ( 

325 (lat_tr < lat_min_bound) 

326 or (lat_tr > lat_max_bound) 

327 or (lon_tr < lon_min_bound) 

328 or (lon_tr > lon_max_bound) 

329 ): 

330 warnings.warn( 

331 f"Selected point is within {boundary_margin} gridlengths of the domain edge, data may be unreliable.", 

332 category=BoundaryWarning, 

333 stacklevel=2, 

334 ) 

335 

336 regrid_method = getattr(iris.analysis, method, None) 

337 if not callable(regrid_method): 

338 raise NotImplementedError( 

339 f"Does not currently support {method} regrid method" 

340 ) 

341 cube_rgd = cube.interpolate(((lat, lat_tr), (lon, lon_tr)), regrid_method()) 

342 regridded_cubes.append(cube_rgd) 

343 # Preserve returning a cube if only a cube has been supplied to regrid. 

344 if len(regridded_cubes) == 1: 

345 return regridded_cubes[0] 

346 else: 

347 return regridded_cubes 

348 

349 

350def transform_lat_long_points(lon, lat, cube): 

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

352 

353 Transform the coordinates of a point from the real world 

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

355 

356 Parameters 

357 ---------- 

358 cube: Cube 

359 An iris cube of data defining the rotated grid to be used in 

360 the longitude-latitude transformation. 

361 lon: float 

362 Selected value of longitude: this should be in the range -180 degrees to 

363 180 degrees. 

364 lat: float 

365 Selected value of latitude: this should be in the range -90 degrees to 

366 90 degrees. 

367 

368 Returns 

369 ------- 

370 lon_rot, lat_rot: float 

371 Coordinates of the selected point on the rotated grid specified within 

372 the selected cube. 

373 

374 """ 

375 import cartopy.crs as ccrs 

376 

377 rot_pole = cube.coord_system().as_cartopy_crs() 

378 true_grid = ccrs.Geodetic() 

379 rot_coords = rot_pole.transform_point(lon, lat, true_grid) 

380 lon_rot = rot_coords[0] 

381 lat_rot = rot_coords[1] 

382 

383 return lon_rot, lat_rot