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.
Files changed (33) hide show
  1. ecgprocess/__init__.py +1 -0
  2. ecgprocess/_version.py +1 -0
  3. ecgprocess/constants.py +170 -0
  4. ecgprocess/errors.py +380 -0
  5. ecgprocess/example_data/__init__.py +1 -0
  6. ecgprocess/example_data/example_datasets/.example-XML1.xml.un~ +0 -0
  7. ecgprocess/example_data/example_datasets/.example-XML1.xsd.un~ +0 -0
  8. ecgprocess/example_data/example_datasets/.example-XML1_a.xsd.un~ +0 -0
  9. ecgprocess/example_data/example_datasets/.example-XML1_d.xsd.un~ +0 -0
  10. ecgprocess/example_data/example_datasets/.example-XML1_wrong.xsd.un~ +0 -0
  11. ecgprocess/example_data/example_datasets/example-DICOM1.dcm +0 -0
  12. ecgprocess/example_data/example_datasets/example-DICOM2.dcm +0 -0
  13. ecgprocess/example_data/example_datasets/example-XML1.xml +675 -0
  14. ecgprocess/example_data/example_datasets/example-XML1.xsd +518 -0
  15. ecgprocess/example_data/example_datasets/example-XML1_error.xsd +519 -0
  16. ecgprocess/example_data/example_datasets/example-XML1_wrong.xsd +519 -0
  17. ecgprocess/example_data/example_datasets/string_data.txt +1 -0
  18. ecgprocess/example_data/examples.py +501 -0
  19. ecgprocess/plot_ecgs.py +921 -0
  20. ecgprocess/process_dicom.py +243 -0
  21. ecgprocess/process_xml.py +319 -0
  22. ecgprocess/tabular.py +1394 -0
  23. ecgprocess/utils/__init__.py +1 -0
  24. ecgprocess/utils/config_tools.py +581 -0
  25. ecgprocess/utils/ecg_tools.py +311 -0
  26. ecgprocess/utils/engineering_tools.py +385 -0
  27. ecgprocess/utils/general.py +399 -0
  28. ecgprocess/utils/reader_tools.py +508 -0
  29. ecgprocess-1.1.0.dist-info/METADATA +153 -0
  30. ecgprocess-1.1.0.dist-info/RECORD +33 -0
  31. ecgprocess-1.1.0.dist-info/WHEEL +5 -0
  32. ecgprocess-1.1.0.dist-info/licenses/LICENCE.txt +17 -0
  33. 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
+