Extracting ECG data from DICOM files
The ecgprocess.py module wraps pydicom to standardise and extract patient information tables, as well as lead-specific waveforms and median beats.
[1]:
import os
import tempfile
import numpy as np
import pandas as pd
import ecgprocess.process_dicoms as pdicom
from ecgprocess.process_dicoms import (
ECGDICOMReader,
ECGDICOMTable,
)
from ecgprocess.example_data.examples import (
list_data_paths,
)
# Example file
dicom_file_path = list_data_paths()['example_dicom_2']
The ECGDICOMReader dataclass
[2]:
ECG_DCM = ECGDICOMReader(retain_raw=False)
dicom_record = ECG_DCM(path=dicom_file_path)
Participant information
[3]:
# patient information
pd.Series(dicom_record.GeneralInfo)
[3]:
SOPinstanceUID 1.3.6.1.4.1.40744.65.2218354498692802781165553...
SERIESinstanceUID 1.3.6.1.4.1.40744.65.2534925851648250593315477...
STUDYinstanceUID 1.3.6.1.4.1.40744.65.1513822871218815858359656...
PatientID 66954
StudyDate 2006-02-01
...
T Offset UNIT NaN
PR Interval 0.0
PR Interval UNIT millisecond
UnformattedTextValue
Duration 10.0
Length: 75, dtype: object
Waveforms and Median beats
[4]:
dicom_record.Waveforms
[4]:
{'I (Einthoven)': array([-105., -105., -105., ..., -75., -80., -85.]),
'II': array([-185., -185., -185., ..., -215., -220., -220.]),
'III': array([ -75., -75., -75., ..., -140., -140., -135.]),
'aVR': array([145., 145., 145., ..., 145., 150., 155.]),
'aVL': array([-10., -10., -10., ..., 25., 25., 20.]),
'aVF': array([-130., -130., -130., ..., -180., -185., -180.]),
'V1': array([65., 65., 65., ..., 40., 45., 45.]),
'V2': array([-175., -175., -175., ..., -250., -245., -240.]),
'V3': array([ -55., -55., -55., ..., -155., -150., -145.]),
'V4': array([-210., -210., -210., ..., -125., -125., -125.]),
'V5': array([-145., -145., -145., ..., -165., -165., -165.]),
'V6': array([-125., -125., -125., ..., -190., -185., -185.])}
[5]:
dicom_record.MedianWaveforms['II'][0:20]
[5]:
array([20., 15., 10., 10., 10., 10., 5., 0., 0., 0., 0., 5., 5.,
5., 5., 5., 0., 0., 0., 0.])
Custom behaviour
The class object has a number of attributes (dictionaries) which can be used to extract more (or less) data from pydicom instance.
[6]:
# extract `any comments` - which is rather uninformative in this case: `nan`.
# add the comments name to the METADATA dict
# here the value should map to the pydicom attribute name
# and the key to the new name we want to use internally
ECG_DCM.METADATA['Image Comment'] = 'ImageComments'
dicom_record_new = ECG_DCM(path=dicom_file_path)
dicom_record_new.GeneralInfo['Image Comment']
[6]:
nan
Exclusively extract metadata
Some dicom files will only have metadata. These can also be processed ECGDICOMReader.
[7]:
_, metadata, _ = ECG_DCM.get_metadata(path=dicom_file_path)
print(pd.Series(metadata))
SOPinstanceUID 1.3.6.1.4.1.40744.65.2218354498692802781165553...
SERIESinstanceUID 1.3.6.1.4.1.40744.65.2534925851648250593315477...
STUDYinstanceUID 1.3.6.1.4.1.40744.65.1513822871218815858359656...
PatientID 66954
StudyDate 20060201
StudyTime 102403
AccessionNumber ST20060201
PatientBirthDate
PatientName (6, 6, 9, 5, 4)
PatientSex
StudyDescription DT4H
AcquisitionDateTime 20060201102403
AcquisitionTimeZone +0100
Manufacturer GE
ManufacturerModelName MAC55
SoftwareVersions NaN
DataExportedBy AUMC^EVA
Image Comment NaN
dtype: object
ECGDICOMTable
A class to extract data from multiple records and map these to tabular data.
We will first simply initilise the class and differentiate between valid and invalid files.
[8]:
dicom_path_list = [
list_data_paths()['example_dicom_1'],
list_data_paths()['example_dicom_2'],
]
ecgreader = ECGDICOMReader()
ecgtable = ECGDICOMTable(ecgreader, path_list=dicom_path_list)
res = ecgtable(verbose=True)
print('Failed paths: {0}\nCurated paths: {1}.'.format(
None if len(res.FailedPathList) == 0 else res.FailedPathList,
res.CuratedPathList))
Failed paths: None
Curated paths: ['/home/amand/google_drive/Research/ECGProcess/ecgprocess/example_data/example_datasets/example-DICOM1.dcm', '/home/amand/google_drive/Research/ECGProcess/ecgprocess/example_data/example_datasets/example-DICOM2.dcm'].
Process the two files
Our first try at this will raise an error because the headers do not match.
[9]:
try:
res.get_table()
except Exception as e:
print(f"\n\nAn error occurred: {e}")
Processing path: /home/amand/google_drive/Research/ECGProcess/ecgprocess/example_data/example_datasets/example-DICOM1.dcm.
Processing path: /home/amand/google_drive/Research/ECGProcess/ecgprocess/example_data/example_datasets/example-DICOM2.dcm.
An error occurred: "The dictionaries contain distinct keys. The last set of valid keys was ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6'], compared to ['I (Einthoven)', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']."
Next we will solve this by providing a dictionary mapping the old name to the new name. These dictionaries can be expanded for multiple potential names and ignored if there are no matches. This will results in a long table with multiple files row-binded.
[10]:
combined=res.get_table(update_keys={'I (Einthoven)': 'I'})
combined.WaveFormsTable
Processing path: /home/amand/google_drive/Research/ECGProcess/ecgprocess/example_data/example_datasets/example-DICOM1.dcm.
Processing path: /home/amand/google_drive/Research/ECGProcess/ecgprocess/example_data/example_datasets/example-DICOM2.dcm.
[10]:
| SOPinstanceUID | SamplingSequence | Lead | Voltage | Waveform type | |
|---|---|---|---|---|---|
| 0 | 2.25.269796857626990821315969488216511468638 | 0 | I | 0.00 | rhythm |
| 1 | 2.25.269796857626990821315969488216511468638 | 1 | I | 0.00 | rhythm |
| 2 | 2.25.269796857626990821315969488216511468638 | 2 | I | 29.28 | rhythm |
| 3 | 2.25.269796857626990821315969488216511468638 | 3 | I | 43.92 | rhythm |
| 4 | 2.25.269796857626990821315969488216511468638 | 4 | I | 48.80 | rhythm |
| ... | ... | ... | ... | ... | ... |
| 119995 | 1.3.6.1.4.1.40744.65.2218354498692802781165553... | 4995 | V6 | -195.00 | rhythm |
| 119996 | 1.3.6.1.4.1.40744.65.2218354498692802781165553... | 4996 | V6 | -190.00 | rhythm |
| 119997 | 1.3.6.1.4.1.40744.65.2218354498692802781165553... | 4997 | V6 | -190.00 | rhythm |
| 119998 | 1.3.6.1.4.1.40744.65.2218354498692802781165553... | 4998 | V6 | -185.00 | rhythm |
| 119999 | 1.3.6.1.4.1.40744.65.2218354498692802781165553... | 4999 | V6 | -185.00 | rhythm |
120000 rows × 5 columns
Write to disk
These tables can of course be written to disk. However, loading data from multiple files to simply write these to disk will be ineffecient. A more memory efficient solution is to extract each individual DICOM and append results one-at-a-time to a directory file. This is what write_ecg does … Actually it writes four files to disk!
[11]:
with tempfile.TemporaryDirectory() as temp_dir:
combined=res.write_ecg(update_keys={'I (Einthoven)': 'I'}, target_path=temp_dir)
# list saved files
print('Directory content:')
for file_name in os.listdir(getattr(combined, 'target_path')):
print(' - {}'.format(file_name))
Processing path: /home/amand/google_drive/Research/ECGProcess/ecgprocess/example_data/example_datasets/example-DICOM1.dcm.
Processing path: /home/amand/google_drive/Research/ECGProcess/ecgprocess/example_data/example_datasets/example-DICOM2.dcm.
Directory content:
- GeneralInfoTable.tsv.gz
- WaveFormsTable.tsv.gz
- FailedFiles.txt
- MedianWaveTable.tsv.gz
[12]:
help(pdicom)
Help on module ecgprocess.process_dicoms in ecgprocess:
NAME
ecgprocess.process_dicoms
DESCRIPTION
A module to extract median beats and raw waveforms from DICOM ECGs wrapping
pydicom.
CLASSES
builtins.object
BaseECGDICOMReader
ECGDICOMReader
ECGDICOMTable
class BaseECGDICOMReader(builtins.object)
| BaseECGDICOMReader(lead_voltages=None, lead_voltages2=None, results_dict=None)
|
| An ECGDICOMReader base class implementing the more efficient __slots__ for
| the waveform arrays, while still retaining __dict__ dynamic attribute
| creation.
|
| Methods defined here:
|
| __init__(self, lead_voltages=None, lead_voltages2=None, results_dict=None)
| Initialises slots entries to `None`.
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| GeneralInfo
|
| MedianWaveforms
|
| Waveforms
class ECGDICOMReader(BaseECGDICOMReader)
| ECGDICOMReader(augment_leads: bool = False, resample_500: bool = True, retain_raw: bool = False) -> None
|
| Takes an ECG DICOM file and extracts metadata, median beats (if available)
| and raw waveforms.
|
| Parameters
| ----------
| augment_leads : bool, default `False`
| Whether the augmented leads are available in the DICOM, if not
| these are calculated.
| resample : bool, default `True`
| Whether to resample the ECG to a frequency of 500 Hertz.
| retain_raw : bool, default `False`
| Whether the raw pydicom instance and raw waveforms should be retained.
| Set to `False` to decrease memory usage. Set to `True` to explore the
| orignal pydicom instance. For example, use this one a few files to
| identify none-standard information to extract.
|
| Attributes
| ----------
| augment_leads : bool
| Whether the augmented leads were calculated if these were unavailable.
| resample : bool
| Whether the ECG was resampled to a 500 Hertz frequency.
| retain_raw : bool
| Whether the raw pydicom data was retained.
| METADATA : dict [`str`, `str`]
| A dictionary describing the metadata one wants to extract from a
| DICOM. The dictionary keys represents the `target` (new) name and the
| dictionary values the `source` (old) names.
| ECG_TRAIT_DICT : dict [`str`, `list[str]`]
| A dictionary with the keys reflecting the desired name of the ECG trait.
| Each key will have a list of strings as a value. These strings will be
| compared to the names in `WaveformAnnotationSequence` attribute.
| Matching is done without case-sensitivity. If for any key there are
| multiple matching strings the algorithm will check if the extracted
| values are all the same, if not multiple entries will be returned for
| the user to decide what to do next. The extracted ECG traits will be
| included with the extracted METADATA.
| STUDY_DATE_SOURCE : str
| The study date format of the source DICOM. This will be used in
| `datetime.strptime` as format argument.
| STUDY_DATE_TARGET : str
| The target formatting of the study date. This will be used in
| `datetime.strptime` as format argument.
| ACQUISITION_DATE_SOURCE : str
| The acquisition date and time format of the source DICOM. This will be
| used in `datetime.strptime` as format argument.
| ACQUISITION_DATE_TARGET : str
| The target formatting of the acquisition date and time. This will be
| used in `datetime.strptime` as format argument.
|
| Methods
| -------
| get_metadata(path, skip_empty)
| Extract the dicom metadata.
| make_leadvoltages(waveform_array, lead_info, augment_leads)
| Extracts the voltages from a DICOM file. Will automatically extract the
| limb leads if missing.
|
| Notes
| -----
| The waveforms are stored using `__slots__` to decrease memory usage, which
| is further improved by setting `retain_raw` to `False` - the default
| behaviour.
|
| Method resolution order:
| ECGDICOMReader
| BaseECGDICOMReader
| builtins.object
|
| Methods defined here:
|
| __call__(self, path: str, skip_empty: bool = True, verbose: bool = False) -> Self
| Read a `.dcm` DICOM file and extracts metadata, raw waveforms, and
| median beats.
|
| see `constants.DICOMTags` for the `METADATA`, `WAVE_FORMS`, and
| `MEADIAN_BEATS` tags looked for.
|
| Parameters
| ----------
| path : str
| The path to the .dcm file.
| skip_empty : bool, default `True`
| Whether empty tags should be skipped or throw an error.
| verbose : bool, default `False`
| Prints missing tags if skip_empty is set to `True`.
|
| Attributes
| ----------
| GeneralInfo : list [`str`]
| A list of dcmread extracted attributes.
| Waveforms : dict [`str`, `np.array`]
| The lead specific ECG waveforms.
| MedianWaveforms : dict [`str`, `np.array`]
| The lead specific ECG median beats.
|
| Returns
| -------
| self : `ECGDICOMReader` instance
| Returns the class instance with updated attributes extracted
| from `dcmread`.
|
| __eq__(self, other)
| Return self==value.
|
| __init__(self, augment_leads: bool = False, resample_500: bool = True, retain_raw: bool = False) -> None
| Initialises slots entries to `None`.
|
| __repr__(self)
| Return repr(self).
|
| get_metadata(self, path: str | None = None, dicom_instance: pydicom.dataset.FileDataset | None = None, skip_empty: bool = True) -> tuple[pydicom.dataset.FileDataset, dict[str, typing.Any], list[str]]
| Takes a dicom file and extracts its metedata
|
| Parameters
| ----------
| path : str, default `NoneType`.
| The path to the .dcm file.
| dicom_instance : DCM_Class, default `NoneType`.
| A DCM_Class instance.
|
| Returns
| -------
| results : dict,DCM_Class
| - A `DCM_Class` instance.
| - A dictionary with extracted metadata.
| - A list of missing `DCM_Class` attribute names.
|
| Notes
| -----
| Either supply a path to a dicom file or a DCM_Class instance
|
| make_leadvoltages(self, waveform_array: numpy.ndarray, lead_info: Dict[int, str], augment_leads: bool) -> Dict[str, numpy.ndarray]
| Extracts the voltages from a DICOM file. Will automatically extract the
| limb leads if missing, please see:
| `url <https://ecgwaves.com/topic/ekg-ecg-leads-electrodes-systems-limb-chest-precordial/>`_.
|
| Parameters
| ----------
| waveform_array : np.ndarray
| An array with the waveforms.
| lead_info: dict [`int`, `str`]
| A dictionary where the keys match the `waveform_array` rows and
| the dictionary values contain the lead name strings.
| augment_leads : bool
| Whether to calculate the additional augmented leads. Will only work
| if there are exactly 8 leads available.
|
| Returns
| -------
| leads: dict [`str`, `np.ndarray`]
| A dictionary with the lead name string as keys and leads as
| np.ndarray values.
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables
|
| __weakref__
| list of weak references to the object
|
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|
| ACQUISITION_DATE_SOURCE = '%Y%m%d%H%M%S'
|
| ACQUISITION_DATE_TARGET = '%Y-%m-%d %H:%M:%S'
|
| ECG_TRAIT_DICT = {'ARate': ['Atrial Heart Rate'], 'P Axis': ['P Axis']...
|
| MEDIAN_BEATS = {'ChannelBaseline (MEDIAN)': 'ChannelBaseline', 'Channe...
|
| MEDIAN_BEATS_DICT = {'MedianWaveforms': ['make_leadvoltages', 1], 'Raw...
|
| METADATA = {'AccessionNumber': 'AccessionNumber', 'AcquisitionDateTime...
|
| STUDY_DATE_SOURCE = '%Y%m%d'
|
| STUDY_DATE_TARGET = '%Y-%m-%d'
|
| WAVE_FORMS = {'ChannelBaseline': 'ChannelBaseline', 'ChannelNumber': '...
|
| WAVE_FORMS_DICT = {'Raw_Waveforms': ['waveform_array', 0]}
|
| __annotations__ = {'augment_leads': <class 'bool'>, 'resample_500': <c...
|
| __dataclass_fields__ = {'augment_leads': Field(name='augment_leads',ty...
|
| __dataclass_params__ = _DataclassParams(init=True,repr=True,eq=True,or...
|
| __hash__ = None
|
| __match_args__ = ('augment_leads', 'resample_500', 'retain_raw')
|
| augment_leads = False
|
| resample_500 = True
|
| retain_raw = False
|
| ----------------------------------------------------------------------
| Data descriptors inherited from BaseECGDICOMReader:
|
| GeneralInfo
|
| MedianWaveforms
|
| Waveforms
class ECGDICOMTable(builtins.object)
| ECGDICOMTable(ecgdicomreader: ecgprocess.process_dicoms.ECGDICOMReader, path_list: List[str]) -> None
|
| Takes an `ECGDICOMReader` instance and loops over a list of dicom paths
| and maps these to which can be used in analyses or saved to disk.
|
| Attributes
| ----------
| RawPathList : list [`str`]
| A list of dcm file paths.
|
| Methods
| -------
| get_table(update_keys,**kwargs)
| extracts data from multiple dicom files and maps these to
| pandas.DataFrames.
| write_ecg(target_tar, target_path, sep, mode, compression, update_keys,
| **kwargs)
| writes each dicom file to a single sets of files (metadata,
| waveforms, median beats). This is done by appending the extracted
| data from each file the target file set, minimising the memory
| footprint.
|
| Methods defined here:
|
| __call__(self, skip_missing: Literal['Permissions', 'Data', 'None'] = 'Permissions', verbose: bool = False) -> Self
| Will take a ECGDICOMReader and loops over a list of dcm file paths and
| and confirm the files exist and have appropriate read permission
|
| Parameters
| ----------
| skip_missing : {'Permissions', 'Data', 'None'}, default `Permissions`
| Whether files with a `PermissionError` or `AttributeError` should
| be skipped, or whether all errors should be raised.
| A PermissionError implies either the file is absent or it cannot
| be read. An AttributeError indicates the waveform_array
| attribute is absent from the pydicom instance.
| `Permissions` will only skip PermissionErrors, while `Data` will
| skip both PermssionErrors and AttributeErrors (due to a missing
| waveform_array), `None` will not skip errors.
| verbose : bool, default `False`
| Prints missing files if skip_missing is set to `True`.
|
| Attributes
| ----------
| FailedPathList : list [`str`]
| File paths which were either absent or without read permission.
| CuratedPathList : list [`str`]
| File paths which are readable.
|
| Returns
| -------
| self : `ECGDICOMTable` instance
| Returns the class instance with updated attributes.
|
| __init__(self, ecgdicomreader: ecgprocess.process_dicoms.ECGDICOMReader, path_list: List[str]) -> None
| Initialises a new instance of `ECGDICOMTable`.
|
| Parameters
| ----------
| ecgdicomreader : ECGDICOMReader
| An instance of the ECGDICOMReader data class.
| path_list : list [`str`]
| A list of paths to one or more .dcm files.
|
| __repr__(self)
| Return repr(self).
|
| __str__(self)
| Return str(self).
|
| get_table(self, update_keys: Optional[Dict[str, str]] = None, **kwargs: Optional[Any]) -> Self
| Will extracted dicom data to tables.
|
| Parameters
| ----------
| update_keys: dict [`str`, `str`], default `NoneType`
| A dictionary to remap lead names: [`old`, `new`]
| **kwargs: optional
| Keyword arguments used in the call method of a `ECGDICOMReader`
| instance
|
| Attributes
| ----------
| IndexList: list [`str`]
| A list of unique identifiers.
| GeneralInfoTable: pandas.DataFrame
| A table of the dicom metadata.
| WaveFormsTable: pandas.DataFrame
| A long-format table with waveforms.
| MedianWaveTable: pandas.DataFrame
| A long-format table with the median beat waveforms.
| NoDataList : `list` [`str`]
| A list of dicom files without a waveform_array.
|
| Returns
| -------
| self : `ECGDICOMTable` instance
| Returns the class instance with updated attributes.
|
| write_ecg(self, target_tar: Optional[str] = None, target_path: str = '.', sep: str = '\t', mode: str = 'w:gz', compression: str = 'gzip', update_keys: Optional[Dict[str, str]] = None, **kwargs: Optional[Any])
| Extracts dicom files, and write these one by one to a set of target
| files. By appending each dicom individually to the target files the
| memory footprint of this function will be minimal.
|
| Parameters
| ----------
| target_tar : str, default `NoneType`
| The `name` of an optional tarfile where the individual files will
| be written to. The target_tar will be concatenated to
| `target_path` and depending on `mode` this directory will be
| tar.gz compressed. Set `target_tar` to `NoneType` to simply add
| the files directly to `target_path`. Note this will overwrite
| any potential directory or files with the identical names.
| target_path : str, default '.'
| The full path where the files should be written to. if provided
| `target_tar` will be created underneath this path, otherwise the
| files will be directly written to the `target_path` terminal
| directory (assuming this is writable).
| sep : str, default ' '`
| The file separator, which will be passed to
| pandas.DataFrame.to_csv.
| mode : str, default `w:gz`
| The tarfile.open mode.
| compression : str, default `gzip`
| The file compression passed to pandas.DataFrame.write_csv.
| update_keys : dict [`str`, `str`], default `NoneType`
| A dictionary to remap lead names: [`old`, `new`]
| **kwargs : Optional[Any],
| Keyword arguments used in the call method of a `ECGDICOMReader`
| instance.
|
| Attributes
| ----------
| target_path : str
| The directory or tar file path were the files are written to.
| NoDataList : `list` [`str`]
| A list of dicom files without a waveform_array.
|
| Returns
| -------
| None
|
| Notes
| -----
| This method writes the following files to disk:
| - `GeneralInfoTable.tsv`
| - `WaveFormsTable.tsv`
| - `MedianWaveTable.tsv`
| - `FailedFiles.txt`
|
| Raises
| ------
| NotADirectoryError or PermissionError
| If the target directory does not exist or is not writable.
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables
|
| __weakref__
| list of weak references to the object
DATA
Dict = typing.Dict
A generic version of dict.
List = typing.List
A generic version of list.
Literal = typing.Literal
Special typing form to define literal types (a.k.a. value types).
This form can be used to indicate to type checkers that the corresponding
variable or function parameter has a value equivalent to the provided
literal (or one of several literals)::
def validate_simple(data: Any) -> Literal[True]: # always returns True
...
MODE = Literal['r', 'rb', 'w', 'wb']
def open_helper(file: str, mode: MODE) -> str:
...
open_helper('/some/path', 'r') # Passes type check
open_helper('/other/path', 'typo') # Error in type checker
Literal[...] cannot be subclassed. At runtime, an arbitrary value
is allowed as type argument to Literal[...], but type checkers may
impose restrictions.
Optional = typing.Optional
Optional[X] is equivalent to Union[X, None].
Self = typing.Self
Used to spell the type of "self" in classes.
Example::
from typing import Self
class Foo:
def return_self(self) -> Self:
...
return self
This is especially useful for:
- classmethods that are used as alternative constructors
- annotating an `__enter__` method which returns self
Tuple = typing.Tuple
Deprecated alias to builtins.tuple.
Tuple[X, Y] is the cross-product type of X and Y.
Example: Tuple[T1, T2] is a tuple of two elements corresponding
to type variables T1 and T2. Tuple[int, float, str] is a tuple
of an int, a float and a string.
To specify a variable-length tuple of homogeneous type, use Tuple[T, ...].
Type = typing.Type
Deprecated alias to builtins.type.
builtins.type or typing.Type can be used to annotate class objects.
For example, suppose we have the following classes::
class User: ... # Abstract base for User classes
class BasicUser(User): ...
class ProUser(User): ...
class TeamUser(User): ...
And a function that takes a class argument that's a subclass of
User and returns an instance of the corresponding class::
def new_user[U](user_class: Type[U]) -> U:
user = user_class()
# (Here we could write the user object to a database)
return user
joe = new_user(BasicUser)
At this point the type checker knows that joe has type BasicUser.
Union = typing.Union
Union type; Union[X, Y] means either X or Y.
On Python 3.10 and higher, the | operator
can also be used to denote unions;
X | Y means the same thing to the type checker as Union[X, Y].
To define a union, use e.g. Union[int, str]. Details:
- The arguments must be types and there must be at least one.
- None as an argument is a special case and is replaced by
type(None).
- Unions of unions are flattened, e.g.::
assert Union[Union[int, str], float] == Union[int, str, float]
- Unions of a single argument vanish, e.g.::
assert Union[int] == int # The constructor actually returns int
- Redundant arguments are skipped, e.g.::
assert Union[int, str, int] == Union[int, str]
- When comparing unions, the argument order is ignored, e.g.::
assert Union[int, str] == Union[str, int]
- You cannot subclass or instantiate a union.
- You can use Optional[X] as a shorthand for Union[X, None].
FILE
/home/amand/google_drive/Research/ECGProcess/ecgprocess/process_dicoms.py