Metadata-Version: 2.5
Name: pysnape
Version: 0.1.0
Summary: SimultaNeous basis function Approximation and Parameter Estimation for ODE/PDE models
Project-URL: Homepage, https://sutanubhowmick.com
Project-URL: Documentation, https://sutanubhowmick.github.io/pysnape/
Project-URL: Repository, https://github.com/sutanubhowmick/pysnape
Project-URL: Issues, https://github.com/sutanubhowmick/pysnape/issues
Author-email: Sutanu Bhowmick <sutanu@sutanubhowmick.com>
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.10
Requires-Dist: joblib>=1.3
Requires-Dist: numpy>=1.24
Requires-Dist: pyyaml>=6.0
Requires-Dist: scipy>=1.10
Requires-Dist: sympy>=1.12
Provides-Extra: dev
Requires-Dist: matplotlib>=3.7; extra == 'dev'
Requires-Dist: nbstripout>=0.7; extra == 'dev'
Requires-Dist: pre-commit>=3.7; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff==0.16.7; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
Requires-Dist: mkdocs<2,>=1.6; extra == 'docs'
Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
Provides-Extra: mat73
Requires-Dist: h5py>=3.9; extra == 'mat73'
Provides-Extra: plot
Requires-Dist: matplotlib>=3.7; extra == 'plot'
Provides-Extra: sparse
Requires-Dist: scikit-sparse>=0.5.0; extra == 'sparse'
Description-Content-Type: text/markdown

# pysnape

