4343 blend_means_sigmas
4444"""
4545
46+ import math
4647import time
4748
4849import numpy as np
50+ from scipy .linalg import inv
4951from scipy .ndimage import binary_dilation , generate_binary_structure , iterate_structure
5052
5153from pysteps import cascade
@@ -74,7 +76,7 @@ def forecast(
7476 timestep ,
7577 issuetime ,
7678 n_ens_members ,
77- n_cascade_levels = 8 ,
79+ n_cascade_levels = 6 ,
7880 blend_nwp_members = False ,
7981 precip_thr = None ,
8082 norain_thr = 0.0 ,
@@ -90,6 +92,8 @@ def forecast(
9092 conditional = False ,
9193 probmatching_method = "cdf" ,
9294 mask_method = "incremental" ,
95+ resample_distribution = True ,
96+ smooth_radar_mask_range = 0 ,
9397 callback = None ,
9498 return_output = True ,
9599 seed = None ,
@@ -153,8 +157,8 @@ def forecast(
153157 equal to or larger than the number of NWP ensemble members / number of
154158 NWP models.
155159 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 .
158162 blend_nwp_members: bool
159163 Check if NWP models/members should be used individually, or if all of
160164 them are blended together per nowcast ensemble member. Standard set to
@@ -204,18 +208,32 @@ def forecast(
204208 If set to True, compute the statistics of the precipitation field
205209 conditionally by excluding pixels where the values are below the threshold
206210 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.
213211 probmatching_method: {'cdf','mean',None}, optional
214212 Method for matching the statistics of the forecast field with those of
215213 the most recently observed one. 'cdf'=map the forecast CDF to the observed
216214 one, 'mean'=adjust only the conditional mean value of the forecast field
217215 in precipitation areas, None=no matching applied. Using 'mean' requires
218216 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.
219237 callback: function, optional
220238 Optional function that is called after computation of each time step of
221239 the nowcast. The function takes one argument: a three-dimensional array
@@ -1396,7 +1414,6 @@ def worker(j):
13961414 # latest extrapolated radar rainfall field blended with the
13971415 # nwp model(s) rainfall forecast fields as 'benchmark'.
13981416
1399- # TODO: Check probability matching method
14001417 # 8.7.1 first blend the extrapolated rainfall field (the field
14011418 # that is only used for post-processing steps) with the NWP
14021419 # rainfall forecast for this time step using the weights
@@ -1451,10 +1468,49 @@ def worker(j):
14511468 # forecast outside the radar domain. Therefore, fill these
14521469 # areas with the "..._mod_only" blended forecasts, consisting
14531470 # of the NWP and noise components.
1471+
14541472 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+
14581514 # Finally, fill the remaining nan values, if present, with
14591515 # the minimum value in the forecast
14601516 nan_indices = np .isnan (R_f_new )
@@ -1491,19 +1547,39 @@ def worker(j):
14911547 # Set to min value outside of mask
14921548 R_f_new [~ MASK_prec_ ] = R_cmin
14931549
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+
14941568 if probmatching_method == "cdf" :
14951569 # Adjust the CDF of the forecast to match the most recent
14961570 # benchmark rainfall field (R_pm_blended). If the forecast
14971571 if np .any (np .isfinite (R_f_new )):
14981572 R_f_new = probmatching .nonparam_match_empirical_cdf (
1499- R_f_new , R_pm_blended
1573+ R_f_new , R_pm_resampled
15001574 )
1575+ R_pm_resampled = None
15011576 elif probmatching_method == "mean" :
15021577 # 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 ])
15041579 MASK = R_f_new >= precip_thr
15051580 mu_fct = np .mean (R_f_new [MASK ])
15061581 R_f_new [MASK ] = R_f_new [MASK ] - mu_fct + mu_0
1582+ R_pm_resampled = None
15071583
15081584 R_f_out .append (R_f_new )
15091585
@@ -1666,7 +1742,7 @@ def calculate_weights_spn(correlations, cov):
16661742 if isinstance (cov , type (None )):
16671743 raise ValueError ("cov must contain a covariance matrix" )
16681744 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
16701746 cov = np .where (cov == 0.0 , 10e-5 , cov )
16711747 # Make sure the determinant of the matrix is not zero, otherwise
16721748 # subtract 10e-5 from the cross-correlations between the models
@@ -1675,26 +1751,30 @@ def calculate_weights_spn(correlations, cov):
16751751 # Ensure the correlation of the model with itself is always 1.0
16761752 for i , _ in enumerate (cov ):
16771753 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 )
16851760 weights = np .nan_to_num (
16861761 weights , copy = True , nan = 10e-5 , posinf = 10e-5 , neginf = 10e-5
16871762 )
1763+ weights_dot_correlations = np .dot (weights , correlations )
16881764 # If the dot product of the weights with the correlations is
16891765 # larger than 1.0, we assign a weight of 0.0 to the noise (to make
16901766 # it numerically stable)
1691- if weights . dot ( correlations ) > 1.0 :
1767+ if weights_dot_correlations > 1.0 :
16921768 noise_weight = np .array ([0 ])
16931769 # Calculate the noise weight
16941770 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 ()
16961776 # 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 )
16981778
16991779 # Otherwise, the weight equals the correlation on that scale level and
17001780 # the noise component weight equals 1 - this weight. This only occurs for
@@ -1808,7 +1888,7 @@ def _check_inputs(
18081888 if isinstance (timesteps , list ) and not sorted (timesteps ) == timesteps :
18091889 raise ValueError ("timesteps is not in ascending order" )
18101890 if isinstance (timesteps , list ):
1811- if precip_models .shape [1 ] != len (timesteps ) + 1 :
1891+ if precip_models .shape [1 ] != math . ceil (timesteps [ - 1 ] ) + 1 :
18121892 raise ValueError (
18131893 "precip_models does not contain sufficient lead times for this forecast"
18141894 )
0 commit comments