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,508 @@
1
+ """
2
+ Tools to help read and process ECG files.
3
+
4
+ Functions and classes are predominantly aimed to map ECG files to native
5
+ python objects such as dictionary and systematically process these. Functions
6
+ specifically focussed on ECG signals, such as calculating the limb leads,
7
+ are collected in `ecgprocess.utils.ecg_tools`.
8
+ """
9
+
10
+ import re
11
+ import pydicom
12
+ import warnings
13
+ import xmltodict
14
+ import numpy as np
15
+ from lxml import etree
16
+ from pathlib import Path
17
+ from typing import (
18
+ Optional, Any,
19
+ )
20
+ from ecgprocess.utils.general import(
21
+ parse_number,
22
+ )
23
+ from ecgprocess.constants import (
24
+ CoreData as Core,
25
+ UtilsReaderNames as RNames,
26
+ )
27
+ from ecgprocess.errors import (
28
+ is_type,
29
+ MissingTagError,
30
+ )
31
+ from ecgprocess.constants import UtilsGeneralNames as UGNames
32
+ from ecgprocess.errors import XMLValidationError
33
+
34
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
35
+ # constants
36
+ WARN1 = "XML file is valid (ignoring XML elements not in the supplied XSD)."
37
+ CTypes = Core.DataTypes
38
+ CProc = Core.ProcessingData
39
+
40
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
41
+ # Base class with slots
42
+ class BaseReader:
43
+ '''
44
+ An ECGDICOMReader base class implementing the more efficient __slots__ for
45
+ the waveform arrays, while still retaining __dict__ dynamic attribute
46
+ creation.
47
+ '''
48
+ __slots__ = (CTypes.WaveForms, CTypes.MedianBeats,
49
+ CTypes.MetaData, CTypes.OtherData, CProc.RAW)
50
+ # /////////////////////////////////////////////////////////////////////////
51
+ def __init__(self, waveforms=None, medianbeats=None, metadata=None,
52
+ otherdata=None, raw=None):
53
+ '''
54
+ Initialises slots entries to `None`.
55
+ '''
56
+ setattr(self, CTypes.WaveForms, waveforms)
57
+ setattr(self, CTypes.MedianBeats,medianbeats)
58
+ setattr(self, CTypes.MetaData,metadata)
59
+ setattr(self, CTypes.OtherData, otherdata)
60
+ setattr(self, CProc.RAW,raw)
61
+
62
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
63
+ def validate_xml(xml_path:str|Path, xsd_path:str|Path, strict:bool=True,
64
+ verbose:bool=True,
65
+ ) -> etree._ElementTree:
66
+ """
67
+ Validates an XML file against an XSD schema.
68
+
69
+ Parameters
70
+ ----------
71
+ xml_path : `str` or `Path`
72
+ Path to the XML file.
73
+ xsd_path : `str` or `Path`
74
+ Path to the XSD file.
75
+ strict : `bool`, default `True`
76
+ If False, ignores elements in the XML that are not in the XSD.
77
+
78
+ Returns
79
+ -------
80
+ etree._ElementTree
81
+ The parsed XML document.
82
+
83
+ Raises
84
+ ------
85
+ XMLValidationError
86
+ Raised if the XSD and XML are incompatible.
87
+ """
88
+ # #### Check input
89
+ is_type(strict, bool)
90
+ is_type(verbose, bool)
91
+ is_type(xml_path, (str, Path))
92
+ is_type(xsd_path, (str, Path))
93
+ # #### Load the XSD schema
94
+ with open(xsd_path, 'rb') as xsd:
95
+ schema_root = etree.XML(xsd.read())
96
+ schema = etree.XMLSchema(schema_root)
97
+ # #### Parse the XML file
98
+ try:
99
+ with open(xml_path, 'rb') as xml:
100
+ xml_doc = etree.parse(xml)
101
+ except etree.XMLSyntaxError:
102
+ parser = etree.XMLParser(resolve_entities=False, huge_tree=True)
103
+ with open(xml_path, 'rb') as xml:
104
+ xml_doc = etree.parse(xml, parser=parser,)
105
+ # #### Compare XML against XSD
106
+ is_valid = schema.validate(xml_doc)
107
+ # #### Do we want to raise errors when XML elements are absent in the XSD
108
+ if not is_valid and not strict:
109
+ # if strict is False remove error relating to missing XSD elements
110
+ errors = [err.message for err in schema.error_log]
111
+ filtered_errs = [err for err in schema.error_log if\
112
+ not UGNames.LXML_ERROR in err.message]
113
+ # check if there are any other type of errors
114
+ is_valid = len(filtered_errs) == 0
115
+ if is_valid:
116
+ # Where there any errors to begin with
117
+ if verbose == True and len(schema.error_log) > 0:
118
+ warnings.warn(WARN1)
119
+ else:
120
+ errors = [err.message for err in filtered_errs]
121
+ raise XMLValidationError(errors)
122
+ elif is_valid == False:
123
+ errors = [err.message for err in schema.error_log]
124
+ raise XMLValidationError(errors)
125
+ # return xml_doc
126
+ return xml_doc
127
+
128
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
129
+ def xml_to_dict(xml_doc:etree._ElementTree, encoding:str='utf-8',
130
+ ) -> dict[str, Any]:
131
+ """
132
+ Converts an lxml ElementTree document to a dictionary.
133
+
134
+ Parameters
135
+ ----------
136
+ xml_doc : `etree._ElementTree`
137
+ A validated XML document.
138
+ encoding : `str`, default `utf-8`
139
+
140
+ Returns
141
+ -------
142
+ dict [`str`, `any`]
143
+ A dictionary representation of the XML data.
144
+ """
145
+ is_type(encoding,str)
146
+ is_type(xml_doc, etree._ElementTree)
147
+ # convert to string
148
+ xml_str = etree.tostring(xml_doc, encoding=encoding)
149
+ # parste to dict
150
+ xml_dict = xmltodict.parse(xml_str)
151
+ # returning stuf.
152
+ return xml_dict
153
+
154
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
155
+ def dicom_to_dict(ds: pydicom.dataset.Dataset) -> dict[str, Any]:
156
+ """
157
+ Turn a pydicom Dataset into a dict with keys derived from the Element names.
158
+
159
+ Parameters
160
+ ----------
161
+ ds : `pydicom.dataset.Dataset`
162
+ The DICOM dataset to convert.
163
+
164
+ Returns
165
+ -------
166
+ dict [`str`, `any`]
167
+ A dictionary representation of the dataset.
168
+ """
169
+ output = {}
170
+ for elem in ds:
171
+ # SQ: sequences should be dealt with through recursion
172
+ # Each sequence element contains a list of sub-elements
173
+ # (nested datasets), which may themselves have multiple elements.
174
+ if elem.VR != "SQ":
175
+ output[elem.name] = elem.value
176
+ else:
177
+ output[elem.name] = [dicom_to_dict(item) for item in elem]
178
+ # Extract waveform data if available
179
+ if hasattr(ds, RNames.DICOM_WAVE_ARRAY):
180
+ # Attempt to extract waveform data if the method exists
181
+ waveform_data = {}
182
+ # Extract the waveform data
183
+ try:
184
+ waveform_data["ECG_Leads"] = ds.waveform_array(0).T.tolist()
185
+ except (IndexError, AttributeError):
186
+ pass
187
+ # Extract median beats
188
+ try:
189
+ waveform_data["Median_Beats"] = ds.waveform_array(1).T.tolist()
190
+ except (IndexError, AttributeError):
191
+ pass
192
+ if waveform_data:
193
+ output["WaveformData"] = waveform_data
194
+ # return
195
+ return output
196
+
197
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
198
+ def flatten_dict(d:dict[str, Any], parent_prefix:str='', sep:str='.',
199
+ skip_root:bool=True,
200
+ ) -> dict[str, Any]:
201
+ """
202
+ Recursively flatten a nested dictionary, optionally skipping the root
203
+ element.
204
+
205
+ Parameters
206
+ ----------
207
+ d : dict
208
+ The dictionary to flatten.
209
+ parent_prefix : str, default ''
210
+ The base string added as a prefix to all keys during recursion.
211
+ Useful for maintaining context or indicating a higher-level structure.
212
+ sep : str, default '.'
213
+ The key separator
214
+ skip_root : bool, default True
215
+ If True, skips the first level (root) key.
216
+
217
+ Returns
218
+ -------
219
+ dict
220
+ A flattened dictionary where nested keys are concatenated into a single
221
+ key.
222
+
223
+ Examples
224
+ --------
225
+ >>> nested_dict = {
226
+ ... 'a': {
227
+ ... 'b': 1,
228
+ ... 'c': {
229
+ ... 'd': 2
230
+ ... }
231
+ ... }
232
+ ... }
233
+ >>> flatten_dict(nested_dict)
234
+ {'b': 1, 'c.d': 2}
235
+
236
+ >>> flatten_dict(nested_dict, skip_root=False)
237
+ {'a.b': 1, 'a.c.d': 2}
238
+
239
+ >>> flatten_dict(nested_dict, sep='_', skip_root=False)
240
+ {'a_b': 1, 'a_c_d': 2}
241
+ """
242
+ # check input
243
+ is_type(d, dict)
244
+ is_type(parent_prefix, str)
245
+ is_type(sep, str)
246
+ is_type(skip_root, bool)
247
+ # initiate empty results
248
+ res = []
249
+ # If skipping the root, adjust d to be the first nested dictionary level
250
+ if skip_root and isinstance(d, dict) and len(d) == 1:
251
+ _, d = next(iter(d.items()))
252
+ # loop over the remaining keys and values
253
+ for k, v in d.items():
254
+ new_key = f"{parent_prefix}{sep}{k}" if parent_prefix else k
255
+ # if v is dict, unpack
256
+ if isinstance(v, dict):
257
+ res.extend(flatten_dict(v, new_key, sep=sep, skip_root=False).items())
258
+ # if v is list, unpack
259
+ elif isinstance(v, list):
260
+ for i, item in enumerate(v):
261
+ # flatten lists
262
+ if item is not None and isinstance(item, dict):
263
+ res.extend(flatten_dict(item, f"{new_key}_{i}",
264
+ sep=sep, skip_root=False).\
265
+ items())
266
+ # otherwise add non-dict and None types
267
+ else:
268
+ res.append((f"{new_key}_{i}", item))
269
+ # otherwise simply assign
270
+ else:
271
+ res.append((new_key, v))
272
+ return dict(res)
273
+
274
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
275
+ def get_ecg_data(data_dict:dict[str, Any],
276
+ config:dict[str, str],
277
+ parse_numeric:bool=True,
278
+ as_array:bool=False,
279
+ bits:np.dtype | None =None,
280
+ skip_empty:bool=True,
281
+ **kwargs:Optional[Any],
282
+ ) -> tuple[dict[str, Any], list[str]]:
283
+ '''
284
+ Extracts metadata or signal data from a `data_dict` based on a supplied
285
+ `config` dictionary.
286
+
287
+ Parameters
288
+ ----------
289
+ data_dict : `dict` [`str`, `any`]
290
+ A dictionary with keys and values matching the config object
291
+ config : `dict` [`str`, `str`]
292
+ a dictionary where the values match some keys in `data_dict` and the
293
+ keys represent the names these will be stored to.
294
+ parse_numeric : `bool`, default `True`
295
+ Will check if a numbers are accidentally presented as strings and parse
296
+ these to numbers.
297
+ as_array : `bool`, default `False`
298
+ Whether data should be mapped to `np.array` using a direct map:
299
+ `np.array(., dtype=bits)`.
300
+ bits : `np.dtype`, default `None`
301
+ np.array bits passed to numpy.array dtype.
302
+ skip_empty : `bool`, default `True`
303
+ Whether to skip config values not matching data_dict keys.
304
+ **kwargs
305
+ keyword arguments to `parse_number`.
306
+
307
+ Returns
308
+ -------
309
+ dict
310
+ A dictionary with the extracted signal data or metadata.
311
+ list
312
+ A list with `config` values which did not match `data_dict` keys.
313
+
314
+ Notes
315
+ -----
316
+ Whenever a config value starts with "[STARTSWITH]", the algorithm gathers
317
+ all relevant values in data_dict that share the specific prefix remaining
318
+ after stripping the quoted text. These values are concatenated into a
319
+ single string, where the original breaks between the values is indicated
320
+ by `[DELIM]`.
321
+ '''
322
+ # #### check input
323
+ is_type(data_dict, dict)
324
+ is_type(config, dict)
325
+ is_type(skip_empty, bool)
326
+ is_type(as_array, bool)
327
+ is_type(parse_numeric, bool)
328
+ STARTS = '[STARTSWITH]'
329
+ DELIM = '[DELIM]'
330
+ # #### initialise object
331
+ results_dict = {}
332
+ results_missing = []
333
+ # #### extract signal data
334
+ for t, s in config.items():
335
+ # cutting this in half using HIT
336
+ HIT = False
337
+ if not s is None:
338
+ if s.startswith(STARTS):
339
+ HIT = True
340
+ # Strip the [STARTSWITH] part
341
+ prefix = s[len(STARTS):]
342
+ # extracting entries which start with STARTS
343
+ combi = DELIM.join(str(v) for k, v in data_dict.items()\
344
+ if k.startswith(prefix)) or None
345
+ if combi is None:
346
+ results_missing.append(s)
347
+ # setting these to none to ensure key consistency
348
+ results_dict[t] = None
349
+ if skip_empty == False:
350
+ # Should an Error be returned
351
+ raise MissingTagError(s)
352
+ else:
353
+ results_dict[t] = combi
354
+ if HIT == False:
355
+ if s in data_dict:
356
+ # if s in data_dict:
357
+ # Check if numerics are accidentally strings.
358
+ if parse_numeric:
359
+ parsed_string = parse_number(data_dict[s], **kwargs)
360
+ results_dict[t] = parsed_string
361
+ if isinstance(parsed_string, list):
362
+ if len(parsed_string) == 1:
363
+ results_dict[t] = parsed_string[0]
364
+ else:
365
+ results_dict[t] = data_dict[s]
366
+ # Do we want to extract signals and store these in a numpy array..
367
+ if as_array == True:
368
+ try:
369
+ results_dict[t] = np.array(results_dict[t], dtype=bits)
370
+ except ValueError:
371
+ pass
372
+ elif skip_empty == False and not s is None:
373
+ # Should an Error be returned
374
+ raise MissingTagError(s)
375
+ else:
376
+ # assign None and append missing metadata
377
+ results_dict[t] = None
378
+ results_missing.append(s)
379
+ # return
380
+ return results_dict, results_missing
381
+
382
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
383
+ def subset_dict(data:dict[str, Any], pattern:dict[str, str],
384
+ substitute:tuple[str,str]|None=(r'_[0-9]{1,2}\.*', ' '),
385
+ character_trim:int=0,
386
+ verbose:bool=True, skip_empty:bool=True,
387
+ ) -> dict[str, Any]:
388
+ """
389
+ This will identify a subset of `data` items based on a `startswith` call
390
+ using the `pattern` key, and based on the patternl values this function
391
+ will identify the single entry in the subset whose values contains a
392
+ unique name which will be added as a prefix to the subset keys.
393
+
394
+ Parameters
395
+ ---------
396
+ data : `dict` [`str`, `any`]
397
+ The dictionary to be subsetted and transformed.
398
+ pattern : `dict` [`str`, `str`]
399
+ A dictionary where each key is a prefix to match against the keys of
400
+ `data`, and each value defines the suffix to search within the matching
401
+ keys in `data`. The value found in `data` corresponding to this suffix
402
+ is used as the prefix for the resulting dictionary keys. The pattern
403
+ keys will be matched to the data keys based on a `startswith`, while
404
+ the pattern values will be matched to the data keys using:
405
+ `value in key`.
406
+ substitute : `tuple` [`str`,`str`] or `None`, default `(r"_[0-9]{1,2}\\.*", " ")`
407
+ A tuple containing a regular expression pattern and replacement string.
408
+ This substitution is applied to the remaining portion of the `data` key
409
+ after removing the matching prefix.
410
+ character_trim : int, default `0`
411
+ The number of characters which should be removed from the right-hand
412
+ side of the `data` key which did not match the `pattern` key.
413
+ verbose : `bool`, default `True`
414
+ Whether warnings should be issued.
415
+
416
+ Returns
417
+ -------
418
+ dict [`str`, `any`]
419
+ A dictionary with keys grouped and transformed based on the `pattern`
420
+ and `substitute`. The keys are prefixed with values derived from `data`.
421
+
422
+ Examples
423
+ --------
424
+ >>> data = {
425
+ ... "Sequence_0.Referenced Waveform Channels_0": "Channel 1",
426
+ ... "Sequence_0.Referenced Waveform Channels_1": "Channel 2",
427
+ ... "Sequence_0.Annotation Group Number": 1,
428
+ ... "Sequence_0.Unformatted Text Value": "Event A",
429
+ ... "Sequence_8.Measurement Units Code Sequence_0.Code Value": "bpm",
430
+ ... "Sequence_8.Measurement Units Code Sequence_0.Code Meaning": "Heart Rate",
431
+ ... "Sequence_15.Measurement Units Code Sequence_0.Code Meaning": "Temperature",
432
+ ... "Sequence_15.Referenced Waveform Channels_1": "Channel 10",
433
+ ... "Sequence_15.Numeric Value": 36.7,
434
+ ... "Other Annotation Sequence_1.Some Value": "Other Data",
435
+ ... "Other Annotation Sequence_1.Code Meaning": "Other Code Meaning",
436
+ ... }
437
+ >>> pattern = {
438
+ ... "Sequence_15": "Code Meaning",
439
+ ... "Sequence_8": "Code Meaning",
440
+ ... "Sequence_16": "Code Meaning",
441
+ ... "Sequence_11": "Code Meaning",
442
+ ... "Other Annotation Sequence_1": "Code Meaning",
443
+ ... }
444
+ >>> subset_dict(data, pattern)
445
+ {'Temperature (Referenced Waveform Channels)': 'Channel 10',
446
+ 'Temperature (Numeric Value)': 36.7,
447
+ 'Heart Rate (Measurement Units Code SequenceCode Value)': 'bpm',
448
+ 'Other Code Meaning (Some Value)': 'Other Data'}
449
+ """
450
+ # #### confirm input
451
+ is_type(data, dict)
452
+ is_type(pattern, dict)
453
+ is_type(substitute, (type(None), tuple))
454
+ if not substitute is None:
455
+ if len(substitute) != 2:
456
+ raise ValueError("`substitute` should be a `Nonetype` or a tuple "
457
+ "with exactly two entries.")
458
+ # #### empty default
459
+ # regex=True
460
+ grouped = {}
461
+ unmatched_keys = set(pattern.keys())
462
+ # #### loop
463
+ for d_name, g_name in pattern.items():
464
+ # fist get all data entries where the key starts with `d_name`
465
+ prefix = None
466
+ sub_dict = {}
467
+ for k, v in data.items():
468
+ mtch = k.startswith(d_name)
469
+ if mtch:
470
+ # remove from unmatched
471
+ unmatched_keys.discard(d_name)
472
+ # Subset the non-matching string
473
+ d_name_len = len(d_name)
474
+ str_remainder = k[d_name_len + character_trim:]
475
+ # Do we want a cleaner string
476
+ str_remainder_clean = str_remainder
477
+ if not substitute is None:
478
+ m, r = substitute
479
+ str_remainder_clean = re.sub(m, r, str_remainder).strip()
480
+ # Check g_name is in str_remainder
481
+ if g_name in str_remainder:
482
+ if prefix is not None:
483
+ raise ValueError(
484
+ f"For pattern {d_name}, `prefix`: {prefix} is "
485
+ f"already set, cannot assign new value: {v} "
486
+ f"please ensure `{g_name}` is unique.")
487
+ prefix = v
488
+ else:
489
+ sub_dict[str_remainder_clean] = v
490
+ # next confirm prefix is not None, and add the prefix
491
+ if len(sub_dict) > 0:
492
+ if prefix is None:
493
+ if skip_empty:
494
+ continue
495
+ else:
496
+ raise ValueError(f"`{g_name}` was not found in data.")
497
+ grouped.update(
498
+ {f"{prefix} ({key})": val for key, val in sub_dict.items()}
499
+ )
500
+ del sub_dict
501
+ # do we want to return un-matching keys
502
+ if verbose and unmatched_keys:
503
+ warnings.warn("The following keys did not match any pattern: "
504
+ f"{', '.join(unmatched_keys)}")
505
+ # return
506
+ return grouped
507
+
508
+
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: ECGProcess
3
+ Version: 1.1.0
4
+ Summary: A python module to process and standardise ECG data
5
+ Author-email: "A. Floriaan Schmidt" <floriaanschmidt@gmail.com>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://gitlab.com/SchmidtAF/ECGProcess
8
+ Project-URL: Documentation, https://SchmidtAF.gitlab.io/ECGProcess/
9
+ Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
15
+ Requires-Python: <3.14,>=3.11
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENCE.txt
18
+ Requires-Dist: lxml>=5
19
+ Requires-Dist: xmltodict>=0.14
20
+ Requires-Dist: pandas>=2.1.1
21
+ Requires-Dist: numpy>=1.26
22
+ Requires-Dist: scipy>=1.7
23
+ Requires-Dist: tensorflow>=2.16
24
+ Requires-Dist: pydicom>=2
25
+ Requires-Dist: matplotlib>=3.9
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=6; extra == "dev"
28
+ Requires-Dist: pytest-mock>=3.0; extra == "dev"
29
+ Requires-Dist: pytest-dependency>=0.5; extra == "dev"
30
+ Requires-Dist: codespell; extra == "dev"
31
+ Requires-Dist: ruff; extra == "dev"
32
+ Requires-Dist: bump2version>=1; extra == "dev"
33
+ Requires-Dist: build; extra == "dev"
34
+ Requires-Dist: twine; extra == "dev"
35
+ Requires-Dist: sphinx; extra == "dev"
36
+ Requires-Dist: sphinx-rtd-theme; extra == "dev"
37
+ Requires-Dist: sphinx-bootstrap-theme; extra == "dev"
38
+ Requires-Dist: furo; extra == "dev"
39
+ Requires-Dist: myst-parser; extra == "dev"
40
+ Requires-Dist: nbsphinx; extra == "dev"
41
+ Requires-Dist: nbsphinx-link; extra == "dev"
42
+ Requires-Dist: sphinx-argparse; extra == "dev"
43
+ Requires-Dist: numpydoc; extra == "dev"
44
+ Requires-Dist: docutils<0.21; extra == "dev"
45
+ Requires-Dist: notebook; extra == "dev"
46
+ Requires-Dist: jupyter; extra == "dev"
47
+ Dynamic: license-file
48
+
49
+ ![Schematic](https://ecgprocess-schmidtaf-f9abe0f74d20c469a1e6cca62c7f636ef6bf6b9ffe.gitlab.io/_static/ECGProcessSchematic.png)
50
+
51
+ # An ECG processing module
52
+ __version__: `1.1.0`
53
+
54
+
55
+ This repository host a mixture of function dealing with processing
56
+ electrocardiogram (ECG) data, and preparing these data for analysis.
57
+ The processing functions are intended for user defined parametrisation
58
+ (using user supplied configuration files `cnf`) about the data which
59
+ should be extracted, covering signal data such as waveforms or
60
+ median beats, as well as metadata relevant for downstream QC or as
61
+ tabular information.
62
+
63
+ The module implements a basic application programming interface, mapping
64
+ ECG data from distinct file types (e.g. XML or DICOM) to class attributes
65
+ providing a fixed interface for downstream analysis and interrogation.
66
+
67
+ The documentation for ECGProcess can be found [here](https://SchmidtAF.gitlab.io/ECGProcess/).
68
+
69
+
70
+ ## Installation
71
+ At present, the repository is undergoing development and no packages exist yet
72
+ on PyPy or in Conda.
73
+ Therefore it is recommended that it is installed in either of the two ways
74
+ listed below.
75
+ First, clone this repository and then `cd` to the root of the repository.
76
+
77
+ ```sh
78
+ git clone git@gitlab.com:SchmidtAF/ECGProcess.git
79
+ cd ECGProcess
80
+ ```
81
+
82
+ ### Installation using conda dependencies
83
+ A conda environment is provided in a `yaml` file in the directory
84
+ `./resources/conda/envs/`.
85
+ A new conda environment called `ecgprocess` can be built using the command:
86
+
87
+ ```sh
88
+ # From the root of the repository
89
+ conda env create --file ./resources/conda/envs/conda_create.yaml
90
+ ```
91
+
92
+ To add to an existing environment use:
93
+
94
+ ```sh
95
+ # From the root of the repository
96
+ conda env update --file ./resources/conda/envs/conda_update.yaml
97
+ ```
98
+
99
+ Next the package can be installed:
100
+
101
+ ```sh
102
+ python -m pip install .
103
+ ```
104
+
105
+ Or for an editable (developer) install run the command below from the root of
106
+ the repository.
107
+ The difference with this is that you can just to a `git pull` to
108
+ update repository, or switch branches without re-installing:
109
+
110
+ ```sh
111
+ python -m pip install -e .
112
+
113
+ ```
114
+ ### Installation not using any conda dependencies
115
+ If you are not using conda in any way then install the dependencies via `pip`
116
+ and install repository as an editable install also via pip:
117
+
118
+ Install dependencies:
119
+
120
+ ```sh
121
+ python -m pip install --upgrade -r requirements.txt
122
+ ```
123
+
124
+ Then to install repository you can either do:
125
+
126
+ ```sh
127
+ python -m pip install .
128
+ ```
129
+
130
+ Or for an editable (developer) install run the command below from the root of
131
+ the repository.
132
+ The difference with this is that you can just to a `git pull` to update
133
+ repository, or switch branches without re-installing:
134
+
135
+ ```sh
136
+ python -m pip install -e .
137
+ ```
138
+
139
+ ## Next steps...
140
+ After installation you might wish to try the `pytest` to confirm
141
+ everything is in working order.
142
+
143
+ ```sh
144
+ # From the root of the repository
145
+ pytest tests
146
+ ```
147
+
148
+ ## Usage
149
+
150
+ Please have a look at the examples in
151
+ [resources](https://gitlab.com/SchmidtAF/ECGProcess/-/tree/master/resources/examples)
152
+ for some possible recipes.
153
+
@@ -0,0 +1,33 @@
1
+ ecgprocess/__init__.py,sha256=ZygAIkX6Nbjag1czWdQa-yP-GM1mBE_9ss21Xh__JFc,34
2
+ ecgprocess/_version.py,sha256=b6-WiVk0Li5MaV2rBnHYl104TsouINojSWKqHDas0js,22
3
+ ecgprocess/constants.py,sha256=otryxvYntmB6c_xH_eMMpVXNe0F-7OKqvbh1zTaYA40,5933
4
+ ecgprocess/errors.py,sha256=BDZPl_4cLgGV3AAiiphS3xnx9f57VBcvr0hmRLz_Ks4,12025
5
+ ecgprocess/plot_ecgs.py,sha256=ia6ypRperf5GBNYvizyw_Xv5H8AQQU6oR8hAjb_U4xg,38832
6
+ ecgprocess/process_dicom.py,sha256=tm8IMU0D-3xKNE2MfUB7BtbmSdhlwk_WgiX45HZx_PI,9667
7
+ ecgprocess/process_xml.py,sha256=ep9l69q5WYf3PKUMhguGSLtQsqyX1dbPxw1R5jt7rp0,12750
8
+ ecgprocess/tabular.py,sha256=Yk3UbqV2nEugNT54HNVL2o6_bXEOOfL3nwjW_OW0N4I,63742
9
+ ecgprocess/example_data/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
10
+ ecgprocess/example_data/examples.py,sha256=nbdHfEWyruNwYo0vtoKTwfLldGOwx6fFgXPK8t6kXw4,17864
11
+ ecgprocess/example_data/example_datasets/.example-XML1.xml.un~,sha256=8VbZNIN8SRTsrhE-hC_OKtScnKcd2oT2XlR7GMNiOjs,2872
12
+ ecgprocess/example_data/example_datasets/.example-XML1.xsd.un~,sha256=jSMJf_mChOZ-LP6Rk9ikjQmpkQ0e3-AC49ykZ1QHbKU,377534
13
+ ecgprocess/example_data/example_datasets/.example-XML1_a.xsd.un~,sha256=raUIHMAEEB9vtz40UdXQntp-uj4ZW35reqzUw338ceY,22780
14
+ ecgprocess/example_data/example_datasets/.example-XML1_d.xsd.un~,sha256=1zEIOMdzGlbu5OlOiyxzWwncGsP1QtDyQk6PWRFKu2Q,238478
15
+ ecgprocess/example_data/example_datasets/.example-XML1_wrong.xsd.un~,sha256=e_Vkp2EQ-xDHtPU8qP4p6nuCCnlmWAcWhWVZh_RjliU,2558
16
+ ecgprocess/example_data/example_datasets/example-DICOM1.dcm,sha256=dBkuaxVeZU21tpJ9ELwKg60MqT65v4rPcBBj7JHD0oM,142376
17
+ ecgprocess/example_data/example_datasets/example-DICOM2.dcm,sha256=Tg75DfNb8ZyPITWEU0paJqaz30a4QSL0IX1NSxnhu0Q,143650
18
+ ecgprocess/example_data/example_datasets/example-XML1.xml,sha256=sAuj7rTz9rWXcBN5Z0o3uD7UNRSn9aJ1vYsAS1NaycI,517675
19
+ ecgprocess/example_data/example_datasets/example-XML1.xsd,sha256=auIlBkM2Ps8NRLRQE0ZEBqXiSHezZZO3DfFqrbdw5U8,30011
20
+ ecgprocess/example_data/example_datasets/example-XML1_error.xsd,sha256=CCOYDskNP_2xxcgLIu4vyt_rKpE6oVRdi7kvDxfVXks,30178
21
+ ecgprocess/example_data/example_datasets/example-XML1_wrong.xsd,sha256=CCOYDskNP_2xxcgLIu4vyt_rKpE6oVRdi7kvDxfVXks,30178
22
+ ecgprocess/example_data/example_datasets/string_data.txt,sha256=Ss_TTlNXIF_-oI6RNCx9Xc2YXv_6OZhmY1f6iq4GK_E,18
23
+ ecgprocess/utils/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
24
+ ecgprocess/utils/config_tools.py,sha256=KznuK0vENK5eyXz2a2WYWlGVzyZH_alTApIG0nsjUEE,23155
25
+ ecgprocess/utils/ecg_tools.py,sha256=chCZ9mP8yEAprRqGFw88R9h1fDteIRXAPecogsWMQYE,11027
26
+ ecgprocess/utils/engineering_tools.py,sha256=lUuj5qajRVOFm1QWQ78iI6B_6I-HcsBKx1P0bF3aVV8,14802
27
+ ecgprocess/utils/general.py,sha256=v3UA70Og_YAw50PePPQ0Vi5grjumabqxINnyqE-bnCU,13057
28
+ ecgprocess/utils/reader_tools.py,sha256=Uwd6QT_crn446B45P43ZOZOi2FdyVQHvoWO2o9FJWlY,19283
29
+ ecgprocess-1.1.0.dist-info/licenses/LICENCE.txt,sha256=IGTG-FiCwz8h8OpA1dOMUEGxlg0US7wOW3ZDmQI-ouo,778
30
+ ecgprocess-1.1.0.dist-info/METADATA,sha256=a9t-XhFP3VZO8Ddu8XQaHfOMzVzCGFZAiTjGuuh85d0,4928
31
+ ecgprocess-1.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
32
+ ecgprocess-1.1.0.dist-info/top_level.txt,sha256=i1wlvtScBPYBll7dyIgwR3qyrg9_u7m-uQ2_z0L6snw,11
33
+ ecgprocess-1.1.0.dist-info/RECORD,,