ecgdatakit 2.0.2__tar.gz → 2.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ecgdatakit-2.0.2/src/ecgdatakit.egg-info → ecgdatakit-2.0.3}/PKG-INFO +1 -1
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/__init__.py +1 -1
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/parser.py +69 -28
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/parsers/xle.py +46 -8
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/nn/weights/deepfade_exp_1_ddp.pt +1 -1
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3/src/ecgdatakit.egg-info}/PKG-INFO +1 -1
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_parser.py +41 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_xle.py +45 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/LICENSE +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/MANIFEST.in +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/README.md +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/pyproject.toml +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/setup.cfg +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/__init__.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/_catalog.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/_codes.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/_daemon.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/_engine.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/_filenames.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/_identity.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/_shell.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/_stats.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/_verify.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/formats/__init__.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/formats/_base.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/formats/_xml.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/formats/alivecor_kardia.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/formats/dicom_waveform.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/formats/edf.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/formats/ge_muse_xml.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/formats/hl7_aecg.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/formats/ishne_holter.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/formats/mfer.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/formats/mortara_el250.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/formats/scp_ecg.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/formats/sierra_xml.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/formats/wfdb.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/anonymize/formats/xle.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/cli.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/exceptions.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/models.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/__init__.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/batch.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/codecs/__init__.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/codecs/lzw.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/codecs/xli.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/helpers/__init__.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/helpers/labels.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/helpers/record.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/helpers/waveform_text.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/helpers/xml.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/parsers/__init__.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/parsers/alivecor_kardia.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/parsers/dicom_waveform.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/parsers/edan_arc.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/parsers/edf.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/parsers/ge_muse_xml.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/parsers/hl7_aecg.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/parsers/ishne_holter.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/parsers/mfer.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/parsers/mortara_el250.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/parsers/scp_ecg.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/parsers/sierra_xml.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/parsing/parsers/wfdb.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/plotting/__init__.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/plotting/_core.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/plotting/interactive.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/plotting/static.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/__init__.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/_core.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/clean.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/denoise.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/filters.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/hrv.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/leads.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/nn/__init__.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/nn/deepfade.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/nn/dense_net.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/nn/layers.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/normalize.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/peaks.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/quality.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/resample.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/transforms.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/py.typed +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit.egg-info/SOURCES.txt +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit.egg-info/dependency_links.txt +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit.egg-info/entry_points.txt +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit.egg-info/requires.txt +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit.egg-info/top_level.txt +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/__init__.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/conftest.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_alivecor_kardia.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_anonymize.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_clean.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_codecs.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_denoise.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_dicom_waveform.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_dispatch.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_edan_arc.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_edf.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_filters.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_ge_muse_xml.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_hl7_aecg.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_hrv.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_ishne_holter.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_leads.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_mfer.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_models.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_mortara_el250.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_normalize.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_numpy_input.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_peaks.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_peaks_shannon.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_plotting_interactive.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_plotting_static.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_quality.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_resample.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_scp_ecg.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_sierra_xml.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_transforms.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_wfdb.py +0 -0
- {ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/tests/test_xml_helpers.py +0 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import dataclasses
|
|
6
|
+
import functools
|
|
6
7
|
import importlib
|
|
7
8
|
import pkgutil
|
|
8
9
|
import warnings
|
|
@@ -52,31 +53,35 @@ class Parser(ABC):
|
|
|
52
53
|
...
|
|
53
54
|
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
@functools.cache
|
|
57
|
+
def _discover() -> tuple[type[Parser], ...]:
|
|
58
|
+
"""Every Parser subclass of ``ecgdatakit.parsing.parsers``, in
|
|
59
|
+
detection order."""
|
|
60
|
+
package = importlib.import_module("ecgdatakit.parsing.parsers")
|
|
61
|
+
found: list[type[Parser]] = []
|
|
62
|
+
for _, name, _ in pkgutil.iter_modules(package.__path__):
|
|
63
|
+
module = importlib.import_module(f"ecgdatakit.parsing.parsers.{name}")
|
|
64
|
+
for attr in vars(module).values():
|
|
65
|
+
if (
|
|
66
|
+
isinstance(attr, type)
|
|
67
|
+
and issubclass(attr, Parser)
|
|
68
|
+
and attr is not Parser
|
|
69
|
+
):
|
|
70
|
+
found.append(attr)
|
|
71
|
+
return tuple(sorted(found, key=lambda cls: (cls.PRIORITY, cls.__name__)))
|
|
57
72
|
|
|
58
|
-
def __init__(self) -> None:
|
|
59
|
-
self._parsers: list[type[Parser]] = []
|
|
60
|
-
self._discover_parsers()
|
|
61
73
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
isinstance(attr, type)
|
|
70
|
-
and issubclass(attr, Parser)
|
|
71
|
-
and attr is not Parser
|
|
72
|
-
):
|
|
73
|
-
self._parsers.append(attr)
|
|
74
|
-
self._parsers.sort(key=lambda cls: (cls.PRIORITY, cls.__name__))
|
|
74
|
+
class FileParser:
|
|
75
|
+
"""Auto-discovers parsers and dispatches files to the right one.
|
|
76
|
+
|
|
77
|
+
No method needs an instance: ``FileParser.parse(path)`` and
|
|
78
|
+
``FileParser().parse(path)`` do the same.
|
|
79
|
+
"""
|
|
75
80
|
|
|
76
81
|
@property
|
|
77
82
|
def parsers(self) -> list[type[Parser]]:
|
|
78
83
|
"""List of discovered :class:`Parser` subclasses."""
|
|
79
|
-
return list(
|
|
84
|
+
return list(_discover())
|
|
80
85
|
|
|
81
86
|
@staticmethod
|
|
82
87
|
def supported_formats() -> list[dict[str, str | list[str]]]:
|
|
@@ -112,8 +117,49 @@ class FileParser:
|
|
|
112
117
|
for p in parsers
|
|
113
118
|
]
|
|
114
119
|
|
|
120
|
+
@staticmethod
|
|
121
|
+
def detect_format(file_path: str | Path) -> dict | None:
|
|
122
|
+
"""Detect the format of an ECG file without parsing it.
|
|
123
|
+
|
|
124
|
+
Returns the format and the parser :meth:`parse` would use, or
|
|
125
|
+
``None`` when no parser recognises the file. The dict holds
|
|
126
|
+
``name``, ``description`` and ``extensions`` (as in
|
|
127
|
+
:meth:`supported_formats`), ``parser`` (the class name, e.g.
|
|
128
|
+
``"SCPECGParser"``) and ``parser_class`` (the class itself)::
|
|
129
|
+
|
|
130
|
+
fmt = FileParser.detect_format("ecg.scp")
|
|
131
|
+
record = fmt["parser_class"]().parse("ecg.scp")
|
|
132
|
+
|
|
133
|
+
Raises
|
|
134
|
+
------
|
|
135
|
+
FileNotFoundError
|
|
136
|
+
If the file does not exist.
|
|
137
|
+
"""
|
|
138
|
+
path = Path(file_path)
|
|
139
|
+
if not path.exists():
|
|
140
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
141
|
+
parser_cls = FileParser._find_parser(path)
|
|
142
|
+
if parser_cls is None:
|
|
143
|
+
return None
|
|
144
|
+
return {
|
|
145
|
+
"name": parser_cls.FORMAT_NAME or parser_cls.__name__,
|
|
146
|
+
"description": parser_cls.FORMAT_DESCRIPTION or (parser_cls.__doc__ or "").strip(),
|
|
147
|
+
"extensions": list(parser_cls.FILE_EXTENSIONS),
|
|
148
|
+
"parser": parser_cls.__name__,
|
|
149
|
+
"parser_class": parser_cls,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _find_parser(path: Path) -> type[Parser] | None:
|
|
154
|
+
"""First parser, in detection order, that recognises *path*."""
|
|
155
|
+
header = b""
|
|
156
|
+
if path.is_file():
|
|
157
|
+
with open(path, "rb") as f:
|
|
158
|
+
header = f.read(_SNIFF_SIZE)
|
|
159
|
+
return next((p for p in _discover() if p.can_parse(path, header)), None)
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
115
162
|
def parse(
|
|
116
|
-
self,
|
|
117
163
|
file_path: str | Path,
|
|
118
164
|
auto_scale: bool = True,
|
|
119
165
|
units: str = "mV",
|
|
@@ -159,12 +205,7 @@ class FileParser:
|
|
|
159
205
|
path = Path(file_path)
|
|
160
206
|
if not path.exists():
|
|
161
207
|
raise FileNotFoundError(f"File not found: {path}")
|
|
162
|
-
|
|
163
|
-
if path.is_file():
|
|
164
|
-
with open(path, "rb") as f:
|
|
165
|
-
header = f.read(_SNIFF_SIZE)
|
|
166
|
-
|
|
167
|
-
parser_cls = next((p for p in self._parsers if p.can_parse(path, header)), None)
|
|
208
|
+
parser_cls = FileParser._find_parser(path)
|
|
168
209
|
if parser_cls is None:
|
|
169
210
|
raise UnsupportedFormatError(f"No parser found for: {path.name}")
|
|
170
211
|
|
|
@@ -174,7 +215,7 @@ class FileParser:
|
|
|
174
215
|
try:
|
|
175
216
|
with warnings.catch_warnings(record=True) as caught:
|
|
176
217
|
warnings.simplefilter("always")
|
|
177
|
-
record =
|
|
218
|
+
record = FileParser._run_parser(parser_cls, path)
|
|
178
219
|
if record.leads and all(len(lead.samples) == 0 for lead in record.leads):
|
|
179
220
|
raise CorruptedFileError(
|
|
180
221
|
f"{parser_cls.FORMAT_NAME or parser_cls.__name__}: "
|
|
@@ -182,7 +223,7 @@ class FileParser:
|
|
|
182
223
|
)
|
|
183
224
|
fill_signal_summary(record)
|
|
184
225
|
if auto_scale:
|
|
185
|
-
record =
|
|
226
|
+
record = FileParser._auto_scale(record, target)
|
|
186
227
|
finally:
|
|
187
228
|
for w in caught:
|
|
188
229
|
warnings.warn(w.message, w.category, stacklevel=2)
|
|
@@ -33,8 +33,11 @@ Not stored in the file, and taken from outside it:
|
|
|
33
33
|
|
|
34
34
|
``XLEParser(wilson_correction=True)`` refers the chest leads to Wilson's
|
|
35
35
|
central terminal, ``V = CR - (I + II) / 3``, and names them ``V1`` to
|
|
36
|
-
``V6``, with a warning. Leads III, aVR, aVL and aVF are not stored
|
|
37
|
-
|
|
36
|
+
``V6``, with a warning. Leads III, aVR, aVL and aVF are not stored.
|
|
37
|
+
``XLEParser(wilson_correction=True, compute_missing_leads=True)`` also
|
|
38
|
+
computes them from I and II (Einthoven and Goldberger), with a warning,
|
|
39
|
+
so the record holds the standard 12 leads. ``compute_missing_leads``
|
|
40
|
+
requires ``wilson_correction``.
|
|
38
41
|
"""
|
|
39
42
|
|
|
40
43
|
from __future__ import annotations
|
|
@@ -150,10 +153,27 @@ class XLEParser(Parser):
|
|
|
150
153
|
FILE_EXTENSIONS = [_EXT]
|
|
151
154
|
PRIORITY = 45
|
|
152
155
|
|
|
153
|
-
def __init__(self, wilson_correction: bool = False
|
|
156
|
+
def __init__(self, wilson_correction: bool = False,
|
|
157
|
+
compute_missing_leads: bool = False) -> None:
|
|
154
158
|
"""*wilson_correction*: refer the chest leads, stored against the
|
|
155
|
-
right arm, to Wilson's central terminal (experimental).
|
|
159
|
+
right arm, to Wilson's central terminal (experimental).
|
|
160
|
+
*compute_missing_leads*: add III, aVR, aVL and aVF, computed from
|
|
161
|
+
I and II. Requires *wilson_correction*, so that the record holds
|
|
162
|
+
the standard 12 leads.
|
|
163
|
+
|
|
164
|
+
Raises
|
|
165
|
+
------
|
|
166
|
+
ValueError
|
|
167
|
+
If *compute_missing_leads* is set without *wilson_correction*.
|
|
168
|
+
"""
|
|
169
|
+
if compute_missing_leads and not wilson_correction:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
"compute_missing_leads=True requires wilson_correction=True: "
|
|
172
|
+
"the 12 standard leads need the chest leads referred to "
|
|
173
|
+
"Wilson's central terminal."
|
|
174
|
+
)
|
|
156
175
|
self.wilson_correction = wilson_correction
|
|
176
|
+
self.compute_missing_leads = compute_missing_leads
|
|
157
177
|
|
|
158
178
|
@staticmethod
|
|
159
179
|
def can_parse(file_path: Path, header: bytes) -> bool:
|
|
@@ -197,7 +217,24 @@ class XLEParser(Parser):
|
|
|
197
217
|
)
|
|
198
218
|
samples[:, 2:] -= (samples[:, :1] + samples[:, 1:2]) / 3
|
|
199
219
|
chest = "V"
|
|
200
|
-
|
|
220
|
+
lead_i, lead_ii = samples[:, 0], samples[:, 1]
|
|
221
|
+
leads = [("I", lead_i), ("II", lead_ii)]
|
|
222
|
+
computed: list[str] = []
|
|
223
|
+
if self.compute_missing_leads:
|
|
224
|
+
warnings.warn(
|
|
225
|
+
"Leads III, aVR, aVL and aVF are not stored in the XLE file: "
|
|
226
|
+
"they were computed from leads I and II.",
|
|
227
|
+
UserWarning,
|
|
228
|
+
stacklevel=2,
|
|
229
|
+
)
|
|
230
|
+
leads += [
|
|
231
|
+
("III", lead_ii - lead_i),
|
|
232
|
+
("aVR", -(lead_i + lead_ii) / 2),
|
|
233
|
+
("aVL", lead_i - lead_ii / 2),
|
|
234
|
+
("aVF", lead_ii - lead_i / 2),
|
|
235
|
+
]
|
|
236
|
+
computed = ["III", "aVR", "aVL", "aVF"]
|
|
237
|
+
leads += [(f"{chest}{k}", samples[:, k + 1]) for k in range(1, 7)]
|
|
201
238
|
|
|
202
239
|
fields = _fields(data)
|
|
203
240
|
patient = PatientInfo(
|
|
@@ -215,10 +252,10 @@ class XLEParser(Parser):
|
|
|
215
252
|
start, date_text, time_text = _recording_date(data)
|
|
216
253
|
record.recording.date = start
|
|
217
254
|
record.recording.duration = timedelta(seconds=frames / _SAMPLING_RATE)
|
|
218
|
-
for
|
|
255
|
+
for label, values in leads:
|
|
219
256
|
record.leads.append(Lead(
|
|
220
|
-
label=
|
|
221
|
-
samples=np.ascontiguousarray(
|
|
257
|
+
label=label,
|
|
258
|
+
samples=np.ascontiguousarray(values),
|
|
222
259
|
sampling_rate=_SAMPLING_RATE,
|
|
223
260
|
resolution=_RESOLUTION_UV,
|
|
224
261
|
resolution_unit="uV",
|
|
@@ -244,6 +281,7 @@ class XLEParser(Parser):
|
|
|
244
281
|
meta["wilson_correction"] = self.wilson_correction
|
|
245
282
|
meta["chest_lead_reference"] = (
|
|
246
283
|
"Wilson central terminal" if self.wilson_correction else "right arm")
|
|
284
|
+
meta["computed_leads"] = computed
|
|
247
285
|
meta["header_size"] = _HEADER_SIZE
|
|
248
286
|
meta["samples_per_channel"] = frames
|
|
249
287
|
meta["wraps_undone"] = wraps
|
{ecgdatakit-2.0.2 → ecgdatakit-2.0.3}/src/ecgdatakit/processing/nn/weights/deepfade_exp_1_ddp.pt
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
|
-
name: ecgdatakit-2.0.
|
|
2
|
+
name: ecgdatakit-2.0.3/src/ecgdatakit/processing/nn/weights/deepfade_exp_1_ddp.pt
|
|
3
3
|
size: 64288532 bytes
|
|
4
4
|
sha256: 80be2980936c57c00106dec23c01c9f51089d5dce724d4c99938dad5b2b92f13
|
|
@@ -77,6 +77,47 @@ class TestFileParserDiscovery:
|
|
|
77
77
|
with pytest.raises(UnsupportedFormatError):
|
|
78
78
|
fp.parse(unknown)
|
|
79
79
|
|
|
80
|
+
def test_parse_needs_no_instance(self, mortara_file: Path):
|
|
81
|
+
a = FileParser.parse(mortara_file)
|
|
82
|
+
b = FileParser().parse(mortara_file)
|
|
83
|
+
assert a.file_format.parser == b.file_format.parser == "MortaraEL250Parser"
|
|
84
|
+
assert [lead.label for lead in a.leads] == [lead.label for lead in b.leads]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestDetectFormat:
|
|
88
|
+
@pytest.mark.parametrize("fixture, parser", [
|
|
89
|
+
("hl7_aecg_file", "HL7aECGParser"),
|
|
90
|
+
("mortara_file", "MortaraEL250Parser"),
|
|
91
|
+
("ishne_file", "ISHNEHolterParser"),
|
|
92
|
+
("edf_file", "EDFParser"),
|
|
93
|
+
("scp_ecg_file", "SCPECGParser"),
|
|
94
|
+
("ge_muse_xml_file", "GEMuseXMLParser"),
|
|
95
|
+
("wfdb_file", "WFDBParser"),
|
|
96
|
+
("mfer_file", "MFERParser"),
|
|
97
|
+
])
|
|
98
|
+
def test_detects_the_parser_parse_uses(self, fixture, parser, request):
|
|
99
|
+
path = request.getfixturevalue(fixture)
|
|
100
|
+
fmt = FileParser.detect_format(path)
|
|
101
|
+
assert fmt["parser"] == parser
|
|
102
|
+
assert fmt["parser_class"].__name__ == parser
|
|
103
|
+
assert fmt["name"] == fmt["parser_class"].FORMAT_NAME
|
|
104
|
+
assert fmt["extensions"] == fmt["parser_class"].FILE_EXTENSIONS
|
|
105
|
+
assert FileParser.parse(path).file_format.parser == parser
|
|
106
|
+
|
|
107
|
+
def test_suggested_class_parses_the_file(self, mortara_file: Path):
|
|
108
|
+
fmt = FileParser.detect_format(mortara_file)
|
|
109
|
+
record = fmt["parser_class"]().parse(mortara_file)
|
|
110
|
+
assert record.leads
|
|
111
|
+
|
|
112
|
+
def test_unknown_format_gives_none(self, tmp_path: Path):
|
|
113
|
+
unknown = tmp_path / "data.xyz"
|
|
114
|
+
unknown.write_text("not an ecg file")
|
|
115
|
+
assert FileParser.detect_format(unknown) is None
|
|
116
|
+
|
|
117
|
+
def test_missing_file(self):
|
|
118
|
+
with pytest.raises(FileNotFoundError):
|
|
119
|
+
FileParser.detect_format("/nonexistent/file.xml")
|
|
120
|
+
|
|
80
121
|
|
|
81
122
|
class TestCanParse:
|
|
82
123
|
# --- Original 4 parsers ---
|
|
@@ -20,6 +20,7 @@ FIELDS = (("Given name", 2673), ("Family name", 2712), ("ID", 2761), ("Birth Dat
|
|
|
20
20
|
DATE_OFFSET, TIME_OFFSET = 182972, 182982
|
|
21
21
|
CR_LEADS = ["I", "II", "CR1", "CR2", "CR3", "CR4", "CR5", "CR6"]
|
|
22
22
|
V_LEADS = ["I", "II", "V1", "V2", "V3", "V4", "V5", "V6"]
|
|
23
|
+
TWELVE_LEADS = ["I", "II", "III", "aVR", "aVL", "aVF", "V1", "V2", "V3", "V4", "V5", "V6"]
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def _swap16(data: bytes) -> bytes:
|
|
@@ -164,6 +165,50 @@ class TestXLE:
|
|
|
164
165
|
assert meta["lead_order_stated"] is False
|
|
165
166
|
assert meta["wilson_correction"] is False
|
|
166
167
|
assert meta["chest_lead_reference"] == "right arm"
|
|
168
|
+
assert meta["computed_leads"] == []
|
|
169
|
+
|
|
170
|
+
def test_compute_missing_leads(self, tmp_path):
|
|
171
|
+
signal = ecg_like()
|
|
172
|
+
path = make_xle(tmp_path / "rec.xle", signal)
|
|
173
|
+
record, messages = caught_messages(
|
|
174
|
+
XLEParser(wilson_correction=True, compute_missing_leads=True).parse, path)
|
|
175
|
+
assert has(messages, "computed from leads I and II")
|
|
176
|
+
leads = {lead.label: lead.samples for lead in record.leads}
|
|
177
|
+
assert list(leads) == TWELVE_LEADS
|
|
178
|
+
i, ii = signal[:, 0], signal[:, 1]
|
|
179
|
+
np.testing.assert_allclose(leads["III"], ii - i)
|
|
180
|
+
np.testing.assert_allclose(leads["aVR"], -(i + ii) / 2)
|
|
181
|
+
np.testing.assert_allclose(leads["aVL"], i - ii / 2)
|
|
182
|
+
np.testing.assert_allclose(leads["aVF"], ii - i / 2)
|
|
183
|
+
# stored limb leads unchanged
|
|
184
|
+
np.testing.assert_array_equal(leads["I"], i)
|
|
185
|
+
np.testing.assert_array_equal(leads["II"], ii)
|
|
186
|
+
assert record.raw_metadata["computed_leads"] == ["III", "aVR", "aVL", "aVF"]
|
|
187
|
+
# computed leads are converted with the same scale
|
|
188
|
+
mv = record.to_physical().convert_units("mV")
|
|
189
|
+
np.testing.assert_allclose(mv.leads[2].samples, (ii - i) * 0.9375 / 1000)
|
|
190
|
+
|
|
191
|
+
def test_missing_leads_require_wilson(self):
|
|
192
|
+
with pytest.raises(ValueError, match="requires wilson_correction=True"):
|
|
193
|
+
XLEParser(compute_missing_leads=True)
|
|
194
|
+
|
|
195
|
+
def test_standard_twelve_leads(self, tmp_path):
|
|
196
|
+
signal = ecg_like()
|
|
197
|
+
path = make_xle(tmp_path / "rec.xle", signal)
|
|
198
|
+
record, messages = caught_messages(
|
|
199
|
+
XLEParser(wilson_correction=True, compute_missing_leads=True).parse, path)
|
|
200
|
+
assert [lead.label for lead in record.leads] == TWELVE_LEADS
|
|
201
|
+
assert has(messages, "Wilson correction applied")
|
|
202
|
+
assert has(messages, "computed from leads I and II")
|
|
203
|
+
wct = (signal[:, 0] + signal[:, 1]) / 3
|
|
204
|
+
np.testing.assert_allclose(record.leads[6].samples, signal[:, 2] - wct)
|
|
205
|
+
# Einthoven: I + III = II
|
|
206
|
+
np.testing.assert_allclose(record.leads[0].samples + record.leads[2].samples,
|
|
207
|
+
record.leads[1].samples)
|
|
208
|
+
# Goldberger: aVR + aVL + aVF = 0
|
|
209
|
+
np.testing.assert_allclose(
|
|
210
|
+
record.leads[3].samples + record.leads[4].samples + record.leads[5].samples, 0,
|
|
211
|
+
atol=1e-9)
|
|
167
212
|
|
|
168
213
|
@pytest.mark.parametrize("date, time, expected", [
|
|
169
214
|
("27.04.99", "08:15", datetime(1999, 4, 27, 8, 15)), # dots: day first
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|