55
66from pynumdiff .utils import utility
77
8-
9- def spectraldiff ( x , dt , params = None , options = None , high_freq_cutoff = None , even_extension = True , pad_to_zero_dxdt = True ):
8+ def spectraldiff ( x , dt , params = None , options = None , high_freq_cutoff = None , even_extension = True ,
9+ pad_to_zero_dxdt = True , axis = 0 ):
1010 """Take a derivative in the Fourier domain, with high frequency attentuation.
1111
1212 :param np.array[float] x: data to differentiate
@@ -33,48 +33,50 @@ def spectraldiff(x, dt, params=None, options=None, high_freq_cutoff=None, even_e
3333 elif high_freq_cutoff is None :
3434 raise ValueError ("`high_freq_cutoff` must be given." )
3535
36- L = len ( x )
36+ L = x . shape [ axis ]
3737
38- # make derivative go to zero at ends (optional)
38+ # Make derivative go to zero at the ends (optional)
3939 if pad_to_zero_dxdt :
4040 padding = 100
41- pre = getattr ( x , 'values' , x ) [0 ]* np . ones ( padding ) # getattr to use .values if x is a pandas Series
42- post = getattr ( x , 'values' , x ) [- 1 ]* np . ones ( padding )
43- x = np .hstack ((pre , x , post )) # extend the edges
41+ pre = np . repeat ( np . take ( x , [0 ], axis = axis ), padding , axis = axis ) # take keeps dimensions, unlike x[0]
42+ post = np . repeat ( np . take ( x , [- 1 ], axis = axis ), padding , axis = axis )
43+ x = np .concatenate ((pre , x , post ), axis = axis ) # extend the edges
4444 kernel = utility .mean_kernel (padding // 2 )
45- x_hat = utility .convolutional_smoother (x , kernel ) # smooth the edges in
46- x_hat [padding :- padding ] = x [padding :- padding ] # replace middle with original signal
47- x = x_hat
45+ x_smoothed = utility .convolutional_smoother (x , kernel , axis = axis ) # smooth the padded edges in
46+ m = (slice (None ),)* axis + (slice (padding , L + padding ),) + (slice (None ),)* (x .ndim - axis - 1 ) # middle
47+ x_smoothed [m ] = x [m ] # restore original signal in the middle
48+ x = x_smoothed
4849 else :
49- padding = 0
50+ m = ( slice ( None ),) * axis + ( slice ( 0 , L ),) + ( slice ( None ),) * ( x . ndim - axis - 1 ) # indices where signal lives
5051
5152 # Do even extension (optional)
5253 if even_extension is True :
53- x = np .hstack ((x , x [::- 1 ]))
54+ x = np .concatenate ((x , np .flip (x , axis = axis )), axis = axis )
55+
56+ s = [np .newaxis for dim in x .shape ]; s [axis ] = slice (None ); s = tuple (s ) # for elevating vectors to have same dimension as data
5457
5558 # Form wavenumbers
56- N = len ( x )
59+ N = x . shape [ axis ]
5760 k = np .concatenate ((np .arange (N // 2 + 1 ), np .arange (- N // 2 + 1 , 0 )))
5861 if N % 2 == 0 : k [N // 2 ] = 0 # odd derivatives get the Nyquist element zeroed out
5962
6063 # Filter to zero out higher wavenumbers
61- discrete_cutoff = int (high_freq_cutoff * N / 2 ) # Nyquist is at N/2 location, and we're cutting off as a fraction of that
62- filt = np .ones (k .shape ); filt [discrete_cutoff :N - discrete_cutoff ] = 0
64+ discrete_cutoff = int (high_freq_cutoff * N / 2 ) # Nyquist is at N/2 location, and we're cutting off as a fraction of that
65+ filt = np .ones (k .shape ) # start with all frequencies passing
66+ filt [discrete_cutoff :- discrete_cutoff ] = 0 # zero out high-frequency components
6367
6468 # Smoothed signal
65- X = np .fft .fft (x )
66- x_hat = np .real (np .fft .ifft (filt * X ))
67- x_hat = x_hat [padding :L + padding ]
69+ X = np .fft .fft (x , axis = axis )
70+ x_hat = np .real (np .fft .ifft (filt [s ] * X , axis = axis ))
6871
6972 # Derivative = 90 deg phase shift
7073 omega = 2 * np .pi / (dt * N ) # factor of 2pi/T turns wavenumbers into frequencies in radians/s
71- dxdt_hat = np .real (np .fft .ifft (1j * k * omega * filt * X ))
72- dxdt_hat = dxdt_hat [padding :L + padding ]
74+ dxdt_hat = np .real (np .fft .ifft (1j * k [s ] * omega * filt [s ] * X , axis = axis ))
7375
74- return x_hat , dxdt_hat
76+ return x_hat [ m ] , dxdt_hat [ m ]
7577
7678
77- def rbfdiff (x , dt_or_t , sigma = 1 , lmbd = 0.01 ):
79+ def rbfdiff (x , dt_or_t , sigma = 1 , lmbd = 0.01 , axis = 0 ):
7880 """Find smoothed function and derivative estimates by fitting noisy data with radial-basis-functions. Naively,
7981 fill a matrix with basis function samples and solve a linear inverse problem against the data, but truncate tiny
8082 values to make columns sparse. Each basis function "hill" is topped with a "tower" of height :code:`lmbd` to reach
@@ -85,17 +87,24 @@ def rbfdiff(x, dt_or_t, sigma=1, lmbd=0.01):
8587 :math:`\\ Delta t` if given as a single float, or data locations if given as an array of same length as :code:`x`.
8688 :param float sigma: controls width of radial basis functions
8789 :param float lmbd: controls smoothness
90+ :param int axis: data dimension along which differentiation is performed
8891
8992 :return: - **x_hat** (np.array) -- estimated (smoothed) x
9093 - **dxdt_hat** (np.array) -- estimated derivative of x
9194 """
95+ N = x .shape [axis ]
96+ x = np .moveaxis (x , axis , 0 ) # bring axis of differentiation to front so each N repeats comprise vector
97+ plump = x .shape
98+ x_flattened = x .reshape (N , - 1 ) # (N, M) matrix where each column is a vector along the original axis
99+
92100 if np .isscalar (dt_or_t ):
93- t = np .arange (len ( x ) )* dt_or_t
101+ t = np .arange (N )* dt_or_t
94102 else : # support variable step size for this function
95- if len ( x ) != len (dt_or_t ): raise ValueError ("If `dt_or_t` is given as array-like, must have same length as `x`." )
103+ if N != len (dt_or_t ): raise ValueError ("If `dt_or_t` is given as array-like, must have same length as `x`." )
96104 t = dt_or_t
97105
98- # The below does the approximate equivalent of this code, but sparsely in O(N sigma^2), since the rbf falls off rapidly
106+ # For each vector along the axis of differentiation, the below does the approximate equivalent of this code,
107+ # but sparsely in O(N sigma^2), since the rbf falls off rapidly
99108 # t_i, t_j = np.meshgrid(t,t)
100109 # r = t_j - t_i # radius
101110 # rbf = np.exp(-(r**2) / (2 * sigma**2)) # radial basis function kernel, O(N^2) entries
@@ -115,9 +124,12 @@ def rbfdiff(x, dt_or_t, sigma=1, lmbd=0.01):
115124 dv = - radius / sigma ** 2 * v # take derivative of radial basis function, because d/dt coef*f(t) = coef*df/dt
116125 rows .append (n ); cols .append (j ); vals .append (v ); dvals .append (dv )
117126
118- rbf = sparse .csr_matrix ((vals , (rows , cols )), shape = (len (t ), len (t ))) # Build sparse kernels, O(N sigma) entries
119- drbfdt = sparse .csr_matrix ((dvals , (rows , cols )), shape = (len (t ), len (t )))
120- rbf_regularized = rbf + lmbd * sparse .eye (len (t ), format = "csr" ) # identity matrix gives a little extra height at the centers
121- alpha = sparse .linalg .spsolve (rbf_regularized , x ) # solve sparse system targeting the noisy data, O(N sigma^2)
127+ rbf = sparse .csr_matrix ((vals , (rows , cols )), shape = (N , N )) # Build sparse kernels, O(N sigma) entries
128+ drbfdt = sparse .csr_matrix ((dvals , (rows , cols )), shape = (N , N ))
129+ rbf_regularized = rbf + lmbd * sparse .eye (N , format = "csr" ) # identity matrix gives a little extra height at the centers
130+ alpha = sparse .linalg .spsolve (rbf_regularized , x_flattened ) # solve sparse system targeting the noisy data,
131+ # can take matrix target, O(N sigma^2) for each vector
132+ x_hat_flattened = rbf @ alpha # find samples of reconstructions using the smooth bases
133+ dxdt_hat_flattened = drbfdt @ alpha
122134
123- return rbf @ alpha , drbfdt @ alpha # find samples of reconstructions using the smooth bases
135+ return np . moveaxis ( x_hat_flattened . reshape ( plump ), 0 , axis ), np . moveaxis ( dxdt_hat_flattened . reshape ( plump ), 0 , axis )
0 commit comments