Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
0c16951
Add JOSS 2.0 paper draft
pavelkomarov Apr 1, 2026
b86064d
Revise JOSS paper draft (second pass)
pavelkomarov Apr 1, 2026
63e31a2
JOSS paper third pass: address reviewer feedback
pavelkomarov Apr 1, 2026
4eb746d
JOSS 2.0 paper: 4th pass + bib refs + draft PDF workflow
pavelkomarov Apr 2, 2026
86075b9
paper: use \checkmark instead of Unicode ✓ in capability table
pavelkomarov Apr 2, 2026
a66f4b5
paper: tighten prose throughout for punch and concision
pavelkomarov Apr 2, 2026
bb8ee83
paper: replace most em dashes with commas/colons/parens; fix AI discl…
pavelkomarov Apr 2, 2026
943804f
paper: shorten table column headers to keep table on one page
pavelkomarov Apr 2, 2026
d590860
paper: tighten paragraphs before table to fit it on one page
pavelkomarov Apr 2, 2026
4d7aa22
added back longer table headers
pavelkomarov Apr 2, 2026
b70e672
updated title to be more different from original joss paper so we don…
pavelkomarov Apr 2, 2026
792ce76
futzing at this point
pavelkomarov Apr 2, 2026
825a54b
removed lines of MIT license that are confusing the editorbot, and ad…
pavelkomarov Apr 3, 2026
65fef9f
paper: trim two phrases to prevent single-line page 5 overflow
pavelkomarov Apr 3, 2026
304955b
trying to make it fit on 4 pages
pavelkomarov Apr 3, 2026
1626407
paper: emphasize optimization improvements, add categorical params
pavelkomarov Apr 3, 2026
4e7d894
Merge branch 'master' into joss2.0
pavelkomarov Jun 12, 2026
d02f186
paper: add Maria Protogerou and the wavelet differentiation method
pavelkomarov Jun 12, 2026
347093a
Merge branch 'master' into joss2.0
pavelkomarov Jun 12, 2026
f61b008
paper: tighten Software Design prose to keep Table 1 on one page
pavelkomarov Jun 12, 2026
a194634
Merge branch 'master' into joss2.0
pavelkomarov Jun 12, 2026
f25be8b
ci: bump draft-pdf workflow to Node 24 actions
pavelkomarov Jun 12, 2026
8cff0e0
paper: note Claude Opus 4.8 in the AI usage disclosure
pavelkomarov Jun 12, 2026
8ca4dc2
Merge branch 'master' into joss2.0
pavelkomarov Jul 28, 2026
dd36a3a
Merge branch 'master' into joss2.0
pavelkomarov Jul 28, 2026
f5654c9
Merge branch 'master' into joss2.0
pavelkomarov Jul 28, 2026
e439c5d
Merge branch 'master' into joss2.0
pavelkomarov Jul 28, 2026
fed718a
Merge branch 'master' of github.com:florisvb/PyNumDiff into joss2.0
pavelkomarov Aug 20, 2026
592872c
Merge branch 'master' of github.com:florisvb/PyNumDiff into joss2.0
pavelkomarov Aug 20, 2026
37f8470
Merge branch 'master' of github.com:florisvb/PyNumDiff into joss2.0
pavelkomarov Aug 20, 2026
a55465c
improved robustdiff docstring some
pavelkomarov Aug 20, 2026
fd32491
Merge branch 'master' of github.com:florisvb/PyNumDiff into joss2.0
pavelkomarov Aug 21, 2026
196f4ac
Merge branch 'master' of github.com:florisvb/PyNumDiff into joss2.0
pavelkomarov Aug 21, 2026
a9bcbc1
Clarify what the second generation adds, and tighten the paper
pavelkomarov Aug 21, 2026
6747ce0
Note suggest_method and optimizer caching
pavelkomarov Aug 21, 2026
e34bc67
Show the optimization objective, and align sections with JOSS require…
pavelkomarov Aug 21, 2026
e78a8b0
Add ORCIDs for van Breugel and Kutz, restore submission date
pavelkomarov Aug 21, 2026
2571b44
Add Komarov ORCID
pavelkomarov Aug 21, 2026
9b0dc98
Merge branch 'master' of github.com:florisvb/PyNumDiff into joss2.0
pavelkomarov Aug 21, 2026
370b3fa
Describe the notebooks as reproducible material, fix van Breugel cita…
pavelkomarov Aug 21, 2026
8e10fa4
Keep Table 1 on one page, and fix the van Breugel surname
pavelkomarov Aug 21, 2026
3fd0132
Group Table 1 by capability so every method appears
pavelkomarov Aug 21, 2026
5850b07
Return Table 1 to a method-keyed matrix, with the plain methods on on…
pavelkomarov Aug 21, 2026
3a81739
Revert Table 1 to the capability-keyed form, which fits on one page
pavelkomarov Aug 21, 2026
74eb775
Merge branch 'master' of github.com:florisvb/PyNumDiff into joss2.0
pavelkomarov Aug 21, 2026
41d899e
Merge branch 'master' of github.com:florisvb/PyNumDiff into joss2.0
pavelkomarov Aug 24, 2026
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 numberDiff line numberDiff 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
2 changes: 0 additions & 2 deletions LICENSE.txt
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
The MIT License (MIT)

