Metadata-Version: 2.4
Name: opendsp
Version: 0.1.0
Summary: Numpy-only DSP primitives for biosignal analysis — Butterworth IIR, filtfilt, find_peaks, wavelets, resampling. scipy.signal-compatible, no scipy required.
Project-URL: Homepage, https://github.com/vitaldb/opendsp
Project-URL: Issues, https://github.com/vitaldb/opendsp/issues
Author-email: Hyung-Chul Lee <vital@snu.ac.kr>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: abp,biosignal,butterworth,dsp,ecg,eeg,filtfilt,find-peaks,ppg,signal-processing,wavelets
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Healthcare Industry
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: numpy>=1.26
Provides-Extra: speed-numba
Requires-Dist: numba>=0.59; extra == 'speed-numba'
Provides-Extra: speed-scipy
Requires-Dist: scipy>=1.11; extra == 'speed-scipy'
Provides-Extra: speed-torch
Requires-Dist: torch>=2.0; extra == 'speed-torch'
Requires-Dist: torchaudio>=2.0; extra == 'speed-torch'
Description-Content-Type: text/markdown

# opendsp

Numpy-only DSP primitives for biosignal analysis. A small, focused
`scipy.signal` / `PyWavelets` subset implemented in pure NumPy, with
optional backends (scipy, numba, torchaudio) for speed when available.

`opendsp` is the shared DSP layer underneath the open biosignal family
([`openvital`](https://pypi.org/project/openvital/),
[`openecg`](https://pypi.org/project/openecg/),
[`openeeg`](https://pypi.org/project/openeeg/)). The primitives here are
modality-agnostic — bandpass filtering, peak-finding, wavelets,
resampling — and don't belong to any single signal type.

## Why

`scipy.signal` is excellent but heavy: scipy + its scipy.sparse / BLAS
backing pulls 30+ MB and is awkward to install in stripped-down
inference environments (Lambda, edge boxes, TFLite runners). `opendsp`
matches scipy's coefficient output to within ~1e-12 on the filter
orders biosignal pipelines actually use (2 and 4), so you can keep your
filter design code identical while shrinking your deploy image.

## Installation

```bash
pip install opendsp

# Optional speed backends (auto-selected on first lfilter call):
pip install opendsp[speed-scipy]   # ~10x speedup, C-backed
pip install opendsp[speed-numba]   # near-C after JIT warmup
pip install opendsp[speed-torch]   # GPU/MKL via torchaudio
```

Set `OPENDSP_LFILTER_BACKEND` (one of `scipy`, `numba`, `torch`,
`numpy`) to force a specific backend — useful for benchmarking or
reproducibility-sensitive contexts.

## API

```python
import opendsp

# Butterworth IIR design (scipy.signal.butter drop-in)
b, a = opendsp.butter(4, 0.1, btype="low")
b, a = opendsp.butter(4, [0.01, 0.4], btype="band")

# Direct filtering
y = opendsp.lfilter(b, a, x)
y = opendsp.filtfilt(b, a, x)   # zero-phase forward-backward

# Convenience bandpass (combines butter + filtfilt)
y = opendsp.band_pass(x, srate=128, fl=0.5, fh=30, order=4)

# Peak finding (scipy.signal.find_peaks subset)
peaks, props = opendsp.find_peaks(x, height=0.5, distance=10,
                                   prominence=0.1)

# Wavelets (PyWavelets minimal subset)
coeffs = opendsp.wavedec(x, "db2", level=4)
y = opendsp.waverec(coeffs, "db2")
cwt = opendsp.cwt(x, scales=[1, 2, 4, 8], wavelet="gaus1")

# Resampling
y = opendsp.resample(x, dest_len=1000)
y = opendsp.resample_hz(x, srate_from=500, srate_to=128)

# Misc primitives biosignal pipelines need
y = opendsp.interp_undefined(x)             # NaN-aware linear interp
y = opendsp.savitzky_golay(x, window_size=91, order=3)
y = opendsp.moving_average(x, N=10)
y = opendsp.rank_normalize(x)               # amplitude-invariant
y = opendsp.remove_baseline_wander(x, fs=500, cutoff_hz=0.5)
```

## Algorithms

- **Butterworth**: analog prototype poles → bilinear-transform
  pre-warping → digital (z, p, k) → polynomial (b, a). Matches scipy
  exactly because both follow Oppenheim & Schafer §7.1.
- **filtfilt**: reflect-pad ↔ forward ↔ reverse ↔ forward ↔ reverse ↔
  trim, with initial conditions set via the `lfilter_zi` steady-state
  method.
- **find_peaks**: strict-left/non-strict-right local maxima (scipy's
  convention), height filter, greedy-from-largest distance filter,
  Wim Spalt's two-sided prominence walk.
- **wavedec/waverec**: DWT with `mode='symmetric'` boundary extension
  (PyWavelets default). Daubechies-2 ships built-in; pass a 4-tuple
  `(lo_d, hi_d, lo_r, hi_r)` for any other wavelet.
- **cwt**: Gaussian 1st derivative continuous wavelet transform — the
  one used by ECG annotators. Other mother wavelets: pass `pywt`.

## Scope

`opendsp` is intentionally narrow:

- ✓ Linear-time-invariant filter design + application
- ✓ Peak / extremum detection
- ✓ Wavelet decomposition (DWT + CWT)
- ✓ Resampling, smoothing, interpolation primitives

What it deliberately does **not** include:

- ✗ Spectral analysis higher than 1-D periodogram / FFT helpers (use
  scipy.signal.welch / numpy.fft directly)
- ✗ Adaptive filters, Kalman, ML-based denoising
- ✗ Modality-specific features (QRS, BSR, SEF95 etc. — those live in
  `openecg`, `openeeg`, `openvital`)

If you find yourself reaching for one of those, you probably want
`scipy.signal` or one of the modality libs above.

## License

Apache-2.0. See `LICENSE`.