[![CI](https://img.shields.io/github/actions/workflow/status/sutanubhowmick/pysnape/ci.yml?branch=main&label=CI)](https://github.com/sutanubhowmick/pysnape/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](https://github.com/sutanubhowmick/pysnape/blob/main/LICENSE)
[![Python](<https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue.svg>)](https://github.com/sutanubhowmick/pysnape/blob/main/pyproject.toml)
[![Status](https://img.shields.io/badge/status-alpha-orange.svg)](https://github.com/sutanubhowmick/pysnape/blob/main/CHANGELOG.md)
[![PyPI](https://img.shields.io/pypi/v/pysnape.svg)](https://pypi.org/project/pysnape/)
[![Docs](https://img.shields.io/badge/docs-online-blue.svg)](https://sutanubhowmick.github.io/pysnape/)

**pysnape** fits the unknown parameters of an ODE or PDE directly from noisy
measurements, without differentiating the noisy data and without needing
initial or boundary conditions. It represents each measured field as a
B-spline and estimates the spline and the model's parameters together.

The solver, **`Nested`**, works in two levels: for each candidate set of
parameters it fits the spline with the equation as a penalty, and it keeps the
parameters whose spline matches the data best. In our tests it stays accurate
from noise-free data to heavy noise (see the examples). Statisticians know this
structure as profiled estimation (Ramsay et al., 2007).

Two peer-reviewed papers back the method:
[MSSP 2023](https://doi.org/10.1016/j.ymssp.2022.110059) (systems of ODEs and
PDEs, such as Burgers, KdV, Kuramoto–Sivashinsky and coupled oscillators)
and [Engineering Structures 2023](https://doi.org/10.1016/j.engstruct.2023.116231)
(beam vibration identification with unknown boundary conditions, including a
real experiment reconstructed from smartphone video).

## Install

```bash
pip install pysnape
```

For large problems, `pip install "pysnape[sparse]"` adds CHOLMOD, a sparse solver that
is many times faster. It needs the SuiteSparse library first (`brew install
suite-sparse` on macOS, `sudo apt install libsuitesparse-dev` on Debian or Ubuntu);
pysnape works without it. For plots: `pip install "pysnape[plot]"`.

## Quickstart

Write the equation the way you would on paper; pysnape reads the fields and
coordinates from the data, works out which terms are unknown, chooses a basis
and fits. This runs in a few seconds:

```python
import pysnape as ps
from pysnape.data.generators import ode

# Van der Pol with mu = 2, measured with 20 % noise
data = ode.vanderpol(t_end=20.0, n=1001, mu=2.0).add_noise(percent=20, seed=2)
result = ps.fit(
    "x_tt + th1*x_t + th2*x**2*x_t + th3*x = 0",
    data,
    unknowns={"th1": 5.0, "th2": 5.0, "th3": 5.0},  # starting values
    multistart=False,  # the default, True, also tries other starts: slower, safer
)
print(result.summary())  # th1 = -2.06, th2 = 2.06, th3 = 1.02; the truth is (-2, 2, 1)
```

`result.field()` is the fitted signal on the data grid (`result.field(deriv="t")`
its derivative), and `result.plot()` draws it against the measurements (needs
`pip install "pysnape[plot]"`).

How sure are the estimates? For measured data, refit with resampled residuals;
for a synthetic study, refit fresh noise draws on clean data:

```python
boot = ps.bootstrap(result, n=100, jobs=4)
print(boot.summary())  # mean, coefficient of variation and 95 % interval

clean = ps.fit(
    "x_tt + th1*x_t + th2*x**2*x_t + th3*x = 0",
    ode.vanderpol(t_end=20.0, n=1001, mu=2.0),
    unknowns={"th1": 5.0, "th2": 5.0, "th3": 5.0},
    multistart=False,
)
print(ps.ensemble(clean, n=20, noise=5, jobs=4).summary(truth={"th1": -2, "th2": 2, "th3": 1}))
```

The paper cases are ready-made in `ps.models` (Burgers, KdV,
Kuramoto–Sivashinsky, Duffing, Van der Pol, the 2-D wave), as is a chain of
masses, springs and dampers, `ps.models.mdof_chain(n)`. Pass one to `ps.fit` in
place of the equation.

The basis is chosen automatically: about one knot per three grid points, and
an order of the highest derivative plus three. With the equation enforced, the
basis must be fine enough to follow the solution's fastest feature. When unsure,
pass candidates, `knots=[100, 200, 400]`, and `fit` keeps the one that matches
the data best.

## Published data

The PDE-FIND datasets of Rudy et al. (Burgers, KdV, Kuramoto–Sivashinsky) load
in one line:

```python
data = ps.datasets.load("burgers")  # 256 x 101 grid
result = ps.fit(ps.models.burgers(), data)  # about two minutes
print(result.theta)  # theta1 = 0.9998, theta2 = -0.099998; the truth is (1, -0.1)
print(ps.datasets.info("burgers")["citation"])  # please cite the data's source
```

Large or fast-varying fields need the basis set by hand: on the chaotic
Kuramoto–Sivashinsky data, `knots={"x": 100, "t": 60}` on a 524 x 151 window
recovers the parameters within 0.5 %, where the automatic choice does not.
`fit` warns when the fitted fields miss the data by more than 25 %, the usual
sign of a basis too coarse.

## Documentation

**[sutanubhowmick.github.io/pysnape](https://sutanubhowmick.github.io/pysnape/)**:
getting started, writing equations, data, ready-made models, how the solver works,
uncertainty and the API reference. Five [example notebooks](https://github.com/sutanubhowmick/pysnape/tree/main/examples) run from an
oscillator to published data. Every example in the docs and the notebooks is run by the
test suite.

## Status

Alpha: the interface may still change between minor versions. Not yet: complex-valued
fields, a command-line interface with case files, and coefficients that vary in space
or time. See [`CHANGELOG.md`](https://github.com/sutanubhowmick/pysnape/blob/main/CHANGELOG.md). Issue
reports and contributions are welcome; see [`CONTRIBUTING.md`](https://github.com/sutanubhowmick/pysnape/blob/main/CONTRIBUTING.md).

## Citing

If you use this software, please cite the two papers above;
[`CITATION.cff`](https://github.com/sutanubhowmick/pysnape/blob/main/CITATION.cff) has the details (GitHub's "Cite this
repository" button reads it).

## License

Apache License 2.0 — see [`LICENSE`](https://github.com/sutanubhowmick/pysnape/blob/main/LICENSE) and [`NOTICE`](https://github.com/sutanubhowmick/pysnape/blob/main/NOTICE).
