Skip to content

Commit ca028e3

Browse files
authored
Generate calibrated forecasts from EMOS with an alternative percentile set (metoppv#1587)
* Add capability to pass a comma separated list of percentiles to the apply EMOS CLI. These will be the percentiles output within the calibrated forecast, if these percentiles are provided. Otherwise, the percentiles output will match the percentiles input. * Minor updates.
1 parent b5d501f commit ca028e3

5 files changed

Lines changed: 66 additions & 6 deletions

File tree

improver/calibration/ensemble_calibration.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
3939
"""
4040
import warnings
41-
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
41+
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union
4242

4343
import iris
4444
import numpy as np
@@ -1712,6 +1712,15 @@ class ApplyEMOS(PostProcessingPlugin):
17121712
Class to calibrate an input forecast given EMOS coefficients
17131713
"""
17141714

1715+
def __init__(self, percentiles: Optional[Sequence] = None):
1716+
"""Initialise class.
1717+
1718+
Args:
1719+
percentiles:
1720+
The set of percentiles used to create the calibrated forecast.
1721+
"""
1722+
self.percentiles = [np.float32(p) for p in percentiles] if percentiles else None
1723+
17151724
@staticmethod
17161725
def _get_attribute(
17171726
coefficients: CubeList, attribute_name: str, optional: bool = False
@@ -1818,11 +1827,12 @@ def _convert_to_realizations(
18181827

18191828
return forecast_as_realizations
18201829

1821-
def _calibrate_forecast(
1830+
def _format_forecast(
18221831
self, forecast: Cube, randomise: bool, random_seed: int
18231832
) -> Cube:
18241833
"""
1825-
Generate calibrated probability, percentile or realization output
1834+
Generate calibrated probability, percentile or realization output in
1835+
the desired format.
18261836
18271837
Args:
18281838
forecast:
@@ -1860,7 +1870,9 @@ def _calibrate_forecast(
18601870
self.distribution["location"],
18611871
self.distribution["scale"],
18621872
forecast,
1863-
percentiles=perc_coord.points,
1873+
percentiles=self.percentiles
1874+
if self.percentiles
1875+
else perc_coord.points,
18641876
)
18651877
else:
18661878
no_of_percentiles = len(forecast.coord("realization").points)
@@ -1952,7 +1964,7 @@ def process(
19521964
),
19531965
}
19541966

1955-
result = self._calibrate_forecast(forecast, randomise, random_seed)
1967+
result = self._format_forecast(forecast, randomise, random_seed)
19561968

19571969
if land_sea_mask:
19581970
# fill in masked sea points with uncalibrated data

improver/cli/apply_emos_coefficients.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def process(
5656
random_seed: int = None,
5757
ignore_ecc_bounds=False,
5858
predictor="mean",
59+
percentiles: cli.comma_separated_list = None,
5960
):
6061
"""Applying coefficients for Ensemble Model Output Statistics.
6162
@@ -108,6 +109,8 @@ def process(
108109
the location parameter when estimating the EMOS coefficients.
109110
Currently the ensemble mean ("mean") and the ensemble
110111
realizations ("realizations") are supported as the predictors.
112+
percentiles (List[float]):
113+
The set of percentiles used to create the calibrated forecast.
111114
112115
Returns:
113116
iris.cube.Cube:
@@ -139,7 +142,7 @@ def process(
139142
msg = "The land_sea_mask cube does not have the name 'land_binary_mask'"
140143
raise ValueError(msg)
141144

142-
calibration_plugin = ApplyEMOS()
145+
calibration_plugin = ApplyEMOS(percentiles=percentiles)
143146
result = calibration_plugin(
144147
cube,
145148
coefficients,

improver_tests/acceptance/SHA256SUMS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
a6a84d0142796e4b9ca7bd3f0ad78586ea77684f5df02732fdda2ab54233cbb6 ./aggregate-reliability-tables/basic/multiple_tables_kgo.nc
33
c0c38c5b1ba16fd5f7310b6d2ee0ab9d8b6bcdb3b23c8475193eefae1eb14a17 ./aggregate-reliability-tables/basic/reliability_table.nc
44
9eeda326cdc66d93e591c87e9f7ceb17cea329397169fb0518f50da3bf4dc61f ./aggregate-reliability-tables/basic/reliability_table_2.nc
5+
52b02f4c6e4adc604d4961ce87b4ada5985d07eac1d3c703cbab985de223228f ./apply-emos-coefficients/alternative_percentiles/kgo.nc
56
ff3a00a16fa94697d6e529a6f98384e4602f70a7d6ce26669c3947c8ac2e6f7e ./apply-emos-coefficients/land_sea/landmask.nc
67
bee125371a9db4c80fb7f8d1b0644a2710903aad27023b5939331f7ec5f3b617 ./apply-emos-coefficients/land_sea/percentiles_kgo.nc
78
7f5cabc9b5de4c13aebf833d5372159edf1deafd19bba8721318b889a09b407f ./apply-emos-coefficients/land_sea/probabilities_kgo.nc

improver_tests/acceptance/test_apply_emos_coefficients.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,28 @@ def test_percentiles_input_land_sea(tmp_path):
258258
acc.compare(output_path, kgo_path, atol=LOOSE_TOLERANCE)
259259

260260

261+
def test_alternative_percentiles(tmp_path):
262+
"""Test using percentiles as input with an alternative set of
263+
percentiles specified."""
264+
kgo_dir = acc.kgo_root() / "apply-emos-coefficients/alternative_percentiles"
265+
kgo_path = kgo_dir / "kgo.nc"
266+
input_path = kgo_dir / "../percentiles/input.nc"
267+
emos_est_path = kgo_dir / "../normal/normal_coefficients.nc"
268+
output_path = tmp_path / "output.nc"
269+
args = [
270+
input_path,
271+
emos_est_path,
272+
"--realizations-count",
273+
"18",
274+
"--percentiles",
275+
"25,50,75",
276+
"--output",
277+
output_path,
278+
]
279+
run_cli(args)
280+
acc.compare(output_path, kgo_path, atol=LOOSE_TOLERANCE, rtol=LOOSE_TOLERANCE)
281+
282+
261283
def test_percentiles_error(tmp_path):
262284
"""Test using percentiles as input"""
263285
kgo_dir = acc.kgo_root() / "apply-emos-coefficients/percentiles"

improver_tests/calibration/ensemble_calibration/test_ApplyEMOS.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ def setUp(self):
179179
np.full((3, 3), 10.534898),
180180
]
181181
)
182+
self.alternative_percentiles = [25.0, 50.0, 75.0]
182183

183184
def test_null_percentiles(self):
184185
"""Test effect of "neutral" emos coefficients in percentile space
@@ -375,6 +376,27 @@ def test_null_percentiles_frt_fp_mismatch(self):
375376
np.mean(result.data), self.null_percentiles_expected_mean
376377
)
377378

379+
def test_alternative_percentiles(self):
380+
"""Test that the calibrated forecast is at a specified set of
381+
percentiles."""
382+
result = ApplyEMOS(percentiles=self.alternative_percentiles)(
383+
self.percentiles, self.coefficients, realizations_count=3
384+
)
385+
self.assertArrayEqual(
386+
result.coord("percentile").points, self.alternative_percentiles
387+
)
388+
389+
def test_alternative_string_percentiles(self):
390+
"""Test that the calibrated forecast is at a specified set of
391+
percentiles where the input percentiles are strings."""
392+
str_percentiles = list(map(str, self.alternative_percentiles))
393+
result = ApplyEMOS(percentiles=str_percentiles)(
394+
self.percentiles, self.coefficients, realizations_count=3
395+
)
396+
self.assertArrayEqual(
397+
result.coord("percentile").points, self.alternative_percentiles
398+
)
399+
378400
def test_invalid_attribute(self):
379401
"""Test that an exception is raised if multiple different distribution
380402
attributes are provided within the coefficients cubelist."""

0 commit comments

Comments
 (0)