Copyright (c) 2025
Floris van Breugel, The University of Nevada, Reno, Mechanical Engineering Department
Pavel Komarov, Univerity of Washington Electrical and Computer Engineering
Expand Down
167 changes: 167 additions & 0 deletions paper/paper.bib
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
@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{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}
}

@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{ahnert2007,
author = {Karsten Ahnert and Markus Abel},
title = {Numerical differentiation of experimental data: local versus global methods},
journal = {Computer Physics Communications},
year = {2007},
volume = {177},
number = {10},
pages = {764--774},
doi = {10.1016/j.cpc.2007.03.009}
}

@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}
}
109 changes: 109 additions & 0 deletions paper/paper.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
---
title: 'PyNumDiff: Practical Numerical Differentiation for Noisy Data'
tags:
- Python
- numerical differentiation
- time series
- denoising
- dynamics
- signal processing
authors:
- name: Pavel Komarov
orcid: 0009-0007-7482-2807
corresponding: true
affiliation: 1
- name: Floris van Breugel
orcid: 0000-0001-6538-7179
affiliation: 2
- name: Maria Protogerou
affiliation: 4
- name: J. Nathan Kutz
orcid: 0000-0002-6004-2275
affiliation: 3
affiliations:
- name: Department of Electrical and Computer Engineering, University of Washington, USA
index: 1
- name: Department of Mechanical Engineering, University of Nevada, Reno, USA
index: 2
- name: Autodesk Research, London, UK
index: 3
- name: Department of Applied Mathematics, University of Washington, USA
index: 4
date: 1 April 2026
bibliography: paper.bib
---

# Summary

Derivatives of measured data are a prerequisite across science and engineering: identifying governing equations, designing controllers, and processing sensor streams alike. The textbook remedy, finite differencing, amplifies noise as $1/\Delta t$ and deteriorates rapidly as data grows noisier or more finely sampled. Smoothing before differencing helps, but algorithm choice and tuning substantially affect the result, and no single approach wins universally.

PyNumDiff is an open-source Python package consolidating a broad suite of numerical differentiation methods under a unified API. Seven algorithm families are implemented: (1) prefiltering followed by finite difference calculation; (2) iterated finite differencing; (3) polynomial fitting [@savitzky1964]; (4) spectral, radial basis function, and wavelet fitting; (5) total variation regularization [@chartrand2011numerical]; (6) Kalman smoothing [@kalman1960; @rauch1965]; and (7) local approximation with linear models. @ahnert2007 provides a useful taxonomy of these families, distinguishing local methods (which estimate derivatives from a surrounding window) from global methods that fit the entire signal at once. All PyNumDiff methods return a matched pair `(x_hat, dxdt_hat)`. A companion paper [@komarov2025] benchmarks all methods across test signals and guides selection for different application scenarios. This paper describes the package's second generation, the 0.2.x release series.


