Skip to content

Commit 6f663c0

Browse files
committed
Allow collapse_by_ operators to handle CubeLists
Remove collapse_by_lead_time as it can be replicated with collapse and remove unnecessary tests.
1 parent 84d8bf1 commit 6f663c0

File tree

2 files changed

+119
-247
lines changed

2 files changed

+119
-247
lines changed

src/CSET/operators/collapse.py

Lines changed: 114 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@
2424

2525
from CSET._common import iter_maybe
2626
from CSET.operators._utils import is_time_aggregatable
27-
from CSET.operators.aggregate import (
28-
add_hour_coordinate,
29-
ensure_aggregatable_across_cases,
30-
)
27+
from CSET.operators.aggregate import add_hour_coordinate
3128

3229

3330
def collapse(
@@ -46,15 +43,17 @@ def collapse(
4643
Arguments
4744
---------
4845
cubes: iris.cube.Cube | iris.cube.CubeList
49-
Cube or CubeList to collapse and iterate over one dimension
46+
Cube or CubeList to collapse and iterate over one dimension
5047
coordinate: str | list[str]
51-
Coordinate(s) to collapse over e.g. 'time', 'longitude', 'latitude',
52-
'model_level_number', 'realization'. A list of multiple coordinates can
53-
be given.
48+
Coordinate(s) to collapse over e.g. 'time', 'longitude', 'latitude',
49+
'model_level_number', 'realization'. A list of multiple coordinates can
50+
be given.
5451
method: str
55-
Type of collapse i.e. method: 'MEAN', 'MAX', 'MIN', 'MEDIAN',
56-
'PERCENTILE' getattr creates iris.analysis.MEAN, etc For PERCENTILE
57-
YAML file requires i.e. method: 'PERCENTILE' additional_percent: 90
52+
Type of collapse i.e. method: 'MEAN', 'MAX', 'MIN', 'MEDIAN',
53+
'PERCENTILE' getattr creates iris.analysis.MEAN, etc For PERCENTILE YAML
54+
file requires i.e. method: 'PERCENTILE' additional_percent: 90
55+
additional_percent: float, optional
56+
Required for the PERCENTILE method. This is a number between 0 and 100.
5857
5958
Returns
6059
-------
@@ -95,56 +94,8 @@ def collapse(
9594
return collapsed_cubes
9695

9796

98-
def collapse_by_lead_time(
99-
cube: iris.cube.Cube | iris.cube.CubeList,
100-
method: str,
101-
additional_percent: float = None,
102-
**kwargs,
103-
) -> iris.cube.Cube:
104-
"""Collapse a cube around lead time for multiple cases.
105-
106-
First checks if the data can be aggregated by lead time easily. Then
107-
collapses by lead time for a specified method using the collapse function.
108-
This operator provides the average of all T+1, T+2, etc.
109-
110-
Arguments
111-
---------
112-
cube: iris.cube.Cube | iris.cube.CubeList
113-
Cube to collapse by lead time or CubeList that will be converted
114-
to a cube before collapsing by lead time.
115-
method: str
116-
Type of collapse i.e. method: 'MEAN', 'MAX', 'MIN', 'MEDIAN',
117-
'PERCENTILE'. For 'PERCENTILE' the additional_percent must be specified.
118-
119-
Returns
120-
-------
121-
cube: iris.cube.Cube
122-
Single variable collapsed by lead time based on chosen method.
123-
124-
Raises
125-
------
126-
ValueError
127-
If additional_percent wasn't supplied while using PERCENTILE method.
128-
"""
129-
if method == "PERCENTILE" and additional_percent is None:
130-
raise ValueError("Must specify additional_percent")
131-
# Ensure the cube can be aggregated over multiple cases.
132-
cube_to_collapse = ensure_aggregatable_across_cases(cube)
133-
# Collapse by lead time.
134-
if method == "PERCENTILE":
135-
collapsed_cube = collapse(
136-
cube_to_collapse,
137-
"forecast_reference_time",
138-
method,
139-
additional_percent=additional_percent,
140-
)
141-
else:
142-
collapsed_cube = collapse(cube_to_collapse, "forecast_reference_time", method)
143-
return collapsed_cube
144-
145-
14697
def collapse_by_hour_of_day(
147-
cube: iris.cube.Cube | iris.cube.CubeList,
98+
cubes: iris.cube.Cube | iris.cube.CubeList,
14899
method: str,
149100
additional_percent: float = None,
150101
multi_case: bool = True,
@@ -158,7 +109,7 @@ def collapse_by_hour_of_day(
158109
159110
Arguments
160111
---------
161-
cube: iris.cube.Cube | iris.cube.CubeList
112+
cubes: iris.cube.Cube | iris.cube.CubeList
162113
Cube to collapse and iterate over one dimension or CubeList to
163114
convert to a cube and then collapse prior to aggregating by hour.
164115
If a CubeList is provided multi_case must be set to True as the Cube List
@@ -209,60 +160,70 @@ def collapse_by_hour_of_day(
209160
"""
210161
if method == "PERCENTILE" and additional_percent is None:
211162
raise ValueError("Must specify additional_percent")
212-
elif (
213-
isinstance(cube, iris.cube.Cube)
214-
and is_time_aggregatable(cube)
215-
and not multi_case
216-
):
217-
raise TypeError(
218-
"multi_case must be true for a cube containing two time dimensions"
219-
)
220-
elif (
221-
isinstance(cube, iris.cube.Cube)
222-
and not is_time_aggregatable(cube)
223-
and multi_case
224-
):
225-
raise TypeError(
226-
"multi_case must be false for a cube containing one time dimension"
227-
)
228-
elif isinstance(cube, iris.cube.CubeList) and not multi_case:
229-
raise TypeError("multi_case must be true for a CubeList")
230163

231-
if multi_case:
232-
# Collapse by lead time to get a single time dimension.
233-
cube = collapse_by_lead_time(
234-
cube, method, additional_percent=additional_percent
235-
)
236-
237-
# Categorise the time coordinate by hour of the day.
238-
cube = add_hour_coordinate(cube)
239-
# Aggregate by the new category coordinate.
240-
if method == "PERCENTILE":
241-
collapsed_cube = cube.aggregated_by(
242-
"hour", getattr(iris.analysis, method), percent=additional_percent
243-
)
164+
collapsed_cubes = iris.cube.CubeList([])
165+
for cube in iter_maybe(cubes):
166+
if (
167+
isinstance(cube, iris.cube.Cube)
168+
and is_time_aggregatable(cube)
169+
and not multi_case
170+
):
171+
raise TypeError(
172+
"multi_case must be true for a cube containing two time dimensions"
173+
)
174+
elif (
175+
isinstance(cube, iris.cube.Cube)
176+
and not is_time_aggregatable(cube)
177+
and multi_case
178+
):
179+
raise TypeError(
180+
"multi_case must be false for a cube containing one time dimension"
181+
)
182+
elif isinstance(cube, iris.cube.CubeList) and not multi_case:
183+
raise TypeError("multi_case must be true for a CubeList")
184+
185+
if multi_case:
186+
# Collapse by lead time to get a single time dimension.
187+
cube = collapse(
188+
cube,
189+
"forecast_reference_time",
190+
method,
191+
additional_percent=additional_percent,
192+
)
193+
194+
# Categorise the time coordinate by hour of the day.
195+
cube = add_hour_coordinate(cube)
196+
# Aggregate by the new category coordinate.
197+
if method == "PERCENTILE":
198+
collapsed_cube = cube.aggregated_by(
199+
"hour", getattr(iris.analysis, method), percent=additional_percent
200+
)
201+
else:
202+
collapsed_cube = cube.aggregated_by("hour", getattr(iris.analysis, method))
203+
204+
# Remove unnecessary time coordinates.
205+
collapsed_cube.remove_coord("time")
206+
collapsed_cube.remove_coord("forecast_period")
207+
208+
# Sort hour coordinate.
209+
collapsed_cube.coord("hour").points.sort()
210+
211+
# Remove forecast_reference_time if a single case, as collapse_by_lead_time
212+
# will have effectively done this if multi_case is True.
213+
if not multi_case:
214+
collapsed_cube.remove_coord("forecast_reference_time")
215+
216+
# Promote "hour" to dim_coord.
217+
iris.util.promote_aux_coord_to_dim_coord(collapsed_cube, "hour")
218+
collapsed_cubes.append(collapsed_cube)
219+
if len(collapsed_cubes) == 1:
220+
return collapsed_cubes[0]
244221
else:
245-
collapsed_cube = cube.aggregated_by("hour", getattr(iris.analysis, method))
246-
247-
# Remove unnecessary time coordinates.
248-
collapsed_cube.remove_coord("time")
249-
collapsed_cube.remove_coord("forecast_period")
250-
251-
# Sort hour coordinate.
252-
collapsed_cube.coord("hour").points.sort()
253-
254-
# Remove forecast_reference_time if a single case, as collapse_by_lead_time
255-
# will have effectively done this if multi_case is True.
256-
if not multi_case:
257-
collapsed_cube.remove_coord("forecast_reference_time")
258-
259-
# Promote "hour" to dim_coord.
260-
iris.util.promote_aux_coord_to_dim_coord(collapsed_cube, "hour")
261-
return collapsed_cube
222+
return collapsed_cubes
262223

263224

264225
def collapse_by_validity_time(
265-
cube: iris.cube.Cube | iris.cube.CubeList,
226+
cubes: iris.cube.Cube | iris.cube.CubeList,
266227
method: str,
267228
additional_percent: float = None,
268229
**kwargs,
@@ -277,7 +238,7 @@ def collapse_by_validity_time(
277238
278239
Arguments
279240
---------
280-
cube: iris.cube.Cube | iris.cube.CubeList
241+
cubes: iris.cube.Cube | iris.cube.CubeList
281242
Cube to collapse by validity time or CubeList that will be converted
282243
to a cube before collapsing by validity time.
283244
method: str
@@ -296,45 +257,50 @@ def collapse_by_validity_time(
296257
"""
297258
if method == "PERCENTILE" and additional_percent is None:
298259
raise ValueError("Must specify additional_percent")
299-
# Ensure the cube can be aggregated over multiple times.
300-
cubes_to_collapse = ensure_aggregatable_across_cases(cube)
301-
if len(cubes_to_collapse) != 1:
302-
raise ValueError("Cubes should be compatible when collapsing by validity time.")
303-
# Convert to a cube that is split by validity time.
304-
cube_to_collapse = cubes_to_collapse[0]
305-
# Slice over cube by both time dimensions to create a CubeList.
306-
new_cubelist = iris.cube.CubeList(
307-
cube_to_collapse.slices_over(["forecast_period", "forecast_reference_time"])
308-
)
309-
# Remove forecast_period and forecast_reference_time coordinates.
310-
for sub_cube in new_cubelist:
311-
sub_cube.remove_coord("forecast_period")
312-
sub_cube.remove_coord("forecast_reference_time")
313-
# Create new CubeList by merging with unique = False to produce a validity
314-
# time cube.
315-
merged_list_1 = new_cubelist.merge(unique=False)
316-
# Create a new "fake" coordinate and apply to each remaining cube to allow
317-
# final merging to take place into a single cube.
318-
equalised_validity_time = iris.coords.AuxCoord(
319-
points=0, long_name="equalised_validity_time", units="1"
320-
)
321-
for sub_cube, eq_valid_time in zip(
322-
merged_list_1, range(len(merged_list_1)), strict=True
323-
):
324-
sub_cube.add_aux_coord(equalised_validity_time.copy(points=eq_valid_time))
325-
326-
# Merge CubeList to create final cube.
327-
final_cube = merged_list_1.merge_cube()
328-
# Collapse over fake_time_coord to represent collapsing over validity time.
329-
if method == "PERCENTILE":
330-
return collapse(
331-
final_cube,
332-
"equalised_validity_time",
333-
method,
334-
additional_percent=additional_percent,
260+
261+
collapsed_cubes = iris.cube.CubeList([])
262+
for cube in iter_maybe(cubes):
263+
# Slice over cube by both time dimensions to create a CubeList.
264+
new_cubelist = iris.cube.CubeList(
265+
cube.slices_over(["forecast_period", "forecast_reference_time"])
335266
)
267+
# Remove forecast_period and forecast_reference_time coordinates.
268+
for sub_cube in new_cubelist:
269+
sub_cube.remove_coord("forecast_period")
270+
sub_cube.remove_coord("forecast_reference_time")
271+
# Create new CubeList by merging with unique = False to produce a validity
272+
# time cube.
273+
merged_list_1 = new_cubelist.merge(unique=False)
274+
# Create a new "fake" coordinate and apply to each remaining cube to allow
275+
# final merging to take place into a single cube.
276+
equalised_validity_time = iris.coords.AuxCoord(
277+
points=0, long_name="equalised_validity_time", units="1"
278+
)
279+
for sub_cube, eq_valid_time in zip(
280+
merged_list_1, range(len(merged_list_1)), strict=True
281+
):
282+
sub_cube.add_aux_coord(equalised_validity_time.copy(points=eq_valid_time))
283+
284+
# Merge CubeList to create final cube.
285+
final_cube = merged_list_1.merge_cube()
286+
# Collapse over fake_time_coord to represent collapsing over validity time.
287+
if method == "PERCENTILE":
288+
collapsed_cubes.append(
289+
collapse(
290+
final_cube,
291+
"equalised_validity_time",
292+
method,
293+
additional_percent=additional_percent,
294+
)
295+
)
296+
else:
297+
collapsed_cubes.append(
298+
collapse(final_cube, "equalised_validity_time", method)
299+
)
300+
if len(collapsed_cubes) == 1:
301+
return collapsed_cubes[0]
336302
else:
337-
return collapse(final_cube, "equalised_validity_time", method)
303+
return collapsed_cubes
338304

339305

340306
# TODO

0 commit comments

Comments
 (0)