Skip to content

Commit be238b6

Browse files
authored
Rapsd fix (#211)
* Fix invalid range assignment for even/odd dimensions * Update docstring * Replace single-letter uppercase variables with more descriptive ones and use lowercase * Add tests for rapsd * Remove unnecessary coding: utf-8 line * Update error message
1 parent 88fa819 commit be238b6

File tree

2 files changed

+52
-26
lines changed

2 files changed

+52
-26
lines changed

pysteps/tests/test_utils_spectral.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import numpy as np
2+
import pytest
3+
from pysteps.utils import spectral
4+
5+
_rapsd_input_fields = [
6+
np.random.uniform(size=(255, 255)),
7+
np.random.uniform(size=(256, 256)),
8+
np.random.uniform(size=(255, 256)),
9+
np.random.uniform(size=(256, 255)),
10+
]
11+
12+
13+
@pytest.mark.parametrize("field", _rapsd_input_fields)
14+
def test_rapsd(field):
15+
rapsd, freq = spectral.rapsd(field, return_freq=True)
16+
17+
m, n = field.shape
18+
l = max(m, n)
19+
20+
if l % 2 == 0:
21+
assert len(rapsd) == int(l / 2)
22+
else:
23+
assert len(rapsd) == int(l / 2 + 1)
24+
assert len(rapsd) == len(freq)
25+
assert np.all(freq >= 0.0)

pysteps/utils/spectral.py

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# -*- coding: utf-8 -*-
21
"""
32
pysteps.utils.spectral
43
======================
@@ -96,19 +95,21 @@ def mean(X, shape):
9695
return np.real(X[0, 0]) / (shape[0] * shape[1])
9796

9897

99-
def rapsd(Z, fft_method=None, return_freq=False, d=1.0, normalize=False, **fft_kwargs):
98+
def rapsd(
99+
field, fft_method=None, return_freq=False, d=1.0, normalize=False, **fft_kwargs
100+
):
100101
"""Compute radially averaged power spectral density (RAPSD) from the given
101102
2D input field.
102103
103104
Parameters
104105
----------
105-
Z: array_like
106-
A 2d array of shape (M,N) containing the input field.
106+
field: array_like
107+
A 2d array of shape (m, n) containing the input field.
107108
fft_method: object
108109
A module or object implementing the same methods as numpy.fft and
109-
scipy.fftpack. If set to None, Z is assumed to represent the shifted
110-
discrete Fourier transform of the input field, where the origin is at
111-
the center of the array
110+
scipy.fftpack. If set to None, field is assumed to represent the
111+
shifted discrete Fourier transform of the input field, where the
112+
origin is at the center of the array
112113
(see numpy.fft.fftshift or scipy.fftpack.fftshift).
113114
return_freq: bool
114115
Whether to also return the Fourier frequencies.
@@ -122,7 +123,7 @@ def rapsd(Z, fft_method=None, return_freq=False, d=1.0, normalize=False, **fft_k
122123
-------
123124
out: ndarray
124125
One-dimensional array containing the RAPSD. The length of the array is
125-
int(L/2)+1 (if L is even) or int(L/2) (if L is odd), where L=max(M,N).
126+
int(l/2) (if l is even) or int(l/2)+1 (if l is odd), where l=max(m,n).
126127
freq: ndarray
127128
One-dimensional array containing the Fourier frequencies.
128129
@@ -131,45 +132,45 @@ def rapsd(Z, fft_method=None, return_freq=False, d=1.0, normalize=False, **fft_k
131132
:cite:`RC2011`
132133
"""
133134

134-
if len(Z.shape) != 2:
135+
if len(field.shape) != 2:
135136
raise ValueError(
136-
f"{len(Z.shape)} dimensions are found, but the number "
137+
f"{len(field.shape)} dimensions are found, but the number "
137138
"of dimensions should be 2"
138139
)
139140

140-
if np.sum(np.isnan(Z)) > 0:
141-
raise ValueError("input array Z should not contain nans")
141+
if np.sum(np.isnan(field)) > 0:
142+
raise ValueError("input field should not contain nans")
142143

143-
M, N = Z.shape
144+
m, n = field.shape
144145

145-
YC, XC = arrays.compute_centred_coord_array(M, N)
146-
R = np.sqrt(XC * XC + YC * YC).round()
147-
L = max(Z.shape[0], Z.shape[1])
146+
yc, xc = arrays.compute_centred_coord_array(m, n)
147+
r_grid = np.sqrt(xc * xc + yc * yc).round()
148+
l = max(field.shape[0], field.shape[1])
148149

149-
if L % 2 == 0:
150-
r_range = np.arange(0, int(L / 2) + 1)
150+
if l % 2 == 1:
151+
r_range = np.arange(0, int(l / 2) + 1)
151152
else:
152-
r_range = np.arange(0, int(L / 2))
153+
r_range = np.arange(0, int(l / 2))
153154

154155
if fft_method is not None:
155-
F = fft_method.fftshift(fft_method.fft2(Z, **fft_kwargs))
156-
F = np.abs(F) ** 2 / F.size
156+
psd = fft_method.fftshift(fft_method.fft2(field, **fft_kwargs))
157+
psd = np.abs(psd) ** 2 / psd.size
157158
else:
158-
F = Z
159+
psd = field
159160

160161
result = []
161162
for r in r_range:
162-
MASK = R == r
163-
F_vals = F[MASK]
164-
result.append(np.mean(F_vals))
163+
mask = r_grid == r
164+
psd_vals = psd[mask]
165+
result.append(np.mean(psd_vals))
165166

166167
result = np.array(result)
167168

168169
if normalize:
169170
result /= np.sum(result)
170171

171172
if return_freq:
172-
freq = np.fft.fftfreq(L, d=d)
173+
freq = np.fft.fftfreq(l, d=d)
173174
freq = freq[r_range]
174175
return result, freq
175176
else:

0 commit comments

Comments
 (0)