# Statement of Need

Numerical differentiation has a diverse ecosystem of specialized algorithms, each with different strengths in outlier robustness, computational cost, irregular sampling, or missing observations, but no consolidated home. Without one, practitioners are left assembling solutions piecemeal from disparate packages [@vanBreugel2022].

PyNumDiff addresses this gap. Its unified interface lets users compare methods on the same data, exploit specialized capabilities, and select hyperparameters without ground-truth derivatives. Derivative estimation trades data fidelity against smoothness, and PyNumDiff frames that tradeoff as a multi-objective optimization spanning all methods, with a single scalar weight steering between the two. When a ground-truth derivative is available, it can serve as the optimization target instead [@vanBreugel2020numerical]. A natural and growing application is SINDy [@brunton2016discovering], which discovers governing equations by regressing measured derivatives, making the quality of those estimates a direct determinant of model accuracy.


# State of the Field

Relevant Python tools exist, but none covers PyNumDiff's breadth. `numpy.gradient` and `scipy.signal.savgol_filter` [@virtanen2020scipy] handle only a sliver of the method space; `findiff` offers high-order finite difference stencils suited to clean simulation data, not noisy measurements. Historically, practitioners have had to stitch together PyKalman, PyDMD, and standalone TVR scripts [@chartrand2011numerical] with no shared API or principled way to compare results. The `derivative` package [@derivative_pkg] overlaps substantially but lacks multidimensional support, NaN handling, and hyperparameter optimization. No existing package spans PyNumDiff's seven method families with a consistent interface.

The original PyNumDiff publication [@vanBreugel2022] established the core method set and optimization framework. This second generation rewrites it from the ground up and consolidates it. `kerneldiff`, `rtsdiff`, and `tvrdiff` each cover by parameter what had been separate functions, so the package presents fewer entry points than before while spanning more methods. `rbfdiff`, `waveletdiff`, and `robustdiff` are new, bringing the set to the twelve methods of Table 1, all organized into the seven families above behind one keyword-argument signature. A single optimizer serves every one of them, and four capabilities now span the package, covering multidimensional data, irregular sample spacing, missing observations, and circular domains.


# Software Design

**Package design.** All differentiation methods share the call signature

```python
x_hat, dxdt_hat = method(x, dt_or_t, **params)
```

where `x` is a NumPy array [@harris2020array] of measurements; `dt_or_t` is either a scalar step size or an array of sample locations; and keyword arguments configure the method, making calls self-documenting. Prior positional signatures are preserved with deprecation warnings.

**Software architecture.** PyNumDiff is organized into seven method modules plus shared `utils` and `optimize` modules in a flat structure. Where strong alternatives exist, PyNumDiff delegates rather than reimplements: SciPy [@virtanen2020scipy] provides spline fitting, Savitzky-Golay filtering, and signal processing routines; NumPy [@harris2020array] provides the FFT; PyWavelets [@lee2019pywavelets] provides the discrete wavelet transform for `waveletdiff`; CVXPY [@diamond2016cvxpy] handles convex optimization for `robustdiff` and `tvrdiff` as an optional dependency. The public `kalman_filter` and `rts_smooth` primitives let users with known dynamics bypass `rtsdiff`'s constant-derivative model.

**Method capabilities.** Table 1 groups the twelve non-deprecated methods by capability. All handle multidimensional data via `axis`, and several go further.

| Capability | Methods |
|---|---|
| Multidimensional data | `kerneldiff`, `finitediff`, `polydiff`, `savgoldiff`, `splinediff`, `spectraldiff`, `rbfdiff`, `waveletdiff`, `tvrdiff`, `rtsdiff`, `robustdiff`, `lineardiff` |
| Variable step | `polydiff`, `splinediff`, `rbfdiff`, `rtsdiff`, `robustdiff` |
| Missing data | `polydiff`, `splinediff`, `rtsdiff`, `robustdiff` |
| Outlier robustness | `tvrdiff`, `robustdiff` |
| Circular domain | `rtsdiff` |

