43
43
blend_means_sigmas
44
44
"""
45
45
46
+ import math
46
47
import time
47
48
48
49
import numpy as np
50
+ from scipy .linalg import inv
49
51
from scipy .ndimage import binary_dilation , generate_binary_structure , iterate_structure
50
52
51
53
from pysteps import cascade
@@ -74,7 +76,7 @@ def forecast(
74
76
timestep ,
75
77
issuetime ,
76
78
n_ens_members ,
77
- n_cascade_levels = 8 ,
79
+ n_cascade_levels = 6 ,
78
80
blend_nwp_members = False ,
79
81
precip_thr = None ,
80
82
norain_thr = 0.0 ,
@@ -90,6 +92,8 @@ def forecast(
90
92
conditional = False ,
91
93
probmatching_method = "cdf" ,
92
94
mask_method = "incremental" ,
95
+ resample_distribution = True ,
96
+ smooth_radar_mask_range = 0 ,
93
97
callback = None ,
94
98
return_output = True ,
95
99
seed = None ,
@@ -153,8 +157,8 @@ def forecast(
153
157
equal to or larger than the number of NWP ensemble members / number of
154
158
NWP models.
155
159
n_cascade_levels: int, optional
156
- The number of cascade levels to use. Default set to 8 due to default
157
- climatological skill values on 8 levels .
160
+ The number of cascade levels to use. Defaults to 6,
161
+ see issue #385 on GitHub .
158
162
blend_nwp_members: bool
159
163
Check if NWP models/members should be used individually, or if all of
160
164
them are blended together per nowcast ensemble member. Standard set to
@@ -204,18 +208,32 @@ def forecast(
204
208
If set to True, compute the statistics of the precipitation field
205
209
conditionally by excluding pixels where the values are below the threshold
206
210
precip_thr.
207
- mask_method: {'obs','incremental',None}, optional
208
- The method to use for masking no precipitation areas in the forecast field.
209
- The masked pixels are set to the minimum value of the observations.
210
- 'obs' = apply precip_thr to the most recently observed precipitation intensity
211
- field, 'incremental' = iteratively buffer the mask with a certain rate
212
- (currently it is 1 km/min), None=no masking.
213
211
probmatching_method: {'cdf','mean',None}, optional
214
212
Method for matching the statistics of the forecast field with those of
215
213
the most recently observed one. 'cdf'=map the forecast CDF to the observed
216
214
one, 'mean'=adjust only the conditional mean value of the forecast field
217
215
in precipitation areas, None=no matching applied. Using 'mean' requires
218
216
that mask_method is not None.
217
+ mask_method: {'obs','incremental',None}, optional
218
+ The method to use for masking no precipitation areas in the forecast field.
219
+ The masked pixels are set to the minimum value of the observations.
220
+ 'obs' = apply precip_thr to the most recently observed precipitation intensity
221
+ field, 'incremental' = iteratively buffer the mask with a certain rate
222
+ (currently it is 1 km/min), None=no masking.
223
+ resample_distribution: bool, optional
224
+ Method to resample the distribution from the extrapolation and NWP cascade as input
225
+ for the probability matching. Not resampling these distributions may lead to losing
226
+ some extremes when the weight of both the extrapolation and NWP cascade is similar.
227
+ Defaults to True.
228
+ smooth_radar_mask_range: int, Default is 0.
229
+ Method to smooth the transition between the radar-NWP-noise blend and the NWP-noise
230
+ blend near the edge of the radar domain (radar mask), where the radar data is either
231
+ not present anymore or is not reliable. If set to 0 (grid cells), this generates a
232
+ normal forecast without smoothing. To create a smooth mask, this range should be a
233
+ positive value, representing a buffer band of a number of pixels by which the mask
234
+ is cropped and smoothed. The smooth radar mask removes the hard edges between NWP
235
+ and radar in the final blended product. Typically, a value between 50 and 100 km
236
+ can be used. 80 km generally gives good results.
219
237
callback: function, optional
220
238
Optional function that is called after computation of each time step of
221
239
the nowcast. The function takes one argument: a three-dimensional array
@@ -1396,7 +1414,6 @@ def worker(j):
1396
1414
# latest extrapolated radar rainfall field blended with the
1397
1415
# nwp model(s) rainfall forecast fields as 'benchmark'.
1398
1416
1399
- # TODO: Check probability matching method
1400
1417
# 8.7.1 first blend the extrapolated rainfall field (the field
1401
1418
# that is only used for post-processing steps) with the NWP
1402
1419
# rainfall forecast for this time step using the weights
@@ -1451,10 +1468,49 @@ def worker(j):
1451
1468
# forecast outside the radar domain. Therefore, fill these
1452
1469
# areas with the "..._mod_only" blended forecasts, consisting
1453
1470
# of the NWP and noise components.
1471
+
1454
1472
nan_indices = np .isnan (R_f_new )
1455
- R_f_new [nan_indices ] = R_f_new_mod_only [nan_indices ]
1456
- nan_indices = np .isnan (R_pm_blended )
1457
- R_pm_blended [nan_indices ] = R_pm_blended_mod_only [nan_indices ]
1473
+ if smooth_radar_mask_range != 0 :
1474
+ # Compute the smooth dilated mask
1475
+ new_mask = blending .utils .compute_smooth_dilated_mask (
1476
+ nan_indices ,
1477
+ max_padding_size_in_px = smooth_radar_mask_range ,
1478
+ )
1479
+
1480
+ # Ensure mask values are between 0 and 1
1481
+ mask_model = np .clip (new_mask , 0 , 1 )
1482
+ mask_radar = np .clip (1 - new_mask , 0 , 1 )
1483
+
1484
+ # Handle NaNs in R_f_new and R_f_new_mod_only by setting NaNs to 0 in the blending step
1485
+ R_f_new_mod_only_no_nan = np .nan_to_num (
1486
+ R_f_new_mod_only , nan = 0
1487
+ )
1488
+ R_f_new_no_nan = np .nan_to_num (R_f_new , nan = 0 )
1489
+
1490
+ # Perform the blending of radar and model inside the radar domain using a weighted combination
1491
+ R_f_new = np .nansum (
1492
+ [
1493
+ mask_model * R_f_new_mod_only_no_nan ,
1494
+ mask_radar * R_f_new_no_nan ,
1495
+ ],
1496
+ axis = 0 ,
1497
+ )
1498
+
1499
+ nan_indices = np .isnan (R_pm_blended )
1500
+ R_pm_blended = np .nansum (
1501
+ [
1502
+ R_pm_blended * mask_radar ,
1503
+ R_pm_blended_mod_only * mask_model ,
1504
+ ],
1505
+ axis = 0 ,
1506
+ )
1507
+ else :
1508
+ R_f_new [nan_indices ] = R_f_new_mod_only [nan_indices ]
1509
+ nan_indices = np .isnan (R_pm_blended )
1510
+ R_pm_blended [nan_indices ] = R_pm_blended_mod_only [
1511
+ nan_indices
1512
+ ]
1513
+
1458
1514
# Finally, fill the remaining nan values, if present, with
1459
1515
# the minimum value in the forecast
1460
1516
nan_indices = np .isnan (R_f_new )
@@ -1491,19 +1547,39 @@ def worker(j):
1491
1547
# Set to min value outside of mask
1492
1548
R_f_new [~ MASK_prec_ ] = R_cmin
1493
1549
1550
+ # If probmatching_method is not None, resample the distribution from
1551
+ # both the extrapolation cascade and the model (NWP) cascade and use
1552
+ # that for the probability matching
1553
+ if probmatching_method is not None and resample_distribution :
1554
+ # deal with missing values
1555
+ arr1 = R_pm_ep [t_index ]
1556
+ arr2 = precip_models_pm_temp [j ]
1557
+ arr2 = np .where (np .isnan (arr2 ), np .nanmin (arr2 ), arr2 )
1558
+ arr1 = np .where (np .isnan (arr1 ), arr2 , arr1 )
1559
+ # resample weights based on cascade level 2
1560
+ R_pm_resampled = probmatching .resample_distributions (
1561
+ first_array = arr1 ,
1562
+ second_array = arr2 ,
1563
+ probability_first_array = weights_pm_normalized [0 ],
1564
+ )
1565
+ else :
1566
+ R_pm_resampled = R_pm_blended .copy ()
1567
+
1494
1568
if probmatching_method == "cdf" :
1495
1569
# Adjust the CDF of the forecast to match the most recent
1496
1570
# benchmark rainfall field (R_pm_blended). If the forecast
1497
1571
if np .any (np .isfinite (R_f_new )):
1498
1572
R_f_new = probmatching .nonparam_match_empirical_cdf (
1499
- R_f_new , R_pm_blended
1573
+ R_f_new , R_pm_resampled
1500
1574
)
1575
+ R_pm_resampled = None
1501
1576
elif probmatching_method == "mean" :
1502
1577
# Use R_pm_blended as benchmark field and
1503
- mu_0 = np .mean (R_pm_blended [ R_pm_blended >= precip_thr ])
1578
+ mu_0 = np .mean (R_pm_resampled [ R_pm_resampled >= precip_thr ])
1504
1579
MASK = R_f_new >= precip_thr
1505
1580
mu_fct = np .mean (R_f_new [MASK ])
1506
1581
R_f_new [MASK ] = R_f_new [MASK ] - mu_fct + mu_0
1582
+ R_pm_resampled = None
1507
1583
1508
1584
R_f_out .append (R_f_new )
1509
1585
@@ -1666,7 +1742,7 @@ def calculate_weights_spn(correlations, cov):
1666
1742
if isinstance (cov , type (None )):
1667
1743
raise ValueError ("cov must contain a covariance matrix" )
1668
1744
else :
1669
- # Make a numpy matrix out of cov and get the inverse
1745
+ # Make a numpy array out of cov and get the inverse
1670
1746
cov = np .where (cov == 0.0 , 10e-5 , cov )
1671
1747
# Make sure the determinant of the matrix is not zero, otherwise
1672
1748
# subtract 10e-5 from the cross-correlations between the models
@@ -1675,26 +1751,30 @@ def calculate_weights_spn(correlations, cov):
1675
1751
# Ensure the correlation of the model with itself is always 1.0
1676
1752
for i , _ in enumerate (cov ):
1677
1753
cov [i ][i ] = 1.0
1678
- # Make a numpy matrix out of the array
1679
- cov_matrix = np .asmatrix (cov )
1680
- # Get the inverse of the matrix
1681
- cov_matrix_inv = cov_matrix .getI ()
1682
- # The component weights are the dot product between cov_matrix_inv
1683
- # and cor_vec
1684
- weights = cov_matrix_inv .dot (correlations )
1754
+ # Use a numpy array instead of a matrix
1755
+ cov_matrix = np .array (cov )
1756
+ # Get the inverse of the matrix using scipy's inv function
1757
+ cov_matrix_inv = inv (cov_matrix )
1758
+ # The component weights are the dot product between cov_matrix_inv and cor_vec
1759
+ weights = np .dot (cov_matrix_inv , correlations )
1685
1760
weights = np .nan_to_num (
1686
1761
weights , copy = True , nan = 10e-5 , posinf = 10e-5 , neginf = 10e-5
1687
1762
)
1763
+ weights_dot_correlations = np .dot (weights , correlations )
1688
1764
# If the dot product of the weights with the correlations is
1689
1765
# larger than 1.0, we assign a weight of 0.0 to the noise (to make
1690
1766
# it numerically stable)
1691
- if weights . dot ( correlations ) > 1.0 :
1767
+ if weights_dot_correlations > 1.0 :
1692
1768
noise_weight = np .array ([0 ])
1693
1769
# Calculate the noise weight
1694
1770
else :
1695
- noise_weight = np .asarray (np .sqrt (1.0 - weights .dot (correlations )))[0 ]
1771
+ noise_weight = np .sqrt (1.0 - weights_dot_correlations )
1772
+ # Convert weights to a 1D array
1773
+ weights = np .array (weights ).flatten ()
1774
+ # Ensure noise_weight is a 1D array before concatenation
1775
+ noise_weight = np .array (noise_weight ).flatten ()
1696
1776
# Finally, add the noise_weights to the weights variable.
1697
- weights = np .concatenate ((np . array ( weights )[ 0 ] , noise_weight ), axis = 0 )
1777
+ weights = np .concatenate ((weights , noise_weight ), axis = 0 )
1698
1778
1699
1779
# Otherwise, the weight equals the correlation on that scale level and
1700
1780
# the noise component weight equals 1 - this weight. This only occurs for
@@ -1808,7 +1888,7 @@ def _check_inputs(
1808
1888
if isinstance (timesteps , list ) and not sorted (timesteps ) == timesteps :
1809
1889
raise ValueError ("timesteps is not in ascending order" )
1810
1890
if isinstance (timesteps , list ):
1811
- if precip_models .shape [1 ] != len (timesteps ) + 1 :
1891
+ if precip_models .shape [1 ] != math . ceil (timesteps [ - 1 ] ) + 1 :
1812
1892
raise ValueError (
1813
1893
"precip_models does not contain sufficient lead times for this forecast"
1814
1894
)
0 commit comments