Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/draft-pdf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
on: [push]

jobs:
paper:
runs-on: ubuntu-latest
name: Paper Draft
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Build draft PDF
uses: openjournals/openjournals-draft-action@master
with:
journal: joss
paper-path: paper/paper.md
- name: Upload
uses: actions/upload-artifact@v7
with:
name: paper
path: paper/paper.pdf
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ All are ultimately smoothing with similar runtime and accuracy, but some have fl

All methods have hyperparameters, described in the [Sphinx documentation](https://pynumdiff.readthedocs.io/master/). We take a principled approach and propose a multi-objective optimization framework for choosing settings that minimize a loss function that balances faithfulness to data with smoothness of the derivative estimate. For more details, refer to [this paper](https://doi.org/10.1109/ACCESS.2020.3034077).

![Three simulated signals and their derivatives, estimated by six of the seven method families, with hyperparameters chosen by `pynumdiff.optimize`.](paper/methods_comparison.png)

Above, three simulated signals are differentiated by two methods each, drawn from six of the seven families, with hyperparameters chosen automatically by `pynumdiff.optimize`. Reproduce with `python paper/make_figure.py`.

## Installing

Dependencies are listed in [pyproject.toml](https://github.com/florisvb/PyNumDiff/blob/master/pyproject.toml). They include the usual suspects like `numpy` and `scipy`, plus `pywavelets` for `waveletdiff`, `tqdm` for the optimizer, and `cvxpy` for `robustdiff` and `tvrdiff`.
Expand Down
62 changes: 62 additions & 0 deletions paper/make_figure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Generates paper/methods_comparison.png. Run from the repo root: python paper/make_figure.py"""
import numpy as np, matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from pynumdiff.utils import simulate
from pynumdiff import polydiff, tvrdiff, rtsdiff, spectraldiff, butterdiff, lineardiff
from pynumdiff.optimize import optimize

# One color per method, fixed wherever it appears. Okabe-Ito, legible to colorblind readers.
STYLE = {spectraldiff:("spectraldiff","#CC79A7"), polydiff:("polydiff","#0072B2"), tvrdiff:("tvrdiff","#D55E00"),
butterdiff:("butterdiff","#5D3A9B"), rtsdiff:("rtsdiff","#009E73"), lineardiff:("lineardiff","#E69F00")}

# Six of the seven families, paired so each panel contrasts two. Bandlimits are optimizer inputs, round numbers a
# user reads off a power spectrum; Lorenz is the faster signal. Comment out FITTED to re-run the search (minutes).
panels = [("Sum of sines", simulate.sine, [spectraldiff, polydiff], 1),
("Triangle wave", simulate.triangle, [tvrdiff, butterdiff], 1),
("Lorenz $x$", simulate.lorenz_x, [rtsdiff, lineardiff], 2)]
FITTED = {"spectraldiff": dict(cutoff_freq=0.0625, even_extension=True, pad_to_zero_dxdt=True),
"polydiff": dict(stride=7, degree=2, window_size=41, kernel='gaussian'),
"tvrdiff": dict(gamma=10.6015625, huberM=6.0, order=1),
"butterdiff": dict(cutoff_freq=0.05, num_iterations=1, filter_order=2),
"rtsdiff": dict(log_qr_ratio=3.81328125, order=1, forwardbackward=False),
"lineardiff": dict(gamma=0.00011125, window_size=53, order=2, kernel='friedrichs')}
#FITTED = {}

def main():
fig, axes = plt.subplots(2, 3, figsize=(13, 5.4), sharex=True,
gridspec_kw={'height_ratios':[1, 1.6], 'hspace':0.13, 'wspace':0.2})
for j, (sname, sim, methods, bandlimit) in enumerate(panels):
x, x_truth, dxdt_truth = sim(duration=4, dt=0.01, noise_parameters=(0, 0.2), random_seed=3)
t = np.arange(len(x))*0.01
top, bot = axes[0, j], axes[1, j]

top.plot(t, x, '.', color='0.72', markersize=1.6, label="noisy data")
top.plot(t, x_truth, '-', color='black', linewidth=1.1, label="true $x$")
top.set_title(sname, fontsize=11, pad=6)
bot.plot(t, dxdt_truth, '-', color='0.55', linewidth=2.6, label="true $\\dot{x}$", zorder=1)

for method in methods:
name, color = STYLE[method]
# Order 1 represents the triangle's piecewise-constant derivative exactly; without it tvrdiff goes spiky.
ssu = {'order':{1, 2, 3}} if (method is tvrdiff and sim is simulate.triangle) else {}
params = FITTED.get(name) or optimize(method, x, 0.01, bandlimit=bandlimit, search_space_updates=ssu)[0]
print(f" {sname:<14} {name:<13} {params}", flush=True)
bot.plot(t, method(x, 0.01, **params)[1], '-', color=color, linewidth=1.1, label=name, zorder=2)
bot.set_xlabel("time (s)", fontsize=10)

for ax, pad in ((top, 0.17 if j == 0 else 0.04), (bot, 0.28)): # only the headroom each legend needs
lo, hi = ax.get_ylim(); ax.set_ylim(lo, hi + pad*(hi - lo))
ax.tick_params(labelsize=8)
for s in ('top', 'right'): ax.spines[s].set_visible(False)
bot.legend(fontsize=8, frameon=False, loc='upper center', ncol=3, handlelength=1.6, columnspacing=1.1,
borderpad=0.1, title=f"optimized at bandlimit {bandlimit} Hz", title_fontsize=8)

axes[0, 0].set_ylabel("$x$", fontsize=11)
axes[1, 0].set_ylabel("$dx/dt$", fontsize=11)
axes[0, 0].legend(fontsize=8, frameon=False, loc='upper center', ncol=2, handlelength=1.6, borderpad=0.1)
fig.savefig("paper/methods_comparison.png", dpi=200, bbox_inches='tight')
print("wrote paper/methods_comparison.png")

if __name__ == "__main__": # spawned workers re-import this module, so nothing heavy may run at import time
main()
Binary file added paper/methods_comparison.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
181 changes: 181 additions & 0 deletions paper/paper.bib
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
@article{vanBreugel2022,
doi = {10.21105/joss.04078},
url = {https://doi.org/10.21105/joss.04078},
year = {2022},
publisher = {The Open Journal},
volume = {7},
number = {71},
pages = {4078},
author = {{van Breugel}, Floris and Liu, Yuying and Brunton, Bingni W. and Kutz, J. Nathan},
title = {{PyNumDiff}: A {Python} package for numerical differentiation of noisy time-series data},
journal = {Journal of Open Source Software}
}

@misc{komarov2025,
title = {A Taxonomy of Numerical Differentiation Methods},
author = {Komarov, Pavel and {van Breugel}, Floris and Kutz, J. Nathan},
year = {2025},
eprint = {2512.09090},
archivePrefix = {arXiv},
primaryClass = {math.NA},
url = {https://arxiv.org/abs/2512.09090}
}

@article{vanBreugel2020numerical,
doi = {10.1109/ACCESS.2020.3034077},
year = {2020},
author = {{van Breugel}, Floris and Kutz, J. Nathan and Brunton, Bingni W.},
journal = {IEEE Access},
title = {Numerical differentiation of noisy data: A unifying multi-objective optimization framework},
volume = {8},
pages = {196865--196877}
}

@article{chartrand2011numerical,
author = {Rick Chartrand},
title = {Numerical differentiation of noisy, nonsmooth data},
journal = {ISRN Applied Mathematics},
year = {2011},
volume = {2011},
pages = {164564},
doi = {10.5402/2011/164564}
}

@article{brunton2016discovering,
author = {Steven L. Brunton and Joshua L. Proctor and J. Nathan Kutz},
title = {Discovering governing equations from data by sparse identification of nonlinear dynamical systems},
journal = {Proceedings of the National Academy of Sciences},
year = {2016},
volume = {113},
number = {15},
pages = {3932--3937},
doi = {10.1073/pnas.1517384113}
}

@article{virtanen2020scipy,
author = {Pauli Virtanen and Ralf Gommers and Travis E. Oliphant and others},
title = {{SciPy} 1.0: Fundamental algorithms for scientific computing in {Python}},
journal = {Nature Methods},
year = {2020},
volume = {17},
pages = {261--272},
doi = {10.1038/s41592-019-0686-2}
}

@article{harris2020array,
author = {Charles R. Harris and K. Jarrod Millman and St{\'{e}}fan J. {van der Walt} and others},
title = {Array programming with {NumPy}},
journal = {Nature},
year = {2020},
volume = {585},
pages = {357--362},
doi = {10.1038/s41586-020-2649-2}
}

@article{diamond2016cvxpy,
author = {Steven Diamond and Stephen Boyd},
title = {{CVXPY}: A {Python}-embedded modeling language for convex optimization},
journal = {Journal of Machine Learning Research},
year = {2016},
volume = {17},
number = {83},
pages = {1--5}
}

@article{savitzky1964,
author = {Abraham Savitzky and Marcel J. E. Golay},
title = {Smoothing and Differentiation of Data by Simplified Least Squares Procedures},
journal = {Analytical Chemistry},
year = {1964},
volume = {36},
number = {8},
pages = {1627--1639},
doi = {10.1021/ac60214a047}
}

@article{huber1964,
author = {Peter J. Huber},
title = {Robust Estimation of a Location Parameter},
journal = {The Annals of Mathematical Statistics},
year = {1964},
volume = {35},
number = {1},
pages = {73--101},
doi = {10.1214/aoms/1177703732}
}

@article{rauch1965,
author = {Herbert E. Rauch and F. Tung and Charlotte T. Striebel},
title = {Maximum likelihood estimates of linear dynamic systems},
journal = {AIAA Journal},
year = {1965},
volume = {3},
number = {8},
pages = {1445--1450},
doi = {10.2514/3.3166}
}

@article{aravkin2013,
author = {Aleksandr Y. Aravkin and James V. Burke and Gianluigi Pillonetto},
title = {Optimization viewpoint on {Kalman} smoothing with applications to robust and sparse estimation},
journal = {Journal of Machine Learning Research},
year = {2013},
volume = {14},
pages = {2513--2558},
url = {https://jmlr.org/papers/volume14/aravkin13a/aravkin13a.pdf},
doi = {10.1007/978-3-642-38398-4_8}
}

@software{derivative_pkg,
author = {Andy Goldschmidt},
title = {derivative: Numerical differentiation in {Python}},
year = {2021},
url = {https://github.com/andgoldschmidt/derivative}
}

@software{findiff,
author = {Matthias Baer},
title = {findiff: Finite difference derivatives in {Python}},
year = {2018},
url = {https://github.com/maroba/findiff}
}

@software{pykalman,
author = {Daniel Duckworth and {The pykalman developers}},
title = {pykalman: {Kalman} filters and smoothers for {Python}},
year = {2012},
url = {https://github.com/pykalman/pykalman}
}

@article{kalman1960,
author = {Rudolf E. Kalman},
title = {A new approach to linear filtering and prediction problems},
journal = {Journal of Basic Engineering},
year = {1960},
volume = {82},
number = {1},
pages = {35--45},
doi = {10.1115/1.3662552}
}

@article{pysindy,
author = {Brian M. de Silva and Kathleen Champion and Markus Quade and Jean-Christophe Loiseau and J. Nathan Kutz and Steven L. Brunton},
title = {{PySINDy}: A {Python} package for the sparse identification of nonlinear dynamics from data},
journal = {Journal of Open Source Software},
year = {2020},
volume = {5},
number = {49},
pages = {2104},
doi = {10.21105/joss.02104}
}

@article{lee2019pywavelets,
author = {Gregory R. Lee and Ralf Gommers and Filip Wasilewski and Kai Wohlfahrt and Aaron O'Leary},
title = {{PyWavelets}: A {Python} package for wavelet analysis},
journal = {Journal of Open Source Software},
year = {2019},
volume = {4},
number = {36},
pages = {1237},
doi = {10.21105/joss.01237}
}
Loading