██████╗ ██╗ ██╗███╗ ███╗ ██████╗ ██████╗ █████╗ ██╗
██╔══██╗╚██╗ ██╔╝████╗ ████║██╔═══██╗██╔══██╗██╔══██╗██║
██████╔╝ ╚████╔╝ ██╔████╔██║██║ ██║██║ ██║███████║██║
██╔═══╝ ╚██╔╝ ██║╚██╔╝██║██║ ██║██║ ██║██╔══██║██║
██║ ██║ ██║ ╚═╝ ██║╚██████╔╝██████╔╝██║ ██║███████╗
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
A lightweight, zero-MPI toolkit for extracting coherent structures from spatiotemporal data. Every algorithm fits in a few hundred readable lines—study, tweak, or extend the maths without fighting a framework.
Methods: POD | DMD | SPOD | BSMD | ST-POD (planned)
git clone https://github.com/ricardofrantz/pyModal.git
cd pyModal
pip install -e .This installs pymodal as a package with CLI support.
Optional performance backends:
# Intel MKL (2-10x faster FFTs on Intel CPUs)
pip install mkl_fft
# Apple Silicon (Accelerate framework)
pip install pyobjc-framework-Accelerate
# GPU support
pip install cupy torchfrom pymodal import PODAnalyzer, DMDAnalyzer, SPODAnalyzer
# Load your data
pod = PODAnalyzer(file_path="./data/simulation.mat", n_modes_save=10)
pod.run_analysis()
print(f"Modes shape: {pod.modes.shape}")
print(f"Energy in mode 1: {pod.eigenvalues[0]:.2e}")Run benchmark examples:
python examples/benchmarks_2d.pyThis generates synthetic flows (Double Gyre, Taylor-Green, Cylinder Wake) and runs all methods.
Energy-optimal spatial modes via SVD of mean-subtracted snapshots.
from pymodal import PODAnalyzer
pod = PODAnalyzer(
file_path="data.mat",
n_modes_save=10,
results_dir="./results_pod",
figures_dir="./figs_pod",
)
pod.run_analysis()
# Access results
pod.modes # (Nspace, n_modes) — spatial modes
pod.eigenvalues # (n_modes,) — energy per mode
pod.time_coefficients # (Ns, n_modes) — temporal evolution
pod.temporal_mean # (Nspace,) — mean fieldExtracts eigenvalues/modes of the best-fit linear operator.
from pymodal import DMDAnalyzer
import numpy as np
dmd = DMDAnalyzer(file_path="data.mat", n_modes_save=10)
dmd.load_and_preprocess()
dmd.perform_dmd()
dmd.save_results()
# Eigenvalues encode dynamics
frequencies = np.angle(dmd.eigenvalues) / (2 * np.pi * dmd.dt) # Hz
growth_rates = np.log(np.abs(dmd.eigenvalues)) / dmd.dt # 1/s
# |λ| < 1 → decaying, |λ| > 1 → growing, |λ| = 1 → neutralFrequency-resolved modes for stationary data. Towne et al. (2018)
from pymodal import SPODAnalyzer
spod = SPODAnalyzer(
file_path="data.mat",
nfft=256, # FFT block size
overlap=0.5, # 50% overlap
)
spod.run()
spod.perform_spod()
# Results indexed by frequency
spod.freq # frequency bins (Hz)
spod.eigenvalues[f] # energy at frequency index f
spod.modes[f] # spatial modes at frequency fThird-order interactions revealing nonlinear energy transfer. Nekkanti et al. (2025)
from pymodal import BSMDAnalyzer
# BSMD reuses SPOD's cached FFT blocks
bsmd = BSMDAnalyzer(file_path="data.mat", nfft=256)
bsmd.run()# Run specific method
pymodal pod --data ./data/file.mat --n-modes 20
pymodal spod --data ./data/file.mat --nfft 256 --overlap 0.5
pymodal dmd --data ./data/file.mat
# Run all methods
pymodal all --data ./data/file.mat
# Staged execution
pymodal pod --data ./data/file.mat --compute # compute only
pymodal pod --data ./data/file.mat --plot # plot onlypyModal auto-detects .mat, .h5, and .npz files. Expected structure:
{
'q': np.ndarray, # (Ns, Nspace) — snapshots × flattened spatial points
'dt': float, # time step between snapshots
'Nx': int, # grid points in x
'Ny': int, # grid points in y
'x': np.ndarray, # x-coordinates (optional)
'y': np.ndarray, # y-coordinates (optional)
}Custom data loader:
from pymodal import PODAnalyzer
def my_loader(file_path):
# Load your data however you want
return {
'q': my_data, # (Ns, Nspace)
'dt': 0.01,
'Nx': 100, 'Ny': 50,
'x': x_coords, 'y': y_coords,
}
pod = PODAnalyzer(file_path="ignored", data_loader=my_loader)
pod.run_analysis()Set FFT backend via environment variable:
PYMODAL_FFT_BACKEND=mkl python script.pyOr programmatically:
from pymodal.core.config import load_config
# Load from YAML/JSON
load_config("my_settings.yaml")Available settings (src/pymodal/core/config.py):
| Setting | Default | Options |
|---|---|---|
FFT_BACKEND |
scipy |
scipy, numpy, mkl, accelerate, cupy, torch |
FIG_DPI |
500 |
Any integer |
WINDOW_TYPE |
hamming |
hann, hamming, blackman, etc. |
| Method | Input Assumption | Output | Best For |
|---|---|---|---|
| POD | Any | Energy-ranked modes | Dimensionality reduction, dominant structures |
| DMD | Linear dynamics | Modes + frequencies + growth rates | Stability analysis, forecasting |
| SPOD | Stationary | Frequency-resolved modes | Turbulence, periodic flows |
| BSMD | Nonlinear coupling | Triadic interactions | Energy transfer, nonlinear dynamics |
pyModal/
├── src/pymodal/ # Installable package
│ ├── __init__.py # Exports: PODAnalyzer, DMDAnalyzer, ...
│ ├── pod.py # Proper Orthogonal Decomposition
│ ├── dmd.py # Dynamic Mode Decomposition
│ ├── spod.py # Spectral POD
│ ├── bmsd.py # Bispectral Mode Decomposition
│ ├── cli.py # Command-line interface
│ ├── core/
│ │ ├── base.py # BaseAnalyzer class
│ │ ├── config.py # Global settings
│ │ ├── io.py # Data loaders (.mat, .h5, .npz)
│ │ └── parallel.py # Threading utilities
│ └── fft/
│ ├── fft_backends.py # Backend selection (scipy, mkl, cupy, ...)
│ └── spectral_utils.py # Welch, periodogram, etc.
├── examples/
│ └── benchmarks_2d.py # Double Gyre, Taylor-Green, Cylinder Wake
├── tests/ # 24 unit tests
├── DOC.md # Mathematical details & dev guide
├── pyproject.toml # Package metadata
└── README.md
pytest # Run all tests
pytest -v # Verbose
pytest tests/test_pod.py # Single file| Method | Paper |
|---|---|
| SPOD | Towne, Schmidt & Colonius (2018) — arXiv:1708.04393 |
| BSMD | Nekkanti, Pickering, Schmidt & Colonius (2025) — arXiv:2502.15091 |
| ST-POD | Yeung & Schmidt (2025) — arXiv:2502.09746 (planned) |
MIT — see LICENSE