Refine one-based detection positions to local ECG maxima.
Parameters:
| Name | Type | Description | Default |
ecg | array_like | One-dimensional real numeric ECG signal with at least two samples. NaN values are allowed and act as hard finite-segment boundaries. Infinite values are invalid. | required |
detections | array_like | One-dimensional real numeric vector of approximate detection positions in one-based sample coordinates. Empty input is allowed. NaN detections return NaN in the aligned output position. Infinite values are invalid. | required |
window_size | float | Positive search radius in samples. The value is rounded to the nearest integer before constructing search windows. | 20 |
Returns:
| Type | Description |
ndarray | One-dimensional array of refined detections in one-based sample coordinates, aligned with detections. |
Raises:
| Type | Description |
TypeError | If ecg or detections is not real numeric data. |
ValueError | If ecg is empty, has fewer than two samples, is not a vector, contains infinite values, if finite detections are outside [1, len(ecg)], or if window_size is not positive. |
Notes
This function implements the Biosiglib tools.snap_to_peak specification. The public API uses one-based sample coordinates for Biosiglib and Biosigmat compatibility. Python callers using zero-based indices must convert at the API boundary.
Search windows never cross NaN ECG gaps. If a finite detection points to a NaN ECG sample, the aligned output value is NaN. When multiple samples share the maximum value inside a clipped search window, the first maximum is returned.
Examples:
>>> import numpy as np
>>> from biosigpy.tools import snap_to_peak
>>> ecg = np.array([0.1, 0.4, 1.0, 0.5, 0.2])
>>> snap_to_peak(ecg, np.array([2.0]), window_size=2)
array([3.])
Source code in src/biosigpy/tools/snap_to_peak.py
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102 | def snap_to_peak(
ecg: ArrayLike, detections: ArrayLike, window_size: float = 20
) -> np.ndarray:
"""Refine one-based detection positions to local ECG maxima.
Parameters
----------
ecg : array_like
One-dimensional real numeric ECG signal with at least two samples.
``NaN`` values are allowed and act as hard finite-segment boundaries.
Infinite values are invalid.
detections : array_like
One-dimensional real numeric vector of approximate detection positions
in one-based sample coordinates. Empty input is allowed. ``NaN``
detections return ``NaN`` in the aligned output position. Infinite
values are invalid.
window_size : float, optional
Positive search radius in samples. The value is rounded to the nearest
integer before constructing search windows.
Returns
-------
numpy.ndarray
One-dimensional array of refined detections in one-based sample
coordinates, aligned with ``detections``.
Raises
------
TypeError
If ``ecg`` or ``detections`` is not real numeric data.
ValueError
If ``ecg`` is empty, has fewer than two samples, is not a vector,
contains infinite values, if finite detections are outside
``[1, len(ecg)]``, or if ``window_size`` is not positive.
Notes
-----
This function implements the Biosiglib ``tools.snap_to_peak``
specification. The public API uses one-based sample coordinates for
Biosiglib and Biosigmat compatibility. Python callers using zero-based
indices must convert at the API boundary.
Search windows never cross ``NaN`` ECG gaps. If a finite detection points
to a ``NaN`` ECG sample, the aligned output value is ``NaN``. When multiple
samples share the maximum value inside a clipped search window, the first
maximum is returned.
Examples
--------
>>> import numpy as np
>>> from biosigpy.tools import snap_to_peak
>>> ecg = np.array([0.1, 0.4, 1.0, 0.5, 0.2])
>>> snap_to_peak(ecg, np.array([2.0]), window_size=2)
array([3.])
"""
ecg = as_real_vector(ecg, name="ecg")
if ecg.size == 0:
raise ValueError("ecg must not be empty")
if ecg.size < 2:
raise ValueError("ecg must contain at least two samples")
if np.any(np.isinf(ecg)):
raise ValueError("ecg must not contain infinite values")
detections = as_real_vector(detections, name="detections")
if np.any(np.isinf(detections)):
raise ValueError("detections must not contain infinite values")
finite_detections = detections[np.isfinite(detections)]
if np.any((finite_detections < 1) | (finite_detections > ecg.size)):
raise ValueError("detections must be between 1 and len(ecg)")
window_size = as_positive_real_scalar(window_size, name="window_size")
window_size = _round_positive_half_up(window_size)
refined_detections = np.empty(detections.size, dtype=np.float64)
for detection_index, current_detection in enumerate(detections):
if np.isnan(current_detection):
refined_detections[detection_index] = np.nan
continue
current_detection = _round_positive_half_up(current_detection) - 1
if np.isnan(ecg[current_detection]):
refined_detections[detection_index] = np.nan
continue
segment_start, segment_end = _finite_segment_bounds(ecg, current_detection)
window_start = max(segment_start, current_detection - window_size)
window_end = min(segment_end, current_detection + window_size)
window_signal = ecg[window_start : window_end + 1]
local_index = int(np.argmax(window_signal))
refined_detections[detection_index] = window_start + local_index + 1
return refined_detections
|