Metadata-Version: 2.4
Name: wavedet
Version: 0.1.0
Summary: Wavelet-based ECG delineator, ported from ecg-kit MATLAB with output parity
Project-URL: Homepage, https://github.com/xdandys/wavedet-py
Project-URL: Repository, https://github.com/xdandys/wavedet-py
Author-email: Daniel Strnad <strnadda@gmail.com>
License-Expression: GPL-2.0-only
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: numpy>=2.1
Requires-Dist: scipy>=1.14
Provides-Extra: examples
Requires-Dist: matplotlib>=3.8; extra == 'examples'
Description-Content-Type: text/markdown

# wavedet

A Python port of `wavedet`, the wavelet-based ECG delineator from
[ecg-kit](https://github.com/marianux/ecg-kit) (GPL-2.0, upstream dead since 2020-02). It marks
P/QRS/T onsets, peaks, and offsets. Nothing else from the kit is ported.

![wavedet delineation of QTDB sel16265, lead 0 — P/QRS/T peaks as dots, on/off boundaries as dashed lines](examples/annotated_ecg.png)

```python
from wavedet import delineate

d = delineate(signal, fs=250)         # 1-D → single lead
d = delineate(signal_12lead, fs=500)  # 2-D (n, leads) → SLR multilead fusion
d.qrs, d.qrs_on, d.t_off, d.p_on      # np.ndarray[float], NaN = not found
d.per_lead[0].t_off                   # single-lead results retained
```

## Parity

The goal is **output parity with the MATLAB original**, not per-function parity. The pass/fail bar,
per mark type per record: ≥99% of marks bit-identical to MATLAB, 100% within ±1 sample, and an
identical NaN pattern. `tests/test_parity.py` is that gate; it runs offline against committed
goldens, so it stays runnable without MATLAB. [PARITY.md](PARITY.md) reports the full-database
comparison — the whole of QTDB + LUDB against MATLAB, with per-fiducial deviations and the
wavelet-transform ulp that is the root cause of every divergence.

## Development

```
uv run ruff check
uv run ruff format --check
uv run mypy src/
uv run pytest                  # gate against committed goldens
```

The goldens and the signals they were made from are both committed, so the gate needs neither
MATLAB nor network. Because the gate is bit-exact and the wavelet transform's dot products route
through BLAS, `conftest.py` pins OpenBLAS to a fixed, CPU-independent kernel
(`OPENBLAS_CORETYPE=Prescott`); without it the last ~1e-14 — and so a few knife-edge marks — would
vary by host CPU. `uv run pytest --full-oracle` additionally runs the port over the whole of
QTDB + LUDB as a robustness sweep (downloads the databases via `wfdb`; no goldens).

## Scope

Ported: single-lead wavedet, and SLR multilead (per-lead delineation fused by a k-nearest-neighbour
rule; needs ≥3 leads).

**Not ported: the VCG/3D spatial loop** (`delineate3D`, `delineateP3D`, `twave3D`, `optimline`).
It is dead code upstream and cannot be fed. Reaching it needs three commented-out regions of
`wavedet_3D.m` restored, and even then there is no input path: `leadsynth_flag` is hardcoded to 0,
and `leadcalc.m` — the Dower/Levkov transform that would synthesize the orthogonal XYZ leads it
requires — is absent from the kit entirely. Leads I/II/III are not a substitute, since III = II − I
makes the loop rank-2 and degenerates `optimline`'s fit. `optimline` also depends on `lsqnonlin`,
which has no bit-identical scipy equivalent, so its marks could not clear the parity gate anyway.

## Authorship

Most of this port was written by an AI model (Claude Opus 4.8), under human direction and review.
The parity gate and the [PARITY.md](PARITY.md) full-database comparison are how that work was held
to the MATLAB original.

## License

GPL-2.0-only, inherited from ecg-kit. Its `LICENSE.txt` is the plain GPL-2 text with no "or later"
grant, so this derived work cannot relicense upward.
