Skip to content

Commit 83d2709

Browse files
authored
Merge pull request #67 from ecmwf/feature/cpf-reverse
extreme.cpf: make computation more resilient to 'reverse crossings'
2 parents 268487b + 6c58252 commit 83d2709

File tree

3 files changed

+39
-16
lines changed

3 files changed

+39
-16
lines changed

src/earthkit/meteo/extreme/array/cpf.py

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,46 @@
1010
from earthkit.utils.array import array_namespace
1111

1212

13-
def _cpf(clim, ens, epsilon=None):
13+
def _cpf(clim, ens, epsilon=None, from_zero=False):
1414
xp = array_namespace(clim, ens)
1515
clim = xp.asarray(clim)
1616
ens = xp.asarray(ens)
1717

1818
nclim, npoints = clim.shape
1919
nens, _ = ens.shape
2020

21-
cpf = xp.ones(npoints, dtype=xp.float32)
21+
cpf = xp.zeros(npoints, dtype=xp.float32)
2222
mask = xp.zeros(npoints, dtype=xp.bool)
23+
prim = xp.zeros(npoints, dtype=xp.bool)
24+
25+
# start scanning ensemble from iq_start
26+
iq_start = 0 if from_zero else nens // 2
2327

2428
for icl in range(1, nclim - 1):
2529
# quantile level of climatology
2630
tau_c = icl / (nclim - 1.0)
27-
for iq in range(nens):
31+
for iq in range(iq_start, nens):
2832
# quantile level of forecast
2933
tau_f = (iq + 1.0) / (nens + 1.0)
30-
if tau_f >= tau_c:
31-
# quantile values of forecast and climatology
32-
qv_f = ens[iq, :]
33-
qv_c = clim[icl, :]
3434

35+
# quantile values of forecast and climatology
36+
qv_f = ens[iq, :]
37+
qv_c = clim[icl, :]
38+
39+
# primary condition (to ensure crossing and not reverse-crossing)
40+
if tau_f < tau_c:
41+
idx = (qv_f >= qv_c) & (~mask)
42+
prim[idx] = True
43+
44+
if tau_f >= tau_c:
3545
# lowest climate quantile: interpolate between 2 consecutive quantiles
3646
if iq < 2:
3747
# quantile value and quantile level of climatology at previous
3848
qv_c_2 = clim[icl - 1, :]
3949
tau_c_2 = (icl - 1) / (nclim - 1)
4050

4151
# condition of crossing situtaion:
42-
idx = (qv_f < qv_c) & (qv_c_2 < qv_c)
52+
idx = (qv_f < qv_c) & (qv_c_2 < qv_c) & prim
4353

