Skip to content

Commit b9d73b1

Browse files
committed
Merge branch 'master' of github.com:florisvb/PyNumDiff into tvrdiff-multidim
2 parents ce809b4 + dff1f9c commit b9d73b1

9 files changed

Lines changed: 370 additions & 307 deletions

File tree

notebooks/1_basic_tutorial.ipynb

Lines changed: 56 additions & 98 deletions
Large diffs are not rendered by default.

notebooks/6_multidimensionality_demo.ipynb

Lines changed: 11 additions & 9 deletions
Large diffs are not rendered by default.

pynumdiff/basis_fit.py

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
from 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

Comments
 (0)