44============================
55
66Implementation of the RainFARM stochastic downscaling method as described in
7- :cite:`Rebora2006`.
7+ :cite:`Rebora2006` and :cite:`DOnofrio2014` .
88
99RainFARM is a downscaling algorithm for rainfall fields developed by Rebora et
1010al. (2006). The method can represent the realistic small-scale variability of the
2020import warnings
2121
2222import numpy as np
23- from scipy .ndimage import convolve
23+ from scipy .signal import convolve
24+ from pysteps .utils .spectral import rapsd
25+ from pysteps .utils .dimension import aggregate_fields
26+
27+
28+ def _gaussianize (precip ):
29+ """
30+ Gaussianize field using rank ordering as in :cite:`DOnofrio2014`.
31+ """
32+ m , n = np .shape (precip )
33+ nn = m * n
34+ ii = np .argsort (precip .reshape (nn ))
35+ precip_gaussianize = np .zeros (nn )
36+ precip_gaussianize [ii ] = sorted (np .random .normal (0 , 1 , nn ))
37+ precip_gaussianize = precip_gaussianize .reshape (m , n )
38+ sd = np .std (precip_gaussianize )
39+ if sd == 0 :
40+ sd = 1
41+ return precip_gaussianize / sd
42+
43+
44+ def _compute_freq_array (array , ds_factor = 1 ):
45+ """
46+ Compute the frequency array following a given downscaling factor.
47+ """
48+ freq_i = np .fft .fftfreq (array .shape [0 ] * ds_factor , d = 1 / ds_factor )
49+ freq_j = np .fft .fftfreq (array .shape [1 ] * ds_factor , d = 1 / ds_factor )
50+ freq_sqr = freq_i [:, None ] ** 2 + freq_j [None , :] ** 2
51+ return np .sqrt (freq_sqr )
2452
2553
2654def _log_slope (log_k , log_power_spectrum ):
55+ """
56+ Calculate the log-slope of the power spectrum given an array of logarithmic wavenumbers
57+ and an array of logarithmic power spectrum values.
58+ """
2759 lk_min = log_k .min ()
2860 lk_max = log_k .max ()
2961 lk_range = lk_max - lk_min
3062 lk_min += (1 / 6 ) * lk_range
3163 lk_max -= (1 / 6 ) * lk_range
32-
3364 selected = (lk_min <= log_k ) & (log_k <= lk_max )
3465 lk_sel = log_k [selected ]
3566 ps_sel = log_power_spectrum [selected ]
3667 alpha = np .polyfit (lk_sel , ps_sel , 1 )[0 ]
3768 alpha = - alpha
69+ return alpha
3870
71+
72+ def _estimate_alpha (array , k ):
73+ """
74+ Estimate the alpha parameter using the power spectrum of the input array.
75+ """
76+ fp = np .fft .fft2 (array )
77+ fp_abs = abs (fp )
78+ log_power_spectrum = np .log (fp_abs ** 2 )
79+ valid = (k != 0 ) & np .isfinite (log_power_spectrum )
80+ alpha = _log_slope (np .log (k [valid ]), log_power_spectrum [valid ])
3981 return alpha
4082
4183
42- def _balanced_spatial_average (x , k ):
43- ones = np .ones_like (x )
44- return convolve (x , k ) / convolve (ones , k )
84+ def _compute_noise_field (freq_array_highres , alpha ):
85+ """
86+ Compute a field of correlated noise field using the given frequency array and alpha
87+ value.
88+ """
89+ white_noise_field = np .random .rand (* freq_array_highres .shape )
90+ white_noise_field_complex = np .exp (complex (0 , 1 ) * 2 * np .pi * white_noise_field )
91+ with warnings .catch_warnings ():
92+ warnings .simplefilter ("ignore" )
93+ noise_field_complex = white_noise_field_complex * np .sqrt (
94+ freq_array_highres ** - alpha
95+ )
96+ noise_field_complex [0 , 0 ] = 0
97+ return np .fft .ifft2 (noise_field_complex ).real
4598
4699
47- def downscale (precip , ds_factor , alpha = None , threshold = None , return_alpha = False ):
100+ def _apply_spectral_fusion (
101+ array_low , array_high , freq_array_low , freq_array_high , ds_factor
102+ ):
48103 """
49- Downscale a rainfall field by increasing its spatial resolution by
50- a positive integer factor.
104+ Apply spectral fusion to merge two arrays in the frequency domain.
105+ """
106+
107+ # Validate inputs
108+ if array_low .shape != freq_array_low .shape :
109+ raise ValueError ("Shape of array_low must match shape of freq_array_low." )
110+ if array_high .shape != freq_array_high .shape :
111+ raise ValueError ("Shape of array_high must match shape of freq_array_high." )
112+
113+ nax , _ = np .shape (array_low )
114+ nx , _ = np .shape (array_high )
115+ k0 = nax // 2
116+
117+ # Calculate power spectral density at specific frequency
118+ def compute_psd (array , fft_size ):
119+ return rapsd (array , fft_method = np .fft )[k0 - 1 ] * fft_size ** 2
120+
121+ psd_low = compute_psd (array_low , nax )
122+ psd_high = compute_psd (array_high , nx )
123+
124+ # Normalize high-resolution array
125+ normalization_factor = np .sqrt (psd_low / psd_high )
126+ array_high *= normalization_factor
127+
128+ # Perform FFT on both arrays
129+ fft_low = np .fft .fft2 (array_low )
130+ fft_high = np .fft .fft2 (array_high )
131+
132+ # Initialize the merged FFT array with low-resolution data
133+ fft_merged = np .zeros_like (fft_high , dtype = np .complex128 )
134+ fft_merged [0 :k0 , 0 :k0 ] = fft_low [0 :k0 , 0 :k0 ]
135+ fft_merged [nx - k0 : nx , 0 :k0 ] = fft_low [k0 : 2 * k0 , 0 :k0 ]
136+ fft_merged [0 :k0 , nx - k0 : nx ] = fft_low [0 :k0 , k0 : 2 * k0 ]
137+ fft_merged [nx - k0 : nx , nx - k0 : nx ] = fft_low [k0 : 2 * k0 , k0 : 2 * k0 ]
138+
139+ fft_merged [k0 , 0 ] = np .conj (fft_merged [nx - k0 , 0 ])
140+ fft_merged [0 , k0 ] = np .conj (fft_merged [0 , nx - k0 ])
141+
142+ # Compute frequency arrays
143+ freq_i = np .fft .fftfreq (nx , d = 1 / ds_factor )
144+ freq_i = np .tile (freq_i , (nx , 1 ))
145+ freq_j = freq_i .T
146+
147+ # Compute frequency domain adjustment
148+ ddx = np .pi * (1 / nax - 1 / nx ) / np .abs (freq_i [0 , 1 ] - freq_i [0 , 0 ])
149+ freq_squared_high = freq_array_high ** 2
150+ freq_squared_low_center = freq_array_low [k0 , k0 ] ** 2
151+
152+ # Fuse in the frequency domain
153+ mask_high = freq_squared_high > freq_squared_low_center
154+ mask_low = ~ mask_high
155+ fft_merged = fft_high * mask_high + fft_merged * mask_low * np .exp (
156+ - 1j * ddx * freq_i - 1j * ddx * freq_j
157+ )
158+
159+ # Inverse FFT to obtain the merged array in the spatial domain
160+ merged = np .real (np .fft .ifftn (fft_merged )) / fft_merged .size
161+
162+ return merged
163+
164+
165+ def _compute_kernel_radius (ds_factor ):
166+ return int (round (ds_factor / np .sqrt (np .pi )))
167+
168+
169+ def _make_tophat_kernel (ds_factor ):
170+ """Compute 2d uniform (tophat) kernel"""
171+ radius = _compute_kernel_radius (ds_factor )
172+ (mx , my ) = np .mgrid [- radius : radius + 0.01 , - radius : radius + 0.01 ]
173+ tophat = ((mx ** 2 + my ** 2 ) <= radius ** 2 ).astype (float )
174+ return tophat / tophat .sum ()
175+
176+
177+ def _make_gaussian_kernel (ds_factor ):
178+ """
179+ Compute 2d gaussian kernel
180+ ref: https://github.com/scipy/scipy/blob/de80faf9d3480b9dbb9b888568b64499e0e70c19/scipy/ndimage/_filters.py#L179
181+ the smoothing sigma has width half a large pixel
182+ """
183+ radius = _compute_kernel_radius (ds_factor )
184+ sigma = ds_factor / 2
185+ sigma2 = sigma * sigma
186+ x = np .arange (- radius , radius + 1 )
187+ kern1d = np .exp (- 0.5 / sigma2 * x ** 2 )
188+ kern2d = np .outer (kern1d , kern1d )
189+ return kern2d / kern2d .sum ()
190+
191+
192+ def _balanced_spatial_average (array , kernel ):
193+ """
194+ Compute the balanced spatial average of an array using a given kernel while handling
195+ missing or invalid values.
196+ """
197+ array = array .copy ()
198+ mask_valid = np .isfinite (array )
199+ array [~ mask_valid ] = 0.0
200+ array_conv = convolve (array , kernel , mode = "same" )
201+ array_conv /= convolve (mask_valid , kernel , mode = "same" )
202+ array_conv [~ mask_valid ] = np .nan
203+ return array_conv
204+
205+
206+ _make_kernel = dict ()
207+ _make_kernel ["gaussian" ] = _make_gaussian_kernel
208+ _make_kernel ["tophat" ] = _make_tophat_kernel
209+ _make_kernel ["uniform" ] = _make_tophat_kernel
210+
211+
212+ def downscale (
213+ precip ,
214+ ds_factor ,
215+ alpha = None ,
216+ threshold = None ,
217+ return_alpha = False ,
218+ kernel_type = None ,
219+ spectral_fusion = False ,
220+ ):
221+ """
222+ Downscale a rainfall field by increasing its spatial resolution by a positive
223+ integer factor.
51224
52225 Parameters
53226 ----------
54- precip: array-like
227+ precip: array_like
55228 Array of shape (m, n) containing the input field.
56229 The input is expected to contain rain rate values.
57230 All values are required to be finite.
@@ -65,10 +238,14 @@ def downscale(precip, ds_factor, alpha=None, threshold=None, return_alpha=False)
65238 Set all values lower than the threshold to zero.
66239 return_alpha: bool, optional
67240 Whether to return the estimated spectral slope ``alpha``.
241+ kernel_type: {None, "gaussian", "uniform", "tophat"}
242+ The name of the smoothing operator. If None no smoothing is applied.
243+ spectral_fusion: bool, optional
244+ Whether to apply spectral merging as in :cite:`DOnofrio2014`.
68245
69246 Returns
70247 -------
71- r: array-like
248+ precip_highres: ndarray
72249 Array of shape (m * ds_factor, n * ds_factor) containing
73250 the downscaled field.
74251 alpha: float
@@ -79,54 +256,74 @@ def downscale(precip, ds_factor, alpha=None, threshold=None, return_alpha=False)
79256 Currently, the pysteps implementation of RainFARM only covers spatial downscaling.
80257 That is, it can improve the spatial resolution of a rainfall field. However, unlike
81258 the original algorithm from Rebora et al. (2006), it cannot downscale the temporal
82- dimension.
259+ dimension. It implements spectral merging from D'Onofrio et al. (2014).
83260
84261 References
85262 ----------
86263 :cite:`Rebora2006`
264+ :cite:`DOnofrio2014`
87265
88266 """
89267
90- ki = np .fft .fftfreq (precip .shape [0 ])
91- kj = np .fft .fftfreq (precip .shape [1 ])
92- k_sqr = ki [:, None ] ** 2 + kj [None , :] ** 2
93- k = np .sqrt (k_sqr )
268+ # Validate inputs
269+ if not np .isfinite (precip ).all ():
270+ raise ValueError ("All values in 'precip' must be finite." )
271+ if not isinstance (ds_factor , int ) or ds_factor <= 0 :
272+ raise ValueError ("'ds_factor' must be a positive integer." )
273+
274+ # Preprocess the input field if spectral fusion is enabled
275+ precip_transformed = _gaussianize (precip ) if spectral_fusion else precip
94276
95- ki_ds = np .fft .fftfreq (precip .shape [0 ] * ds_factor , d = 1 / ds_factor )
96- kj_ds = np .fft .fftfreq (precip .shape [1 ] * ds_factor , d = 1 / ds_factor )
97- k_ds_sqr = ki_ds [:, None ] ** 2 + kj_ds [None , :] ** 2
98- k_ds = np .sqrt (k_ds_sqr )
277+ # Compute frequency arrays for the original and high-resolution fields
278+ freq_array = _compute_freq_array (precip_transformed )
279+ freq_array_highres = _compute_freq_array (precip_transformed , ds_factor )
99280
281+ # Estimate spectral slope alpha if not provided
100282 if alpha is None :
101- fp = np .fft .fft2 (precip )
102- fp_abs = abs (fp )
103- log_power_spectrum = np .log (fp_abs ** 2 )
104- valid = (k != 0 ) & np .isfinite (log_power_spectrum )
105- alpha = _log_slope (np .log (k [valid ]), log_power_spectrum [valid ])
283+ alpha = _estimate_alpha (precip_transformed , freq_array )
106284
107- fg = np .exp (complex (0 , 1 ) * 2 * np .pi * np .random .rand (* k_ds .shape ))
108- with warnings .catch_warnings ():
109- warnings .simplefilter ("ignore" )
110- fg *= np .sqrt (k_ds_sqr ** (- alpha / 2 ))
111- fg [0 , 0 ] = 0
112- g = np .fft .ifft2 (fg ).real
113- g /= g .std ()
114- r = np .exp (g )
115-
116- P_u = np .repeat (np .repeat (precip , ds_factor , axis = 0 ), ds_factor , axis = 1 )
117- rad = int (round (ds_factor / np .sqrt (np .pi )))
118- (mx , my ) = np .mgrid [- rad : rad + 0.01 , - rad : rad + 0.01 ]
119- tophat = ((mx ** 2 + my ** 2 ) <= rad ** 2 ).astype (float )
120- tophat /= tophat .sum ()
121-
122- P_agg = _balanced_spatial_average (P_u , tophat )
123- r_agg = _balanced_spatial_average (r , tophat )
124- r *= P_agg / r_agg
285+ # Generate noise field
286+ noise_field = _compute_noise_field (freq_array_highres , alpha )
287+
288+ # Apply spectral fusion if enabled
289+ if spectral_fusion :
290+ noise_field /= noise_field .shape [0 ] ** 2
291+ noise_field = np .exp (noise_field )
292+ noise_field = _apply_spectral_fusion (
293+ precip_transformed , noise_field , freq_array , freq_array_highres , ds_factor
294+ )
295+
296+ # Normalize and exponentiate the noise field
297+ noise_field /= noise_field .std ()
298+ noise_field = np .exp (noise_field )
299+
300+ # Aggregate the noise field to low resolution
301+ noise_lowres = aggregate_fields (noise_field , ds_factor , axis = (0 , 1 ))
302+
303+ # Expand input and noise fields to high resolution
304+ precip_expanded = np .kron (precip , np .ones ((ds_factor , ds_factor )))
305+ noise_lowres_expanded = np .kron (noise_lowres , np .ones ((ds_factor , ds_factor )))
306+
307+ # Apply smoothing if a kernel type is provided
308+ if kernel_type :
309+ if kernel_type not in _make_kernel :
310+ raise ValueError (
311+ f"kernel type '{ kernel_type } ' is invalid, available kernels: { list (_make_kernel )} "
312+ )
313+ kernel = _make_kernel [kernel_type ](ds_factor )
314+ precip_expanded = _balanced_spatial_average (precip_expanded , kernel )
315+ noise_lowres_expanded = _balanced_spatial_average (noise_lowres_expanded , kernel )
316+
317+ # Normalize the high-res precipitation field by the low-res noise field
318+ norm_k0 = precip_expanded / noise_lowres_expanded
319+ precip_highres = noise_field * norm_k0
125320
321+ # Apply thresholding if specified
126322 if threshold is not None :
127- r [ r < threshold ] = 0
323+ precip_highres [ precip_highres < threshold ] = 0
128324
325+ # Return the downscaled field and optionally the spectral slope alpha
129326 if return_alpha :
130- return r , alpha
327+ return precip_highres , alpha
131328
132- return r
329+ return precip_highres
0 commit comments