Skip to content

Commit 48f8185

Browse files
fix offset and simplify template handling (#1)
* fix offset and simplify template handling * bump version
1 parent 5037233 commit 48f8185

File tree

8 files changed

+322
-265
lines changed

8 files changed

+322
-265
lines changed

README.md

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,28 @@ normalizer = rlign.Rlign(scale_method='hrc')
3333
# Input shape has to be (samples, channels, len)
3434
ecg_aligned = normalizer.transform(ecg)
3535

36-
# You can update some configurations later on
37-
template_ = rlign.Template(template_bpm=80)
38-
normalizer.update_configuration(template=template_)
36+
# You can set different configuration like median_beat-averaging or the template_bpm
37+
normalizer = rlign.Rlign(scale_method='hrc', median_beat=True, template_bpm=80)
3938

4039
ecg_aligned_80hz = normalizer.transform(ecg)
4140
```
4241

4342
### Configurations
4443

45-
* `sampling_rate`: Defines the sampling rate for all ECG recordings.
44+
* `sampling_rate`: Defines the sampling rate for all ECG recordings and the template. Default is set to 500.
4645

47-
* `template`: A template ECG created with `create_template()` method. This template is
48-
used as a reference for aligning R-peaks in the ECG signals.
46+
* `seconds_len`: Determines the duration of all ECG recordings and the template in seconds.
4947

50-
* `select_lead`: Specifies the lead (e.g., 'Lead II', 'Lead V1') for R-peak detection. Different leads can provide varying levels of
51-
clarity for these features. Selection via channel numbers 0,1,... .
48+
* `template_bpm`: The desired normalized BPM value for the template.
49+
This parameter sets the heart rate around which the QRST pattern
50+
is structured, thereby standardizing the R-peak positions according to a specific BPM.
51+
52+
* `offset`: The offset specifies the starting point for the first normalized QRS complex in the
53+
template. In percentage of sampling_rate. Default is set to 0.5.
54+
55+
* `select_lead`: Specifies the lead (e.g., 'Lead II', 'Lead V1') for R-peak detection.
56+
Different leads can provide varying levels of clarity for these features.
57+
Selection via channel numbers 0,1,... .
5258

5359
* `num_workers`: Determines the number of CPU cores to be utilized for
5460
parallel processing. Increasing this number can speed up computations
@@ -65,15 +71,16 @@ ecg_aligned_80hz = normalizer.transform(ecg)
6571
* `scale_method`: Selects the scaling method from options like 'resampling'
6672
or 'hrc'. This choice dictates the interval used for resampling
6773
the ECG signal, which can impact the quality of the processed signal.
74+
Default is 'hrc'.
6875

6976
* `remove_fails`: Determines the behavior when scaling is not possible. If
7077
set to True, the problematic ECG is excluded from the dataset. If False,
71-
the original, unscaled ECG signal is returned instead.
78+
the original, unscaled ECG signal is returned instead. Default is False.
7279

7380
* `median_beat`: Calculates the median from a set of aligned beats
7481
and returns a single, representative beat.
7582

76-
* `silent`: Disable all warnings.
83+
* `silent`: Disable all warnings. Default True.
7784

7885
## Citation
7986
Please use the following citation:

examples/example.ipynb

Lines changed: 93 additions & 97 deletions
Large diffs are not rendered by default.

examples/example_hz.ipynb

Lines changed: 105 additions & 106 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ requires = [
1111

1212
[project]
1313
name='rlign'
14-
version='1.0.post1'
14+
version='1.0.post2'
1515
description='Beat normalization for ECG data.'
1616
readme = "README.md"
1717
requires-python = ">=3.11"

rlign/rlign.py

Lines changed: 82 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,24 @@ class Rlign(BaseEstimator, TransformerMixin, auto_wrap_output_keys=None):
2020
ECG, lead selection, and artifact correction.
2121
2222
Parameters:
23-
sampling_rate: Defines the sampling rate for all ECG recordings.
23+
sampling_rate: Defines the sampling rate for all ECG recordings and the template.
2424
25-
template: Path or identifier for the template ECG. This template is
26-
used as a reference for identifying R-peaks in the ECG signals.
25+
seconds_len: Determines the duration of all ECG recordings and the template in seconds.
26+
27+
template_bpm: The desired normalized BPM value for the template. This parameter sets the
28+
heart rate around which the QRST pattern is structured, thereby standardizing the R-peak
29+
positions according to a specific BPM.
30+
31+
offset: The offset specifies the starting point for the first normalized QRS complex in the
32+
template. In percentage of sampling_rate. Default is set to 0.01.
2733
2834
select_lead: Specifies the lead (e.g., 'Lead II', 'Lead V1') for R-peak
2935
and QRST point detection. Different leads can provide varying levels of
3036
clarity for these features. Selection via channel numbers 0,1,... .
3137
3238
num_workers: Determines the number of CPU cores to be utilized for
3339
parallel processing. Increasing this number can speed up computations
34-
but requires more system resources.
40+
but requires more system resources. Default is set to 4.
3541
3642
neurokit_method: Chooses the algorithm for R-peak detection from the
3743
NeuroKit package. Different algorithms may offer varying performance
@@ -44,48 +50,57 @@ class Rlign(BaseEstimator, TransformerMixin, auto_wrap_output_keys=None):
4450
scale_method: Selects the scaling method from options 'identity', 'linear'
4551
or 'hrc'. This choice dictates the interval used for resampling
4652
the ECG signal, which can impact the quality of the processed signal.
53+
Default is 'hrc'.
4754
4855
remove_fails: Determines the behavior when scaling is not possible. If
4956
set to True, the problematic ECG is excluded from the dataset. If False,
50-
the original, unscaled ECG signal is returned instead.
57+
the original, unscaled ECG signal is returned instead. Default is False.
5158
5259
median_beat: Calculates the median from a set of aligned beats and returns
53-
a single, representative beat.
60+
a single, representative beat. Default is False.
5461
55-
silent: Disable all warnings.
62+
silent: Disable all warnings. Default True.
5663
"""
5764

58-
__allowed = ("sampling_rate", "template", "resample_method",
59-
"select_lead", "num_workers", "neurokit_method",
60-
"correct_artifacts", "scale_method")
65+
__allowed = ("sampling_rate", "resample_method", "select_lead",
66+
"num_workers", "neurokit_method", "correct_artifacts",
67+
"scale_method", "seconds_len", "template_bpm", "offset", "silent")
6168

6269
def __init__(
6370
self,
6471
sampling_rate: int = 500,
65-
template: Template = None,
72+
seconds_len: int = 10,
73+
template_bpm: int = 60,
74+
offset: float = .01,
6675
select_lead: int = 1,
67-
num_workers: int = 8, # only applies for multiprocessing
76+
num_workers: int = 4,
6877
neurokit_method: str = 'neurokit',
6978
correct_artifacts: bool = True,
7079
scale_method: str = 'hrc',
7180
remove_fails: bool = False,
7281
median_beat: bool = False,
7382
silent: bool = True
7483
):
75-
self.sampling_rate = sampling_rate
76-
if template:
77-
self.template = template
78-
else:
79-
self.template = Template(
80-
sampling_rate=sampling_rate
81-
)
84+
85+
self._sampling_rate = sampling_rate
86+
self._offset = offset
87+
self._template_bpm = template_bpm
88+
self._seconds_len = seconds_len
89+
90+
self.template = Template(
91+
sampling_rate=self.sampling_rate,
92+
offset=self.offset,
93+
template_bpm=self.template_bpm,
94+
seconds_len=self.seconds_len
95+
)
8296
self.select_lead = select_lead
8397
self.num_workers = num_workers
8498
self.neurokit_method = neurokit_method
8599
self.correct_artifacts = correct_artifacts
86100
self.remove_fails = remove_fails
87101
self.fails = []
88102
self.median_beat = median_beat
103+
self.silent = silent
89104

90105
available_scale_methods = ['identity', 'linear', 'hrc']
91106
if scale_method in available_scale_methods:
@@ -97,13 +112,10 @@ def __init__(
97112
raise ValueError(f'No such scaling method, '
98113
f'please use one of the following: {available_scale_methods}')
99114

100-
if silent:
115+
if self.silent:
101116
warnings.filterwarnings("ignore")
102117

103-
def update_configuration(
104-
self,
105-
**kwargs
106-
):
118+
def update_configuration(self, **kwargs):
107119
"""
108120
Enables modification of existing configuration settings as required.
109121
@@ -116,6 +128,50 @@ def update_configuration(
116128
assert (k in self.__class__.__allowed), f"Disallowed keyword passed: {k}"
117129
setattr(self, k, kwargs[k])
118130

131+
@property
132+
def offset(self):
133+
return self._offset
134+
135+
@property
136+
def template_bpm(self):
137+
return self._template_bpm
138+
139+
@property
140+
def seconds_len(self):
141+
return self._seconds_len
142+
143+
@property
144+
def sampling_rate(self):
145+
return self._sampling_rate
146+
147+
@offset.setter
148+
def offset(self, val):
149+
self._offset = val
150+
self._update_template()
151+
152+
@sampling_rate.setter
153+
def sampling_rate(self, val):
154+
self._sampling_rate = val
155+
self._update_template()
156+
157+
@seconds_len.setter
158+
def seconds_len(self, val):
159+
self._seconds_len = val
160+
self._update_template()
161+
162+
@template_bpm.setter
163+
def template_bpm(self, val):
164+
self._template_bpm = val
165+
self._update_template()
166+
167+
def _update_template(self):
168+
self.template = Template(
169+
sampling_rate=self.sampling_rate,
170+
offset=self.offset,
171+
template_bpm=self.template_bpm,
172+
seconds_len=self.seconds_len
173+
)
174+
119175
def _normalize_interval(
120176
self,
121177
source_ecg: np.ndarray,
@@ -182,7 +238,6 @@ def _normalize_interval(
182238
return source_ecg[:, :self.template.intervals[0]], 1
183239

184240
case "hrc":
185-
186241
template_rr_dist = template_starts[2] - template_starts[1]
187242
dist_upper_template = int((self.template.bpm / 280+0.14) * template_rr_dist)
188243
dist_lower_template = int((-self.template.bpm / 330 + 0.96) * template_rr_dist)
@@ -272,10 +327,7 @@ def fit(self, X: np.ndarray, y=None):
272327
"""
273328
return self
274329

275-
def _template_transform(
276-
self,
277-
ecg: np.ndarray,
278-
):
330+
def _template_transform(self, ecg: np.ndarray):
279331
"""
280332
Normalizes the ECG by recentering R-peaks at equally spaced intervals, as defined in the template.
281333
The QRST-complexes are added, and the baselines are interpolated to match the new connections between complexes.
@@ -329,11 +381,7 @@ def _template_transform(
329381
hr=hr
330382
)
331383

332-
def transform(
333-
self,
334-
X: np.ndarray,
335-
y=None
336-
) -> np.ndarray:
384+
def transform(self, X: np.ndarray, y=None) -> np.ndarray:
337385
"""
338386
Normalizes and returns the ECGs with multiprocessing.
339387

rlign/utils.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class Template(object):
1818
def __init__(self,
1919
seconds_len: float = 10,
2020
sampling_rate: int = 500,
21-
template_bpm: int = 40,
21+
template_bpm: int = 60,
2222
offset: float = 0.5):
2323
"""
2424
Creates a binary template pattern for an ECG, specifically designed for positioning the QRST interval.
@@ -38,21 +38,22 @@ def __init__(self,
3838
positions according to a specific BPM.
3939
4040
offset: The offset specifies the starting point for the first normalized QRS complex in the
41-
template.
41+
template. In percentage of sampling_rate.
4242
4343
4444
Note:
4545
This class is integral to the ECG analysis process, providing a foundational template for
4646
subsequent signal processing and interpretation.
4747
"""
48-
48+
if not (0 <= offset < 1):
49+
raise ValueError("Parameter offset must be between 0 and 1.")
4950
offset = int(offset * sampling_rate)
5051
total_len = seconds_len * sampling_rate
5152
template_bpm_len = (template_bpm / 60) * seconds_len
5253
self.bpm = template_bpm
5354

5455
# compute spacing between rpeaks
55-
template_intervals = [int((total_len - offset) / template_bpm_len)] * int(template_bpm_len)
56+
template_intervals = [int(total_len / template_bpm_len)] * int(template_bpm_len)
5657

5758
# get equally spaced r-peak positions
5859
template_rpeaks = np.cumsum([offset, *template_intervals])
@@ -111,6 +112,8 @@ def find_rpeaks(
111112
logging.warning(f'Failure in neurokit: {e}\n')
112113
return None, None
113114

115+
if not len(rpeaks):
116+
return None, None
114117
return rpeaks
115118

116119

test/test_rlign.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,8 @@ def test_multi_processing(self):
2222
print(self.X.shape)
2323

2424

25-
normalizer_single_cpu = rlign.Rlign(num_workers=1, select_lead=0)
26-
normalizer_multiple_cpu = rlign.Rlign(num_workers=8, select_lead=0)
27-
template_ = rlign.Template(template_bpm=40)
28-
normalizer_single_cpu.update_configuration(template=template_)
29-
normalizer_multiple_cpu.update_configuration(template=template_)
25+
normalizer_single_cpu = rlign.Rlign(num_workers=1, select_lead=0, template_bpm=40)
26+
normalizer_multiple_cpu = rlign.Rlign(num_workers=8, select_lead=0, template_bpm=40)
3027

3128
start_time = time.time()
3229
normalizer_single_cpu.transform(self.X)
@@ -41,21 +38,18 @@ def test_multi_processing(self):
4138
self.assertLess(diff_multiple, diff_single)
4239

4340
def test_scale_method(self):
44-
normalizer_hrc = rlign.Rlign(num_workers=1, select_lead=0, scale_method="hrc")
45-
template_ = rlign.Template(template_bpm=40)
46-
normalizer_hrc.update_configuration(template=template_)
41+
normalizer_hrc = rlign.Rlign(num_workers=1, select_lead=0, scale_method="hrc", template_bpm=40)
4742
X_trans = normalizer_hrc.transform(self.X[:10])
4843
self.assertEquals(X_trans.shape, (10, 1, 5000))
4944

5045
with self.assertRaises(ValueError):
5146
rlign.Rlign(num_workers=1, select_lead=0, scale_method="equal")
5247

5348
def test_zero(self):
54-
normalizer_hrc = rlign.Rlign(num_workers=1, select_lead=0, scale_method="hrc", remove_fails=False)
55-
template_ = rlign.Template(template_bpm=40)
56-
normalizer_hrc.update_configuration(template=template_)
57-
X_trans = normalizer_hrc.transform(np.zeros((1,12,5000)))
58-
self.assertTrue(np.array_equal(X_trans, np.zeros((1,12,5000))))
49+
normalizer_hrc = rlign.Rlign(num_workers=1, select_lead=0, scale_method="hrc",
50+
remove_fails=False, template_bpm=40)
51+
X_trans = normalizer_hrc.transform(np.zeros((1, 12, 5000)))
52+
self.assertTrue(np.array_equal(X_trans, np.zeros((1, 12, 5000))))
5953

6054
a = np.concatenate([np.zeros((1, 1, 5000)), self.X[:10]], axis=0)
6155
X_trans = normalizer_hrc.transform(a)

test/test_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,13 @@ def test_find_rpeaks(self):
3333
ret = find_rpeaks(0*self.ecg_500hz_10s, sampling_rate=500)
3434
self.assertIsNone(ret[0])
3535
self.assertIsNone(ret[1])
36+
37+
def test_interval_length(self):
38+
offset = .5
39+
sampling_rate = 500
40+
template60 = Template(seconds_len=10, sampling_rate=sampling_rate, template_bpm=60, offset=offset)
41+
self.assertEquals(np.min(template60.intervals), sampling_rate)
42+
self.assertEquals(np.max(template60.intervals), sampling_rate)
43+
self.assertEquals(np.min(template60.rpeaks), int(offset*sampling_rate))
44+
self.assertEquals(np.min(np.diff(template60.rpeaks)), sampling_rate)
45+
self.assertEquals(np.max(np.diff(template60.rpeaks)), sampling_rate)

0 commit comments

Comments
 (0)