Skip to content

Data

ps.Data holds measurements on a grid.

import numpy as np
import pysnape as ps

x = np.linspace(-1.0, 1.0, 41)
t = np.linspace(0.0, 2.0, 21)
X, T = np.meshgrid(x, t, indexing="ij")
data = ps.Data(fields={"u": np.sin(np.pi * (X - T))}, coords={"x": x, "t": t})
print(data)
Argument What it holds
fields each measured field: an array with one axis per coordinate, in coordinate order (above, u[i, j] is at x[i], t[j])
coords the grid: one strictly increasing 1-D array per coordinate; the order of the keys is the order of the axes
inputs known signals on the same grid, such as an applied force, used by name in equations
observe what was measured when it is not the field itself: {"x1": "tt"} means the array under x1 holds accelerations
mask a boolean array the shape of the grid: which points were measured; None means all

Use np.meshgrid(..., indexing="ij") to build fields from formulas, so the axes come out in coordinate order.

Accelerations and other derivatives

Accelerometers measure x_tt, not x. Say so with observe, and fit the displacement equation unchanged:

from pysnape.data.generators import ode

acc = ode.mdof_chain(n_dof=3, t_end=30.0, n=751, observe="acceleration")
print(acc.observe)  # {'x1': 'tt', 'x2': 'tt', 'x3': 'tt'}
result = ps.fit(ps.models.mdof_chain(3), acc, multistart=False, weights=(1, 10, 100, 1000))
print(result.theta)  # k = 12, 8, 4 and c = 0.6, 0.4, 0.2

The constant and linear parts of a displacement are invisible in its acceleration; the equation pins them.

Missing points

Set mask to the points you measured. The equation is still enforced everywhere, so the fitted field fills the gaps. subsample makes such a mask at random, for experiments:

sparse = data.subsample(0.5, seed=1)  # half the x samples times half the t samples
print(sparse.n_observed, "of", data.n_points)

Noise

add_noise adds white Gaussian noise. The level is a percentage, the root-mean-square of the noise over that of the signal, or a signal-to-noise ratio in decibels:

noisy = data.add_noise(percent=5, seed=0)  # the same as snr_db=26.02

One seed names one noise realisation of the whole dataset, which is what ensemble loops over.

Files

import tempfile
from pathlib import Path

path = Path(tempfile.mkdtemp()) / "run.npz"
data.save(path)  # everything in one .npz
again = ps.Data.load(path)

Arrays from MATLAB or a CSV file with other names: map them.

# not run: needs your file
data = ps.Data.from_file("experiment.mat", fields={"u": "usol"}, coords={"x": "x", "t": "t"})

.mat files up to version 7.3 are read (7.3 needs pip install "pysnape[mat73]").

Published datasets

ps.datasets fetches the PDE-FIND datasets of Rudy et al. (Burgers, Korteweg–de Vries, Kuramoto–Sivashinsky). Each file is downloaded once from the authors' repository into ~/.cache/pysnape (or $PYSNAPE_DATA), checked against its sha256, and never redistributed with pysnape. The datasets belong to their authors: use them according to their terms, and cite them:

# not run: downloads on first use
print(ps.datasets.available())  # ['burgers', 'kdv', 'ks']
data = ps.datasets.load("burgers")
print(ps.datasets.info("burgers")["citation"])  # please cite the data's source

Generated data

pysnape.data.generators integrates the built-in systems accurately, for experiments with a known truth: ode.duffing, ode.vanderpol, ode.mdof_chain and pde2d.wave. See Ready-made models.