2
2
import pandas as pd
3
3
import xarray as xr
4
4
import numpy .testing as npt
5
+ import scipy .signal as sps
6
+ import scipy .linalg as spl
5
7
import pytest
6
8
import xrft
7
9
@@ -15,6 +17,96 @@ def sample_data_1d():
15
17
"""Create one dimensional test DataArray."""
16
18
pass
17
19
20
+ def numpy_detrend (da ):
21
+ """
22
+ Detrend a 2D field by subtracting out the least-square plane fit.
23
+
24
+ Parameters
25
+ ----------
26
+ da : `numpy.array`
27
+ The data to be detrended
28
+
29
+ Returns
30
+ -------
31
+ da : `numpy.array`
32
+ The detrended input data
33
+ """
34
+ N = da .shape
35
+
36
+ G = np .ones ((N [0 ]* N [1 ],3 ))
37
+ for i in range (N [0 ]):
38
+ G [N [1 ]* i :N [1 ]* i + N [1 ], 1 ] = i + 1
39
+ G [N [1 ]* i :N [1 ]* i + N [1 ], 2 ] = np .arange (1 , N [1 ]+ 1 )
40
+
41
+ d_obs = np .reshape (da .copy (), (N [0 ]* N [1 ],1 ))
42
+ m_est = np .dot (np .dot (spl .inv (np .dot (G .T , G )), G .T ), d_obs )
43
+ d_est = np .dot (G , m_est )
44
+
45
+ lin_trend = np .reshape (d_est , N )
46
+
47
+ return da - lin_trend
48
+
49
+ def test_detrend_1d ():
50
+ N = 16
51
+ x = np .arange (N + 1 )
52
+ y = np .arange (N - 1 )
53
+ t = np .linspace (- int (N / 2 ), int (N / 2 ), N - 6 )
54
+ z = np .arange (int (N / 2 ))
55
+ d4d = (t [:,np .newaxis ,np .newaxis ,np .newaxis ]
56
+ + z [np .newaxis ,:,np .newaxis ,np .newaxis ]
57
+ + y [np .newaxis ,np .newaxis ,:,np .newaxis ]
58
+ + x [np .newaxis ,np .newaxis ,np .newaxis ,:]
59
+ )
60
+ da4d = xr .DataArray (d4d , dims = ['time' ,'z' ,'y' ,'x' ],
61
+ coords = {'time' :range (len (t )),'z' :range (len (z )),'y' :range (len (y )),
62
+ 'x' :range (len (x ))}
63
+ )
64
+ func = xrft ._detrend_wrap (xrft .detrend2 )
65
+ da = da4d .chunk ({'time' : 1 })
66
+ da_prime = func (da .data , axes = [2 ]).compute ()
67
+ npt .assert_allclose (da_prime [0 ,0 ], sps .detrend (d4d [0 ,0 ], axis = 0 ))
68
+
69
+ def test_detrend_2d ():
70
+ N = 16
71
+ x = np .arange (N + 1 )
72
+ y = np .arange (N - 1 )
73
+ t = np .linspace (- int (N / 2 ), int (N / 2 ), N - 6 )
74
+ z = np .arange (int (N / 2 ))
75
+ d4d = (t [:,np .newaxis ,np .newaxis ,np .newaxis ]
76
+ + z [np .newaxis ,:,np .newaxis ,np .newaxis ]
77
+ + y [np .newaxis ,np .newaxis ,:,np .newaxis ]
78
+ + x [np .newaxis ,np .newaxis ,np .newaxis ,:]
79
+ )
80
+ da4d = xr .DataArray (d4d , dims = ['time' ,'z' ,'y' ,'x' ],
81
+ coords = {'time' :range (len (t )),'z' :range (len (z )),
82
+ 'y' :range (len (y )),'x' :range (len (x ))}
83
+ )
84
+ func = xrft ._detrend_wrap (xrft .detrend2 )
85
+ da = da4d .chunk ({'time' :1 })
86
+ with pytest .raises (ValueError ):
87
+ func (da .data , axes = [0 ]).compute ()
88
+ da = da4d .chunk ({'time' :1 , 'z' :1 })
89
+ with pytest .raises (ValueError ):
90
+ func (da .data , axes = [1 ,2 ]).compute ()
91
+ with pytest .raises (ValueError ):
92
+ func (da .data , axes = [2 ,2 ]).compute ()
93
+ da_prime = func (da .data , axes = [2 ,3 ]).compute ()
94
+ npt .assert_allclose (da_prime [0 ,0 ], numpy_detrend (d4d [0 ,0 ]))
95
+
96
+ s = np .arange (2 )
97
+ d5d = d4d [np .newaxis ,:,:,:,:] + s [:,np .newaxis ,np .newaxis ,
98
+ np .newaxis ,np .newaxis ]
99
+ da5d = xr .DataArray (d5d , dims = ['s' ,'time' ,'z' ,'y' ,'x' ],
100
+ coords = {'s' :range (len (s )),'time' :range (len (t )),
101
+ 'z' :range (len (z )),'y' :range (len (y )),
102
+ 'x' :range (len (x ))}
103
+ )
104
+ da = da5d .chunk ({'time' :1 })
105
+ with pytest .raises (ValueError ):
106
+ func (da .data ).compute ()
107
+ with pytest .raises (ValueError ):
108
+ func (da .data , axes = [2 ,3 ,4 ]).compute ()
109
+
18
110
def test_dft_1d ():
19
111
"""Test the discrete Fourier transform function on one-dimensional data."""
20
112
Nx = 16
@@ -24,7 +116,7 @@ def test_dft_1d():
24
116
da = xr .DataArray (np .random .rand (Nx ), coords = [x ], dims = ['x' ])
25
117
26
118
# defaults with no keyword args
27
- ft = xrft .dft (da )
119
+ ft = xrft .dft (da , detrend = 'constant' )
28
120
# check that the frequency dimension was created properly
29
121
assert ft .dims == ('freq_x' ,)
30
122
# check that the coords are correct
@@ -40,10 +132,16 @@ def test_dft_1d():
40
132
npt .assert_allclose (ft_data_expected , ft .values , atol = 1e-14 )
41
133
42
134
# redo without removing mean
43
- ft = xrft .dft (da , remove_mean = False )
135
+ ft = xrft .dft (da )
44
136
ft_data_expected = np .fft .fftshift (np .fft .fft (da ))
45
137
npt .assert_allclose (ft_data_expected , ft .values )
46
138
139
+ # redo with detrending linear least-square fit
140
+ ft = xrft .dft (da , detrend = 'linear' )
141
+ da_prime = sps .detrend (da .values )
142
+ ft_data_expected = np .fft .fftshift (np .fft .fft (da_prime ))
143
+ npt .assert_allclose (ft_data_expected , ft .values , atol = 1e-14 )
144
+
47
145
# modify data to be non-evenly spaced
48
146
da2 = da .copy ()
49
147
da2 [- 1 ] = np .nan
@@ -73,61 +171,88 @@ def test_dft_2d():
73
171
da = xr .DataArray (np .random .rand (N ,N ), dims = ['x' ,'y' ],
74
172
coords = {'x' :range (N ),'y' :range (N )}
75
173
)
76
- ft = xrft .dft (da , shift = False , remove_mean = False )
174
+ ft = xrft .dft (da , shift = False )
77
175
npt .assert_almost_equal (ft .values , np .fft .fftn (da .values ))
78
176
79
- ft = xrft .dft (da , shift = False , window = True )
177
+ ft = xrft .dft (da , shift = False , window = True , detrend = 'constant' )
80
178
dim = da .dims
81
179
window = np .hanning (N ) * np .hanning (N )[:, np .newaxis ]
82
180
da_prime = (da - da .mean (dim = dim )).values
83
181
npt .assert_almost_equal (ft .values , np .fft .fftn (da_prime * window ))
84
182
183
+ with pytest .raises (ValueError ):
184
+ xrft .dft (da , shift = False , window = True , detrend = 'linear' )
185
+
85
186
def test_dft_3d_dask ():
86
187
"""Test the discrete Fourier transform on 3D dask array data"""
87
188
N = 16
88
- da = xr .DataArray (np .random .rand (2 ,N ,N ), dims = ['time' ,'x' ,'y' ],
89
- coords = {'time' :range (2 ),'x' :range (N ),
90
- 'y' :range (N )}). chunk ({ 'time' : 1 }
189
+ da = xr .DataArray (np .random .rand (N ,N ,N ), dims = ['time' ,'x' ,'y' ],
190
+ coords = {'time' :range (N ),'x' :range (N ),
191
+ 'y' :range (N )}
91
192
)
92
- daft = xrft .dft (da , dim = ['x' ,'y' ], shift = False , remove_mean = False )
93
- assert hasattr (daft .data , 'dask' )
94
- npt .assert_almost_equal (daft .values , np .fft .fftn (da .values , axes = [1 ,2 ]))
193
+ daft = xrft .dft (da .chunk ({'time' : 1 }), dim = ['x' ,'y' ], shift = False )
194
+ # assert hasattr(daft.data, 'dask')
195
+ npt .assert_almost_equal (daft .values ,
196
+ np .fft .fftn (da .chunk ({'time' : 1 }).values , axes = [1 ,2 ])
197
+ )
95
198
96
199
with pytest .raises (ValueError ):
97
200
xrft .dft (da .chunk ({'time' : 1 , 'x' : 1 }), dim = ['x' ])
98
201
202
+ daft = xrft .dft (da .chunk ({'x' : 1 }), dim = ['time' ],
203
+ shift = False , detrend = 'linear' )
204
+ # assert hasattr(daft.data, 'dask')
205
+ da_prime = sps .detrend (da .chunk ({'x' : 1 }), axis = 0 )
206
+ npt .assert_almost_equal (daft .values ,
207
+ np .fft .fftn (da_prime , axes = [0 ])
208
+ )
209
+
99
210
def test_power_spectrum ():
100
211
"""Test the power spectrum function"""
101
212
N = 16
102
213
da = xr .DataArray (np .random .rand (N ,N ), dims = ['x' ,'y' ],
103
214
coords = {'x' :range (N ),'y' :range (N )}
104
215
)
105
- ps = xrft .power_spectrum (da , window = True , density = False )
216
+ ps = xrft .power_spectrum (da , window = True , density = False ,
217
+ detrend = 'constant' )
106
218
daft = xrft .dft (da ,
107
- dim = None , shift = True , remove_mean = True ,
219
+ dim = None , shift = True , detrend = 'constant' ,
108
220
window = True )
109
221
npt .assert_almost_equal (ps .values , np .real (daft * np .conj (daft )))
110
222
npt .assert_almost_equal (np .ma .masked_invalid (ps ).mask .sum (), 0. )
111
223
112
224
### Normalized
113
225
dim = da .dims
114
- ps = xrft .power_spectrum (da , window = True )
115
- daft = xrft .dft (da , window = True )
226
+ ps = xrft .power_spectrum (da , window = True , detrend = 'constant' )
227
+ daft = xrft .dft (da , window = True , detrend = 'constant' )
116
228
coord = list (daft .coords )
117
229
test = np .real (daft * np .conj (daft ))/ N ** 4
118
230
for i in range (len (dim )):
119
231
test /= daft [coord [- i - 1 ]].values
120
232
npt .assert_almost_equal (ps .values , test )
121
233
npt .assert_almost_equal (np .ma .masked_invalid (ps ).mask .sum (), 0. )
122
234
235
+ ### Remove mean
123
236
da = xr .DataArray (np .random .rand (5 ,20 ,30 ),
124
237
dims = ['time' , 'y' , 'x' ],
125
238
coords = {'time' : np .arange (5 ),
126
239
'y' : np .arange (20 ), 'x' : np .arange (30 )})
127
240
ps = xrft .power_spectrum (da , dim = ['y' , 'x' ],
128
- window = True , density = False
241
+ window = True , density = False , detrend = 'constant'
242
+ )
243
+ daft = xrft .dft (da , dim = ['y' ,'x' ], window = True , detrend = 'constant' )
244
+ npt .assert_almost_equal (ps .values , np .real (daft * np .conj (daft )))
245
+ npt .assert_almost_equal (np .ma .masked_invalid (ps ).mask .sum (), 0. )
246
+
247
+ ### Remove least-square fit
248
+ da_prime = np .zeros_like (da .values )
249
+ for t in range (5 ):
250
+ da_prime [t ] = numpy_detrend (da [t ].values )
251
+ da_prime = xr .DataArray (da_prime , dims = da .dims , coords = da .coords )
252
+ ps = xrft .power_spectrum (da_prime , dim = ['y' , 'x' ],
253
+ window = True , density = False , detrend = 'constant'
129
254
)
130
- daft = xrft .dft (da , dim = ['y' ,'x' ], window = True )
255
+ daft = xrft .dft (da_prime , dim = ['y' ,'x' ], window = True , detrend = 'constant' )
131
256
npt .assert_almost_equal (ps .values , np .real (daft * np .conj (daft )))
132
257
npt .assert_almost_equal (np .ma .masked_invalid (ps ).mask .sum (), 0. )
133
258
@@ -143,8 +268,8 @@ def test_power_spectrum_dask():
143
268
daft = xrft .dft (da , dim = ['x' ,'y' ])
144
269
npt .assert_almost_equal (ps .values , (daft * np .conj (daft )).real .values )
145
270
146
- ps = xrft .power_spectrum (da , dim = dim , window = True )
147
- daft = xrft .dft (da , dim = dim , window = True )
271
+ ps = xrft .power_spectrum (da , dim = dim , window = True , detrend = 'constant' )
272
+ daft = xrft .dft (da , dim = dim , window = True , detrend = 'constant' )
148
273
coord = list (daft .coords )
149
274
test = (daft * np .conj (daft )).real / N ** 4
150
275
for i in dim :
@@ -161,12 +286,13 @@ def test_cross_spectrum():
161
286
da2 = xr .DataArray (np .random .rand (N ,N ), dims = ['x' ,'y' ],
162
287
coords = {'x' :range (N ),'y' :range (N )}
163
288
)
164
- cs = xrft .cross_spectrum (da , da2 , window = True , density = False )
289
+ cs = xrft .cross_spectrum (da , da2 , window = True , density = False ,
290
+ detrend = 'constant' )
165
291
daft = xrft .dft (da ,
166
- dim = None , shift = True , remove_mean = True ,
292
+ dim = None , shift = True , detrend = 'constant' ,
167
293
window = True )
168
294
daft2 = xrft .dft (da2 ,
169
- dim = None , shift = True , remove_mean = True ,
295
+ dim = None , shift = True , detrend = 'constant' ,
170
296
window = True )
171
297
npt .assert_almost_equal (cs .values , np .real (daft * np .conj (daft2 )))
172
298
npt .assert_almost_equal (np .ma .masked_invalid (cs ).mask .sum (), 0. )
@@ -188,13 +314,21 @@ def test_cross_spectrum_dask():
188
314
daft2 = xrft .dft (da2 , dim = dim )
189
315
npt .assert_almost_equal (cs .values , (daft * np .conj (daft2 )).real .values )
190
316
191
- cs = xrft .cross_spectrum (da , da2 , dim = dim , window = True )
192
- daft = xrft .dft (da , dim = dim , window = True )
193
- daft2 = xrft .dft (da2 , dim = dim , window = True )
317
+ cs = xrft .cross_spectrum (da , da2 ,
318
+ dim = dim , shift = True , window = True ,
319
+ detrend = 'constant' )
320
+ daft = xrft .dft (da ,
321
+ dim = dim , shift = True , window = True ,
322
+ detrend = 'constant' )
323
+ daft2 = xrft .dft (da2 ,
324
+ dim = dim , shift = True , window = True ,
325
+ detrend = 'constant' )
194
326
coord = list (daft .coords )
195
- test = (daft * np .conj (daft2 )).real / N ** 4
196
- for i in dim :
197
- test /= daft ['freq_' + i + '_spacing' ]
327
+ test = (daft * np .conj (daft2 )).real .values / N ** 4
328
+ # for i in dim:
329
+ # test /= daft['freq_' + i + '_spacing']
330
+ dk = np .diff (np .fft .fftfreq (N , 1. ))[0 ]
331
+ test /= dk ** 2
198
332
npt .assert_almost_equal (cs .values , test )
199
333
npt .assert_almost_equal (np .ma .masked_invalid (cs ).mask .sum (), 0. )
200
334
@@ -212,36 +346,19 @@ def test_parseval():
212
346
for d in dim :
213
347
coord = da [d ]
214
348
diff = np .diff (coord )
215
- # if pd.core.common.is_timedelta64_dtype(diff):
216
- # # convert to seconds so we get hertz
217
- # diff = diff.astype('timedelta64[s]').astype('f8')
218
349
delta = diff [0 ]
219
350
delta_x .append (delta )
220
351
221
352
window = np .hanning (N ) * np .hanning (N )[:, np .newaxis ]
222
- ps = xrft .power_spectrum (da , window = True )
353
+ ps = xrft .power_spectrum (da , window = True , detrend = 'constant' )
223
354
da_prime = da .values - da .mean (dim = dim ).values
224
355
npt .assert_almost_equal (ps .values .sum (),
225
356
(np .asarray (delta_x ).prod ()
226
357
* ((da_prime * window )** 2 ).sum ()
227
358
), decimal = 5
228
359
)
229
360
230
- # da = xr.DataArray(np.random.rand(5,2,20,30),
231
- # dims=['time', 'z', 'y', 'x'],
232
- # coords={'time': np.arange(5),'z':range(2),
233
- # 'y': np.arange(20),
234
- # 'x': np.arange(30)}).chunk({'z':1})
235
- # dim = ['y','x']
236
- # ps = xrft.power_spectrum(da, dim=dim, window=True)
237
- # da_prime = da.values - da.mean(dim=dim).values
238
- # npt.assert_almost_equal(ps.values.sum(),
239
- # (np.asarray(delta_x).prod()
240
- # * ((da_prime*window)**2).sum()
241
- # ), decimal=5
242
- # )
243
-
244
- cs = xrft .cross_spectrum (da , da2 , window = True )
361
+ cs = xrft .cross_spectrum (da , da2 , window = True , detrend = 'constant' )
245
362
da2_prime = da2 .values - da2 .mean (dim = dim ).values
246
363
npt .assert_almost_equal (cs .values .sum (),
247
364
(np .asarray (delta_x ).prod ()
@@ -250,6 +367,17 @@ def test_parseval():
250
367
), decimal = 5
251
368
)
252
369
370
+ d3d = xr .DataArray (np .random .rand (N ,N ,N ),
371
+ dims = ['time' ,'y' ,'x' ],
372
+ coords = {'time' :range (N ), 'y' :range (N ), 'x' :range (N )}
373
+ ).chunk ({'time' :1 })
374
+ ps = xrft .power_spectrum (d3d , dim = ['x' ,'y' ], window = True , detrend = 'linear' )
375
+ npt .assert_almost_equal (ps [0 ].values .sum (),
376
+ (np .asarray (delta_x ).prod ()
377
+ * ((numpy_detrend (d3d [0 ].values )* window )** 2 ).sum ()
378
+ ), decimal = 5
379
+ )
380
+
253
381
def _synthetic_field (N , dL , amp , s ):
254
382
"""
255
383
Generate a synthetic series of size N by N
@@ -348,7 +476,7 @@ def test_isotropic_ps_slope(N=512, dL=1., amp=1e1, s=-3.):
348
476
theta = xr .DataArray (_synthetic_field (N , dL , amp , s ),
349
477
dims = ['y' , 'x' ],
350
478
coords = {'y' :range (N ), 'x' :range (N )})
351
- iso_ps = xrft .isotropic_powerspectrum (theta , remove_mean = True ,
479
+ iso_ps = xrft .isotropic_powerspectrum (theta , detrend = 'constant' ,
352
480
density = True )
353
481
npt .assert_almost_equal (np .ma .masked_invalid (iso_ps [1 :]).mask .sum (), 0. )
354
482
y_fit , a , b = xrft .fit_loglog (iso_ps .freq_r .values [4 :],
0 commit comments