Coverage for src/CSET/operators/_utils.py: 100%

73 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-05 21:08 +0000

1# © Crown copyright, Met Office (2022-2025) 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""" 

16Common operator functionality used across CSET. 

17 

18Functions below should only be added if it is not suitable as a standalone 

19operator, and will be used across multiple operators. 

20""" 

21 

22import logging 

23 

24import iris 

25import iris.cube 

26import iris.exceptions 

27import iris.util 

28 

29 

30def get_cube_yxcoordname(cube: iris.cube.Cube) -> tuple[str, str]: 

31 """ 

32 Return horizontal dimension coordinate name(s) from a given cube. 

33 

34 Arguments 

35 --------- 

36 

37 cube: iris.cube.Cube 

38 An iris cube which will be checked to see if it contains coordinate 

39 names that match a pre-defined list of acceptable horizontal 

40 dimension coordinate names. 

41 

42 Returns 

43 ------- 

44 (y_coord, x_coord) 

45 A tuple containing the horizontal coordinate name for latitude and longitude respectively 

46 found within the cube. 

47 

48 Raises 

49 ------ 

50 ValueError 

51 If a unique y/x horizontal coordinate cannot be found. 

52 """ 

53 # Acceptable horizontal coordinate names. 

54 X_COORD_NAMES = ["longitude", "grid_longitude", "projection_x_coordinate", "x"] 

55 Y_COORD_NAMES = ["latitude", "grid_latitude", "projection_y_coordinate", "y"] 

56 

57 # Get a list of dimension coordinate names for the cube 

58 dim_coord_names = [coord.name() for coord in cube.coords(dim_coords=True)] 

59 coord_names = [coord.name() for coord in cube.coords()] 

60 

61 # Check which x-coordinate we have, if any 

62 x_coords = [coord for coord in coord_names if coord in X_COORD_NAMES] 

63 if len(x_coords) != 1: 

64 x_coords = [coord for coord in dim_coord_names if coord in X_COORD_NAMES] 

65 if len(x_coords) != 1: 

66 raise ValueError("Could not identify a unique x-coordinate in cube") 

67 

68 # Check which y-coordinate we have, if any 

69 y_coords = [coord for coord in coord_names if coord in Y_COORD_NAMES] 

70 if len(y_coords) != 1: 

71 y_coords = [coord for coord in dim_coord_names if coord in Y_COORD_NAMES] 

72 if len(y_coords) != 1: 

73 raise ValueError("Could not identify a unique y-coordinate in cube") 

74 

75 return (y_coords[0], x_coords[0]) 

76 

77 

78def get_cube_coordindex(cube: iris.cube.Cube, coord_name) -> int: 

79 """ 

80 Return coordinate dimension for a named coordinate from a given cube. 

81 

82 Arguments 

83 --------- 

84 

85 cube: iris.cube.Cube 

86 An iris cube which will be checked to see if it contains coordinate 

87 names that match a pre-defined list of acceptable horizontal 

88 coordinate names. 

89 

90 coord_name: str 

91 A cube dimension name 

92 

93 Returns 

94 ------- 

95 coord_index 

96 An integer specifying where in the cube dimension list a specified coordinate name is found. 

97 

98 Raises 

99 ------ 

100 ValueError 

101 If a specified dimension coordinate cannot be found. 

102 """ 

103 # Get a list of dimension coordinate names for the cube 

104 coord_names = [coord.name() for coord in cube.coords(dim_coords=True)] 

105 

106 # Check if requested dimension is found in cube and get index 

107 if coord_name in coord_names: 

108 coord_index = cube.coord_dims(coord_name)[0] 

109 else: 

110 raise ValueError("Could not find requested dimension %s", coord_name) 

111 

112 return coord_index 

113 

114 

115def is_spatialdim(cube: iris.cube.Cube) -> bool: 

116 """Determine whether a cube is has two spatial dimension coordinates. 

117 

118 If cube has both spatial dims, it will contain two unique coordinates 

119 that explain space (latitude and longitude). The coordinates have to 

120 be iterable/contain usable dimension data, as cubes may contain these 

121 coordinates as scalar dimensions after being collapsed. 

122 

123 Arguments 

124 --------- 

125 cube: iris.cube.Cube 

126 An iris cube which will be checked to see if it contains coordinate 

127 names that match a pre-defined list of acceptable coordinate names. 

128 

129 Returns 

130 ------- 

131 bool 

132 If true, then the cube has a spatial projection and thus can be plotted 

133 as a map. 

134 """ 

135 # Acceptable horizontal coordinate names. 

136 X_COORD_NAMES = ["longitude", "grid_longitude", "projection_x_coordinate", "x"] 

137 Y_COORD_NAMES = ["latitude", "grid_latitude", "projection_y_coordinate", "y"] 

138 

139 # Get a list of coordinate names for the cube 

140 coord_names = [coord.name() for coord in cube.dim_coords] 

141 x_coords = [coord for coord in coord_names if coord in X_COORD_NAMES] 

142 y_coords = [coord for coord in coord_names if coord in Y_COORD_NAMES] 

143 

144 # If there is one coordinate for both x and y direction return True. 

145 if len(x_coords) == 1 and len(y_coords) == 1: 

146 return True 

147 else: 

148 return False 

149 

150 

151def is_transect(cube: iris.cube.Cube) -> bool: 

152 """Determine whether a cube is a transect. 

