4
4
============================
5
5
6
6
Implementation of the RainFARM stochastic downscaling method as described in
7
- :cite:`Rebora2006`.
7
+ :cite:`Rebora2006` and :cite:`DOnofrio2014` .
8
8
9
9
RainFARM is a downscaling algorithm for rainfall fields developed by Rebora et
10
10
al. (2006). The method can represent the realistic small-scale variability of the
20
20
import warnings
21
21
22
22
import 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 )
24
52
25
53
26
54
def _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
+ """
27
59
lk_min = log_k .min ()
28
60
lk_max = log_k .max ()
29
61
lk_range = lk_max - lk_min
30
62
lk_min += (1 / 6 ) * lk_range
31
63
lk_max -= (1 / 6 ) * lk_range
32
-
33
64
selected = (lk_min <= log_k ) & (log_k <= lk_max )
34
65
lk_sel = log_k [selected ]
35
66
ps_sel = log_power_spectrum [selected ]
36
67
alpha = np .polyfit (lk_sel , ps_sel , 1 )[0 ]
37
68
alpha = - alpha
69
+ return alpha
38
70
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 ])
39
81
return alpha
40
82
41
83
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
45
98
46
99
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
+ ):
48
103
"""
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.
51
224
52
225
Parameters
53
226
----------
54
- precip: array-like
227
+ precip: array_like
55
228
Array of shape (m, n) containing the input field.
56
229
The input is expected to contain rain rate values.
57
230
All values are required to be finite.
@@ -65,10 +238,14 @@ def downscale(precip, ds_factor, alpha=None, threshold=None, return_alpha=False)
65
238
Set all values lower than the threshold to zero.
66
239
return_alpha: bool, optional
67
240
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`.
68
245
69
246
Returns
70
247
-------
71
- r: array-like
248
+ precip_highres: ndarray
72
249
Array of shape (m * ds_factor, n * ds_factor) containing
73
250
the downscaled field.
74
251
alpha: float
@@ -79,54 +256,74 @@ def downscale(precip, ds_factor, alpha=None, threshold=None, return_alpha=False)
79
256
Currently, the pysteps implementation of RainFARM only covers spatial downscaling.
80
257
That is, it can improve the spatial resolution of a rainfall field. However, unlike
81
258
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).
83
260
84
261
References
85
262
----------
86
263
:cite:`Rebora2006`
264
+ :cite:`DOnofrio2014`
87
265
88
266
"""
89
267
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
94
276
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 )
99
280
281
+ # Estimate spectral slope alpha if not provided
100
282
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 )
106
284
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
125
320
321
+ # Apply thresholding if specified
126
322
if threshold is not None :
127
- r [ r < threshold ] = 0
323
+ precip_highres [ precip_highres < threshold ] = 0
128
324
325
+ # Return the downscaled field and optionally the spectral slope alpha
129
326
if return_alpha :
130
- return r , alpha
327
+ return precip_highres , alpha
131
328
132
- return r
329
+ return precip_highres
0 commit comments