4454
# intersection between two lines
4555
tau_i = (tau_c * (qv_c_2[idx] - qv_f[idx]) + tau_c_2 * (qv_f[idx] - qv_c[idx])) / (
@@ -51,7 +61,7 @@ def _cpf(clim, ens, epsilon=None):
5161
mask[idx] = True
5262

5363
# check crossing cases
54-
idx = (qv_f < qv_c) & (~mask)
64+
idx = (qv_f < qv_c) & (~mask) & prim
5565
cpf[idx] = tau_f
5666
mask[idx] = True
5767

@@ -60,7 +70,7 @@ def _cpf(clim, ens, epsilon=None):
6070
qv_c_2 = clim[nclim - 1, :]
6171
tau_c_2 = 1.0
6272

63-
idx = (qv_f > qv_c) & (qv_c_2 > qv_c) & (~mask)
73+
idx = (qv_f > qv_c) & (qv_c_2 > qv_c) & (~mask) & prim
6474

6575
tau_i = (tau_c * (qv_c_2[idx] - qv_f[idx]) + tau_c_2 * (qv_f[idx] - qv_c[idx])) / (
6676
qv_c_2[idx] - qv_c[idx]
@@ -80,7 +90,15 @@ def _cpf(clim, ens, epsilon=None):
8090
return cpf
8191

8292

83-
def cpf(clim, ens, sort_clim=True, sort_ens=True, epsilon=None, symmetric=False):
93+
def cpf(
94+
clim,
95+
ens,
96+
sort_clim=True,
97+
sort_ens=True,
98+
epsilon=None,
99+
symmetric=False,
100+
from_zero=False,
101+
):
84102
"""Compute Crossing Point Forecast (CPF)
85103
86104
WARNING: this code is experimental, use at your own risk!
@@ -101,6 +119,9 @@ def cpf(clim, ens, sort_clim=True, sort_ens=True, epsilon=None, symmetric=False)
101119
symmetric: bool
102120
If True, make CPF values below 0.5 use a symmetric computation (CPF of
103121
opposite values)
122+
from_zero: bool
123+
If True, start looking for a crossing from the minimum, rather than the
124+
median
104125
105126
Returns
106127
-------
@@ -123,10 +144,10 @@ def cpf(clim, ens, sort_clim=True, sort_ens=True, epsilon=None, symmetric=False)
123144
if symmetric:
124145
epsilon = None
125146

126-
cpf_direct = _cpf(clim, ens, epsilon)
147+
cpf_direct = _cpf(clim, ens, epsilon, from_zero)
127148

128149
if symmetric:
129-
cpf_reverse = _cpf(-clim[::-1, :], -ens[::-1, :])
150+
cpf_reverse = _cpf(-clim[::-1, :], -ens[::-1, :], from_zero=from_zero)
130151
mask = cpf_direct < 0.5
131152
cpf_direct[mask] = 1 - cpf_reverse[mask]
132153

tests/extreme/_cpf.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import numpy as np
22

3-
cpf_val = np.array([0.0, 1.0, 0.21153846, 0.53846157, 0.7307692], dtype=np.float32)
3+
cpf_val = np.array([0.0, 1.0, 0.0, 0.53846157, 0.7307692], dtype=np.float32)
4+
cpf_val_fromzero = np.array([0.0, 1.0, 0.21153846, 0.53846157, 0.7307692], dtype=np.float32)
45
cpf_ens = np.array(
56
[
67
[299.3667, 299.97998, 276.91553, 298.83936, 296.2339],
@@ -328,7 +329,7 @@
328329
dtype=np.float32,
329330
)
330331

331-
cpf_val3 = np.array([0.97483208, 0.11538463, 1.0, 0.59615387, 1.0], dtype=np.float32)
332+
cpf_val3 = np.array([0.9809273481, 0.1153846383, 0.9897592068, 0.5961538553, 1.0], dtype=np.float32)
332333
cpf_ens3 = np.array(
333334
[
334335
[298.41162109, 297.77490234, 298.49560547, 284.96630859, 300.28857422],

tests/extreme/test_extreme.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ def test_cpf_highlevel(clim, ens, v_ref, array_backend):
189189
[
190190
(_cpf.cpf_clim, _cpf.cpf_ens, dict(sort_clim=True), _cpf.cpf_val),
191191
(_cpf.cpf_clim2, _cpf.cpf_ens2, dict(sort_clim=True, epsilon=0.5), _cpf.cpf_val2), # eps
192-
# (_cpf.cpf_clim3, _cpf.cpf_ens3, dict(sort_clim=True, symmetric=True), _cpf.cpf_val3), # sym
192+
(_cpf.cpf_clim3, _cpf.cpf_ens3, dict(sort_clim=True, symmetric=True), _cpf.cpf_val3), # sym
193+
(_cpf.cpf_clim, _cpf.cpf_ens, dict(sort_clim=True, from_zero=True), _cpf.cpf_val_fromzero),
193194
],
194195
)
195196
def test_cpf_core(clim, ens, v_ref, kwargs, array_backend):

0 commit comments

Comments
 (0)