153 

154 If cube is a transect, it will contain only one spatial (map) coordinate, 

155 and one vertical coordinate (either pressure or model level). 

156 

157 Arguments 

158 --------- 

159 cube: iris.cube.Cube 

160 An iris cube which will be checked to see if it contains coordinate 

161 names that match a pre-defined list of acceptable coordinate names. 

162 

163 Returns 

164 ------- 

165 bool 

166 If true, then the cube is a transect that contains one spatial (map) 

167 coordinate and one vertical coordinate. 

168 """ 

169 # Acceptable spatial (map) coordinate names. 

170 SPATIAL_MAP_COORD_NAMES = [ 

171 "longitude", 

172 "grid_longitude", 

173 "projection_x_coordinate", 

174 "x", 

175 "latitude", 

176 "grid_latitude", 

177 "projection_y_coordinate", 

178 "y", 

179 "distance", 

180 ] 

181 

182 # Acceptable vertical coordinate names 

183 VERTICAL_COORD_NAMES = ["pressure", "model_level_number", "level_height"] 

184 

185 # Get a list of coordinate names for the cube 

186 coord_names = [coord.name() for coord in cube.coords(dim_coords=True)] 

187 

188 # Check which spatial coordinates we have. 

189 spatial_coords = [ 

190 coord for coord in coord_names if coord in SPATIAL_MAP_COORD_NAMES 

191 ] 

192 if len(spatial_coords) != 1: 

193 return False 

194 

195 # Check which vertical coordinates we have. 

196 vertical_coords = [coord for coord in coord_names if coord in VERTICAL_COORD_NAMES] 

197 if len(vertical_coords) != 1: 

198 return False 

199 

200 # Passed criteria so return True 

201 return True 

202 

203 

204def fully_equalise_attributes(cubes: iris.cube.CubeList): 

205 """Remove any unique attributes between cubes or coordinates in place.""" 

206 # Equalise cube attributes. 

207 removed = iris.util.equalise_attributes(cubes) 

208 logging.debug("Removed attributes from cube: %s", removed) 

209 

210 # Equalise coordinate attributes. 

211 coord_sets = [{coord.name() for coord in cube.coords()} for cube in cubes] 

212 

213 all_coords = set.union(*coord_sets) 

214 coords_to_equalise = set.intersection(*coord_sets) 

215 coords_to_remove = set.difference(all_coords, coords_to_equalise) 

216 

217 logging.debug("All coordinates: %s", all_coords) 

218 logging.debug("Coordinates to remove: %s", coords_to_remove) 

219 logging.debug("Coordinates to equalise: %s", coords_to_equalise) 

220 

221 for coord in coords_to_remove: 

222 for cube in cubes: 

223 try: 

224 cube.remove_coord(coord) 

225 logging.debug("Removed coordinate %s from %s cube.", coord, cube.name()) 

226 except iris.exceptions.CoordinateNotFoundError: 

227 pass 

228 

229 for coord in coords_to_equalise: 

230 removed = iris.util.equalise_attributes([cube.coord(coord) for cube in cubes]) 

231 logging.debug("Removed attributes from coordinate %s: %s", coord, removed) 

232 

233 return cubes 

234 

235 

236def is_time_aggregatable(cube: iris.cube.Cube) -> bool: 

237 """Determine whether a cube can be aggregated in time. 

238 

239 If a cube is aggregatable it will contain both a 'forecast_reference_time' 

240 and 'forecast_period' coordinate as dimensional coordinates. 

241 

242 Arguments 

243 --------- 

244 cube: iris.cube.Cube 

245 An iris cube which will be checked to see if it is aggregatable based 

246 on a set of pre-defined dimensional time coordinates: 

247 'forecast_period' and 'forecast_reference_time'. 

248 

249 Returns 

250 ------- 

251 bool 

252 If true, then the cube is aggregatable and contains dimensional 

253 coordinates including both 'forecast_reference_time' and 

254 'forecast_period'. 

255 """ 

256 # Acceptable time coordinate names for aggregatable cube. 

257 TEMPORAL_COORD_NAMES = ["forecast_period", "forecast_reference_time"] 

258 

259 # Coordinate names for the cube. 

260 coord_names = [coord.name() for coord in cube.coords(dim_coords=True)] 

261 

262 # Check which temporal coordinates we have. 

263 temporal_coords = [coord for coord in coord_names if coord in TEMPORAL_COORD_NAMES] 

264 # Return whether both coordinates are in the temporal coordinates. 

265 return len(temporal_coords) == 2