Skip to content

Commit

Permalink
Move ensures_aggregatable_across_cases to aggregate.py
Browse files Browse the repository at this point in the history
  • Loading branch information
daflack committed Jan 31, 2025
1 parent c165088 commit a1d488b
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 102 deletions.
61 changes: 61 additions & 0 deletions src/CSET/operators/aggregate.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import iris.cube
import isodate

from CSET.operators._utils import is_time_aggregatable


def time_aggregate(
cube: iris.cube.Cube,
Expand Down Expand Up @@ -82,3 +84,62 @@ def time_aggregate(
aggregated_cube = cube.aggregated_by("interval", getattr(iris.analysis, method))
aggregated_cube.remove_coord("interval")
return aggregated_cube


def ensure_aggregatable_across_cases(
cube: iris.cube.Cube | iris.cube.CubeList,
) -> iris.cube.Cube:
"""Ensure a Cube or CubeList can be aggregated across multiple cases.
Arguments
---------
cube: iris.cube.Cube | iris.cube.CubeList
If a Cube is provided it is checked to determine if it has the
the necessary dimensional coordinates to be aggregatable.
These necessary coordinates are 'forecast_period' and
'forecast_reference_time'.If a CubeList is provided a Cube is created
by slicing over all time coordinates and the resulting list is merged
to create an aggregatable cube.
Returns
-------
cube: iris.cube.Cube
A time aggregatable cube with dimension coordinates including
'forecast_period' and 'forecast_reference_time'.
Raises
------
ValueError
If a Cube is provided and it is not aggregatable a ValueError is
raised. The user should then provide a CubeList to be turned into an
aggregatable cube to allow aggregation across multiple cases to occur.
Notes
-----
This is a simple operator designed to ensure that a Cube is aggregatable
across cases. If a CubeList is presented it will create an aggregatable Cube
from that list. Its functionality is for case study (or trial) aggregation
to ensure that the full dataset can be loaded as a single cube. This
functionality is particularly useful for percentiles, Q-Q plots, and
histograms.
"""
# Check to see if a cube is input and if that cube is iterable.
if isinstance(cube, iris.cube.Cube):
if is_time_aggregatable(cube):
return cube
else:
raise ValueError(
"Single Cube should have 'forecast_period' and"
"'forecast_reference_time' dimensional coordinates. "
"To make a time aggregatable Cube input a CubeList."
)
# Create an aggregatable cube from the provided CubeList.
else:
new_cube_list = iris.cube.CubeList()
for sub_cube in cube:
for cube_slice in sub_cube.slices_over(
["forecast_period", "forecast_reference_time"]
):
new_cube_list.append(cube_slice)
new_merged_cube = new_cube_list.merge_cube()
return new_merged_cube
2 changes: 1 addition & 1 deletion src/CSET/operators/collapse.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from CSET._common import iter_maybe
from CSET.operators._utils import is_time_aggregatable
from CSET.operators.misc import ensure_aggregatable_across_cases
from CSET.operators.aggregate import ensure_aggregatable_across_cases


def collapse(
Expand Down
60 changes: 0 additions & 60 deletions src/CSET/operators/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
from CSET.operators._utils import (
fully_equalise_attributes,
get_cube_yxcoordname,
is_time_aggregatable,
)


Expand Down Expand Up @@ -333,62 +332,3 @@ def is_increasing(sequence: list) -> bool:
difference.rename(base.name() + "_difference")
difference.data = base.data - other.data
return difference


def ensure_aggregatable_across_cases(
cube: iris.cube.Cube | iris.cube.CubeList,
) -> iris.cube.Cube:
"""Ensure a Cube or CubeList can be aggregated across multiple cases.
Arguments
---------
cube: iris.cube.Cube | iris.cube.CubeList
If a Cube is provided it is checked to determine if it has the
the necessary dimensional coordinates to be aggregatable.
These necessary coordinates are 'forecast_period' and
'forecast_reference_time'.If a CubeList is provided a Cube is created
by slicing over all time coordinates and the resulting list is merged
to create an aggregatable cube.
Returns
-------
cube: iris.cube.Cube
A time aggregatable cube with dimension coordinates including
'forecast_period' and 'forecast_reference_time'.
Raises
------
ValueError
If a Cube is provided and it is not aggregatable a ValueError is
raised. The user should then provide a CubeList to be turned into an
aggregatable cube to allow aggregation across multiple cases to occur.
Notes
-----
This is a simple operator designed to ensure that a Cube is aggregatable
across cases. If a CubeList is presented it will create an aggregatable Cube
from that list. Its functionality is for case study (or trial) aggregation
to ensure that the full dataset can be loaded as a single cube. This
functionality is particularly useful for percentiles, Q-Q plots, and
histograms.
"""
# Check to see if a cube is input and if that cube is iterable.
if isinstance(cube, iris.cube.Cube):
if is_time_aggregatable(cube):
return cube
else:
raise ValueError(
"Single Cube should have 'forecast_period' and"
"'forecast_reference_time' dimensional coordinates. "
"To make a time aggregatable Cube input a CubeList."
)
# Create an aggregatable cube from the provided CubeList.
else:
new_cube_list = iris.cube.CubeList()
for sub_cube in cube:
for cube_slice in sub_cube.slices_over(
["forecast_period", "forecast_reference_time"]
):
new_cube_list.append(cube_slice)
new_merged_cube = new_cube_list.merge_cube()
return new_merged_cube
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ steps:
coordinate: pressure
levels: $PLEVEL

- operator: misc.ensure_aggregatable_across_cases
- operator: aggregate.ensure_aggregatable_across_cases

- operator: plot.plot_histogram_series
sequence_coordinate: forecast_period
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ steps:
coordinate: pressure
levels: []

- operator: misc.ensure_aggregatable_across_cases
- operator: aggregate.ensure_aggregatable_across_cases

- operator: write.write_cube_to_nc
overwrite: True
Expand Down
44 changes: 44 additions & 0 deletions tests/operators/test_aggregate.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@

"""Test aggregate operators."""

import iris
import iris.cube
import numpy as np
import pytest

from CSET.operators import aggregate


Expand All @@ -34,3 +39,42 @@ def test_aggregate(cube):
assert len(aggregated_cube.coords()) == len(cube.coords()), (
"aggregated cube does not have additional aux coordinate"
)


def test_ensure_aggregatable_across_cases_true_aggregatable_cube(
long_forecast_multi_day,
):
"""Check that an aggregatable cube is returned with no changes."""
assert np.allclose(
aggregate.ensure_aggregatable_across_cases(long_forecast_multi_day).data,
long_forecast_multi_day.data,
rtol=1e-06,
atol=1e-02,
)


def test_ensure_aggregatable_across_cases_false_aggregatable_cube(long_forecast):
"""Check that a non-aggregatable cube raises an error."""
with pytest.raises(ValueError):
aggregate.ensure_aggregatable_across_cases(long_forecast)


def test_ensure_aggregatable_across_cases_cubelist(
long_forecast_many_cubes, long_forecast_multi_day
):
"""Check that a CubeList turns into an aggregatable Cube."""
# Check output is a Cube.
output_data = aggregate.ensure_aggregatable_across_cases(long_forecast_many_cubes)
assert isinstance(output_data, iris.cube.Cube)
# Check output can be aggregated in time.
assert isinstance(
aggregate.ensure_aggregatable_across_cases(output_data), iris.cube.Cube
)
# Check output is identical to a pre-calculated cube.
pre_calculated_data = long_forecast_multi_day
assert np.allclose(
pre_calculated_data.data,
output_data.data,
rtol=1e-06,
atol=1e-02,
)
39 changes: 0 additions & 39 deletions tests/operators/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,42 +212,3 @@ def test_difference_different_model_types(cube):
assert np.allclose(
difference_cube.data, np.zeros_like(difference_cube.data), atol=1e-9
)


def test_ensure_aggregatable_across_cases_true_aggregatable_cube(
long_forecast_multi_day,
):
"""Check that an aggregatable cube is returned with no changes."""
assert np.allclose(
misc.ensure_aggregatable_across_cases(long_forecast_multi_day).data,
long_forecast_multi_day.data,
rtol=1e-06,
atol=1e-02,
)


def test_ensure_aggregatable_across_cases_false_aggregatable_cube(long_forecast):
"""Check that a non-aggregatable cube raises an error."""
with pytest.raises(ValueError):
misc.ensure_aggregatable_across_cases(long_forecast)


def test_ensure_aggregatable_across_cases_cubelist(
long_forecast_many_cubes, long_forecast_multi_day
):
"""Check that a CubeList turns into an aggregatable Cube."""
# Check output is a Cube.
output_data = misc.ensure_aggregatable_across_cases(long_forecast_many_cubes)
assert isinstance(output_data, iris.cube.Cube)
# Check output can be aggregated in time.
assert isinstance(
misc.ensure_aggregatable_across_cases(output_data), iris.cube.Cube
)
# Check output is identical to a pre-calculated cube.
pre_calculated_data = long_forecast_multi_day
assert np.allclose(
pre_calculated_data.data,
output_data.data,
rtol=1e-06,
atol=1e-02,
)

0 comments on commit a1d488b

Please sign in to comment.