Skip to content

Commit 55c32d4

Browse files
committed
merging to get up to date
2 parents 0086c1d + b38199f commit 55c32d4

15 files changed

Lines changed: 634 additions & 374 deletions

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Python methods for numerical differentiation of noisy data, including multi-obje
2424

2525
## Introduction
2626

27-
PyNumDiff is a Python package that implements various methods for computing numerical derivatives of noisy data, which can be a critical step in developing dynamic models or designing control. There are seven different families of methods implemented in this repository:
27+
PyNumDiff is a Python package that implements many methods for computing numerical derivatives and smooth estimates of noisy data, which can be a critical step in developing dynamic models or designing control. There are seven different families of methods implemented in this repository:
2828

2929
1. prefiltering followed by finite difference calculation
3030
2. iterated finite differencing
@@ -34,9 +34,11 @@ PyNumDiff is a Python package that implements various methods for computing nume
3434
6. generalized Kalman smoothing
3535
7. local approximation with linear model
3636

37-
For a full list, explore modules in the [Sphinx documentation](https://pynumdiff.readthedocs.io/master/), or read section 7 of our [Taxonomy Paper](https://arxiv.org/abs/2512.09090).
37+
All are ultimately smoothing with similar runtime and accuracy, but some have situational advantages over others: For example, `robustdiff` is specialized to handle outliers; `splinediff`, `polydiff`, `rtsdiff`, and `robustdiff` can handle missing data; `splinediff`, `polydiff`, `rbfdiff`, `rtsdiff`, and `robustdiff` can handle irregularly-spaced data; and `rtsdiff` can handle inputs on a wrapped domain, like angles. All methods can accept blocks of multidimensional data, differentiating all vectors along the dimension given by the `axis` parameter.
3838

39-
Most of these methods have multiple parameters, so we take a principled approach and propose a multi-objective optimization framework for choosing parameters that minimize a loss function to balance the faithfulness and smoothness of the derivative estimate. For more details, refer to [this paper](https://doi.org/10.1109/ACCESS.2020.3034077).
39+
For a full list and comparison, see section 7 of our [Taxonomy Paper](https://arxiv.org/abs/2512.09090) and explore modules in the [Sphinx documentation](https://pynumdiff.readthedocs.io/master/).
40+
41+
All methods have hyperparameters, so we take a principled approach and propose a multi-objective optimization framework for choosing settings that minimize a loss function to balance the faithfulness and smoothness of the derivative estimate. For more details, refer to [this paper](https://doi.org/10.1109/ACCESS.2020.3034077).
4042

4143
## Installing
4244

@@ -52,7 +54,7 @@ For more details, read our [Sphinx documentation](https://pynumdiff.readthedocs.
5254
somethingdiff(x, dt, **kwargs)
5355
```
5456

55-
where `x` is data, `dt` is a step size, and various keyword arguments control the behavior. Some methods support variable step size, in which case the second parameter is renamed `dt_or_t` and can receive either a constant step size or an array of values to denote sample locations. Some methods support multidimensional data, in which case there is an `axis` argument to control the dimension differentiated along.
57+
where `x` is data, `dt` is a step size, and various keyword arguments control the behavior. Some methods support variable step size, in which case the second parameter is renamed `dt_or_t` and can receive either a constant step size or an array of values to denote sample locations. All major methods support multidimensional data, so look for an `axis` argument to control the dimension differentiated along.
5658

5759
You can set the hyperparameters:
5860
```python

notebooks/1_basic_tutorial.ipynb

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

notebooks/7_circular_domain.ipynb

Lines changed: 140 additions & 0 deletions
Large diffs are not rendered by default.

notebooks/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
| [Automatic Method Suggestion](https://github.com/florisvb/PyNumDiff/blob/master/notebooks/3_automatic_method_suggestion.ipynb) | A short demo of how to allow `pynumdiff` to choose a differentiation method for your data. |
88
| [Performance Analysis](https://github.com/florisvb/PyNumDiff/blob/master/notebooks/4_performance_analysis.ipynb) | Experiments to compare methods' accuracy and bias across simulations. |
99
| [Robustness to Outliers Demo](https://github.com/florisvb/PyNumDiff/blob/master/notebooks/5_robust_outliers_demo.ipynb) | This notebook shows a head-to-head of `RTSDiff`'s and `RobustDiff`'s minimum-RMSE performances on simulations with outliers, to illustrate the value of using a Huber loss in the Kalman MAP problem. |
10-
| [Multidimensionality Demo](https://github.com/florisvb/PyNumDiff/blob/master/notebooks/6_multidimensionality_demo.ipynb) | Demonstration of differentating multidimensional data along particular axes. |
10+
| [Multidimensionality Demo](https://github.com/florisvb/PyNumDiff/blob/master/notebooks/6_multidimensionality_demo.ipynb) | Demonstration of differentating multidimensional data along particular axes. |
11+
| [Circular Domain](https://github.com/florisvb/PyNumDiff/blob/master/notebooks/7_circular_domain.ipynb) | Demonstrates improved handling of data on a wrapped domain (e.g. angles) using `RTSDiff` with specialized innovation distance function (inside the Kalman filter). |

pynumdiff/basis_fit.py

Lines changed: 47 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
from pynumdiff.utils import utility
88

9-
def spectraldiff(x, dt, params=None, options=None, high_freq_cutoff=None,
10-
even_extension=True, pad_to_zero_dxdt=True, axis=0):
9+
def spectraldiff(x, dt, params=None, options=None, high_freq_cutoff=None, even_extension=True,
10+
pad_to_zero_dxdt=True, axis=0):
1111
"""Take a derivative in the Fourier domain, with high frequency attentuation.
1212
13-
:param np.array[float] x: data to differentiate
13+
:param np.array[float] x: data to differentiate. May be multidimensional; see :code:`axis`.
1414
:param float dt: step size
1515
:param list[float] or float params: (**deprecated**, prefer :code:`high_freq_cutoff`)
1616
:param dict options: (**deprecated**, prefer :code:`even_extension`
@@ -19,100 +19,93 @@ def spectraldiff(x, dt, params=None, options=None, high_freq_cutoff=None,
1919
and 1. Frequencies below this threshold will be kept, and above will be zeroed.
2020
:param bool even_extension: if True, extend the data with an even extension so signal starts and ends at the same value.
2121
:param bool pad_to_zero_dxdt: if True, extend the data with extra regions that smoothly force the derivative to
22-
2322
zero before taking FFT.
2423
2524
:return: - **x_hat** (np.array) -- estimated (smoothed) x
2625
- **dxdt_hat** (np.array) -- estimated derivative of x
2726
"""
2827
if params is not None: # Warning to support old interface for a while. Remove these lines along with params in a future release.
2928
warn("`params` and `options` parameters will be removed in a future version. Use `high_freq_cutoff`, " +
30-
"`even_extension`, and `pad_to_zero_dxdt` instead.", DeprecationWarning)
29+
"`even_extension`, and `pad_to_zero_dxdt` instead.", DeprecationWarning)
3130
high_freq_cutoff = params[0] if isinstance(params, list) else params
3231
if options is not None:
3332
if 'even_extension' in options: even_extension = options['even_extension']
3433
if 'pad_to_zero_dxdt' in options: pad_to_zero_dxdt = options['pad_to_zero_dxdt']
3534
elif high_freq_cutoff is None:
3635
raise ValueError("`high_freq_cutoff` must be given.")
3736

38-
x = np.asarray(x)
39-
x0 = np.moveaxis(x, axis, 0) # move time axis to the front of the array
40-
# Now x0 dims are (number of data points, number of signals)
41-
L = x0.shape[0]
37+
L = x.shape[axis]
4238

43-
# Make derivative go to zero at the ends (optional):
39+
# Make derivative go to zero at the ends (optional)
4440
if pad_to_zero_dxdt:
4541
padding = 100
46-
pre = x[0] * np.ones(padding)
47-
post = x[-1] * np.ones(padding)
48-
x = np.hstack((pre, x, post)) # extend the edges
49-
50-
# Pad first and last values x100
51-
first = x0[0:1]
52-
last = x0[-1:]
53-
pre = np.repeat(first, padding, axis=0)
54-
post = np.repeat(last, padding, axis=0)
55-
56-
xpad = np.concatenate((pre, x0, post), axis=0) # concatenate along axis 0
42+
pre = np.repeat(np.take(x, [0], axis=axis), padding, axis=axis) # take keeps dimensions, unlike x[0]
43+
post = np.repeat(np.take(x, [-1], axis=axis), padding, axis=axis)
44+
x = np.concatenate((pre, x, post), axis=axis) # extend the edges
45+
kernel = utility.mean_kernel(padding//2)
46+
x_smoothed = utility.convolutional_smoother(x, kernel, axis=axis) # smooth the padded edges in
47+
m = (slice(None),)*axis + (slice(padding, L+padding),) + (slice(None),)*(x.ndim-axis-1) # middle
48+
x_smoothed[m] = x[m] # restore original signal in the middle
49+
x = x_smoothed
5750
else:
58-
padding = 0
51+
m = (slice(None),)*axis + (slice(0, L),) + (slice(None),)*(x.ndim-axis-1) # indices where signal lives
5952

6053
# Do even extension (optional)
6154
if even_extension is True:
62-
x0 = np.concatenate((x0, x0[::-1, ...]), axis=0)
55+
x = np.concatenate((x, np.flip(x, axis=axis)), axis=axis)
56+
57+
s = [np.newaxis for dim in x.shape]; s[axis] = slice(None); s = tuple(s) # for elevating vectors to have same dimension as data
6358

6459
# Form wavenumbers
65-
N = x0.shape[0]
60+
N = x.shape[axis]
6661
k = np.concatenate((np.arange(N//2 + 1), np.arange(-N//2 + 1, 0)))
6762
if N % 2 == 0: k[N//2] = 0 # odd derivatives get the Nyquist element zeroed out
6863

6964
# Filter to zero out higher wavenumbers
7065
discrete_cutoff = int(high_freq_cutoff * N / 2) # Nyquist is at N/2 location, and we're cutting off as a fraction of that
71-
72-
filt = np.ones(k.shape) # start with all frequencies passing
73-
filt[discrete_cutoff:-discrete_cutoff] = 0 # zero out high-frequency components
74-
filt = filt.reshape((N,) + (1,)*(x0.ndim-1))
66+
filt = np.ones(k.shape) # start with all frequencies passing
67+
filt[discrete_cutoff:-discrete_cutoff] = 0 # zero out high-frequency components
7568

7669
# Smoothed signal
77-
X = np.fft.fft(x0, axis=0)
78-
79-
x_hat0 = np.real(np.fft.ifft(filt * X, axis=0))
80-
x_hat0 = x_hat0[padding:L+padding]
70+
X = np.fft.fft(x, axis=axis)
71+
x_hat = np.real(np.fft.ifft(filt[s] * X, axis=axis))
8172

8273
# Derivative = 90 deg phase shift
8374
omega = 2*np.pi/(dt*N) # factor of 2pi/T turns wavenumbers into frequencies in radians/s
84-
k0 = k.reshape((N,) + (1,)*(x0.ndim-1))
85-
dxdt0 = np.real(np.fft.ifft(1j * k0 * omega * filt * X, axis=0))
86-
dxdt0 = dxdt0[padding:L+padding]
87-
# move back to original axis position
88-
x_hat = np.moveaxis(x_hat0, 0, axis)
89-
dxdt_hat = np.moveaxis(dxdt0, 0, axis)
90-
91-
return x_hat, dxdt_hat
75+
dxdt_hat = np.real(np.fft.ifft(1j * k[s] * omega * filt[s] * X, axis=axis))
76+
77+
return x_hat[m], dxdt_hat[m]
9278

9379

94-
def rbfdiff(x, dt_or_t, sigma=1, lmbd=0.01):
80+
def rbfdiff(x, dt_or_t, sigma=1, lmbd=0.01, axis=0):
9581
"""Find smoothed function and derivative estimates by fitting noisy data with radial-basis-functions. Naively,
9682
fill a matrix with basis function samples and solve a linear inverse problem against the data, but truncate tiny
9783
values to make columns sparse. Each basis function "hill" is topped with a "tower" of height :code:`lmbd` to reach
9884
noisy data samples, and the final smoothed reconstruction is found by razing these and only keeping the hills.
9985
100-
:param np.array[float] x: data to differentiate
86+
:param np.array[float] x: data to differentiate. May be multidimensional; see :code:`axis`.
10187
:param float or array[float] dt_or_t: This function supports variable step size. This parameter is either the constant
102-
:math:`\\Delta t` if given as a single float, or data locations if given as an array of same length as :code:`x`.
88+
:math:`\\Delta t` if given as a single float, or data locations if given as an array of same length as :code:`x`.
10389
:param float sigma: controls width of radial basis functions
10490
:param float lmbd: controls smoothness
91+
:param int axis: data dimension along which differentiation is performed
10592
10693
:return: - **x_hat** (np.array) -- estimated (smoothed) x
10794
- **dxdt_hat** (np.array) -- estimated derivative of x
10895
"""
96+
N = x.shape[axis]
97+
x = np.moveaxis(x, axis, 0) # bring axis of differentiation to front so each N repeats comprise vector
98+
plump = x.shape
99+
x_flattened = x.reshape(N, -1) # (N, M) matrix where each column is a vector along the original axis
100+
109101
if np.isscalar(dt_or_t):
110-
t = np.arange(len(x))*dt_or_t
102+
t = np.arange(N)*dt_or_t
111103
else: # support variable step size for this function
112-
if len(x) != len(dt_or_t): raise ValueError("If `dt_or_t` is given as array-like, must have same length as `x`.")
104+
if N != len(dt_or_t): raise ValueError("If `dt_or_t` is given as array-like, must have same length as `x`.")
113105
t = dt_or_t
114106

115-
# The below does the approximate equivalent of this code, but sparsely in O(N sigma^2), since the rbf falls off rapidly
107+
# For each vector along the axis of differentiation, the below does the approximate equivalent of this code,
108+
# but sparsely in O(N sigma^2), since the rbf falls off rapidly
116109
# t_i, t_j = np.meshgrid(t,t)
117110
# r = t_j - t_i # radius
118111
# rbf = np.exp(-(r**2) / (2 * sigma**2)) # radial basis function kernel, O(N^2) entries
@@ -132,12 +125,15 @@ def rbfdiff(x, dt_or_t, sigma=1, lmbd=0.01):
132125
dv = -radius / sigma**2 * v # take derivative of radial basis function, because d/dt coef*f(t) = coef*df/dt
133126
rows.append(n); cols.append(j); vals.append(v); dvals.append(dv)
134127

135-
rbf = sparse.csr_matrix((vals, (rows, cols)), shape=(len(t), len(t))) # Build sparse kernels, O(N sigma) entries
136-
drbfdt = sparse.csr_matrix((dvals, (rows, cols)), shape=(len(t), len(t)))
137-
rbf_regularized = rbf + lmbd*sparse.eye(len(t), format="csr") # identity matrix gives a little extra height at the centers
138-
alpha = sparse.linalg.spsolve(rbf_regularized, x) # solve sparse system targeting the noisy data, O(N sigma^2)
128+
rbf = sparse.csr_matrix((vals, (rows, cols)), shape=(N, N)) # Build sparse kernels, O(N sigma) entries
129+
drbfdt = sparse.csr_matrix((dvals, (rows, cols)), shape=(N, N))
130+
rbf_regularized = rbf + lmbd*sparse.eye(N, format="csr") # identity matrix gives a little extra height at the centers
131+
alpha = sparse.linalg.spsolve(rbf_regularized, x_flattened) # solve sparse system targeting the noisy data,
132+
# can take matrix target, O(N sigma^2) for each vector
133+
x_hat_flattened = rbf @ alpha # find samples of reconstructions using the smooth bases
134+
dxdt_hat_flattened = drbfdt @ alpha
139135

140-
return rbf @ alpha, drbfdt @ alpha # find samples of reconstructions using the smooth bases
136+
return np.moveaxis(x_hat_flattened.reshape(plump), 0, axis), np.moveaxis(dxdt_hat_flattened.reshape(plump), 0, axis)
141137

142138

143139
def waveletdiff(x, dt_or_t, wavelet='db4', level=None, threshold=1.0, axis=0, mode='periodization'):

pynumdiff/finite_difference.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
def finitediff(x, dt, num_iterations=1, order=2, axis=0):
99
"""Perform iterated finite difference of a given order. This serves as the common backing function for
1010
all other methods in this module.
11-
12-
:param np.array[float] x: data to differentiate
11+
12+
:param np.array[float] x: data to differentiate. May be multidimensional; see :code:`axis`.
1313
:param float dt: step size
1414
:param int num_iterations: number of iterations. If >1, the derivative is integrated with trapezoidal
1515
rule, that result is finite-differenced again, and the cycle is repeated num_iterations-1 times

0 commit comments

Comments
 (0)