ECGProcess 1.1.0__py3-none-any.whl
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.
- ecgprocess/__init__.py +1 -0
- ecgprocess/_version.py +1 -0
- ecgprocess/constants.py +170 -0
- ecgprocess/errors.py +380 -0
- ecgprocess/example_data/__init__.py +1 -0
- ecgprocess/example_data/example_datasets/.example-XML1.xml.un~ +0 -0
- ecgprocess/example_data/example_datasets/.example-XML1.xsd.un~ +0 -0
- ecgprocess/example_data/example_datasets/.example-XML1_a.xsd.un~ +0 -0
- ecgprocess/example_data/example_datasets/.example-XML1_d.xsd.un~ +0 -0
- ecgprocess/example_data/example_datasets/.example-XML1_wrong.xsd.un~ +0 -0
- ecgprocess/example_data/example_datasets/example-DICOM1.dcm +0 -0
- ecgprocess/example_data/example_datasets/example-DICOM2.dcm +0 -0
- ecgprocess/example_data/example_datasets/example-XML1.xml +675 -0
- ecgprocess/example_data/example_datasets/example-XML1.xsd +518 -0
- ecgprocess/example_data/example_datasets/example-XML1_error.xsd +519 -0
- ecgprocess/example_data/example_datasets/example-XML1_wrong.xsd +519 -0
- ecgprocess/example_data/example_datasets/string_data.txt +1 -0
- ecgprocess/example_data/examples.py +501 -0
- ecgprocess/plot_ecgs.py +921 -0
- ecgprocess/process_dicom.py +243 -0
- ecgprocess/process_xml.py +319 -0
- ecgprocess/tabular.py +1394 -0
- ecgprocess/utils/__init__.py +1 -0
- ecgprocess/utils/config_tools.py +581 -0
- ecgprocess/utils/ecg_tools.py +311 -0
- ecgprocess/utils/engineering_tools.py +385 -0
- ecgprocess/utils/general.py +399 -0
- ecgprocess/utils/reader_tools.py +508 -0
- ecgprocess-1.1.0.dist-info/METADATA +153 -0
- ecgprocess-1.1.0.dist-info/RECORD +33 -0
- ecgprocess-1.1.0.dist-info/WHEEL +5 -0
- ecgprocess-1.1.0.dist-info/licenses/LICENCE.txt +17 -0
- ecgprocess-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
'''
|
|
2
|
+
A module for extracting metadata, median beats, and raw waveforms from ECG
|
|
3
|
+
DICOM files.
|
|
4
|
+
|
|
5
|
+
This module provides an API through a reader class, which maps ECG data from
|
|
6
|
+
DICOM files to class attributes. These attributes can be programmatically accessed
|
|
7
|
+
and further processed by downstream ECGprocess modules or external programs
|
|
8
|
+
leveraging the API.
|
|
9
|
+
'''
|
|
10
|
+
|
|
11
|
+
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
12
|
+
# imports
|
|
13
|
+
import pathlib
|
|
14
|
+
import pydicom
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
import ecgprocess.utils.ecg_tools as ecg_utils
|
|
18
|
+
import ecgprocess.utils.reader_tools as reader_utils
|
|
19
|
+
from ecgprocess.process_xml import ECGXMLReader
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import (
|
|
22
|
+
Self, Dict, Any, Optional
|
|
23
|
+
)
|
|
24
|
+
from ecgprocess.errors import (
|
|
25
|
+
is_type,
|
|
26
|
+
Error_MSG,
|
|
27
|
+
Warn_MSG,
|
|
28
|
+
_check_readable,
|
|
29
|
+
MissingTagError,
|
|
30
|
+
)
|
|
31
|
+
from ecgprocess.utils.general import(
|
|
32
|
+
parse_number,
|
|
33
|
+
string_concat,
|
|
34
|
+
)
|
|
35
|
+
from ecgprocess.constants import (
|
|
36
|
+
ProcessDicomNames as PDNames,
|
|
37
|
+
CoreData as Core,
|
|
38
|
+
)
|
|
39
|
+
from ecgprocess.utils.reader_tools import(
|
|
40
|
+
BaseReader,
|
|
41
|
+
)
|
|
42
|
+
from ecgprocess.utils.general import(
|
|
43
|
+
ManagedProperty,
|
|
44
|
+
)
|
|
45
|
+
from ecgprocess.utils.config_tools import(
|
|
46
|
+
ConfigParser,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
50
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
51
|
+
# constants
|
|
52
|
+
CTypes = Core.DataTypes
|
|
53
|
+
CProc = Core.ProcessingData
|
|
54
|
+
CMeta = Core.MetaData
|
|
55
|
+
CLeads = Core.Leads
|
|
56
|
+
|
|
57
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
58
|
+
@dataclass
|
|
59
|
+
class ECGDICOMReader(ECGXMLReader):
|
|
60
|
+
"""
|
|
61
|
+
Processes an DICOM file containing ECG data and extracts the metadata,
|
|
62
|
+
median beats, and raw waveforms.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
augment_leads : `bool`, default `False`
|
|
67
|
+
Whether the augmented leads should be calculated if these are not
|
|
68
|
+
already available in the source file.
|
|
69
|
+
resample : `bool`, default `True`
|
|
70
|
+
Whether to resample the ECG to a frequency of 500 Hertz.
|
|
71
|
+
|
|
72
|
+
Attributes
|
|
73
|
+
----------
|
|
74
|
+
augment_leads : `bool`
|
|
75
|
+
Whether the augmented leads were calculated if these were unavailable.
|
|
76
|
+
resample : `bool`
|
|
77
|
+
Whether the ECG was resampled to a 500 Hertz frequency.
|
|
78
|
+
|
|
79
|
+
Methods
|
|
80
|
+
-------
|
|
81
|
+
extract(config, skip_empty, parse_numeric, **kwargs)
|
|
82
|
+
Processes the DICOM file content applying optional lead augmentation
|
|
83
|
+
and resampling. The DICOM content will be mapped to class attributes.
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
# #### properties
|
|
87
|
+
tags = ManagedProperty(CProc.TAGS, list)
|
|
88
|
+
raw_data = ManagedProperty(CProc.RAW, dict)
|
|
89
|
+
_as_array:bool = True
|
|
90
|
+
# #### parameters, with defaults
|
|
91
|
+
augment_leads:bool = field(default=False)
|
|
92
|
+
resample_500:bool = field(default=True)
|
|
93
|
+
# \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
|
|
94
|
+
def __post_init__(self):
|
|
95
|
+
"""Validating inputs."""
|
|
96
|
+
is_type(getattr(self, PDNames.AUG_LEADS), bool)
|
|
97
|
+
is_type(getattr(self, PDNames.RESAMPLE), bool)
|
|
98
|
+
# \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
|
|
99
|
+
def __call__(self, path:str, verbose:bool=False,
|
|
100
|
+
**kwargs:Optional[Any],
|
|
101
|
+
) -> Self:
|
|
102
|
+
"""
|
|
103
|
+
Reads an `.dcm` file containing ECG readings.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
path : `str`
|
|
108
|
+
The path to a .dcm file.
|
|
109
|
+
verbose : `bool`, default `False`
|
|
110
|
+
Whether warnings and process info should be printed.
|
|
111
|
+
**kwargs : any
|
|
112
|
+
keyword arguments passed to flatten_dict.
|
|
113
|
+
|
|
114
|
+
Attributes
|
|
115
|
+
----------
|
|
116
|
+
tags : `list` [`str`]
|
|
117
|
+
A list of strings with parsed tags matching the `raw_data` keys.
|
|
118
|
+
raw_data : `dict` [`str`, `any`]
|
|
119
|
+
The raw parsed data.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
self : `ECGDICOMReader` instance
|
|
124
|
+
Returns the class instance with updated attributes including the
|
|
125
|
+
extracted DICOM data.
|
|
126
|
+
"""
|
|
127
|
+
# #### check input
|
|
128
|
+
is_type(path, (pathlib.PosixPath, pathlib.WindowsPath, str))
|
|
129
|
+
is_type(verbose, bool)
|
|
130
|
+
# #### assign to self
|
|
131
|
+
self.verbose = verbose
|
|
132
|
+
# #### confirm file is readable
|
|
133
|
+
_check_readable(path)
|
|
134
|
+
# #### read dicom file
|
|
135
|
+
try:
|
|
136
|
+
ds = pydicom.dcmread(path)
|
|
137
|
+
except pydicom.errors.InvalidDicomError as e:
|
|
138
|
+
raise ValueError(f"Failed to read DICOM file at {path}: {e}")
|
|
139
|
+
# map to flatten_dict
|
|
140
|
+
dicom_dict = reader_utils.flatten_dict(
|
|
141
|
+
reader_utils.dicom_to_dict(ds),
|
|
142
|
+
skip_root=False,
|
|
143
|
+
**kwargs,
|
|
144
|
+
)
|
|
145
|
+
# ### store keys and data
|
|
146
|
+
getattr(type(self), CProc.RAW).set_with_setter(self, dicom_dict)
|
|
147
|
+
getattr(type(self), CProc.TAGS).set_with_setter(
|
|
148
|
+
self, list(dicom_dict.keys()))
|
|
149
|
+
# ### return
|
|
150
|
+
return self
|
|
151
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
152
|
+
def extract(self, config:ConfigParser, bits:np.dtype | None =None,
|
|
153
|
+
skip_empty:bool=True, parse_numeric:bool=True,
|
|
154
|
+
pattern:dict[str, str]|None=None,
|
|
155
|
+
substitute:tuple[str,str]|None=(r'_[0-9]{1,2}\.*', ' '),
|
|
156
|
+
character_trim:int=0,
|
|
157
|
+
**kwargs:Optional[Any]) -> Self:
|
|
158
|
+
"""
|
|
159
|
+
Processes the raw ECG data and assign these to class attributes
|
|
160
|
+
performing resampling and lead augmentation if requested.
|
|
161
|
+
|
|
162
|
+
Parameters
|
|
163
|
+
----------
|
|
164
|
+
config : `ConfigParser`
|
|
165
|
+
A class instance of a parsed configuration file, mapping the DICOM
|
|
166
|
+
content to class attributes. Specifically this should include
|
|
167
|
+
dictionary attributes `MetaData`, `WaveForms`, `MedianBeats`,
|
|
168
|
+
`OtherData`. The `MetaData` includes some privileged keys including
|
|
169
|
+
essential information to describe an ECG instance, as well as
|
|
170
|
+
non-privileged information. The difference between `OtherData` and
|
|
171
|
+
`MetaData` is the way it is processed by other functions or methods
|
|
172
|
+
with the `OtherData` processed without strong checks on its content.
|
|
173
|
+
`WaveForms` and `MedianBeats` simply include the lead mappings.
|
|
174
|
+
Please refer to the `constants.CoreData` class for the specifics
|
|
175
|
+
parse_numeric : bool, default `True`
|
|
176
|
+
Whether to check for numeric data accidentally recorded as string and
|
|
177
|
+
try to parse these to int or float depending on the presence of a
|
|
178
|
+
decimal separator.
|
|
179
|
+
skip_empty : `bool`, default `True`
|
|
180
|
+
Whether empty tags should be skipped or throw an error.
|
|
181
|
+
bits : `np.dtype`, default `None`
|
|
182
|
+
np.array bits passed to numpy.array dtype.
|
|
183
|
+
pattern : `dict` [`str`, `str`], default `NoneType`
|
|
184
|
+
Use this to extract a subset of items from `MetaData` based on the
|
|
185
|
+
pattern key, and adds a unique name as a prefix to the keys of the
|
|
186
|
+
selected subset. The unique name will be base on the value from the
|
|
187
|
+
key which matches the pattern value.
|
|
188
|
+
substitute : `tuple` [`str`,`str`] or `None`, default `(r"_[0-9]{1,2}\\.*", " ")`
|
|
189
|
+
A tuple containing a regular expression pattern and replacement string.
|
|
190
|
+
This substitution is applied to the remaining portion of the `data` key
|
|
191
|
+
after removing the matching prefix.
|
|
192
|
+
character_trim : int, default `0`
|
|
193
|
+
The number of characters which should be removed from the right-hand
|
|
194
|
+
side of the `data` key which did not match the `pattern` key.
|
|
195
|
+
**kwargs
|
|
196
|
+
The keyword arguments for reader_tools.get_ecg_data.
|
|
197
|
+
For the waveforms and medianbeats as_array and bits are hard coded
|
|
198
|
+
so these will raise an error if supplied as kwargs.
|
|
199
|
+
|
|
200
|
+
Attributes
|
|
201
|
+
----------
|
|
202
|
+
MetaData : `dict` [`str`, `any`]
|
|
203
|
+
ECG metadata.
|
|
204
|
+
Waveforms : `dict` [`str`, `np.array`]
|
|
205
|
+
The lead specific ECG waveforms.
|
|
206
|
+
MedianBeats : `dict` [`str`, `np.array`]
|
|
207
|
+
The lead specific ECG median beats.
|
|
208
|
+
OtherData : `dict` [`str`, `any`]
|
|
209
|
+
Other data.
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
self : `ECGDICOMReader` instance
|
|
214
|
+
Returns the class instance with updated attributes including the
|
|
215
|
+
extracted DICOM data.
|
|
216
|
+
"""
|
|
217
|
+
# #### run ECGXMLReader.extract
|
|
218
|
+
super().extract(config=config, skip_empty=skip_empty,
|
|
219
|
+
parse_numeric=parse_numeric, **kwargs)
|
|
220
|
+
# #### extract based on a string pattern
|
|
221
|
+
if not pattern is None:
|
|
222
|
+
new_meta = reader_utils.subset_dict(
|
|
223
|
+
data=getattr(self, CProc.RAW), pattern=pattern,
|
|
224
|
+
substitute=substitute, verbose=self.verbose,
|
|
225
|
+
skip_empty=skip_empty,
|
|
226
|
+
character_trim=character_trim)
|
|
227
|
+
# make sure everything is af loat add perform optional mapping
|
|
228
|
+
# to numeric
|
|
229
|
+
new_meta = {k:str(v) for k, v in new_meta.items()}
|
|
230
|
+
if parse_numeric:
|
|
231
|
+
for k, v in new_meta.items():
|
|
232
|
+
try:
|
|
233
|
+
new_meta[k] = parse_number(v)
|
|
234
|
+
if isinstance(new_meta[k], list):
|
|
235
|
+
if len(new_meta[k]) == 1:
|
|
236
|
+
new_meta[k] = new_meta[k][0]
|
|
237
|
+
except ValueError:
|
|
238
|
+
new_meta[k] = v
|
|
239
|
+
# assign to meta
|
|
240
|
+
getattr(self, CTypes.MetaData).update(new_meta)
|
|
241
|
+
# #### return
|
|
242
|
+
return self
|
|
243
|
+
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
'''
|
|
2
|
+
A module for extracting metadata, median beats, and raw waveforms from ECG
|
|
3
|
+
XML files, allowing for XML validation.
|
|
4
|
+
|
|
5
|
+
This module provides an API through a reader class, which maps ECG data from
|
|
6
|
+
XML files to class attributes. These attributes can be programmatically accessed
|
|
7
|
+
and further processed by downstream ECGprocess modules or external programs
|
|
8
|
+
leveraging the API.
|
|
9
|
+
'''
|
|
10
|
+
|
|
11
|
+
import pathlib
|
|
12
|
+
import warnings
|
|
13
|
+
import numpy as np
|
|
14
|
+
import ecgprocess.utils.reader_tools as reader_utils
|
|
15
|
+
import ecgprocess.utils.ecg_tools as ecg_utils
|
|
16
|
+
from lxml import etree
|
|
17
|
+
from ecgprocess.utils.reader_tools import(
|
|
18
|
+
BaseReader,
|
|
19
|
+
)
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import (
|
|
22
|
+
Optional, Self, Any, Literal,
|
|
23
|
+
)
|
|
24
|
+
from ecgprocess.utils.general import(
|
|
25
|
+
ManagedProperty,
|
|
26
|
+
)
|
|
27
|
+
from ecgprocess.utils.config_tools import(
|
|
28
|
+
ConfigParser,
|
|
29
|
+
)
|
|
30
|
+
from ecgprocess.errors import (
|
|
31
|
+
is_type,
|
|
32
|
+
_check_readable,
|
|
33
|
+
Warn_MSG,
|
|
34
|
+
Error_MSG,
|
|
35
|
+
)
|
|
36
|
+
from ecgprocess.constants import (
|
|
37
|
+
ProcessXMLNames as PXMLNam,
|
|
38
|
+
CoreData as Core,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
42
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
43
|
+
# constants
|
|
44
|
+
CTypes = Core.DataTypes
|
|
45
|
+
CLeads = Core.Leads
|
|
46
|
+
CMeta = Core.MetaData
|
|
47
|
+
CProc = Core.ProcessingData
|
|
48
|
+
|
|
49
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
50
|
+
@dataclass
|
|
51
|
+
class ECGXMLReader(BaseReader):
|
|
52
|
+
"""
|
|
53
|
+
Processes an XML file containing ECG data and extracts the metadata,
|
|
54
|
+
median beats, and raw waveforms.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
augment_leads : `bool`, default `False`
|
|
59
|
+
Whether the augmented leads should be calculated if these are not
|
|
60
|
+
already available in the source file.
|
|
61
|
+
resample_500 : `bool`, default `True`
|
|
62
|
+
Whether to resample the ECG to a frequency of 500 Hertz. Note this
|
|
63
|
+
will internally calculate the ECG duration in seconds. For the
|
|
64
|
+
duration to be in seconds the sampling frequency/rate should be in
|
|
65
|
+
seconds not milliseconds.
|
|
66
|
+
|
|
67
|
+
Attributes
|
|
68
|
+
----------
|
|
69
|
+
augment_leads : `bool`
|
|
70
|
+
Whether the augmented leads were calculated if these were unavailable.
|
|
71
|
+
resample : `bool`
|
|
72
|
+
Whether the ECG was resampled to a 500 Hertz frequency.
|
|
73
|
+
|
|
74
|
+
Methods
|
|
75
|
+
-------
|
|
76
|
+
extract(config, skip_empty, parse_numeric, **kwargs)
|
|
77
|
+
Processes the XML file content applying optional lead augmentation and
|
|
78
|
+
resampling. The XML content will be mapped to class attributes.
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
# #### properties
|
|
82
|
+
tags = ManagedProperty(CProc.TAGS, list)
|
|
83
|
+
raw_data = ManagedProperty(CProc.RAW, dict)
|
|
84
|
+
_as_array:bool = True
|
|
85
|
+
# #### parameters, with defaults
|
|
86
|
+
augment_leads:bool = field(default=False)
|
|
87
|
+
resample_500:bool = field(default=True)
|
|
88
|
+
# \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
|
|
89
|
+
def __post_init__(self):
|
|
90
|
+
"""Validating inputs."""
|
|
91
|
+
is_type(getattr(self, PXMLNam.AUG_LEADS), bool)
|
|
92
|
+
is_type(getattr(self, PXMLNam.RESAMPLE), bool)
|
|
93
|
+
# \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
|
|
94
|
+
def __call__(self, path:str, schema:str | None = None,
|
|
95
|
+
verbose:bool=False,
|
|
96
|
+
**kwargs:Optional[Any],
|
|
97
|
+
) -> Self:
|
|
98
|
+
"""
|
|
99
|
+
Reads an `.xml` file containing ECG readings, optionally validates this
|
|
100
|
+
based on a .xsd schema, and map the XML file to a flat dictionary.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
path : `str`
|
|
105
|
+
The path to the .xml file.
|
|
106
|
+
schema : `str`, default `NoneType`
|
|
107
|
+
A path to an XML schema which will be used to validate the XML file
|
|
108
|
+
against.
|
|
109
|
+
verbose : `bool`, default `False`
|
|
110
|
+
Whether warnings and process info should be printed.
|
|
111
|
+
**kwargs : any
|
|
112
|
+
keyword arguments passed to flatten_dict.
|
|
113
|
+
|
|
114
|
+
Attributes
|
|
115
|
+
----------
|
|
116
|
+
tags : `list` [`str`]
|
|
117
|
+
A list of strings with parsed tags matching the `raw_data` keys.
|
|
118
|
+
raw_data : `dict` [`str`, `any`]
|
|
119
|
+
The raw parsed data.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
self : `ECGXMLReader` instance
|
|
124
|
+
Returns the class instance with updated attributes including the
|
|
125
|
+
extracted XML data.
|
|
126
|
+
|
|
127
|
+
Raises
|
|
128
|
+
------
|
|
129
|
+
XMLValidationError
|
|
130
|
+
If the XML file is not valid based on the supplied schema.
|
|
131
|
+
"""
|
|
132
|
+
# #### check input
|
|
133
|
+
is_type(path, (pathlib.PosixPath, pathlib.WindowsPath, str))
|
|
134
|
+
is_type(schema, (type(None), pathlib.PosixPath, pathlib.WindowsPath,
|
|
135
|
+
str))
|
|
136
|
+
is_type(verbose, bool)
|
|
137
|
+
# #### assign to self
|
|
138
|
+
self.verbose = verbose
|
|
139
|
+
# #### confirm file is readable
|
|
140
|
+
_check_readable(path)
|
|
141
|
+
# #### validate XML
|
|
142
|
+
if not schema is None:
|
|
143
|
+
_check_readable(schema)
|
|
144
|
+
parsed_xml = reader_utils.validate_xml(
|
|
145
|
+
xml_path=path, xsd_path=schema, verbose=verbose)
|
|
146
|
+
else:
|
|
147
|
+
with open(path, 'rb') as xml:
|
|
148
|
+
parsed_xml = etree.parse(xml)
|
|
149
|
+
# map to flatten_dict
|
|
150
|
+
xml_dict = reader_utils.flatten_dict(
|
|
151
|
+
reader_utils.xml_to_dict(parsed_xml),
|
|
152
|
+
**kwargs,
|
|
153
|
+
)
|
|
154
|
+
# ### store keys and data
|
|
155
|
+
getattr(type(self), CProc.RAW).set_with_setter(self, xml_dict)
|
|
156
|
+
getattr(type(self), CProc.TAGS).set_with_setter(
|
|
157
|
+
self, list(xml_dict.keys()))
|
|
158
|
+
# ### return
|
|
159
|
+
return self
|
|
160
|
+
# \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
|
|
161
|
+
def extract(self, config:ConfigParser, bits:np.dtype | None=None,
|
|
162
|
+
skip_empty:bool=True, parse_numeric:bool=True,
|
|
163
|
+
**kwargs:Optional[Any]) -> Self:
|
|
164
|
+
"""
|
|
165
|
+
Processes the raw ECG data and assign these to class attributes
|
|
166
|
+
performing resampling and lead augmentation if requested.
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
config : `ConfigParser`
|
|
171
|
+
A class instance of a parsed configuration file, mapping the XML
|
|
172
|
+
content to class attributes. Specifically this should include
|
|
173
|
+
dictionary attributes `MetaData`, `WaveForms`, `MedianBeats`,
|
|
174
|
+
`OtherData`. The `MetaData` includes some privileged keys including
|
|
175
|
+
essential information to describe an ECG instance, as well as
|
|
176
|
+
non-privileged information. The difference between `OtherData` and
|
|
177
|
+
`MetaData` is the way it is processed by other functions or methods
|
|
178
|
+
with the `OtherData` processed without strong checks on its content.
|
|
179
|
+
`WaveForms` and `MedianBeats` simply include the lead mappings.
|
|
180
|
+
Please refer to the `constants.CoreData` class for the specifics
|
|
181
|
+
parse_numeric : bool, default `True`
|
|
182
|
+
Whether to check for numeric data accidentally recorded as string and
|
|
183
|
+
try to parse these to int or float depending on the presence of a
|
|
184
|
+
decimal separator.
|
|
185
|
+
skip_empty : `bool`, default `True`
|
|
186
|
+
Whether empty tags should be skipped or throw an error.
|
|
187
|
+
bits : `np.dtype`, default `None`
|
|
188
|
+
np.array bits passed to numpy.array dtype.
|
|
189
|
+
**kwargs
|
|
190
|
+
The keyword arguments for reader_tools.get_ecg_data.
|
|
191
|
+
For the waveforms and medianbeats as_array and bits are hard coded
|
|
192
|
+
so these will raise an error if supplied as kwargs.
|
|
193
|
+
|
|
194
|
+
Attributes
|
|
195
|
+
----------
|
|
196
|
+
MetaData : `dict` [`str`, `any`]
|
|
197
|
+
ECG metadata.
|
|
198
|
+
Waveforms : `dict` [`str`, `np.array`]
|
|
199
|
+
The lead specific ECG waveforms.
|
|
200
|
+
MedianBeats : `dict` [`str`, `np.array`]
|
|
201
|
+
The lead specific ECG median beats.
|
|
202
|
+
OtherData : `dict` [`str`, `any`]
|
|
203
|
+
Other data.
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
self : `ECGXMLReader` instance
|
|
208
|
+
Returns the class instance with updated attributes including the
|
|
209
|
+
extracted XML data.
|
|
210
|
+
"""
|
|
211
|
+
is_type(config, ConfigParser)
|
|
212
|
+
is_type(skip_empty, bool)
|
|
213
|
+
is_type(parse_numeric, bool)
|
|
214
|
+
# update kwargs
|
|
215
|
+
kwargs = {**{'parse_numeric': parse_numeric,
|
|
216
|
+
'skip_empty': skip_empty,
|
|
217
|
+
}, **kwargs}
|
|
218
|
+
# #### get the configs
|
|
219
|
+
md_cnf = config.get_section(CTypes.MetaData)
|
|
220
|
+
wf_cnf = config.get_section(CTypes.WaveForms)
|
|
221
|
+
mb_cnf = config.get_section(CTypes.MedianBeats)
|
|
222
|
+
od_cnf = config.get_section(CTypes.OtherData)
|
|
223
|
+
# #### extract metadata
|
|
224
|
+
meta_data, meta_missing = \
|
|
225
|
+
reader_utils.get_ecg_data(
|
|
226
|
+
getattr(self, CProc.RAW),
|
|
227
|
+
config=md_cnf,
|
|
228
|
+
**kwargs,
|
|
229
|
+
)
|
|
230
|
+
# #### extract waveforms
|
|
231
|
+
wave_data, wave_missing = \
|
|
232
|
+
reader_utils.get_ecg_data(
|
|
233
|
+
getattr(self, CProc.RAW),
|
|
234
|
+
config=wf_cnf,
|
|
235
|
+
as_array=self._as_array,
|
|
236
|
+
bits=bits,
|
|
237
|
+
**kwargs,
|
|
238
|
+
)
|
|
239
|
+
# #### extract median beats
|
|
240
|
+
median_data, median_missing = \
|
|
241
|
+
reader_utils.get_ecg_data(
|
|
242
|
+
getattr(self, CProc.RAW),
|
|
243
|
+
config=mb_cnf,
|
|
244
|
+
as_array=self._as_array,
|
|
245
|
+
bits=bits,
|
|
246
|
+
**kwargs,
|
|
247
|
+
)
|
|
248
|
+
# #### extract other data
|
|
249
|
+
other_data, other_missing = \
|
|
250
|
+
reader_utils.get_ecg_data(
|
|
251
|
+
getattr(self, CProc.RAW),
|
|
252
|
+
config=od_cnf,
|
|
253
|
+
**kwargs,
|
|
254
|
+
)
|
|
255
|
+
# #### resample to 500hz
|
|
256
|
+
# first calculate the duration
|
|
257
|
+
try:
|
|
258
|
+
meta_data[CProc.Duration] =\
|
|
259
|
+
(meta_data[CMeta.SN_W]/meta_data[CMeta.SF]
|
|
260
|
+
)
|
|
261
|
+
except TypeError:
|
|
262
|
+
meta_data[CProc.Duration] = None
|
|
263
|
+
# only run when the parameter is True and SF is present
|
|
264
|
+
meta_data[CProc.SF_NEW] = None
|
|
265
|
+
if getattr(self, PXMLNam.RESAMPLE):
|
|
266
|
+
# confirm SF is present
|
|
267
|
+
if not CMeta.SF in meta_data or meta_data[CMeta.SF] is None:
|
|
268
|
+
raise KeyError(f'`{CMeta.SF}` is necessary to resample '
|
|
269
|
+
'the ECG signal.')
|
|
270
|
+
if int(meta_data[CMeta.SF]) == 500:
|
|
271
|
+
# already at target — record but skip resampling
|
|
272
|
+
meta_data[CProc.SF_NEW] = 500
|
|
273
|
+
elif meta_data[CProc.Duration] is None:
|
|
274
|
+
# cannot resample without duration; warn and leave SF_NEW None
|
|
275
|
+
warnings.warn(Warn_MSG.RESAMPLE_NO_DURATION)
|
|
276
|
+
else:
|
|
277
|
+
# set the new SF
|
|
278
|
+
meta_data[CProc.SF_NEW] = 500
|
|
279
|
+
# confirm there is waveform data
|
|
280
|
+
if all(v is None for v in wave_data.values()):
|
|
281
|
+
if not skip_empty:
|
|
282
|
+
raise AttributeError(
|
|
283
|
+
Error_MSG.MISSING_SIGNAL.format('waveform'))
|
|
284
|
+
else:
|
|
285
|
+
wave_data = ecg_utils.resampling_500hz(
|
|
286
|
+
wave_data, duration=meta_data[CProc.Duration])
|
|
287
|
+
# confirm there is median beats data
|
|
288
|
+
if all(v is None for v in median_data.values()):
|
|
289
|
+
if not skip_empty:
|
|
290
|
+
raise AttributeError(
|
|
291
|
+
Error_MSG.MISSING_SIGNAL.format('median beat'))
|
|
292
|
+
else:
|
|
293
|
+
median_data = ecg_utils.resampling_500hz(
|
|
294
|
+
median_data, median=True)
|
|
295
|
+
# ### See if we need to get the augmented leads
|
|
296
|
+
# leads that can be derived from I and II
|
|
297
|
+
_aug = {CLeads.III, CLeads.aVR, CLeads.aVL, CLeads.aVF}
|
|
298
|
+
if getattr(self, PXMLNam.AUG_LEADS):
|
|
299
|
+
# augment each dataset when any derived lead is absent;
|
|
300
|
+
# get_limb_leads raises KeyError if source leads I/II are missing
|
|
301
|
+
if any(wave_data.get(l) is None for l in _aug):
|
|
302
|
+
wave_data = ecg_utils.get_limb_leads(wave_data)
|
|
303
|
+
if any(median_data.get(l) is None for l in _aug):
|
|
304
|
+
median_data = ecg_utils.get_limb_leads(median_data)
|
|
305
|
+
# ### do we print missing tags
|
|
306
|
+
if self.verbose:
|
|
307
|
+
missing_tags = [tag for tag in (
|
|
308
|
+
meta_missing + wave_missing + median_missing + other_missing
|
|
309
|
+
) if tag is not None]
|
|
310
|
+
if len(missing_tags)>0:
|
|
311
|
+
warnings.warn(Warn_MSG.MISSING_TAG.format(missing_tags))
|
|
312
|
+
# #### assign stuff to attributes
|
|
313
|
+
setattr(self, CTypes.MetaData, meta_data)
|
|
314
|
+
setattr(self, CTypes.OtherData, other_data)
|
|
315
|
+
setattr(self, CTypes.WaveForms, wave_data)
|
|
316
|
+
setattr(self, CTypes.MedianBeats, median_data)
|
|
317
|
+
# #### return stuff
|
|
318
|
+
return self
|
|
319
|
+
|