Table: Methods by capability.

**Irregular and incomplete sampling.** Methods that support variable step size accept an array of sample locations in place of a scalar step, and Kalman-based methods then compute the transition matrix by matrix exponential at each actual interval. NaN entries are treated as missing observations, excluded from fitting and imputed from the model, so sensors that drop samples need no preprocessing.

**Outlier robustness.** `robustdiff` replaces the quadratic Kalman cost with Huber loss terms on both measurement and process residuals, following @aravkin2013, with CVXPY [@diamond2016cvxpy] as the optimization backend; the sparse problem formulation scales linearly with signal length. `tvrdiff` similarly applies Huber loss on data fidelity; its total variation penalty on the derivative additionally promotes piecewise-smooth solutions for signals with abrupt transitions.

**Circular and wrapped domains.** `rtsdiff` accepts `circular=True` for quantities like angles on a periodic domain. Innovation residuals are wrapped to $[-\pi, \pi]$ before each Kalman update via an `innovation_fn` hook, and `x_hat` is returned in the same range, avoiding the large spurious spikes naive smoothers produce when a signal crosses the $\pm\pi$ boundary.

**Hyperparameter optimization.** `pynumdiff.optimize` selects hyperparameters $\Phi$ by minimizing
$$L(\Phi) = \text{RMSE}\big(\textstyle\int\hat{\dot{x}}(\Phi) + c,\; x\big) + \gamma\,\text{TV}\big(\hat{\dot{x}}(\Phi)\big),$$
which requires no ground truth, because fidelity is measured by integrating the estimated derivative back against the measured signal [@vanBreugel2020numerical]. The smoothness weight $\gamma$, exposed as `tvgamma`, can be initialized automatically from the signal's estimated cutoff frequency. This version robustifies the first term with a Huber penalty so outliers do not bias parameter selection, and it reduces the Kalman parameter space from two independent noise variances to their log-ratio, the only salient factor [@komarov2025]. Categorical and boolean hyperparameters are now supported natively, so discrete choices such as derivative order can be optimized jointly with continuous ones. Repeated evaluations are cached to avoid redundant work. A single call to `suggest_method` runs this search across nearly every method in the package and reports the best fit, which is the practical payoff of putting them all behind one interface.

**Testing and continuous integration.** The test suite validates all methods against analytic functions with known derivatives, covering both noiseless and noisy cases. Care was taken to avoid tautological tests where the implementation directly determines the expected result. GitHub Actions runs the suite on every push and pull request, and Coveralls tracks line coverage, currently 90%.


# Research Impact Statement

The original PyNumDiff paper [@vanBreugel2022] has been applied in experimental biology (flight kinematics from motion capture), control engineering (observer design), and data-driven dynamics identification via SINDy [@brunton2016discovering]. Tutorial notebooks in the repository cover basic usage, hyperparameter optimization, automatic method suggestion, a head-to-head performance analysis across simulations, and demonstrations of the outlier, multidimensional, and circular-domain capabilities described above. Each runs end to end against the released package, so the claims made here can be reproduced directly. Full API documentation is published at [pynumdiff.readthedocs.io](https://pynumdiff.readthedocs.io/master/). The companion Taxonomy paper [@komarov2025], submitted to the Journal of Computational Physics, provides the theoretical underpinning and benchmarks all included methods. The PySINDy project [@pysindy] maintains its own differentiation submodule substantially overlapping with PyNumDiff's capabilities; integration discussions are ongoing.


# AI Usage Disclosure

This paper was drafted with assistance from Claude Sonnet 4.6 and Claude Opus 4.8 (Anthropic), which also implemented successive code revisions to address recent issues and author feedback. All outputs were reviewed and further edited by hand, and the authors take full responsibility for accuracy.


# Acknowledgements

The authors thank Yuying Liu and Bingni W. Brunton for their contributions to the original PyNumDiff package [@vanBreugel2022], and Sasha Aravkin for discussions on convex optimization techniques that informed the robust differentiation methods. This work was supported by the NSF AI Institute in Dynamic Systems (grant number 2112085).


# References