| Title: | Intracardiac Electrograms |
|---|---|
| Description: | A system for importing electrophysiological signal, based on the 'Waveform Database (WFDB)' software package, written by Moody et al 2022 <doi:10.13026/gjvw-1m31>. A R-based system to utilize 'WFDB' functions for reading and writing signal data, as well as functions for analysis, are provided. A stable and broadly compatible class for working with signal data, supporting the reading in of cardiac electrophysiological files such as intracardiac electrograms, is introduced. |
| Authors: | Anish S. Shah [aut, cre, cph] (ORCID: <https://orcid.org/0000-0002-9729-1558>), Darren Seaney [ctb] |
| Maintainer: | Anish S. Shah <[email protected]> |
| License: | GPL (>= 3) |
| Version: | 0.3.0 |
| Built: | 2026-10-06 08:44:18 UTC |
| Source: | https://github.com/shah-in-boots/egm |
windows objectSubset a windows object
## S3 method for class 'windows' x[i, ...]## S3 method for class 'windows' x[i, ...]
x |
A |
i |
Index to subset. |
... |
Additional arguments passed to methods. |
A windows object with the specified subset of elements.
EGM objectAdd an annotation_table to an EGM object, with validation
to ensure compatibility. The annotation table is added to the list of
annotations using the annotator name as the list name. If an annotation
with the same annotator name already exists, you can choose to overwrite
it or merge the annotations.
add_annotation(x, annotation, overwrite = FALSE)add_annotation(x, annotation, overwrite = FALSE)
x |
An |
annotation |
An |
overwrite |
Logical. If an annotation with the same annotator name
already exists, should it be overwritten? If |
The modified EGM object with the annotation table added
The function performs several validation checks to ensure the annotation
table is compatible with the EGM object:
Channel validation: A channel given as a lead name ("II") is
resolved against the record's header to its signal number, so the table
stored on the object always counts channels the way channels describes.
Every channel other than the global channel 0 has to exist in the
header; a name the record does not carry is an error.
Sample validation: Checks that all sample indices in the annotation table are within the valid range of samples in the recording.
When adding an annotation table with an annotator name that already exists:
Overwrite mode (overwrite = TRUE): The existing annotation table
is completely replaced with the new one.
Merge mode (overwrite = FALSE): The new annotations are combined
with the existing ones, duplicates are removed, and the result is sorted
by sample number.
If the EGM object has no existing annotations (empty list or blank
annotation table), the new annotation table will replace the empty
annotations.
## Not run: # Add a new annotation table to an `EGM` object `EGM` <- add_annotation(`EGM`, my_annotation_table) # Overwrite an existing annotation `EGM` <- add_annotation(`EGM`, new_annotation, overwrite = TRUE) # Merge with existing annotation (default) `EGM` <- add_annotation(`EGM`, additional_annotation, overwrite = FALSE) ## End(Not run)## Not run: # Add a new annotation table to an `EGM` object `EGM` <- add_annotation(`EGM`, my_annotation_table) # Overwrite an existing annotation `EGM` <- add_annotation(`EGM`, new_annotation, overwrite = TRUE) # Merge with existing annotation (default) `EGM` <- add_annotation(`EGM`, additional_annotation, overwrite = FALSE) ## End(Not run)
Analyse an isolated atrial signal
analyze_atrial_signal( atrial_signal, frequency, characteristics = c("amplitude", "dominant_frequency", "organization", "sample_entropy"), original_signal = NULL, raw_signal = original_signal, qrs_loc = NULL, annotation = NULL, amplitude_window = c("tq", "all"), band = c(4, 10), dominant_frequency = NULL, entropy_rate = 256, entropy_input = c("broadband", "main_atrial_wave"), entropy_bandwidth = 3, entropy_tolerance = 0.2, tol = 0.15 )analyze_atrial_signal( atrial_signal, frequency, characteristics = c("amplitude", "dominant_frequency", "organization", "sample_entropy"), original_signal = NULL, raw_signal = original_signal, qrs_loc = NULL, annotation = NULL, amplitude_window = c("tq", "all"), band = c(4, 10), dominant_frequency = NULL, entropy_rate = 256, entropy_input = c("broadband", "main_atrial_wave"), entropy_bandwidth = 3, entropy_tolerance = 0.2, tol = 0.15 )
atrial_signal |
Numeric vector of the cancelled (atrial) signal |
frequency |
Sampling frequency in Hz |
characteristics |
Character vector of features to compute |
original_signal |
The uncancelled, filtered signal for the same lead, which is what the cancellation residual and the harmonic evidence are measured against. |
raw_signal |
The same lead before bandpass filtering, used for the QRS
excursion. Falls back to |
qrs_loc |
Integer vector of QRS positions |
annotation |
Optional annotation table used to locate TQ segments. It
must already be resolved to a single lead; see |
amplitude_window |
Either |
band |
Numeric length-2 frequency band in Hz |
dominant_frequency |
Dominant frequency in Hz to use for the rate, the
organisation index and the main atrial wave, or |
entropy_rate |
Rate in Hz to decimate to before computing entropy |
entropy_input, entropy_bandwidth, entropy_tolerance
|
What the entropy is
taken of, how wide a band around |
tol |
Tolerance for the harmonic coincidence test |
A named list of features
annotation_table() modifies the data.table class to work
with annotation data. The columns are of all equal length, and each row
describes a single annotation (although there may be duplicate time points).
annotation_table( annotator = character(), time = character(), sample = integer(), frequency = integer(), type = character(), subtype = character(), channel = integer(), number = integer(), aux = character(), channel_zero = c("global", "signal"), ... ) is_annotation_table(x)annotation_table( annotator = character(), time = character(), sample = integer(), frequency = integer(), type = character(), subtype = character(), channel = integer(), number = integer(), aux = character(), channel_zero = c("global", "signal"), ... ) is_annotation_table(x)
annotator |
Character vector of WFDB-compatible annotation types.
When reading, multiple annotators can be specified to read multiple
annotation files simultaneously (e.g., |
time |
A |
sample |
An |
frequency |
An |
type |
A |
subtype |
A |
channel |
An |
number |
An additional |
aux |
A |
channel_zero |
How the |
... |
Additional arguments to be passed to the function |
x |
A |
The annotation_table() function creates a compatible table that
can be used with write_annotation() and read_annotation() functions.
A data.table that has invariant columns that are compatible with
the WFDB library. The key columns include the sample index, the type of
annotation (and its subtype and number qualifier), and the channel.
The following annotation file types are described below.
ecgpuwaveecgpuwave analyzes an ECG signal from the specified record, detecting the
QRS complexes and locating the beginning, peak, and end of the P, QRS, and
ST-T waveforms. The output of ecgpuwave is written as a standard WFDB-format
annotation file (the extension is "*.ecgpuwave", as would be expected). This
file can be converted into text format using rdann. Further details are
given at the ECGPUWAVE
page.
The type column can be p, t, or N for the peak of the P wave, T
wave, and QRS (R peak) directly. The output notation also includes waveform
onset ( and waveform offset ). The number column gives further
information about each of these type labels.
The number column gives modifier information. If the type classifier is a T wave annotation, the number column can be 0 (normal), 1 (inverted), 2 (positive), 3 (negative), 4 (biphasic negative-positive), 5 (biphasic positive-negative). If the type is an waveform onset or offset, then number can be 0 (P wave), 1 (QRS complex), 2 (T wave).
These functions create templates for annotation in R and extend the ability for developers to create their own annotation systems that are stable for WFDB objects. They are compatible with WFDB annotations and can be written out to a WFDB-compatible file. This also allows extensibility.
What this package needs from a delineating annotator is narrower than it
looks. Wave identity is recovered positionally by label_waves(), from the
peak symbol enclosed by each (/) bracket pair, so an annotator that leaves
the WFDB number column at zero throughout - which by the letter of the
convention means every onset marks a P wave - is still perfectly usable. The
requirement is only that each wave is bracketed around a typed peak (p, N,
or t).
An annotator run per lead writes one such set of brackets for each lead,
distinguished by the channel column, which is resolved by naming a guiding
channel; see channels.
label_waves(), read_annotation(), channels
Extracts the surface ECG from a record. Recognised surface leads are kept and renamed to their canonical form; every other channel is discarded. On a 12-lead recording this is a relabelling; on an electrophysiology study it is the step that separates the body-surface leads from the intracardiac ones.
as_ECG(x, ...)as_ECG(x, ...)
x |
An object of class |
... |
Additional arguments |
Channels are matched case-insensitively and ignoring separators, so
aVR, av r and AV-R all resolve to AVR. A record with no surface
leads at all is an error: an intracardiac channel cannot stand in for a
surface lead, and the analyses gated on this class would otherwise return a
number that looks reasonable and is not.
Annotation channel indices refer to positions in the original record. When
channels are dropped those indices no longer address the leads they name, so
a warning is raised if any channel-specific annotation is carried across.
For the same reason the kept leads stay in the order the record wrote them,
rather than being put into display order. A channel index addresses a signal
column by position, so permuting the columns would repoint every per-lead
fiducial at a different lead - silently, since the indices remain valid.
Renumbering them is not open to this function either: whether an annotator
counted from zero or from one is a property of the file, and channel 0 is
also how a fiducial says it belongs to no particular lead. Order at the
point of use instead, with ecg_leads() and lead_factor().
An object of class ECG, holding its leads in the source record's
order.
ecg_leads() and lead_factor() for the display order, ECG() for
the class, extract_f_waves() and vcg() for analyses that require it.
This function allows for reading in LS Pro data based on their text export of signals. Signals can be exported directly from the LS Pro system. The actual software is written by Bard.
The LabSystem Pro was acquired by Boston Scientific from the original company Bard. They are a common electrophysiology signal processing device for visualization and measurement of intracardiac signals.
read_bard(file, n = Inf) read_bard_header(file) read_bard_signal(file, n = Inf)read_bard(file, n = Inf) read_bard_header(file) read_bard_signal(file, n = Inf)
file |
The path to the file where the data is located. It must be a *.txt file. See details below about its format. |
n |
Number of signal values to return (this will be the same for each channel of data). Defaults to all values. |
An EGM class object that is a list of EP signals the format of a
data.table, with an attached header attribute that contains
additional recording data.
The steps to data export are as follows.
Start LabSystem PRO
Open a patient record
Display a waveform recording in a Review Window
Scroll to a point of interest in a waveform recording
Right click on the review window to the left of the region of interest
Select an Export option, either a default time range or the entire visible page (which depends on the sweep speed).
[Header] Recording info – contains (example): [Header]<CR><LF> File Type: 1<CR><LF> Version: 1<CR><LF> Channels exported: 22<CR><LF> Samples per channel: 5000<CR><LF> Start time: 6:55:24<CR><LF> End time: 6:55:29<CR><LF> Ch. Info. Pointer: 320<CR><LF> Stamp Data: T<CR><LF> Mux format: 0<CR><LF> Mux Block Size: <CR><LF> Data Format 1<CR><LF> Sample Rate: 1000Hz<CR><LF> [Header] Channel info (per channel example): Channel #: 1<CR><LF> Label: III<CR><LF> Range: 5mv <CR><LF> Low: 1Hz<CR><LF> High: 100Hz<CR><LF> Sample rate: 1000Hz<CR><LF> Color: 0000FF<CR><LF> Scale: -7<CR><LF> [Data] As described below: -256,-1056,576,-256,320,-736,144,576,-592,176,608,240,176,-560,496,- 144,0,0,-32,-48,-32,-80<CR><LF>
Channel Data is interleaved in the example above (sample indexed at 1):
| 1 | 2 | 3 | ... | 22 |
| Ch1:1 | Ch2:1 | Ch3:1 | ... | Ch22:1 |
| Ch1:2 | Ch2:2 | Ch3:2 | ... | Ch22:2 |
| Ch1:3 | Ch2:3 | Ch3:3 | ... | Ch22:3 |
| ... | ... | ... | ... | ... |
| Ch1:5000 | Ch2:5000 | Ch3:5000 | ... | Ch22:5000 |
Subtracts each window's own baseline from every lead, so that the isoelectric line sits at zero. Nothing else in the windowing chain does this: reading, cutting, padding, warping and reducing all preserve whatever DC offset the recording carried, and that offset is not small next to a P wave.
baseline_window( x, reference = "(", width = 20, channel = NULL, preserve_class = TRUE, ... )baseline_window( x, reference = "(", width = 20, channel = NULL, preserve_class = TRUE, ... )
x |
A |
reference |
What marks the isoelectric segment: a fiducial (a |
width |
Milliseconds of signal averaged to estimate the level. Default
20. Ignored when |
channel |
The lead whose annotations locate |
preserve_class |
Logical. If TRUE (default), a |
... |
Additional arguments (currently unused). |
The offset survives every step that does not go looking for it, and it dominates the ones that do. Run a principal components analysis over a set of median beats without correcting first and the leading components are a constant vertical shift of the whole window rather than any morphology - the offset is the largest thing varying between records, so a variance-based method finds it before anything else. It is invisible in a scree table and obvious the moment a component is plotted as a waveform.
Correct before reducing, not after. A per-beat correction also removes the beat-to-beat baseline wander that would otherwise smear the reduction:
get_windows(ecg, by = by_beat(channel = 2)) |>
baseline_window(reference = "(", channel = 2) |>
median_window()
Which samples count as isoelectric depends on reference:
A fiducial - given as a type symbol such as "(", or a named list of
annotation criteria - takes the width milliseconds before it. Anchored
on a wave onset this is the PR or TP segment, which is the segment a
clinician would call the baseline.
"start" takes the first width milliseconds of the window, and "end"
the last. Use these where the window has no delineation to anchor to, or
where it was cut so that its edges are known to be quiet.
A numeric scalar is subtracted from every lead as given, which is the
escape hatch for a baseline estimated some other way.
The level is the median over the reference samples rather than the mean, so a single noisy sample cannot displace the whole window. Each lead is corrected by its own level; a window whose reference fiducial is missing is returned uncorrected with a warning, since shifting it by some other window's baseline would be worse than leaving it alone.
The input with every lead shifted so its baseline is zero, in the
shape it was given: a windows object, a list, or a single EGM.
A delineating annotator may be run over a record once, or once per lead. Run
per lead it writes twelve independent copies of every fiducial, separated
only by the channel column, and such a table does not describe beats until
it is resolved to a single lead: pooled, it reports twelve times as many
beats as the record contains, and taking the first match at each point
silently follows whichever lead happens to sort first.
So every function that reads fiducials - get_windows(),
learn_template(), median_window() and the other window transforms,
extract_f_waves(), vcg() - takes a channel, and resolves
it the same way:
annotations spanning more than one channel with no channel given are an
error, not a guess;
a channel that the annotations do not carry is an error, which is what
catches a numbering convention that does not match;
the global channel 0 is not a lead, so it never counts toward the span
and is always kept alongside the chosen lead.
The channel may be a number or a channel name ("II"), resolved against the
record's own header. A number is matched literally against the channel
column, and the numbering there belongs to the annotator that wrote the
file. Read it off the record rather than assuming:
table(get_annotation(x)$channel)
Only the boundaries are restricted to one lead. Windows still carry the signal for every channel, which is what makes a windowed beat usable as a vectorcardiogram.
pad_window(), median_window(), vcg(), which
references each loop to its own onset by the same rule
## Not run: ecg <- read_wfdb("ecg", test_path(), "ecgpuwave") beats <- get_windows(ecg, by = by_beat(channel = 1)) # The PR segment, taken as the 20 ms before each QRS onset baseline_window(beats, reference = list(type = "(", wave = "QRS"), channel = 1) # A median beat corrected after the fact median_window(beats) |> baseline_window(reference = "start") ## End(Not run)## Not run: ecg <- read_wfdb("ecg", test_path(), "ecgpuwave") beats <- get_windows(ecg, by = by_beat(channel = 1)) # The PR segment, taken as the 20 ms before each QRS onset baseline_window(beats, reference = list(type = "(", wave = "QRS"), channel = 1) # A median beat corrected after the fact median_window(beats) |> baseline_window(reference = "start") ## End(Not run)
Builds a window_strategy that cuts the same span of signal around every
occurrence of a fiducial, by default the QRS peak. Every window is the same
length by construction, which is what by_rhythm() cannot promise.
by_beat( before = 300, after = 500, feature = "N", channel = NULL, adjust_sample_indices = TRUE )by_beat( before = 300, after = 500, feature = "N", channel = NULL, adjust_sample_indices = TRUE )
before, after
|
Milliseconds of signal kept before and after the fiducial. The defaults span a full PQRST at ordinary rates. |
feature |
The fiducial each window is built around, given as a
|
channel |
The lead whose annotations locate the fiducial, given as a
channel number or name, as in |
adjust_sample_indices |
Logical, whether to rebase sample indices in the returned windows to be zero-based and relative to the window start. |
That guarantee is what a representative beat needs. Reducing ragged
windows means padding them onto a common grid first, and a padded sample is
a fabricated one: it pulls median_window() toward whatever the padding
says, wherever the windows do not all reach. Cutting a fixed span out of the
continuous recording leaves nothing to pad, which is how the standard
representative beat is derived (Kligfield et al. 2007).
Beats too near either end of the record for the full span to be cut are
dropped rather than truncated, since a short window gives up the guarantee
the strategy exists for. How many were dropped is recorded on the returned
collection, where printing it shows the count and window_dropped() reads
it back. It is not also announced: a fixed span overhangs at least one end
of a short strip almost every time, so the notice carried no information,
and a message is invisible on a background worker regardless.
The span is given in milliseconds so that one strategy can be reused across records of differing sampling frequency.
A window_strategy object.
A delineating annotator may be run over a record once, or once per lead. Run
per lead it writes twelve independent copies of every fiducial, separated
only by the channel column, and such a table does not describe beats until
it is resolved to a single lead: pooled, it reports twelve times as many
beats as the record contains, and taking the first match at each point
silently follows whichever lead happens to sort first.
So every function that reads fiducials - get_windows(),
learn_template(), median_window() and the other window transforms,
extract_f_waves(), vcg() - takes a channel, and resolves
it the same way:
annotations spanning more than one channel with no channel given are an
error, not a guess;
a channel that the annotations do not carry is an error, which is what
catches a numbering convention that does not match;
the global channel 0 is not a lead, so it never counts toward the span
and is always kept alongside the chosen lead.
The channel may be a number or a channel name ("II"), resolved against the
record's own header. A number is matched literally against the channel
column, and the numbering there belongs to the annotator that wrote the
file. Read it off the record rather than assuming:
table(get_annotation(x)$channel)
Only the boundaries are restricted to one lead. Windows still carry the signal for every channel, which is what makes a windowed beat usable as a vectorcardiogram.
Kligfield P, Gettes LS, Bailey JJ, et al. Recommendations for the standardization and interpretation of the electrocardiogram: part I. Circulation. 2007;115(10):1306-1324. doi:10.1161/CIRCULATIONAHA.106.180200
get_windows(), median_window(), by_rhythm(),
window_dropped()
# A fixed PQRST window around every QRS peak by_beat() # Tight around the P wave instead, guided by lead II by_beat(before = 200, after = 200, feature = "p", channel = 2)# A fixed PQRST window around every QRS peak by_beat() # Tight around the P wave instead, guided by lead II by_beat(before = 200, after = 200, feature = "p", channel = 2)
Builds a window_strategy that cuts the atrial portion of each beat: from the P onset to either the QRS onset (default) or the P offset. Isolating the P wave is what makes atrial morphology modellable, since the QRS is an order of magnitude taller and otherwise absorbs the variance in any basis expansion fitted over a whole beat.
by_pwave( to = c("qrs_onset", "p_offset"), channel = NULL, adjust_sample_indices = TRUE )by_pwave( to = c("qrs_onset", "p_offset"), channel = NULL, adjust_sample_indices = TRUE )
to |
Where the window ends: |
channel |
The lead whose annotations define the window boundaries, given as a channel number or name. See the channels section. |
adjust_sample_indices |
Logical, whether to rebase sample indices in the returned windows to be zero-based and relative to the window start. |
Ending at the QRS onset keeps the PR segment, which costs nothing - it is isoelectric - and buys robustness, since the P offset is the least reliably placed of the two fiducials and a P wave truncated by a mis-delineated offset is a distorted one. End at the P offset instead when the window itself, rather than what it contains, is the measurement.
Windows containing a second P onset are discarded: that means a QRS onset went undetected and the window has run into the following beat.
This is by_rhythm() with a fixed set of criteria, so it uses the same
engine and returns windows of differing length. Windows are named pwave1,
pwave2, and so on.
A window_strategy object.
A delineating annotator may be run over a record once, or once per lead. Run
per lead it writes twelve independent copies of every fiducial, separated
only by the channel column, and such a table does not describe beats until
it is resolved to a single lead: pooled, it reports twelve times as many
beats as the record contains, and taking the first match at each point
silently follows whichever lead happens to sort first.
So every function that reads fiducials - get_windows(),
learn_template(), median_window() and the other window transforms,
extract_f_waves(), vcg() - takes a channel, and resolves
it the same way:
annotations spanning more than one channel with no channel given are an
error, not a guess;
a channel that the annotations do not carry is an error, which is what
catches a numbering convention that does not match;
the global channel 0 is not a lead, so it never counts toward the span
and is always kept alongside the chosen lead.
The channel may be a number or a channel name ("II"), resolved against the
record's own header. A number is matched literally against the channel
column, and the numbering there belongs to the annotator that wrote the
file. Read it off the record rather than assuming:
table(get_annotation(x)$channel)
Only the boundaries are restricted to one lead. Windows still carry the signal for every channel, which is what makes a windowed beat usable as a vectorcardiogram.
get_windows(), by_rhythm(), vcg()
# P onset to QRS onset, guided by one lead by_pwave(channel = 2) # The P wave alone by_pwave(to = "p_offset")# P onset to QRS onset, guided by one lead by_pwave(channel = 2) # The P wave alone by_pwave(to = "p_offset")
Builds a window_strategy that finds windows bounded by a pair of annotation fiducials - an onset and the next matching offset - optionally requiring a reference fiducial to fall between them. For sinus rhythm this yields one window per beat, from P-onset to T-offset with the QRS as the reference.
by_rhythm( rhythm = "sinus", onset = NULL, offset = NULL, reference = NULL, channel = NULL, reject_overlap = NULL, adjust_sample_indices = TRUE )by_rhythm( rhythm = "sinus", onset = NULL, offset = NULL, reference = NULL, channel = NULL, reject_overlap = NULL, adjust_sample_indices = TRUE )
rhythm |
A |
onset |
A named list of criteria identifying window onsets. Defaults to
|
offset |
A named list of criteria identifying window offsets. Defaults to
|
reference |
A named list of criteria for a fiducial that must exist
between onset and offset, or |
channel |
The lead whose annotations define the window boundaries, given
as a channel number or a channel name. An annotator run per lead (an
|
reject_overlap |
Logical, whether to discard a window that contains a
second onset. |
adjust_sample_indices |
Logical, whether to rebase signal and annotation
sample indices in the returned windows to be zero-based and relative to the
window start. Default |
Criteria are named lists matched column-by-column against the
annotation table, with the addition of a virtual wave field ("P",
"QRS", or "T") inferred positionally from the peak symbol enclosed by
each (/) bracket pair (see label_waves()). This recovers wave identity
even when the WFDB number column is unpopulated, as with ecgpuwave run
per lead.
When rhythm = "sinus" the criteria default to a full beat and windows that
contain a second onset are rejected, so that only clean non-overlapping
beats are returned.
A window_strategy object.
rhythm is a free-form label: it names the windows and their info strings,
and it selects the sinus defaults. Any other name is an extension point -
give onset and offset explicitly and the same engine will find whatever
pair of fiducials you describe. Windowing from one P onset to the next, for
instance:
by_rhythm(
rhythm = "atrial",
onset = list(type = "(", wave = "P"),
offset = list(type = "(", wave = "P"),
reference = list(type = "p")
)
by_pwave() is that pattern named and given defaults, for the segmentation
worth having a constructor of its own.
A delineating annotator may be run over a record once, or once per lead. Run
per lead it writes twelve independent copies of every fiducial, separated
only by the channel column, and such a table does not describe beats until
it is resolved to a single lead: pooled, it reports twelve times as many
beats as the record contains, and taking the first match at each point
silently follows whichever lead happens to sort first.
So every function that reads fiducials - get_windows(),
learn_template(), median_window() and the other window transforms,
extract_f_waves(), vcg() - takes a channel, and resolves
it the same way:
annotations spanning more than one channel with no channel given are an
error, not a guess;
a channel that the annotations do not carry is an error, which is what
catches a numbering convention that does not match;
the global channel 0 is not a lead, so it never counts toward the span
and is always kept alongside the chosen lead.
The channel may be a number or a channel name ("II"), resolved against the
record's own header. A number is matched literally against the channel
column, and the numbering there belongs to the annotator that wrote the
file. Read it off the record rather than assuming:
table(get_annotation(x)$channel)
Only the boundaries are restricted to one lead. Windows still carry the signal for every channel, which is what makes a windowed beat usable as a vectorcardiogram.
get_windows(), by_pwave(), label_waves()
# Sinus defaults: one clean P-onset -> T-offset beat per window by_rhythm() # Guided by a single lead in a multi-lead annotation file by_rhythm(channel = 2) # P-onset to next P-onset instead of a full beat by_rhythm(offset = list(type = "(", wave = "P"))# Sinus defaults: one clean P-onset -> T-offset beat per window by_rhythm() # Guided by a single lead in a multi-lead annotation file by_rhythm(channel = 2) # P-onset to next P-onset instead of a full beat by_rhythm(offset = list(type = "(", wave = "P"))
windows objectsConcatenate windows objects
## S3 method for class 'windows' c(...)## S3 method for class 'windows' c(...)
... |
|
A windows object containing all the elements of the inputs.
Computes approximate entropy by the method of Pincus (1991),
comparing vectors embedded in m and m + 1 dimensions and taking the
difference of the resulting correlation integrals.
calculate_approximate_entropy(x, m = 2, r = NULL)calculate_approximate_entropy(x, m = 2, r = NULL)
x |
Numeric vector of the time series |
m |
Embedding dimension. Default 2. |
r |
Tolerance. Default |
Approximate entropy counts self-matches, which biases it toward
regularity and makes it dependent on record length.
calculate_sample_entropy() avoids both and is generally preferable.
The tolerance r defaults to 0.2 times the standard deviation of x. Note
that this default changed: earlier versions used 3.5 times the standard
deviation, which admitted nearly every pair of vectors as a match and drove
the statistic toward zero regardless of the input.
The calculation is O(n^2). Decimate before calling; see
calculate_sample_entropy() for how far.
Approximate entropy value
Pincus SM. Approximate entropy as a measure of system complexity. Proceedings of the National Academy of Sciences of the USA. 1991;88(6):2297-2301. doi:10.1073/pnas.88.6.2297
set.seed(123) calculate_approximate_entropy(rnorm(500))set.seed(123) calculate_approximate_entropy(rnorm(500))
Returns the frequency carrying the most power within a search band, estimated from a Welch-averaged periodogram.
calculate_dominant_frequency(x, frequency, f_min = 4, f_max = 10, ...)calculate_dominant_frequency(x, frequency, f_min = 4, f_max = 10, ...)
x |
Numeric vector of the time series |
frequency |
Sampling frequency of the signal in Hz |
f_min |
Lower edge of the search band in Hz. Default 4. |
f_max |
Upper edge of the search band in Hz. Default 10. Sustained atrial fibrillation is usually quoted at 4-10 Hz; typical atrial flutter is 4-5.5 Hz and slow or drug-modified flutter falls below 4 Hz, so widen the band when flutter is expected. |
... |
Passed to |
Dominant frequency in Hz. NA when the band holds no finite power.
x <- sin(2 * pi * 6 * seq(0, 10, by = 1 / 500)) + rnorm(5001, sd = 0.1) calculate_dominant_frequency(x, frequency = 500)x <- sin(2 * pi * 6 * seq(0, 10, by = 1 / 500)) + rnorm(5001, sd = 0.1) calculate_dominant_frequency(x, frequency = 500)
The share of spectral power carried by the dominant frequency and its harmonics.
calculate_organization_index( x, frequency, dominant_frequency = NULL, n_harmonics = 4L, band = c(0.5, 50), half_width = 0.5 )calculate_organization_index( x, frequency, dominant_frequency = NULL, n_harmonics = 4L, band = c(0.5, 50), half_width = 0.5 )
x |
Numeric vector of the atrial signal |
frequency |
Sampling frequency in Hz |
dominant_frequency |
Dominant frequency in Hz. Estimated from |
n_harmonics |
Number of harmonics summed with the dominant peak.
Default 4, as in An et al.; Everett's four peaks are |
band |
Numeric length-2 vector for the total-power reference band.
Default |
half_width |
Half-width in Hz of the window placed on the dominant frequency. The harmonic windows are 1.5 times as wide. Default 0.5. |
A highly organised atrium concentrates its energy in a narrow peak and its harmonics; a disorganised one spreads it across the spectrum. This is one of the few f-wave features with a reasonably direct electrophysiological reading, being related to the number of independent wavefronts the atrium is holding.
The definition follows the published one: the area under the dominant peak
and its first n_harmonics harmonics, over the total area of the spectrum.
Everett et al. introduced it on an interatrial electrogram as "the ratio of
the area under the first four harmonic peaks to the total area of the
spectrum" – four peaks in all, the dominant and three harmonics. An et al.
give the clearest surface-ECG implementation, "the area under the highest
peak and its first 4 harmonics" over the total area from 0 to 50 Hz on a
QRST-cancelled lead V1 – five peaks. The defaults here are An's, and on a
surface record the fifth peak is immaterial, as the next section says.
Organisation index between 0 and 1
Not a published cut-point, and the gap is not small. An et al. report a
median of 0.33 (IQR 0.27-0.39) in lead V1 of 102 patients; other surface
implementations report 0.26-0.32 and 0.70-0.75. This function returns about
0.13 on the bundled muse-af record with the same formula and the same
band, so the remaining difference is in the cancellation, the peak
integration and the cohort rather than in the definition. Published
surface organisation indices are already implementation-dependent enough
that they do not transfer between groups; treat these values as comparable
within a cohort analysed this way and with nothing else.
Two further details are local choices with no published basis. The peak is
integrated over a fixed window rather than out to the shoulders of the peak
itself, and the harmonic windows are 1.5 times as wide as the fundamental's
to allow for the harmonic drifting. On a surface record the harmonic count
barely matters: at a dominant frequency of 7.5 Hz the third harmonic and
above sit where the f-wave carries no energy, and moving from two peaks to
five changes the value on muse-af by 0.007.
Everett TH 4th, Kok LC, Vaughn RH, Moorman JR, Haines DE. Frequency domain algorithm for quantifying atrial fibrillation organization to increase defibrillation efficacy. IEEE Transactions on Biomedical Engineering. 2001;48(9):969-978. doi:10.1109/10.942586
Everett TH 4th, Moorman JR, Kok LC, Akar JG, Haines DE. Assessment of global atrial fibrillation organization to optimize timing of atrial defibrillation. Circulation. 2001;103(23):2857-2861. doi:10.1161/01.CIR.103.23.2857
An K, Li H, Yu C, Zheng Z. Surface electrocardiogram f wave analysis in patients with atrial fibrillation undergoing thoracoscopic epicardial ablation. Interdisciplinary CardioVascular and Thoracic Surgery. 2024;38(5):ivae057. doi:10.1093/icvts/ivae057
Stavrakis S, Dyer JW, Koomson E, et al. Spectral analysis of baseline electrocardiogram during atrial fibrillation predicts response to antiarrhythmic drug therapy in patients with persistent atrial fibrillation. Journal of Cardiovascular Electrophysiology. 2016;27(11):1312-1318. doi:10.1111/jce.13064
x <- sin(2 * pi * 6 * seq(0, 10, by = 1 / 500)) + rnorm(5001, sd = 0.5) calculate_organization_index(x, frequency = 500)x <- sin(2 * pi * 6 * seq(0, 10, by = 1 / 500)) + rnorm(5001, sd = 0.5) calculate_organization_index(x, frequency = 500)
Sample entropy measures the irregularity of a time series as the
negative log conditional probability that two sequences similar for m
points remain similar at the next point.
calculate_sample_entropy(x, m = 2, r = NULL)calculate_sample_entropy(x, m = 2, r = NULL)
x |
Numeric vector of the time series |
m |
Embedding dimension. Default 2. |
r |
Tolerance. Default |
Unlike approximate entropy, sample entropy excludes self-matches.
Counting self-matches biases approximate entropy toward regularity and makes
it depend on record length, which are the two problems sample entropy was
introduced to fix. Prefer this over calculate_approximate_entropy(),
particularly when record length varies.
The calculation is O(n^2), which is a standing temptation to decimate
further than the measurement tolerates. Alcaraz et al. tuned m, r and
the sampling rate for atrial fibrillation organisation specifically and
found classification degraded below 256 Hz, with m of 1 or 2 and r
between 0.1 and 0.25 times the standard deviation. The defaults here sit
inside that, and extract_f_waves() decimates to 256 Hz rather than to
something derived from the fibrillatory bandwidth.
They tuned them on the main atrial wave – lead V1's atrial signal
narrowed by selective filtering to a 3 Hz band around its dominant
frequency – and not on a broadband signal, and on that wave Alcaraz and
Rieta (2009) tested m of 1 and 2 with r of 0.1, 0.15, 0.2 and 0.25,
found the outcomes very similar, and selected m = 2 and a tolerance of
0.25 standard deviations.
extract_f_waves() computes on the broadband cancelled signal by default
and offers the main atrial wave through entropy_input; the two are not
on one scale.
Sample entropy. NA when no matches are found at either length.
Richman JS, Moorman JR. Physiological time-series analysis using approximate entropy and sample entropy. American Journal of Physiology. Heart and Circulatory Physiology. 2000;278(6):H2039-H2049. doi:10.1152/ajpheart.2000.278.6.H2039
Alcaraz R, Abasolo D, Hornero R, Rieta JJ. Optimized assessment of atrial fibrillation organization through suitable parameters of sample entropy. Annual International Conference of the IEEE Engineering in Medicine and Biology Society. 2010;2010:118-121. doi:10.1109/IEMBS.2010.5627169
Alcaraz R, Rieta JJ. Sample entropy of the main atrial wave predicts spontaneous termination of paroxysmal atrial fibrillation. Medical Engineering & Physics. 2009;31(8):917-922. doi:10.1016/j.medengphy.2009.05.002
set.seed(123) calculate_sample_entropy(rnorm(500))set.seed(123) calculate_sample_entropy(rnorm(500))
Averages the periodograms of overlapping, Hann-tapered segments.
calculate_welch_spectrum( x, frequency, segment_sec = 4, overlap = 0.5, resolution = 0.05 )calculate_welch_spectrum( x, frequency, segment_sec = 4, overlap = 0.5, resolution = 0.05 )
x |
Numeric vector |
frequency |
Sampling frequency in Hz |
segment_sec |
Segment length in seconds. Default 4. |
overlap |
Fractional overlap between segments. Default 0.5. |
resolution |
Target frequency resolution in Hz, achieved by zero padding. Default 0.05. |
A raw periodogram is an inconsistent estimator: its variance does not fall as the record lengthens, so on a ten second record the spectrum is very noisy and an argmax taken over a several-Hz band is correspondingly unstable. Averaging tapered segments trades frequency resolution for variance, and zero-padding to a fixed resolution keeps the bin edges from moving with record length.
A list with freq and spec
Welch PD. The use of fast Fourier transform for the estimation of power spectra: a method based on time averaging over short, modified periodograms. IEEE Transactions on Audio and Electroacoustics. 1967;15(2):70-73. doi:10.1109/TAU.1967.1161901
x <- sin(2 * pi * 6 * seq(0, 10, by = 1 / 500)) + rnorm(5001, sd = 0.1) psd <- calculate_welch_spectrum(x, frequency = 500) psd$freq[which.max(psd$spec)]x <- sin(2 * pi * 6 * seq(0, 10, by = 1 / 500)) + rnorm(5001, sd = 0.1) psd <- calculate_welch_spectrum(x, frequency = 500) psd$freq[which.max(psd$spec)]
Removes the QRST complex from each lead so that the atrial signal can be measured. Two methods are available.
cancel_ventricular_signal( signals, frequency, qrs_loc = NULL, method = c("spatiotemporal", "average_beat"), min_beats = 3L, aberrancy_threshold = 0.9 )cancel_ventricular_signal( signals, frequency, qrs_loc = NULL, method = c("spatiotemporal", "average_beat"), min_beats = 3L, aberrancy_threshold = 0.9 )
signals |
A named list of numeric vectors, one per lead, already bandpass filtered and all of the same length. |
frequency |
Sampling frequency in Hz. Required. |
qrs_loc |
Integer vector of QRS sample positions shared by all leads.
Detected from a composite if |
method |
Either |
min_beats |
Minimum number of beats required before any template-based subtraction is attempted. Default 3. Below this an "average" beat is effectively a copy of a single beat, and subtracting it returns zero. |
aberrancy_threshold |
Correlation below which a beat is called morphologically aberrant and excluded from template construction. Default 0.9. Aberrancy is judged on QRS morphology, never on RR interval: in atrial fibrillation the RR interval is irregular by definition, so an RR-based criterion fires on normally conducted beats in the exact rhythm this function targets. |
"spatiotemporal" fits each beat, in each lead, to a linear combination of
the median beat templates of that lead and its adjacent leads plus an
intercept, and subtracts the fit (Stridh and Sornmo, 2001). Adjacency runs
along the frontal leads in Cabrera order and along V1 to V6; a lead that is
not a surface lead takes its neighbours in the order given. The cross-lead
terms give the model enough freedom to absorb beat-to-beat rotation of the
electrical axis, which a single-lead template cannot represent. They are
deliberately few: fitted against every lead's template, as earlier versions
did, the model absorbs the atrial signal in the window as well, and on
muse-sinus with a known f-wave added it kept 43-62% of that wave inside
the beat windows against 82-88% for the adjacent-lead fit. The cancellation
section of extract_f_waves() has the table.
"average_beat" subtracts each lead's own median template, scaled by a single
least-squares coefficient. This is the Slocum average-beat predecessor and is
provided for comparison.
Beats are never blanked or interpolated across. A beat that the template
models badly is still fitted and subtracted, and the quality of that fit is
reported through cancellation_residual rather than being hidden by
replacing the segment with a straight line.
Both methods share one assumption, and it is worth stating plainly: what
the template holds is whatever repeats at a fixed phase to the QRS. In atrial
fibrillation the atrial signal has no such phase, which is why the method
works. In atrial flutter conducting at a fixed ratio it does, so the flutter
wave is built into the template and subtracted along with the QRST – around
90% of it, on a synthetic 2:1 record. The fit is better for having absorbed
it, so cancellation_residual does not report the loss. See the cancellation
section of extract_f_waves(), which flags the condition through
record$rr_regular.
A list with atrial (named list of cancelled signals), qrs_loc,
n_beats_cancelled, n_beats_skipped, and n_beats_aberrant.
Stridh M, Sornmo L. Spatiotemporal QRST cancellation techniques for analysis of atrial fibrillation. IEEE Transactions on Biomedical Engineering. 2001;48(1):105-111. doi:10.1109/10.900266
Converts signal data from one sampling frequency to another, up- or
down-sampling as needed while preserving the recording's duration. This
harmonises records captured at different rates (e.g. mixing 250 Hz and 500 Hz
ECGs) so that downstream operations such as median_window() or
normalize_window() compare like with like, and lets analyses that assume
a fixed rate be fed at that rate.
change_frequency( x, to, from = NULL, method = c("linear", "spline", "step", "polyphase"), anti_alias = TRUE, preserve_class = TRUE, ... )change_frequency( x, to, from = NULL, method = c("linear", "spline", "step", "polyphase"), anti_alias = TRUE, preserve_class = TRUE, ... )
x |
The signal data to convert. Either an |
to |
The target sampling rate in Hz. |
from |
The rate the data is currently sampled at, in Hz. Defaults to the
rate the object's own header declares, and must agree with it when given.
Required for a bare |
method |
The resampling method, one of |
anti_alias |
Logical. If |
preserve_class |
Logical, for |
... |
Additional arguments (currently unused). |
Only the target rate (to) has to be stated. The source rate is
taken from the object's own header, which already carries it; state from
as well when you want it checked, and a disagreement with the recorded rate
is an error. A bare numeric lead has no header, so there from is
required - the sampling rate is the one property that cannot be recovered
from the samples themselves. Read the recorded rate with frequency().
The change is duration-preserving: a 0.8 second beat stays 0.8 seconds long,
but its sample count scales with the frequency ratio. For EGM input the
signal values are recomputed on the new grid, the header frequency and
sample count are updated, and every annotator's sample indices are rescaled
onto the new grid. Annotation time strings are left alone, because
absolute time is unchanged by definition.
The annotations carried by the object are rescaled; a separate copy of
them is not. Annotations read separately from disk, or held from before
the conversion, remain on the original grid, and mixing the two halves every
interval measured from them (or doubles it). Take the annotations from the
converted object with get_annotation() rather than re-reading them:
slow <- change_frequency(ecg, to = 250) get_annotation(slow) # rescaled, on the 250 Hz grid read_annotation(...) # still on the 500 Hz grid
It is written as a pipe stage rather than an argument of the functions that need it, so the rate change is explicit and happens exactly once:
read_wfdb("ecg", ".", "ecgpuwave") |>
change_frequency(to = 250) |>
get_windows(by = "rhythm")
Four method choices are offered, covering the standard approaches:
linear (default) - piecewise-linear interpolation onto the new grid.
Fast, monotone, and free of overshoot, which matters when the sharp edges
of a QRS complex would otherwise ring.
spline - natural cubic spline interpolation. Smoother through curved
segments such as T waves, at the cost of possible overshoot at steep
transitions.
step - nearest-preceding sample (piecewise constant). Non-interpolating,
so it never invents intermediate voltages; mainly useful for step-like or
categorical channels such as pacing markers.
polyphase - anti-aliased polyphase FIR resampling via
signal::resample(), the conventional DSP approach. The rate ratio is
approximated by a rational p/q, so it is most natural for related rates
(500 to 250 Hz, 250 to 1000 Hz) and applies its own anti-alias filter.
Down-sampling folds any energy above the new Nyquist frequency back into the
retained band. The interpolating methods do not guard against this on their
own, so anti_alias = TRUE (the default) low-pass filters each lead with a
zero-phase Butterworth filter before the grid change. Records too short to
filter stably are left unfiltered, and up-sampling needs no filter at all.
The polyphase method ignores this argument because its own FIR stage
already performs the equivalent filtering.
An object of the same kind as x, sampled at to Hz.
frequency(), normalize_window() to change the number of samples
without changing the physical rate.
## Not run: ecg <- read_wfdb("ecg", test_path(), "ecgpuwave") # Down-sample the whole record to 250 Hz; the source rate is on the header slow <- change_frequency(ecg, 250) frequency(slow) # State `from` as well to assert what you believe you were given change_frequency(ecg, to = 250, from = 500) # Or harmonise a set of windowed beats after the fact beats <- get_windows(ecg, by = "rhythm") change_frequency(beats, to = 1000, method = "polyphase") # A bare lead works too, for signal-processing pipelines. It carries no # header, so `from` is required change_frequency(ecg$signal$II, to = 1000, from = 500) ## End(Not run)## Not run: ecg <- read_wfdb("ecg", test_path(), "ecgpuwave") # Down-sample the whole record to 250 Hz; the source rate is on the header slow <- change_frequency(ecg, 250) frequency(slow) # State `from` as well to assert what you believe you were given change_frequency(ecg, to = 250, from = 500) # Or harmonise a set of windowed beats after the fact beats <- get_windows(ecg, by = "rhythm") change_frequency(beats, to = 1000, method = "polyphase") # A bare lead works too, for signal-processing pipelines. It carries no # header, so `from` is required change_frequency(ecg$signal$II, to = 1000, from = 500) ## End(Not run)
Reports the convention of the file an annotation table was read from, and
will be written back to: "global", where channels count signals from 1
and 0 marks a fiducial belonging to no lead in particular, or "signal",
where they count from 0 as the WFDB tools do.
channel_zero(x)channel_zero(x)
x |
An |
In memory every table counts from 1, with 0 the global channel.
A file declared "signal" at read_annotation() or read_wfdb() has one
added to each channel as it is read, so the label here says nothing about
the numbers in the table; it says how write_annotation() should number
them on the way out, which it does by default. See channels for why the
WFDB specification leaves the two readings open.
A single character, "global" or "signal".
channels for the policy, read_annotation() to declare it,
write_annotation() to override it on the way out.
## Not run: channel_zero(get_annotation(read_wfdb("ecg", test_path(), "ecgpuwave"))) #> [1] "global" ## End(Not run)## Not run: channel_zero(get_annotation(read_wfdb("ecg", test_path(), "ecgpuwave"))) #> [1] "global" ## End(Not run)
Why several functions in this package ask which lead to follow, and how they decide when they have to.
A delineating annotator may be run over a record once, or once per lead. Run
per lead it writes twelve independent copies of every fiducial, separated
only by the channel column, and such a table does not describe beats until
it is resolved to a single lead: pooled, it reports twelve times as many
beats as the record contains, and taking the first match at each point
silently follows whichever lead happens to sort first.
So every function that reads fiducials - get_windows(),
learn_template(), median_window() and the other window transforms,
extract_f_waves(), vcg() - takes a channel, and resolves
it the same way:
annotations spanning more than one channel with no channel given are an
error, not a guess;
a channel that the annotations do not carry is an error, which is what
catches a numbering convention that does not match;
the global channel 0 is not a lead, so it never counts toward the span
and is always kept alongside the chosen lead.
The channel may be a number or a channel name ("II"), resolved against the
record's own header. A number is matched literally against the channel
column, and the numbering there belongs to the annotator that wrote the
file. Read it off the record rather than assuming:
table(get_annotation(x)$channel)
Only the boundaries are restricted to one lead. Windows still carry the signal for every channel, which is what makes a windowed beat usable as a vectorcardiogram.
Zero is the one value the WFDB specification leaves undecided. annot(5)
says the chan field starts at 0 and persists until a CHN record
changes it; nothing reserves a value for "no particular signal", and nothing
requires the field to name a real one. So on disk 0 may mean the first
signal, or it may mean the annotator never said.
In memory the package settles it: channel counts signals from 1, as
header$number and the signal columns do, and 0 is the global channel,
a fiducial belonging to no lead. Every function that resolves a channel
relies on that one reading, so a file that counts from 0 is renumbered as
it is read rather than carried as an exception:
ecg <- read_wfdb("record", dir, "ann", channel_zero = "signal")
That adds one to every channel, so lead I is 1 whichever way the file
counted, and write_annotation() takes one off again by default, so the
file goes back out the way it came in - the table remembers which, and
channel_zero() reports it.
Nothing has to be declared for the files most annotators write. An all-zero
column - ecgpuwave, wqrs, and the twelve per-lead files of the bundled
LUDB record, which carry the lead in the file extension - is an absence
of information, read as global, and such a record windows without a
channel at all. A column running 1 .. nsig already counts from one. The
one column the reader cannot settle is 0 .. nsig-1 exactly, which fills
the signals if 0 is the first of them and is a global channel plus every
lead but the last otherwise; that file is refused with a message naming
both declarations, since either reading taken silently would misplace a
lead. A column holding 0 beside a few low channels is read as global, and
a channel it turns out not to carry is caught when it is asked for.
get_windows(), label_waves(), get_annotation(),
channel_zero()
detect_QRS() implements a modified Pan-Tompkins algorithm to
detect QRS complexes in ECG signals. The function applies a sequence of
processing steps including bandpass filtering, differentiation, squaring, and
moving window integration to identify R peaks in the signal.
detect_QRS(signal, frequency, window_size = 0.15)detect_QRS(signal, frequency, window_size = 0.15)
signal |
Numeric vector representing the ECG signal |
frequency |
Sampling frequency of the signal in Hz |
window_size |
Width of the integration window in seconds, default is 0.150 seconds |
The Pan-Tompkins algorithm is a widely-used method for QRS detection in ECG signals. This implementation follows these steps:
Bandpass filtering (5-15 Hz) to reduce noise and emphasize QRS complexes
Differentiation to highlight the steep slopes of QRS complexes
Squaring to amplify high-frequency components
Moving window integration to consider the overall QRS morphology
Thresholding at the mean plus half a standard deviation of the integrated signal to identify peaks
Application of a refractory period to prevent multiple detections of the same QRS complex
The threshold is a single static one over the whole record, not the pair of adaptive signal and noise thresholds with searchback that Pan and Tompkins describe. That is enough on a clean ten-second strip and will drop beats on a record whose amplitude drifts.
Positions are reported at the peak of the integration window, which lags the
true R peak by roughly half the integration width. extract_f_waves()
refines them against the signal before use, and anything else that needs beat
alignment should do the same.
Integer vector containing the sample indices of detected QRS complexes
Pan, J., & Tompkins, W. J. (1985). A real-time QRS detection algorithm. IEEE Transactions on Biomedical Engineering, (3), 230-236. doi:10.1109/TBME.1985.325532
## Not run: ecg_data <- read_wfdb("muse-af", system.file("extdata", package = "EGM")) freq <- attributes(ecg_data$header)$record_line$frequency qrs_locations <- detect_QRS(ecg_data$signal$II, freq) ## End(Not run)## Not run: ecg_data <- read_wfdb("muse-af", system.file("extdata", package = "EGM")) freq <- attributes(ecg_data$header)$record_line$frequency qrs_locations <- detect_QRS(ecg_data$signal$II, freq) ## End(Not run)
A specialization of the EGM class for the standard 12-lead surface electrocardiogram. It carries the same three components - signal, header, and annotation - and adds the guarantee that every channel is a recognised surface ECG lead, named canonically.
ECG( signal = signal_table(), header = header_table(), annotation = annotation_table(), ... ) is_ECG(x)ECG( signal = signal_table(), header = header_table(), annotation = annotation_table(), ... ) is_ECG(x)
signal |
A |
header |
A |
annotation |
A |
... |
Additional arguments to be passed to the function |
x |
An |
An electrophysiology study records surface and intracardiac channels
side by side, so the 12-lead ECG is often a subset of a larger record.
as_ECG() extracts that subset: it keeps the surface leads, renames them to
their canonical form (I, II, III, AVF, AVL, AVR, V1 ... V6),
and discards every other channel. This is the intended route from a study to
an ECG analysis.
Functions that are only defined on the surface ECG - extract_f_waves(),
vcg() - coerce their input through that same path and then check that the
leads each one needs are actually present. A record carrying only intracardiac channels is an error
rather than a silent result.
Construction is otherwise identical to EGM(). A record with a lead set
that is not the standard twelve is accepted with a warning, since partial
surface sets are common and several analyses tolerate them.
Leads are held in the order the source record wrote them, which is not
always the display order and is not always the same order twice: a MUSE
export and a PhysioNet record disagree about where the augmented limb leads
sit. Index by name rather than by position, and use ecg_leads() and
lead_factor() where an order is needed - for a plot, a sort, or the
columns of a matrix stacked across records.
An object of class ECG, which inherits from EGM, containing
signal, header, and annotation components.
as_ECG() to extract the surface leads from a study, EGM() for the
general class.
ecg_leads() returns the standard twelve leads as an ordered factor.
lead_factor() puts a set of lead labels onto that order, whatever they were
called and whatever sequence they arrived in.
ecg_leads(order = c("standard", "cabrera")) lead_factor(x, order = c("standard", "cabrera"), drop = FALSE)ecg_leads(order = c("standard", "cabrera")) lead_factor(x, order = c("standard", "cabrera"), drop = FALSE)
order |
The display sequence. |
x |
A |
drop |
Logical. If |
The display sequence is the AHA/ACCF/HRS one (Kligfield et al. 2007): the three bipolar limb leads, the three augmented limb leads, and then the precordials left to right.
It is what every ECG cart prints and what PhysioNet's twelve-lead databases
are written in, so it is the order to hold a cohort in. order = "cabrera"
gives the Cabrera sequence (aVL, I, -aVR, II, aVF, III, then the
precordials), the recognised alternative, in which the frontal leads run
contiguously from -30 to +120 degrees. Note that Cabrera displays -aVR;
the lead named here is still AVR, and inverting its signal is the caller's
to do.
A record's leads are stored in whatever order its source wrote them, and
as_ECG() does not reorder them - annotation channel indices address
signal columns by position, so permuting the columns would silently
repoint every per-lead fiducial. Order at the point of use instead:
features$lead <- lead_factor(features$lead) # then sort, facet, or split
signal <- get_signal(ecg)[, c("sample", as.character(ecg_leads()))]
lead_factor() canonicalises before ordering, so aVR, av r and AV-R
all become AVR. A label that is not a surface lead becomes NA, which is
deliberate: it shows up in a plot or a sort rather than being dropped
quietly.
ecg_leads(), an ordered factor of twelve lead names.
lead_factor(), an ordered factor of the same levels, one element per
element of x.
Kligfield P, Gettes LS, Bailey JJ, et al. Recommendations for the standardization and interpretation of the electrocardiogram: part I. Circulation. 2007;115(10):1306-1324. doi:10.1161/CIRCULATIONAHA.106.180200
as_ECG() for extracting the surface leads from a record, ECG for
the class
ecg_leads() ecg_leads("cabrera") # Whatever they were called, and whatever order they came in lead_factor(c("v2", "aVR", "II", "AV-L")) sort(lead_factor(c("V6", "I", "aVF", "V1"))) # Not a surface lead, and said so rather than dropped lead_factor(c("II", "CS 1-2"))ecg_leads() ecg_leads("cabrera") # Whatever they were called, and whatever order they came in lead_factor(c("v2", "aVR", "II", "AV-L")) sort(lead_factor(c("V6", "I", "aVF", "V1"))) # Not a surface lead, and said so rather than dropped lead_factor(c("II", "CS 1-2"))
This class serves as a combinatorial class to describe
cardiovascular electrical signal data in R. It is based off of the formats
available in WFDB, but has been formatted for ease of use within the R
ecosystem. An EGM object contains three components in a list:
signal data in multiple channels
header information
annotation labels at specified time points
These components help to navigate, and visualize data. The EGM class is
the backbone for working with WFDB objects in R, and provides an interface
for integrating or converting other raw signal data to a WFDB format.
EGM( signal = signal_table(), header = header_table(), annotation = annotation_table(), ... ) is_EGM(x)EGM( signal = signal_table(), header = header_table(), annotation = annotation_table(), ... ) is_EGM(x)
signal |
A |
header |
A |
annotation |
Either a single |
... |
Additional arguments to be passed to the function |
x |
An |
The individual components of the class are further defined in their
respective children functions signal_table(), header_table(),
annotation_table(). They are very simple classes that build upon the
data.table class that allow for class safety checks when working with
different data types (particularly WFDB).
IMPORTANT: The EGM class can be built from ground-up by the user,
however it is primarily generated for the user using the other read/write
functions, such as read_bard() or read_wfdb().
An object of class EGM that is always a list of the above three
components. Oftentimes, the annotation_table object may be missing, and
it is replaced with an empty table as a place holder.
extract_f_waves() isolates atrial activity from a surface ECG
by cancelling the ventricular (QRST) contribution, then summarises the
residual fibrillatory signal in each lead.
extract_f_waves( object, lead = NULL, qrs_loc = NULL, channel = NULL, cancel_method = c("spatiotemporal", "average_beat"), f_characteristics = c("amplitude", "dominant_frequency", "organization", "sample_entropy"), amplitude_window = c("tq", "all"), normalize = c("none", "qrs"), band = c(4, 10), entropy_rate = 256, entropy_input = c("broadband", "main_atrial_wave"), entropy_bandwidth = 3, entropy_tolerance = 0.2, spectrum = c("V1", "pooled", "lead"), rhythm = NULL, tol = 0.15, min_beats = 3L, aberrancy_threshold = 0.9, keep_signal = FALSE, verbose = TRUE )extract_f_waves( object, lead = NULL, qrs_loc = NULL, channel = NULL, cancel_method = c("spatiotemporal", "average_beat"), f_characteristics = c("amplitude", "dominant_frequency", "organization", "sample_entropy"), amplitude_window = c("tq", "all"), normalize = c("none", "qrs"), band = c(4, 10), entropy_rate = 256, entropy_input = c("broadband", "main_atrial_wave"), entropy_bandwidth = 3, entropy_tolerance = 0.2, spectrum = c("V1", "pooled", "lead"), rhythm = NULL, tol = 0.15, min_beats = 3L, aberrancy_threshold = 0.9, keep_signal = FALSE, verbose = TRUE )
object |
An object of class |
lead |
Optional. A character vector of leads to analyse. If |
qrs_loc |
Optional integer vector of QRS sample positions, at the
record's own sampling frequency. If |
channel |
The lead whose beat annotations give the QRS positions, given as a channel number or name. Required when the annotations span more than one channel: pooled across twelve leads they report twelve times as many beats as the record holds, and every rhythm measure derived from them follows. See the channels section. Supplying |
cancel_method |
Ventricular cancellation method. Either
|
f_characteristics |
Character vector of features to compute. Any of
|
amplitude_window |
Where amplitude is measured. |
normalize |
Amplitude normalisation.
RMS is the default because peak-to-peak is a maximum over the segment and
so grows with the segment's length: on white noise its expectation rises
58% between a 20-sample and a 400-sample window. TQ segments are as long as
the RR interval lets them be, which is irregular within an AF record by
definition and shorter at higher rates between patients, so
|
band |
Numeric length-2 vector giving the frequency band searched for the
dominant fibrillatory frequency, in Hz. Default |
entropy_rate |
Sampling rate in Hz to which the atrial signal is
decimated before entropy is computed, or Entropy is O(n^2), so this is the argument that decides how long a batch takes. It is not free to lower it: Alcaraz et al. (2010) tuned sample entropy for this exact task and found classification degraded below 256 Hz, which is why that is the default rather than a rate chosen from the fibrillatory bandwidth. A lower value returns a number that is cheaper and outside the range the parameter set was validated on. |
entropy_input |
What the entropy is computed on. |
entropy_bandwidth |
Width in Hz of the band kept around the dominant
frequency for |
entropy_tolerance |
The sample entropy tolerance |
spectrum |
Where the dominant frequency is read from.
|
rhythm |
Optional character string naming the rhythm, e.g. |
tol |
Numeric tolerance for the harmonic test. A peak is flagged
This is a sensitive screen and not a specific test, and the difference is
large enough to plan around. The harmonic index of an uncontaminated peak
is a ratio of two unrelated numbers, so with integers spaced one apart a
tolerance of 0.15 lands on one 30% of the time by arithmetic alone –
simulated at 0.301 for a dominant frequency uniform on 4-10 Hz and a heart
rate uniform on 60-160 bpm, and flat across rate. Excluding every flagged
record therefore discards roughly a third of the good ones too. Read it
with |
min_beats, aberrancy_threshold
|
Passed to
|
keep_signal |
Logical. If |
verbose |
Logical. If |
An object of class f_wave_analysis, a list with:
featuresA data.table with one row per lead, holding
f_amplitude_p2p, f_amplitude_rms, qrs_amplitude, f_ratio,
dominant_rate, organization_index, sample_entropy, and the
per-lead diagnostics harmonic_overlap, on_harmonic,
cancellation_residual, and tq_fraction.
recordA one-row data.table of record-level values, including
n_beats_cancelled, n_beats_skipped, n_beats_aberrant,
heart_rate, rr_cv, af_like, and
rr_regular - the flag that the cancellation may have taken the atrial
signal with it, described in the cancellation section.
signalThe cancelled atrial signals, when keep_signal = TRUE.
The default cancel_method = "spatiotemporal" implements the spatial half
of Stridh and Sornmo (2001). A single set of QRS positions is shared by every
lead, and a template beat is formed for each lead by taking the median
across beats. Each individual beat, in each lead, is then fitted by least
squares to a linear combination of the templates of that lead and its
adjacent leads plus an intercept, and the fit is subtracted. Adjacency runs
along the two chains the leads sit on – the frontal leads in Cabrera order
(aVL, I, -aVR, II, aVF, III) and the precordials V1 to V6 – so a lead is
fitted against at most three templates. A lead that is not a surface lead
takes its neighbours in the order it was given.
Adjacent, and not all twelve, is the published model, and the difference is
not academic. A single-lead template cannot absorb the beat-to-beat rotation
of the heart's electrical axis caused by respiration and by varying
ventricular filling, which is the dominant reason a template fails to fit its
own beat; what it leaves behind is periodic at the heart rate and lands on
heart-rate harmonics, inside the band this function reads. Two neighbouring
templates are enough freedom to follow that rotation. Twelve are enough to
follow the atrial signal lying in the beat window as well, and to subtract
it. Measured on the bundled muse-sinus record with a fibrillatory wave of
known amplitude added at 5.3, 6.4 and 7.7 Hz (heart rate 90, beat windows
covering 95% of the record), the share of that wave surviving inside the
beat windows was:
| Templates fitted | Surviving | Residual at the QRS |
| all twelve | 43-62% | 0.066-0.075 |
| own and adjacent | 82-88% | 0.074-0.084 |
own only ("average_beat") |
91% | 0.079-0.086 |
At fibrillation rates the beat windows cover the whole record, so the
all-lead fit – which earlier versions used – was discarding about half the
atrial signal for a tenth less residual. f_amplitude was never affected,
being measured in the TQ segments; dominant_rate, organization_index and
sample_entropy were.
Two parts of the published method are not implemented. It estimates a
per-beat time shift and time scaling of the template as well, which is what
the temporal in its name refers to; the absence shows up as a larger
cancellation_residual on records whose QRS width varies beat to beat. And
it estimates the f-wave around each beat and subtracts it before the
templates are averaged, so that less of it is built into them; here the
median across beats is relied on for that. Use f_wave_diagnostics to check
what the cancellation left behind.
A template built by stacking beats keeps whatever repeats at a fixed phase to the QRS. That is the ventricular complex, and it is also atrial activity in a rhythm whose AV conduction ratio is fixed – typical atrial flutter. The method has no way to tell the two apart, so the flutter wave joins the template and is subtracted with the QRST.
The size of the effect is not marginal. On a synthetic 12-lead record carrying a 5 Hz (300/min) flutter wave, the fraction of that wave surviving cancellation (median over leads) and the organisation index lead V1 then reports are:
| Ventricular response | Atrial cycles per RR | Surviving | Organisation index |
| 2:1, fixed | 2.00 | 8% | 0.18 |
| 3:1, fixed | 3.00 | 23% | 0.23 |
| 4:1, fixed | 4.00 | 24% | 0.21 |
| fixed, non-integer ratio | 2.50 | 88% | 0.96 |
| irregular (AF-like) | -- | 76% | 0.80 |
The uncancelled signal scores 0.94. So a flutter record conducting at a fixed
ratio comes back with an organisation index in the range fibrillation
occupies, and a dominant rate describing whatever survived rather than the
flutter. A cohort compared on organization_index will not separate flutter
from fibrillation, and the failure looks like a null result rather than an
artefact.
Nothing in the fit reports this. The template models the beat better for
having absorbed the atrial wave, so cancellation_residual is small and
reassuring. What does report it is the ventricular response being regular:
record$rr_regular is TRUE when RR CV is below 0.05, and a warning is
raised. It is deliberately not silenced by rhythm = "flutter", since that is
the case it exists for.
There is no cancellation method here that avoids this – "average_beat"
shares the assumption. Where flutter is the question, read the atrial wave
from a segment between QRS complexes rather than from a cancelled signal.
Every spectral feature is returned alongside on_harmonic. A contaminated
dominant frequency is not noisy, it is precise and wrong, and it is highly
reproducible because heart rate is highly reproducible within a patient.
dominant_rate must not be used without conditioning on on_harmonic.
Ten seconds is a short record for this measurement. Even with correct cancellation a material fraction of atrial fibrillation records will carry a contaminated rate.
The output is meaningful only in atrial fibrillation or flutter. In sinus
rhythm there is no fibrillatory wave to find, so the estimator returns
whatever is largest in the band. When rhythm is not supplied, irregularity
is inferred from the RR series and a warning is issued if the record does not
look like atrial fibrillation.
A delineating annotator may be run over a record once, or once per lead. Run
per lead it writes twelve independent copies of every fiducial, separated
only by the channel column, and such a table does not describe beats until
it is resolved to a single lead: pooled, it reports twelve times as many
beats as the record contains, and taking the first match at each point
silently follows whichever lead happens to sort first.
So every function that reads fiducials - get_windows(),
learn_template(), median_window() and the other window transforms,
extract_f_waves(), vcg() - takes a channel, and resolves
it the same way:
annotations spanning more than one channel with no channel given are an
error, not a guess;
a channel that the annotations do not carry is an error, which is what
catches a numbering convention that does not match;
the global channel 0 is not a lead, so it never counts toward the span
and is always kept alongside the chosen lead.
The channel may be a number or a channel name ("II"), resolved against the
record's own header. A number is matched literally against the channel
column, and the numbering there belongs to the annotator that wrote the
file. Read it off the record rather than assuming:
table(get_annotation(x)$channel)
Only the boundaries are restricted to one lead. Windows still carry the signal for every channel, which is what makes a windowed beat usable as a vectorcardiogram.
Stridh M, Sornmo L. Spatiotemporal QRST cancellation techniques for analysis of atrial fibrillation. IEEE Transactions on Biomedical Engineering. 2001;48(1):105-111. doi:10.1109/10.900266
Slocum J, Sahakian A, Swiryn S. Diagnosis of atrial fibrillation from surface electrocardiograms based on computer-detected atrial activity. Journal of Electrocardiology. 1992;25(1):1-8. doi:10.1016/0022-0736(92)90123-h
Bollmann A, Husser D, Mainardi L, et al. Analysis of surface electrocardiograms in atrial fibrillation: techniques, research, and clinical applications. Europace. 2006;8(11):911-926. doi:10.1093/europace/eul113
Ng J, Goldberger JJ. Understanding and interpreting dominant frequency analysis of AF electrograms. Journal of Cardiovascular Electrophysiology. 2007;18(6):680-685. doi:10.1111/j.1540-8167.2007.00832.x
Lankveld TAR, Zeemering S, Crijns HJGM, Schotten U. The ECG as a tool to determine atrial fibrillation complexity. Heart. 2014;100(14):1077-1084. doi:10.1136/heartjnl-2013-305149
Alcaraz R, Abasolo D, Hornero R, Rieta JJ. Optimized assessment of atrial fibrillation organization through suitable parameters of sample entropy. Annual International Conference of the IEEE Engineering in Medicine and Biology Society. 2010;2010:118-121. doi:10.1109/IEMBS.2010.5627169
Alcaraz R, Abasolo D, Hornero R, Rieta JJ. Optimal parameters study for sample entropy-based atrial fibrillation organization analysis. Computer Methods and Programs in Biomedicine. 2010;99(1):124-132. doi:10.1016/j.cmpb.2010.02.009
Alcaraz R, Rieta JJ. Sample entropy of the main atrial wave predicts spontaneous termination of paroxysmal atrial fibrillation. Medical Engineering & Physics. 2009;31(8):917-922. doi:10.1016/j.medengphy.2009.05.002
Escribano P, Rodenas J, Garcia M, et al. Combination of frequency- and time-domain characteristics of the fibrillatory waves for enhanced prediction of persistent atrial fibrillation recurrence after catheter ablation. Heliyon. 2024;10(3):e25295. doi:10.1016/j.heliyon.2024.e25295
An K, Li H, Yu C, Zheng Z. Surface electrocardiogram f wave analysis in patients with atrial fibrillation undergoing thoracoscopic epicardial ablation. Interdisciplinary CardioVascular and Thoracic Surgery. 2024;38(5):ivae057. doi:10.1093/icvts/ivae057
Li YH, Hwang JJ, Tseng YZ, Kuan P, Lien WP. Clinical significance of fibrillatory wave amplitude. A clue to left atrial appendage function in nonrheumatic atrial fibrillation. Chest. 1995;108(2):359-363. doi:10.1378/chest.108.2.359
cancel_ventricular_signal(), calculate_dominant_frequency(),
calculate_sample_entropy(), calculate_organization_index()
## Not run: af <- read_wfdb("muse-af", system.file("extdata", package = "EGM")) res <- extract_f_waves(af) # Never read the rate without the diagnostics beside it res$features[, .(lead, dominant_rate, on_harmonic, cancellation_residual)] ## End(Not run)## Not run: af <- read_wfdb("muse-af", system.file("extdata", package = "EGM")) res <- extract_f_waves(af) # Never read the rate without the diagnostics beside it res$features[, .(lead, dominant_rate, on_harmonic, cancellation_residual)] ## End(Not run)
Not a function. This documents the diagnostic fields that
extract_f_waves() returns beside each spectral feature, why they must be
read together with it, and – for each output of the fibrillatory analysis
– whether it is a published quantity, an adaptation of one, or specific to
this package.
A number with no citation should be assumed to be wrong until it has one, so the provenance is stated here rather than left to be inferred.
Published, and computed as published. dominant_rate is the atrial
fibrillatory rate, reported in fibrillations per minute as the literature
does and read from lead V1 by default as the literature does (Bollmann et
al. 2006; An et al. 2024; Escribano et al. 2024). f_amplitude_p2p is
fibrillatory wave amplitude, the measure behind the coarse-versus-fine
distinction: Li et al. called atrial fibrillation coarse at a peak-to-peak
amplitude of 1 mm or more in V1, and found left atrial appendage dysfunction
and thrombus more common in those patients. rr_cv is the coefficient of
variation of the RR interval, which Tateno and Glass used to detect atrial
fibrillation at 86.6% sensitivity and 84.3% specificity.
Adapted from a published quantity. sample_entropy is Richman and
Moorman's statistic with the embedding dimension, tolerance and sampling
rate that Alcaraz et al. tuned for atrial fibrillation – but they tuned
them on the main atrial wave, lead V1's atrial signal narrowed to a band
around its dominant frequency, and by default it is computed here on the
broadband cancelled signal, which reads higher (0.16 against 0.12 on
muse-af V1). entropy_input = "main_atrial_wave" computes it on that wave
through the 3 Hz band they selected, and with entropy_tolerance = 0.25 on
their exact parameter set. f_ratio is the
Alcaraz-Rieta group's normalised f-wave amplitude, the RMS f-wave amplitude
over the R-peak magnitude, with the peak-to-peak QRS excursion as the
denominator and as a fraction rather than a percentage, so proportional to
theirs and not equal. organization_index follows Everett's definition and
An's surface implementation but is not on their scale; see
calculate_organization_index(). cancellation_residual is a variant of the
published ventricular residue index, on a different normalisation.
Specific to this package, with no published definition.
harmonic_overlap and on_harmonic, described below.
Package operating points, not published cut-points. af_like fires at an
RR coefficient of variation of 0.12 with a normalised RMSSD of 0.10, and
rr_regular below 0.05. The indices are the published ones; these three
thresholds were chosen here, and exist to decide whether to warn rather than
to diagnose anything.
harmonic_overlapHow many fibrillatory cycles fit into one average
heartbeat: the dominant frequency divided by the heart rate, both in Hz.
Ventricular energy that cancellation failed to remove sits on integer
multiples of the heart rate, so a contaminated peak makes this a whole
number. This is not a published quantity. The concern behind it is
established – Ng and Goldberger set out how a dominant frequency can
report ventricular residual rather than atrial activity – but no paper
defines this ratio, and the name should not be read as naming something
from the literature. It was called harmonic_index in an earlier version,
which implied more than it should have.
on_harmonicTRUE when harmonic_overlap is within tol of an
integer. dominant_rate must not be used without conditioning on
this. A contaminated estimate is precise, wrong, and highly
reproducible, because heart rate is highly reproducible within a patient.
Validating the feature by test-retest reliability will therefore select
the artifact. It is a sensitive screen rather than a specific test: the
overlap of an honest peak is a ratio of two unrelated numbers, so with
integers spaced one apart a tolerance of 0.15 lands on one about three
times in ten by arithmetic alone, and excluding every flagged record
discards roughly a third of the good ones with the bad. Read it beside
cancellation_residual; a flagged record with a small residual is
probably a coincidence.
cancellation_residualResidual energy in a window around each QRS
as a fraction of that window's energy before cancellation. Model-free, so
it is independent of any assumption about the atrial spectrum. It reports
what cancellation failed to remove, never what it removed and should not
have; see rr_regular for that.
rr_regularTRUE when the ventricular response is regular (RR CV
below 0.05). Cancellation subtracts whatever repeats at a fixed phase to
the QRS, so on a regular rhythm it takes the atrial signal with it -
flutter conducting at a fixed ratio loses most of its wave, and its
organization_index collapses into the range fibrillation occupies.
cancellation_residual is small when this happens, because the template
fits better for having absorbed the atrial wave, so this is the only field
that reports it. See the cancellation section of extract_f_waves().
tq_fractionShare of the record that was electrically silent and therefore usable for amplitude measurement.
n_beats_cancelled, n_beats_skipped, n_beats_aberrant
How much of the record was actually processed.
Ng J, Goldberger JJ. Understanding and interpreting dominant frequency analysis of AF electrograms. Journal of Cardiovascular Electrophysiology. 2007;18(6):680-685. doi:10.1111/j.1540-8167.2007.00832.x
Li YH, Hwang JJ, Tseng YZ, Kuan P, Lien WP. Clinical significance of fibrillatory wave amplitude. A clue to left atrial appendage function in nonrheumatic atrial fibrillation. Chest. 1995;108(2):359-363. doi:10.1378/chest.108.2.359
Tateno K, Glass L. Automatic detection of atrial fibrillation using the coefficient of variation and density histograms of RR and deltaRR intervals. Medical & Biological Engineering & Computing. 2001;39(6):664-671. doi:10.1007/BF02345439
Alcaraz R, Rieta JJ. Adaptive singular value cancelation of ventricular activity in single-lead atrial fibrillation electrocardiograms. Physiological Measurement. 2008;29(12):1351-1369. doi:10.1088/0967-3334/29/12/001
Alcaraz R, Sornmo L, Rieta JJ. Reference database and performance evaluation of methods for extraction of atrial fibrillatory waves in the ECG. Physiological Measurement. 2019;40(7):075011. doi:10.1088/1361-6579/ab2b17
Alcaraz R, Rieta JJ. Sample entropy of the main atrial wave predicts spontaneous termination of paroxysmal atrial fibrillation. Medical Engineering & Physics. 2009;31(8):917-922. doi:10.1016/j.medengphy.2009.05.002
Alcaraz R, Abasolo D, Hornero R, Rieta JJ. Optimal parameters study for sample entropy-based atrial fibrillation organization analysis. Computer Methods and Programs in Biomedicine. 2010;99(1):124-132. doi:10.1016/j.cmpb.2010.02.009
Escribano P, Rodenas J, Garcia M, et al. Combination of frequency- and time-domain characteristics of the fibrillatory waves for enhanced prediction of persistent atrial fibrillation recurrence after catheter ablation. Heliyon. 2024;10(3):e25295. doi:10.1016/j.heliyon.2024.e25295
Bollmann A, Husser D, Mainardi L, et al. Analysis of surface electrocardiograms in atrial fibrillation: techniques, research, and clinical applications. Europace. 2006;8(11):911-926. doi:10.1093/europace/eul113
An K, Li H, Yu C, Zheng Z. Surface electrocardiogram f wave analysis in patients with atrial fibrillation undergoing thoracoscopic epicardial ablation. Interdisciplinary CardioVascular and Thoracic Surgery. 2024;38(5):ivae057. doi:10.1093/icvts/ivae057
Creates a criterion for numeric annotation fields that can be placed in a
landmark criteria list, for example
list(voltage = feature_range(-1, 1)).
feature_range(lower, upper, inclusive = TRUE)feature_range(lower, upper, inclusive = TRUE)
lower, upper
|
Finite numeric interval limits. |
inclusive |
Whether both limits are included. |
A feature_range criterion object.
windows object for printingFormat a windows object for printing
## S3 method for class 'windows' format(x, ...)## S3 method for class 'windows' format(x, ...)
x |
A |
... |
Additional arguments passed to methods. |
Invisibly returns x.
Reports the sampling rate, in Hz, that a record was acquired at. This is read
from the header rather than inferred from the signal, and is the value that
converts sample indices into physical time. Use change_frequency() to
change it.
## S3 method for class 'EGM' frequency(x, ...) ## S3 method for class 'header_table' frequency(x, ...) ## S3 method for class 'windows' frequency(x, ...)## S3 method for class 'EGM' frequency(x, ...) ## S3 method for class 'header_table' frequency(x, ...) ## S3 method for class 'windows' frequency(x, ...)
x |
An |
... |
Additional arguments (currently unused). |
These are methods for the stats::frequency() generic, defined for
every object that carries a rate: an EGM (and so an ECG), a bare
header_table, and a windows collection. A header_table needs its own
method because the default one answers 1 for any object without a tsp
attribute, and a 500 Hz record reported as 1 Hz is wrong in a way that
nothing downstream can catch.
For a single record the rate is a scalar. A windows collection may in
principle mix rates - windows from different source records can be
concatenated with c.windows() - so its method returns the distinct rates
present. A result of length one therefore means the collection is already
harmonised, and anything longer means the windows are not directly
comparable.
A record whose header carries no usable rate is an error rather than an
NA. There is no arithmetic that recovers from a missing sampling rate: it
propagates into every interval, every heart rate, and every duration, while
leaving the analyses that do not divide by it looking healthy. Repair the
header, or state the rate with change_frequency().
A numeric sampling rate in Hz; for windows objects a vector of the
distinct rates present (empty for an empty collection).
## Not run: ecg <- read_wfdb("ecg", test_path(), "ecgpuwave") frequency(ecg) # The header answers for itself frequency(ecg$header) # A harmonised collection reports a single rate frequency(get_windows(ecg)) ## End(Not run)## Not run: ecg <- read_wfdb("ecg", test_path(), "ecgpuwave") frequency(ecg) # The header answers for itself frequency(ecg$header) # A harmonised collection reports a single rate frequency(get_windows(ecg)) ## End(Not run)
EGM objectExtract an annotator's annotation table from an EGM object,
which may hold several.
get_annotation(x, annotator = NULL)get_annotation(x, annotator = NULL)
x |
An |
annotator |
Character string specifying which annotator to extract. If NULL (default), returns every annotator's table. |
The shape returned matches read_annotation(): a single
annotation_table when the record carries one annotator, and a named list
of them when it carries several. That symmetry is the point of the function
code can move between a record read from disk and one held in memory
without changing shape, and an empty record answers with an empty
annotation_table rather than an empty list.
It is also the way to reach annotations that only exist in memory. The
sample indices carried by an EGM are rescaled by change_frequency(),
while a copy read separately from disk is not, so after a rate change these
are the annotations that agree with the signal.
An annotation_table for the requested annotator; with annotator = NULL, the single annotation_table present, or a named list of them when
the record carries more than one.
read_annotation(), list_annotators(), change_frequency()
EGM objectRaw signal data may be all that is required, particularly when storing or manipulating data, or for example, feeding it into an analytical pipeline. This means the extraneous elements, such as the meta information, may be unnecessary. This function retrieves just the signal data itself and channel names.
get_signal(object, data_format = c("data.frame", "matrix", "array"), ...)get_signal(object, data_format = c("data.frame", "matrix", "array"), ...)
object |
An |
data_format |
A |
... |
Additional arguments to be passed to the function |
The options to return the data vary based on need. The data can be extracted as follows:
data.frame containing an equal number of rows to the number of samples, with each column named after the recording channel it was derived from. Data frames, as they are columnar by nature, will also include the sample index position.
matrix containing an equal number of rows to the number of samples, with each column named after the recording channel it was derived from
array containing individual vectors of signal, each named after the channel they were derived from
An object as described by the data_format option
Searches a record for every span of signal that satisfies a windowing
strategy, and returns each span as its own EGM. This is how a whole study is
broken into the analysable segments - individual beats, mapping windows,
intervals of interest - that the rest of the window functions operate on.
get_windows(object, by = "rhythm")get_windows(object, by = "rhythm")
object |
An |
by |
A window_strategy object, or the name of one as a string.
Defaults to |
The strategy may be given as a bare name for its defaults, or built
explicitly with a by_*() constructor when its arguments need setting:
get_windows(ecg, by = "rhythm") get_windows(ecg, by = by_rhythm(channel = 2))
Three strategies are built in: by_rhythm() cuts a whole beat between two
fiducials, by_pwave() the atrial portion of one, and by_beat() a fixed
span around a fiducial.
Both forms route through the same constructor, so both are validated the same way. Strategy arguments belong to the constructor rather than to this function, which is what allows a mistyped argument to be reported instead of silently ignored.
Each returned window is a complete EGM: its own header (named
<record>_<strategy><n>, with an info string recording the source sample
range), the signal for every channel, and the annotations falling inside it.
Windows are generally not the same length, since they follow the signal
rather than a fixed duration; see pad_window(), normalize_window(), and
warp_window() for the ways to bring them onto a common basis.
A windows object: a collection of EGM objects, one per window.
A delineating annotator may be run over a record once, or once per lead. Run
per lead it writes twelve independent copies of every fiducial, separated
only by the channel column, and such a table does not describe beats until
it is resolved to a single lead: pooled, it reports twelve times as many
beats as the record contains, and taking the first match at each point
silently follows whichever lead happens to sort first.
So every function that reads fiducials - get_windows(),
learn_template(), median_window() and the other window transforms,
extract_f_waves(), vcg() - takes a channel, and resolves
it the same way:
annotations spanning more than one channel with no channel given are an
error, not a guess;
a channel that the annotations do not carry is an error, which is what
catches a numbering convention that does not match;
the global channel 0 is not a lead, so it never counts toward the span
and is always kept alongside the chosen lead.
The channel may be a number or a channel name ("II"), resolved against the
record's own header. A number is matched literally against the channel
column, and the numbering there belongs to the annotator that wrote the
file. Read it off the record rather than assuming:
table(get_annotation(x)$channel)
Only the boundaries are restricted to one lead. Windows still carry the signal for every channel, which is what makes a windowed beat usable as a vectorcardiogram.
by_rhythm(), by_pwave() and by_beat() for the available
strategies, window_dropped() for the candidates a strategy did not return,
pad_window(), normalize_window() and warp_window() to bring windows
onto a common length, and change_frequency() to harmonise sampling rates
either before windowing or on the returned collection.
## Not run: ecg <- read_wfdb("ecg", test_path(), "ecgpuwave") # Sinus beats, using the strategy defaults beats <- get_windows(ecg, by = "rhythm") # Guided by a single lead, then collapsed to a median beat get_windows(ecg, by = by_rhythm(channel = 2)) |> median_window(align_feature = "N", channel = 2) ## End(Not run)## Not run: ecg <- read_wfdb("ecg", test_path(), "ecgpuwave") # Sinus beats, using the strategy defaults beats <- get_windows(ecg, by = "rhythm") # Guided by a single lead, then collapsed to a median beat get_windows(ecg, by = by_rhythm(channel = 2)) |> median_window(align_feature = "N", channel = 2) ## End(Not run)
header_table() modifies the data.table class to work with
header data. The header data is read in from a similar format as to that of
WFDB files and should be compatible/interchangeable when writing out to disk.
The details extensively cover the type of data that is input. Generally, this
function is called by read_*_header() functions and will generally not be
called by the end-user.
header_table( record_name = character(), number_of_channels = integer(), frequency = integer(), samples = integer(), start_time = as.POSIXct(NA), ADC_saturation = integer(), file_name = character(), storage_format = integer(), ADC_gain = 200L, ADC_baseline = ADC_zero, ADC_units = "mV", ADC_resolution = 12L, ADC_zero = 0L, initial_value = ADC_zero, checksum = 0L, blocksize = 0L, label = character(), info_strings = list(), additional_gain = 1, low_pass = integer(), high_pass = integer(), color = "#000000", scale = integer() ) is_header_table(x)header_table( record_name = character(), number_of_channels = integer(), frequency = integer(), samples = integer(), start_time = as.POSIXct(NA), ADC_saturation = integer(), file_name = character(), storage_format = integer(), ADC_gain = 200L, ADC_baseline = ADC_zero, ADC_units = "mV", ADC_resolution = 12L, ADC_zero = 0L, initial_value = ADC_zero, checksum = 0L, blocksize = 0L, label = character(), info_strings = list(), additional_gain = 1, low_pass = integer(), high_pass = integer(), color = "#000000", scale = integer() ) is_header_table(x)
record_name |
A |
number_of_channels |
An |
frequency |
A |
samples |
An |
start_time |
The |
ADC_saturation |
An |
file_name |
A |
storage_format |
An |
ADC_gain |
An |
ADC_baseline |
An |
ADC_units |
A |
ADC_resolution |
An |
ADC_zero |
An |
initial_value |
An |
checksum |
An |
blocksize |
An |
label |
A |
info_strings |
A |
additional_gain |
A |
low_pass |
An |
high_pass |
An |
color |
A |
scale |
An |
x |
A |
The header_table object is relatively complex in that it directly
deals with properties of the signal, and allows compatibility with WFDB
files and other raw header files for other signal objects. It can be written
out using write_wfdb().
A header_table object that is an extension of the data.table
class. This contains an adaptation of the function arguments, allowing for
compatibility with the WFDB class.
There are three components to the header file:
Record line that contains the following information, in the order documented, however pieces may be missing based on different parameters. From left to right...
Record name
Number of signals: represents number of segments/channels
Sampling frequency (optional)
Number of samples (optional)
Time: in HH:MM:SS format (optional)
Date: in DD/MM/YYYY (optional)
Signal specification lines contains specifications for individual signals, and there must be as many signal lines as there are reported by the above record line. From left to right....
File name: usually *.dat
Format integer: represents storage type, e.g. 8-bit or 16-bit
ADC gain: ADC units per physical unit (optional)
Baseline: corresponds to 0 physical units, sep = '*(0)" (optional)
Units: with '/' as a field separator e.g '*/mV' (optional)
ADC resolution integer: bits, usually 8 or 16 (optional)
ADC zero: represents middle of ADC input range (optional)
Initial value (optional)
Checksum (optional)
Block size (optional)
Description: text or label information (optional)
Info strings are unstructured lines that contains information about the record. Usually are descriptive. Starts with initial '#' without preceding white space at beginning of line.
Test whether an object is a template
is_template(x)is_template(x)
x |
An object to test. |
A single logical value.
windows collectionNamed is_window_set() rather than the shorter is_windows()
so it is not mistaken for the operating-system predicate of that name that
many packages define (a test of .Platform$OS.type). This tests the EGM
windows class, not the platform.
is_window_set(x)is_window_set(x)
x |
An object to test. |
TRUE if x is a windows object, FALSE otherwise.
Test whether an object is a window strategy
is_window_strategy(x)is_window_strategy(x)
x |
An object to test. |
A single logical value.
The linear map from the 12-lead ECG to the orthogonal X, Y, Z
leads of the Frank vectorcardiographic system, used by
vcg().
korskors
A 3 by 8 numeric matrix. Rows are the orthogonal axes X (positive
to the left), Y (positive inferiorly) and Z (positive posteriorly);
columns are the source leads I, II and V1 through V6.
Only eight leads enter the transformation. III, aVR, aVL and aVF are exact linear combinations of I and II, so a matrix over all twelve would be rank deficient and the extra columns would carry nothing.
Kors' matrix is preferred to the inverse Dower matrix: it reproduces the recorded Frank leads more closely, and measures derived from it carry more prognostic information (Man et al. 2011; Kück et al. 2018).
Kors JA, van Herpen G, Sittig AC, van Bemmel JH. Reconstruction of the Frank vectorcardiogram from standard electrocardiographic leads: diagnostic comparison of different methods. European Heart Journal. 1990;11(12):1083-1092. doi:10.1093/oxfordjournals.eurheartj.a059647
Man S, Algra AM, Schreurs CA, et al. Influence of the vectorcardiogram synthesis matrix on the power of the electrocardiogram-derived spatial QRS-T angle to predict arrhythmias in patients with ischemic heart disease and systolic left ventricular dysfunction. Journal of Electrocardiology. 2011;44(4):410-415. doi:10.1016/j.jelectrocard.2011.04.007
Kück K, Isaksen JL, Graff C, et al. Spatial QRS-T angle variants for prediction of all-cause mortality. Journal of Electrocardiology. 2018;51(5):768-775. doi:10.1016/j.jelectrocard.2018.05.011
# The orthogonal leads are row combinations of the eight source leads kors["Z", ]# The orthogonal leads are row combinations of the eight source leads kors["Z", ]
Adds a wave column (one of "P", "QRS", "T", or NA) to an annotation
table by recovering wave identity from the peak symbol enclosed within each
(/) waveform bracket.
label_waves(ann)label_waves(ann)
ann |
An |
Peaks are mapped directly by their type symbol (p -> "P", N
-> "QRS", t -> "T"). Brackets are labelled per channel, in sample
order: each onset ( and its matching offset ) inherit the wave of the
single peak that falls between them (the first peak if several are present,
NA if none). The returned table is a copy; the input is not modified.
A copy of ann with an additional wave column.
This positional inference is what decides whether a delineating annotator
can be used with this package, so it is worth stating plainly. The WFDB
convention is that a waveform onset or offset carries its wave identity in
the number column - 0 for P, 1 for QRS, 2 for T - but plenty of
annotators leave number at zero throughout, which looks disqualifying and
is not. As long as the file brackets each wave with ( and ) around a
typed peak, wave identity is recovered here from the position of that peak,
and the number column is never consulted.
So the requirement on an annotator is only this: (, ), and at least one
of p, N, t between each pair. Anything meeting it can drive
by_rhythm(), by_pwave(), and the wave criteria used throughout, whatever
it writes in number.
Wave identity is available anywhere a criteria list is accepted, as a
virtual wave field alongside the real columns:
by_rhythm(onset = list(type = "(", wave = "P"))
by_rhythm(), get_windows(), annotation_table()
## Not run: ecg <- read_wfdb("ecg", test_path(), "ecgpuwave") # `number` is uninformative here, `wave` is not labelled <- label_waves(get_annotation(ecg)) table(labelled$type, labelled$wave, useNA = "ifany") ## End(Not run)## Not run: ecg <- read_wfdb("ecg", test_path(), "ecgpuwave") # `number` is uninformative here, `wave` is not labelled <- label_waves(get_annotation(ecg)) table(labelled$type, labelled$wave, useNA = "ifany") ## End(Not run)
landmark( name, criteria, channel = NA_integer_, position = NA_real_, required = TRUE )landmark( name, criteria, channel = NA_integer_, position = NA_real_, required = TRUE )
name |
A single non-empty landmark name. |
criteria |
A non-empty named list of annotation matching criteria, such
as |
channel |
One non-negative channel number, a stable channel name, or
|
position |
Target sample position, or |
required |
Whether this landmark must be found for a complete match. |
A landmark identifies one annotation-derived fiducial point on one channel.
Its position is NA until learned, or a sample coordinate on a template's
target grid when supplied manually or returned by learn_template().
A landmark S7 object.
Locates each requested landmark in multiple annotated EGM examples, estimates
its median or mean fractional position, and returns a fitted template. For
manual templates, construct positioned landmark objects and pass them to
template() directly.
learn_template( x, landmarks = list(P_onset = list(type = "(", wave = "P"), QRS = list(type = "N"), T_offset = list(type = ")", wave = "T")), target_samples = 500L, channel = NULL, frequency = NULL, position_estimator = c("median", "mean"), missing = c("complete", "available", "error"), ambiguous = c("error", "first"), order_policy = c("drop", "error"), ... )learn_template( x, landmarks = list(P_onset = list(type = "(", wave = "P"), QRS = list(type = "N"), T_offset = list(type = ")", wave = "T")), target_samples = 500L, channel = NULL, frequency = NULL, position_estimator = c("median", "mean"), missing = c("complete", "available", "error"), ambiguous = c("error", "first"), order_policy = c("drop", "error"), ... )
x |
An annotated |
landmarks |
A named list of concise landmark specifications or a list of
unfitted landmark objects. A concise specification may be a type string or
a named criteria list with optional |
target_samples |
Number of samples in the template target grid. |
channel |
Default channel for landmarks without their own one, given as a channel number or name. Required when the examples' annotations span more than one channel; see the channels section. |
frequency |
Optional source frequency stored as provenance. By default it is read from the first example. |
position_estimator |
Use the median or mean landmark phase. |
missing |
Retain only complete examples, use all available observations, or error on incomplete examples. |
ambiguous |
Error when a landmark has multiple matches, or use the first. |
order_policy |
Drop examples with crossed landmarks, or error. |
... |
Additional arguments, currently unused. |
A learned template S7 object.
A delineating annotator may be run over a record once, or once per lead. Run
per lead it writes twelve independent copies of every fiducial, separated
only by the channel column, and such a table does not describe beats until
it is resolved to a single lead: pooled, it reports twelve times as many
beats as the record contains, and taking the first match at each point
silently follows whichever lead happens to sort first.
So every function that reads fiducials - get_windows(),
learn_template(), median_window() and the other window transforms,
extract_f_waves(), vcg() - takes a channel, and resolves
it the same way:
annotations spanning more than one channel with no channel given are an
error, not a guess;
a channel that the annotations do not carry is an error, which is what
catches a numbering convention that does not match;
the global channel 0 is not a lead, so it never counts toward the span
and is always kept alongside the chosen lead.
The channel may be a number or a channel name ("II"), resolved against the
record's own header. A number is matched literally against the channel
column, and the numbering there belongs to the annotator that wrote the
file. Read it off the record rather than assuming:
table(get_annotation(x)$channel)
Only the boundaries are restricted to one lead. Windows still carry the signal for every channel, which is what makes a windowed beat usable as a vectorcardiogram.
EGM objectGet the names of all annotators present in an EGM object.
list_annotators(x)list_annotators(x)
x |
An |
A character vector of annotator names
Applies f to each EGM in a collection. When every result is itself an
EGM the collection is rebuilt as a windows object with its provenance
carried forward; otherwise a plain list of results is returned, which is the
natural shape for extracting per-window measurements.
map_windows(x, f, ...)map_windows(x, f, ...)
x |
A |
f |
A function applied to each window. |
... |
Additional arguments passed to |
A windows object when all results are EGM objects, otherwise a
list.
## Not run: beats <- get_windows(read_wfdb("ecg", test_path(), "ecgpuwave")) # Per-window measurement -> plain list map_windows(beats, function(w) nrow(w$signal)) # EGM-valued transform -> windows object, provenance preserved map_windows(beats, function(w) w) ## End(Not run)## Not run: beats <- get_windows(read_wfdb("ecg", test_path(), "ecgpuwave")) # Per-window measurement -> plain list map_windows(beats, function(w) nrow(w$signal)) # EGM-valued transform -> windows object, provenance preserved map_windows(beats, function(w) w) ## End(Not run)
Reduces a windows collection to a single representative EGM by taking the
element-wise median across all windows, lead by lead. Given an ECG windowed
into its individual beats, this returns the "median beat" - a robust template
that suppresses beat-to-beat noise and ectopy while preserving the dominant
morphology.
median_window(x, align_feature = NULL, channel = NULL, na.rm = TRUE, ...)median_window(x, align_feature = NULL, channel = NULL, na.rm = TRUE, ...)
x |
A |
align_feature |
Optional feature used to pad-align windows of unequal
length before averaging, given as a |
channel |
The lead whose |
na.rm |
Logical passed to |
... |
Additional arguments (currently unused). |
The median is computed per sample and per lead, so every window must
share the same length and the same set of leads. Beats windowed from raw
rhythm are generally not the same length (see get_windows()), so this
function offers to align them first: when align_feature is supplied and
the windows differ in length, they are padded and anchored on that feature
via pad_window() before the median is taken. If the windows differ in
length and no align_feature is given, an error is raised pointing to
pad_window() or normalize_window() so the caller chooses the alignment
explicitly.
The returned beat carries the fiducials that produced it, each placed at the
median of its own positions across the aligned windows. Annotations are
matched between windows by channel, type, and how many of that pair came
before them in their own window, so the first QRS onset of one beat lines up
with the first of every other. A fiducial that most windows do not carry is
dropped, since it is not part of what the median describes. The header is
named <source>_median and carries an info string recording how many
windows it came from.
A single EGM object representing the median beat.
Its fiducials live where every EGM keeps them, under $annotation, which
is a named list of annotation_tables - one per annotator - and not a
table. Reach them with get_annotation(x), which unwraps the single
annotator case; nrow(x$annotation) is NULL here for the same reason it
is NULL on a record straight from read_wfdb().
Amplitudes are not baseline-corrected. The median of a set of windows that
all share a DC offset still carries that offset, and it will be the first
thing any variance-based method finds; baseline_window() removes it, best
applied to the windows before they are reduced.
A delineating annotator may be run over a record once, or once per lead. Run
per lead it writes twelve independent copies of every fiducial, separated
only by the channel column, and such a table does not describe beats until
it is resolved to a single lead: pooled, it reports twelve times as many
beats as the record contains, and taking the first match at each point
silently follows whichever lead happens to sort first.
So every function that reads fiducials - get_windows(),
learn_template(), median_window() and the other window transforms,
extract_f_waves(), vcg() - takes a channel, and resolves
it the same way:
annotations spanning more than one channel with no channel given are an
error, not a guess;
a channel that the annotations do not carry is an error, which is what
catches a numbering convention that does not match;
the global channel 0 is not a lead, so it never counts toward the span
and is always kept alongside the chosen lead.
The channel may be a number or a channel name ("II"), resolved against the
record's own header. A number is matched literally against the channel
column, and the numbering there belongs to the annotator that wrote the
file. Read it off the record rather than assuming:
table(get_annotation(x)$channel)
Only the boundaries are restricted to one lead. Windows still carry the signal for every channel, which is what makes a windowed beat usable as a vectorcardiogram.
get_annotation() to read the fiducials, baseline_window(),
pad_window(), by_beat()
Combine multiple annotation tables from an EGM object into
a single annotation table with an additional annotator column to
identify the source of each annotation.
merge_annotations(x, annotators = NULL)merge_annotations(x, annotators = NULL)
x |
An |
annotators |
Optional character vector specifying which annotators to merge. If NULL (default), merges all available annotators. |
A single annotation_table object with all annotations combined
and an additional annotator column
This function serves to read/convert XML based files from the MUSE system to digital signal. This can subsequently be written into other formats. The MUSE system is somewhat proprietary, and each version may or may not allow export options into XML.
read_muse(file)read_muse(file)
file |
An ECG file from MUSE in XML format |
GE Healthcare MUSE v9 is currently the model that is being used. These functions have not been tested in older versions.
An EGM class object that is a list of surface ECG signals the
format of a data.table, with an attached header attribute that
contains additional recording data.
MUSE stores each sample as an ADC count together with the scale that converts
it, LeadAmplitudeUnitsPerBit, and the units that scale is in,
LeadAmplitudeUnits. The scale is applied while reading, so the returned
signal holds those units — microvolts on every export seen — and the header
gets the ADC_gain that carries them back to millivolts, 1000 for a
microvolt payload. A record whose units are unrecognised, or that names
different units on different leads, is refused rather than given a gain that
would put its amplitudes on an unknown scale.
The samples are deliberately left on the microvolt scale rather than returned to raw counts. The augmented limb leads are derived here as halves of the recorded ones, so they fall between counts, and rounding them back onto the count grid would throw that precision away.
windows object containing a collection of EGM windowswindows objects are lists of EGM objects that represent segments of one
or more source signals. This allows specialized methods to be applied to a
whole collection of segments at once. This function is the class constructor
and only applies class attributes; collections are normally produced by
get_windows() rather than built by hand.
new_windows( x = list(), method = character(), source_record = character(), history = method, dropped = integer(), ... )new_windows( x = list(), method = character(), source_record = character(), history = method, dropped = integer(), ... )
x |
A list of |
method |
The extraction strategy that produced the collection, e.g.
|
source_record |
The name of the original record. |
history |
A |
dropped |
A named |
... |
Additional arguments passed to methods. |
An object of class windows which inherits from list.
get_windows() to create one from an EGM, window_dropped() for
the candidates it did not return.
Resamples each window so that every window spans the same number of samples.
Without align_feature this stretches or compresses each window as a whole,
mapping its left and right borders (e.g. P-onset and T-offset for a sinus
beat) onto a common [0, target_samples) grid. With align_feature the
windows are instead resampled at native resolution around that fiducial, so
the feature lands at the centre of every output window and the true morphology
timing is preserved.
This is a destructive standardization - in the border-to-border mode the time axis is warped, so absolute intervals are no longer comparable - but it is a fast, simple way to bring ragged beats onto a shared basis for averaging or matrix operations.
normalize_window( x, target_samples = 500L, target_ms = NULL, interpolation_method = c("linear", "spline", "step"), align_feature = NULL, channel = NULL, preserve_amplitude = FALSE, preserve_class = TRUE, ... )normalize_window( x, target_samples = 500L, target_ms = NULL, interpolation_method = c("linear", "spline", "step"), align_feature = NULL, channel = NULL, preserve_amplitude = FALSE, preserve_class = TRUE, ... )
x |
A |
target_samples |
Desired output length in samples. Default 500. |
target_ms |
Alternative target length in milliseconds, converted using
the first window's sampling frequency. Used only when |
interpolation_method |
Interpolation method for the stretch. One of "linear" (default), "spline", or "step". |
align_feature |
Optional feature to centre each window on, given as a
|
channel |
The lead whose |
preserve_amplitude |
Logical. If TRUE, rescales each lead back to its original amplitude range after interpolation. Defaults to FALSE, since a pure time stretch should leave amplitudes to the interpolation. |
preserve_class |
Logical. If TRUE (default), returns a |
... |
Additional arguments (currently unused). |
For alignment on a fiducial without warping the time axis at all, use
pad_window() instead; to align several landmarks at once, use
warp_window(); to change the physical sampling rate while preserving
duration, use change_frequency().
When align_feature is given but not found in a window, that window falls
back to border-to-border resampling with a warning.
A windows object (or list) of time-normalized EGM objects, each
exactly target_samples long. Each carries a resampled signal_table, the
window's own header_table (with samples updated and the per-beat
record/file name preserved), and its annotations remapped onto the resampled
time base.
A delineating annotator may be run over a record once, or once per lead. Run
per lead it writes twelve independent copies of every fiducial, separated
only by the channel column, and such a table does not describe beats until
it is resolved to a single lead: pooled, it reports twelve times as many
beats as the record contains, and taking the first match at each point
silently follows whichever lead happens to sort first.
So every function that reads fiducials - get_windows(),
learn_template(), median_window() and the other window transforms,
extract_f_waves(), vcg() - takes a channel, and resolves
it the same way:
annotations spanning more than one channel with no channel given are an
error, not a guess;
a channel that the annotations do not carry is an error, which is what
catches a numbering convention that does not match;
the global channel 0 is not a lead, so it never counts toward the span
and is always kept alongside the chosen lead.
The channel may be a number or a channel name ("II"), resolved against the
record's own header. A number is matched literally against the channel
column, and the numbering there belongs to the annotator that wrote the
file. Read it off the record rather than assuming:
table(get_annotation(x)$channel)
Only the boundaries are restricted to one lead. Windows still carry the signal for every channel, which is what makes a windowed beat usable as a vectorcardiogram.
## Not run: ecg <- read_wfdb("ecg", test_path(), "ecgpuwave") beats <- get_windows(ecg, by = by_rhythm(channel = 2)) # Stretch every beat border-to-border onto 500 samples normalize_window(beats, target_samples = 500) # Or 500 milliseconds, using the record's sampling frequency normalize_window(beats, target_samples = NULL, target_ms = 500) # Centre each beat on its QRS instead of stretching the borders normalize_window(beats, align_feature = "N", channel = 2) ## End(Not run)## Not run: ecg <- read_wfdb("ecg", test_path(), "ecgpuwave") beats <- get_windows(ecg, by = by_rhythm(channel = 2)) # Stretch every beat border-to-border onto 500 samples normalize_window(beats, target_samples = 500) # Or 500 milliseconds, using the record's sampling frequency normalize_window(beats, target_samples = NULL, target_ms = 500) # Centre each beat on its QRS instead of stretching the borders normalize_window(beats, align_feature = "N", channel = 2) ## End(Not run)
Extends each window with pad_value so that every window shares a common
length. Unlike time normalization, padding is non-destructive: the original
samples are preserved and only the edges are added to. Use it to anchor a
fiducial - typically the QRS peak - at a fixed index across windows, or to
make ragged windows rectangular for stacking.
Where the windows can be cut equal in the first place, by_beat() is the
better route: padding marks the added samples absent, but absent samples
still thin out the reduction that follows.
pad_window( x, target_samples = NULL, target_ms = NULL, align = c("center", "left", "right", "feature"), align_feature = "N", feature_position = NULL, channel = NULL, pad_value = NA, preserve_class = TRUE, ... )pad_window( x, target_samples = NULL, target_ms = NULL, align = c("center", "left", "right", "feature"), align_feature = "N", feature_position = NULL, channel = NULL, pad_value = NA, preserve_class = TRUE, ... )
x |
A |
target_samples |
Desired output length in samples. If |
target_ms |
Alternative target length in milliseconds, converted to
samples using the first window's sampling frequency. Ignored when
|
align |
One of "center" (default), "left", "right", or "feature". See details. |
align_feature |
Feature used for |
feature_position |
Target index (0-based) for the aligned feature when
|
channel |
The lead whose |
pad_value |
The value used for padding: |
preserve_class |
Logical. If TRUE (default), returns a |
... |
Additional arguments (currently unused). |
The align argument controls where the real signal sits within the
padded window:
center - equal padding on both edges.
left - signal at the start, padding appended after ("pad after").
right - padding prepended before, signal at the end ("pad before").
feature - each window is shifted so that align_feature lands on a
shared anchor index, padding the remainder. This is the alignment used to
stack beats on the R wave.
For feature alignment with no explicit target_samples, the output length
is derived so that no window is truncated: it spans the largest pre-feature
extent plus the largest post-feature extent seen across the collection, and
the feature is anchored at that common pre-feature offset. When
target_samples is supplied, the feature is anchored at feature_position
(default: the window centre) and any samples that fall outside the target
are truncated.
A windows object (or list) of padded EGM objects, all sharing the
same sample length. Amplitudes are untouched, so whatever DC offset the
recording carried is still there; baseline_window() removes it, and
variance-based methods find it first if it is left in.
Every padded sample is a claim about signal that was never recorded, and the three fills make different ones.
NA claims nothing, which is why it is the default. The cost is that a
padded collection cannot go straight into a matrix method, and the obvious
handling is the wrong one: where the padded length exceeds the median wave
span - as it must, or nothing would be padded - every window carries some
NA, so dropping incomplete cases empties the matrix rather than trimming
it.
"edge" claims the signal held its last observed value. For a window whose
pad region is isoelectric - the TP and PR segments either side of a P wave,
say - that is close to true and is the fill to reach for. Across a wave it
is not, and it flattens the tails it invents.
0 claims the potential was zero there. Because the windows are not
baseline-corrected (see the return value), zero is not even the isoelectric
line; it is wherever the recording's DC offset happens to put it.
A delineating annotator may be run over a record once, or once per lead. Run
per lead it writes twelve independent copies of every fiducial, separated
only by the channel column, and such a table does not describe beats until
it is resolved to a single lead: pooled, it reports twelve times as many
beats as the record contains, and taking the first match at each point
silently follows whichever lead happens to sort first.
So every function that reads fiducials - get_windows(),
learn_template(), median_window() and the other window transforms,
extract_f_waves(), vcg() - takes a channel, and resolves
it the same way:
annotations spanning more than one channel with no channel given are an
error, not a guess;
a channel that the annotations do not carry is an error, which is what
catches a numbering convention that does not match;
the global channel 0 is not a lead, so it never counts toward the span
and is always kept alongside the chosen lead.
The channel may be a number or a channel name ("II"), resolved against the
record's own header. A number is matched literally against the channel
column, and the numbering there belongs to the annotator that wrote the
file. Read it off the record rather than assuming:
table(get_annotation(x)$channel)
Only the boundaries are restricted to one lead. Windows still carry the signal for every channel, which is what makes a windowed beat usable as a vectorcardiogram.
baseline_window(), median_window(), normalize_window(),
by_beat() for cutting windows equal in the first place
windows objectPrint a windows object
## S3 method for class 'windows' print(x, ...)## S3 method for class 'windows' print(x, ...)
x |
A |
... |
Additional arguments passed to methods. |
Invisibly returns x.
read_prucka() reads both the signal data (.txt) and header information
(.inf) exported from the Prucka cardiac electrophysiology system, which is
the underlying recording software used in GE Healthcare's CardioLab EP
system.
read_prucka(signal_file, header_file = NULL, n = Inf) read_prucka_header(header_file) read_prucka_signal(signal_file, n = Inf)read_prucka(signal_file, header_file = NULL, n = Inf) read_prucka_header(header_file) read_prucka_signal(signal_file, n = Inf)
signal_file |
Path to the *.txt signal data file |
header_file |
Path to the *.inf header file. If NULL, will look for a file with the same base name as signal_file but with .inf extension. |
n |
Number of signal values to return (this will be the same for each channel of data). Defaults to all values. |
To export data from the GE CardioLab system:
Open the study/recording in CardioLab
Select the time sEGMent you want to export
Navigate to File > Export or Tools > Export
Choose ASCII Export or Text Export format
Select the channels to export
Choose export location and filename
The system will create two files:
X####.txt: Space-delimited signal data
X####.inf: Header file with metadata
Signal file (*.txt):
Space-delimited numeric data
Each row represents one time point
First column: sample index/time marker
Subsequent columns: channel data in mV
All channels sampled at the same rate
Header file (*.inf):
Key-value pairs with "=" delimiter
Patient information (name, date, description)
Recording parameters (sampling rate, duration, channel count)
Channel mapping section listing channel numbers and labels
Channel numbers may be non-sequential (e.g., 1-12, 49-50, 75-76)
Both files must have the same base name (e.g., X001.txt and X001.inf).
Default units are mV for electrical signals and mmHg for pressure
The system typically uses 16-bit ADC resolution
Channel labels may include surface ECG leads (I, II, III, aVR, aVL, aVF, V1-V6) and intracardiac catheters (ABL, His, CS, RV, etc.)
Export may be limited by system memory for very long recordings
An EGM class object that is a list of EP signals the format of a
data.table, with an attached header attribute that contains
additional recording data.
The signal_table() function modifies the data.table class to
work with electrical signal data. The input should be a data set of equal
number of rows. It will add a column of index positions called sample if
it does not already exist.
signal_table(..., units = c("digital", "physical")) is_signal_table(x)signal_table(..., units = c("digital", "physical")) is_signal_table(x)
... |
A |
units |
A |
x |
|
Every signal table is labelled with the units its values are in,
"digital" or "physical", readable with signal_units(). The label is
carried through windowing, padding, medians and resampling, so a table many
steps from the file it was read out of still says what its numbers mean.
The distinction is a factor of the ADC gain - commonly 200 - and nothing in
the values themselves reveals which one is in hand. Since the same amplitude
is a plausible number in either, a mislabelled signal is not detectable
downstream; only the label makes it checkable. write_wfdb() refuses to
write a table whose label contradicts the units it was told to expect.
An object of class signal_table, which is an extension of the
data.table class. The sample column is invariant and will always be
present. The other columns represent additional channels.
Reports whether a signal holds raw ADC counts ("digital") or the physical
units its header names ("physical", usually mV). The two differ by the ADC
gain - 200 in a great many records - and nothing in the values themselves
says which is which, so the answer is carried as a label rather than inferred.
signal_units(x)signal_units(x)
x |
An |
A single character, "digital" or "physical".
## Not run: signal_units(read_wfdb("ecg", test_path())) #> [1] "digital" signal_units(read_wfdb("ecg", test_path(), units = "physical")) #> [1] "physical" ## End(Not run)## Not run: signal_units(read_wfdb("ecg", test_path())) #> [1] "digital" signal_units(read_wfdb("ecg", test_path(), units = "physical")) #> [1] "physical" ## End(Not run)
template( landmarks, target_samples = 500L, method = "manual", frequency = NA_real_, statistics = NULL )template( landmarks, target_samples = 500L, method = "manual", frequency = NA_real_, statistics = NULL )
landmarks |
A position-ordered list of fitted landmark objects. |
target_samples |
Length of the target sample grid. |
method |
Either |
frequency |
Source sampling frequency retained as provenance, or |
statistics |
Optional landmark learning statistics. |
A template is an ordered group of positioned landmark objects on a common
target sample grid. Create one manually with template() or learn landmark
positions from multiple EGM examples with learn_template(). It is passed
directly to warp_window().
A template S7 object.
manual_template <- template( landmarks = list( landmark("onset", list(type = "("), channel = 2, position = 0), landmark("QRS", list(type = "N"), channel = 2, position = 250), landmark("offset", list(type = ")"), channel = 2, position = 499) ), target_samples = 500 ) is_template(manual_template)manual_template <- template( landmarks = list( landmark("onset", list(type = "("), channel = 2, position = 0), landmark("QRS", list(type = "N"), channel = 2, position = 250), landmark("offset", list(type = ")"), channel = 2, position = 499) ), target_samples = 500 ) is_template(manual_template)
validate_time_parameters() validates and normalizes the time window used
by the WFDB readers. Time-only begin and end values are elapsed from the
record start, consistent with WFDB standard time. Dated values are absolute.
interval is a duration measured from begin.
validate_time_parameters( begin = NULL, end = NULL, interval = NULL, start_time = as.POSIXct(NA), study_duration = NA_real_ )validate_time_parameters( begin = NULL, end = NULL, interval = NULL, start_time = as.POSIXct(NA), study_duration = NA_real_ )
begin, end
|
A single time value. Character values may use
|
interval |
A single duration. Numeric values are seconds. Character
values combine a number and unit, such as |
start_time |
The record start as a |
study_duration |
The available record duration in seconds. When known,
requested endpoints are clamped to this duration. Records longer than one
day require dated |
A list containing normalized begin, end, and interval values
in elapsed seconds from the beginning of the record.
vcg() applies the kors transformation to the surface ECG and
cuts the result at the annotated wave boundaries, tracing the ventricular
loop by default and the atrial loop where method = "atrial". It returns
the orthogonal X, Y, Z signal together with the components extracted from
it.
vcg( object, method = c("ventricular", "atrial"), beats = c("median", "all"), channel = NULL, baseline = TRUE )vcg( object, method = c("ventricular", "atrial"), beats = c("median", "all"), channel = NULL, baseline = TRUE )
object |
An ECG: a whole record, a single windowed beat, or a median
beat. An |
method |
Which loop to trace. |
beats |
Which beats to trace when the object holds more than one.
|
channel |
The lead whose annotations delineate the waves, given as a channel number or name. Required when the annotations span more than one channel, as they do when an annotator has been run per lead; see the channels section. |
baseline |
Logical. If |
A list of two data.tables:
loopThe orthogonal signal, with columns beat, sample, X,
Y and Z, in the units of the source signal.
componentsOne row per loop, as tabulated below.
Azimuth is measured in the transverse plane from +X toward +Z, and
elevation out of that plane toward +Y, both in degrees.
Not every beat reaches the result, and window_dropped() reads back how
many did not and why: incomplete_span, beats too near an end of the record
for the fixed window to be cut, and no_delineation, beats the annotator
did not mark the wave in. Both are counted rather than announced, since the
first is unavoidable on a short strip and neither is visible from a
background worker.
The transformation is linear, so it commutes with the segmentation: a beat cut from the transformed record and a beat transformed after cutting are the same signal, and it makes no difference that each beat is transformed separately here.
It does not commute with the median, which is not a linear operator. The
median is taken lead by lead, by median_window(), so beats = "median"
returns the loop of the median beat rather than the median of the beats'
loops.
The unit of analysis is one beat. Wave boundaries come from the record's own delineation annotations, since neither loop can be delimited without them; a record without them is an error rather than a guess.
A record is cut into beats by by_beat(), a fixed span around each wave peak.
Equal-length windows are what let median_window() reduce them without
padding, and a padded sample is a fabricated one - it would drag the median
toward whatever the padding says at exactly the loop tails the components are
read from.
An object that already holds one beat - a window, or a median of them - is taken as it stands, since there is no room in it for a span and nothing left to reduce. So
vcg(ecg)
and
ecg |> get_windows() |> median_window(align_feature = "N") |> vcg()
both describe one beat, the first taking beats = "median" from its default
and the second building the beat itself. Reach for the second when the
windowing or the alignment needs to be something other than the default;
map_windows() will run vcg() over every window of a collection.
Both loops yield the same geometric components: the peak and mean spatial vectors with their orientation, the area the loop encloses, and how far it departs from a plane.
The ventricular loop additionally yields the global electric heterogeneity
(GEH) components, which describe the discordance between depolarization and
repolarization and so need the T wave to also be delineated: the spatial
QRS-T angle in its peak and mean forms, the spatial ventricular gradient
(magnitude, azimuth and elevation), and the sum absolute QRST integral (Waks
et al. 2016). Where the T wave is not delineated they are returned as NA
rather than dropped.
magnitude_peak on the atrial loop is the spatial P-wave vector magnitude,
which falls as the left atrium remodels and tracks low-voltage area at
electroanatomic mapping (Yano et al. 2023). A P loop only exists in an
organised atrial rhythm; in atrial fibrillation the atrial signal is
characterised by extract_f_waves() instead.
Half of these components are scale-free and half are not, which matters
because the source signal may be in millivolts or in raw ADC counts
depending on how the record was read (see the units argument of
read_wfdb(), and signal_units() to ask an object which it holds). A gain
of 200 changes every value in the "signal units" rows by 200, and none of
the others. Between-record comparisons of those rows are only meaningful on
signals read the same way.
| Component | Units | Scale-free |
duration |
seconds | yes |
magnitude_peak, magnitude_mean |
signal units | no |
azimuth_peak, elevation_peak |
degrees | yes |
azimuth_mean, elevation_mean |
degrees | yes |
area |
signal units squared | no |
planarity |
proportion (0-1) | yes |
qrst_angle_peak, qrst_angle_mean |
degrees | yes |
svg_magnitude |
signal units x seconds | no |
svg_azimuth, svg_elevation |
degrees | yes |
sai_qrst |
signal units x seconds | no |
The last five are returned under method = "ventricular" only; they are the
global electric heterogeneity components, and the atrial loop declines to
report a QRS-T relationship for a P loop.
A delineating annotator may be run over a record once, or once per lead. Run
per lead it writes twelve independent copies of every fiducial, separated
only by the channel column, and such a table does not describe beats until
it is resolved to a single lead: pooled, it reports twelve times as many
beats as the record contains, and taking the first match at each point
silently follows whichever lead happens to sort first.
So every function that reads fiducials - get_windows(),
learn_template(), median_window() and the other window transforms,
extract_f_waves(), vcg() - takes a channel, and resolves
it the same way:
annotations spanning more than one channel with no channel given are an
error, not a guess;
a channel that the annotations do not carry is an error, which is what
catches a numbering convention that does not match;
the global channel 0 is not a lead, so it never counts toward the span
and is always kept alongside the chosen lead.
The channel may be a number or a channel name ("II"), resolved against the
record's own header. A number is matched literally against the channel
column, and the numbering there belongs to the annotator that wrote the
file. Read it off the record rather than assuming:
table(get_annotation(x)$channel)
Only the boundaries are restricted to one lead. Windows still carry the signal for every channel, which is what makes a windowed beat usable as a vectorcardiogram.
Kors JA, van Herpen G, Sittig AC, van Bemmel JH. Reconstruction of the Frank vectorcardiogram from standard electrocardiographic leads: diagnostic comparison of different methods. European Heart Journal. 1990;11(12):1083-1092. doi:10.1093/oxfordjournals.eurheartj.a059647
Waks JW, Sitlani CM, Soliman EZ, et al. Global electric heterogeneity risk score for prediction of sudden cardiac death in the general population: the Atherosclerosis Risk in Communities (ARIC) and Cardiovascular Health (CHS) studies. Circulation. 2016;133(23):2222-2234. doi:10.1161/CIRCULATIONAHA.116.021306
Havmöller R, Carlson J, Holmqvist F, et al. Age-related changes in P wave morphology in healthy subjects. BMC Cardiovascular Disorders. 2007;7:22. doi:10.1186/1471-2261-7-22
Holmqvist F, Platonov PG, Carlson J, et al. Variable interatrial conduction illustrated in a hypertrophic cardiomyopathy population. Annals of Noninvasive Electrocardiology. 2007;12(3):227-236. doi:10.1111/j.1542-474X.2007.00166.x
Yano M, Egami Y, Kawanami S, et al. Ratio of P-wave duration to P-wave amplitude and left atrial remodeling. The American Journal of Cardiology. 2023;212:109-117. doi:10.1016/j.amjcard.2023.11.046
Tachmatzidis D, Tsarouchas A, Mouselimis D, et al. P-wave beat-to-beat analysis to predict atrial fibrillation recurrence after catheter ablation. Diagnostics. 2022;12(4):830. doi:10.3390/diagnostics12040830
kors for the transformation itself, as_ECG() for the surface
lead contract, get_windows() and median_window() for building a beat by
hand, extract_f_waves() for the atrial signal when there is no P wave to
trace.
## Not run: ecg <- read_wfdb("muse-sinus", system.file("extdata", package = "EGM"), annotator = "ecgpuwave") vcg(ecg)$components vcg(ecg, method = "atrial", beats = "all")$components # The same loop, with the windowing and alignment chosen explicitly ecg |> get_windows(by = by_rhythm(channel = 2)) |> median_window(align_feature = "N", channel = 2) |> vcg() ## End(Not run)## Not run: ecg <- read_wfdb("muse-sinus", system.file("extdata", package = "EGM"), annotator = "ecgpuwave") vcg(ecg)$components vcg(ecg, method = "atrial", beats = "all")$components # The same loop, with the windowing and alignment chosen explicitly ecg |> get_windows(by = by_rhythm(channel = 2)) |> median_window(align_feature = "N", channel = 2) |> vcg() ## End(Not run)
Warps each window so that its landmarks (located from annotations) align to
the positions held in a template, typically created by learn_template().
Between consecutive landmarks the time axis is stretched or compressed
piecewise-linearly, giving a landmark-aligned beat on the template's
common time base. This is the most flexible - and most destructive - of the
standardization methods, aligning multiple fiducials at once rather than a
single anchor.
warp_window( x, template, interpolation_method = c("linear", "spline", "step"), channel = NULL, preserve_amplitude = FALSE, preserve_class = TRUE, missing = c("partial", "drop", "error"), ambiguous = c("error", "first", "drop"), order_policy = c("error", "drop"), ... )warp_window( x, template, interpolation_method = c("linear", "spline", "step"), channel = NULL, preserve_amplitude = FALSE, preserve_class = TRUE, missing = c("partial", "drop", "error"), ambiguous = c("error", "first", "drop"), order_policy = c("error", "drop"), ... )
x |
A |
template |
A template created manually with |
interpolation_method |
Interpolation method for the warp. One of "linear" (default), "spline", or "step". |
channel |
Fallback guiding channel used when locating a landmark whose own spec does not name one, given as a channel number or name. A landmark's own channel always takes precedence; see the channels section. |
preserve_amplitude |
Logical. If TRUE, rescales each lead back to its original amplitude range after warping. Defaults to FALSE. |
preserve_class |
Logical. If TRUE (default), returns a |
missing |
What to do when a required landmark is absent: warp with the remaining landmarks, drop that window, or error. |
ambiguous |
What to do when a landmark matches multiple annotations: error, use the first, or drop that window. |
order_policy |
What to do when landmarks are crossed or duplicated: error or drop that window. |
... |
Additional arguments (currently unused). |
Before warping, each landmark defined by the template is
confirmed against the windows; landmarks missing from some windows raise a
warning (those windows warp on their remaining anchors) and landmarks absent
from every window are dropped. For every window the landmark samples are
located - each on its own channel, so a multi-channel template is
honoured - and combined with the window's own start/end as edge anchors,
forming a set of (source -> target) breakpoints. Output samples are mapped
back to source positions by inverting this piecewise-linear correspondence,
and each lead is interpolated there; annotations are carried forward through
the same map. Any breakpoints that are non-increasing in either axis are
collapsed to keep the mapping monotonic. All leads are warped regardless of
which channel a landmark was detected on.
A windows object (or list) of landmark-warped EGM objects, each
target_samples long (as defined by the template).
A delineating annotator may be run over a record once, or once per lead. Run
per lead it writes twelve independent copies of every fiducial, separated
only by the channel column, and such a table does not describe beats until
it is resolved to a single lead: pooled, it reports twelve times as many
beats as the record contains, and taking the first match at each point
silently follows whichever lead happens to sort first.
So every function that reads fiducials - get_windows(),
learn_template(), median_window() and the other window transforms,
extract_f_waves(), vcg() - takes a channel, and resolves
it the same way:
annotations spanning more than one channel with no channel given are an
error, not a guess;
a channel that the annotations do not carry is an error, which is what
catches a numbering convention that does not match;
the global channel 0 is not a lead, so it never counts toward the span
and is always kept alongside the chosen lead.
The channel may be a number or a channel name ("II"), resolved against the
record's own header. A number is matched literally against the channel
column, and the numbering there belongs to the annotator that wrote the
file. Read it off the record rather than assuming:
table(get_annotation(x)$channel)
Only the boundaries are restricted to one lead. Windows still carry the signal for every channel, which is what makes a windowed beat usable as a vectorcardiogram.
learn_template(), template(), landmark()
This implementation of WFDB is a back-end for the WFDB using a combination of python, C++, and C language. The related functions are documented separately. This serves as an overview of the conversion of WFDB formats to R formats. In this documentation, the specific WFDB generated files will be described.
record |
String that will be used to name the WFDB record. Cannot include extensions, and is not a filepath. alphanumeric characters are acceptable, as well as hyphens (-) and underscores (_) |
record_dir |
File path of directory that should be used read and write files. Defaults to current directory. |
annotator |
Character vector of WFDB-compatible annotation types.
When reading, multiple annotators can be specified to read multiple
annotation files simultaneously (e.g., |
... |
Additional arguments to be passed to the function |
The WFDB (Waveform Database) Software Package has been developed over the past thirty years, providing a large collection of software for processing and analyzing physiological waveforms. The package is written in highly portable C and can be used on all popular platforms, including GNU/Linux, MacOS X, MS-Windows, and all versions of Unix.
The foundation of the WFDB Software Package is the WFDB library, consisting of a set of functions for reading and writing digitized signals and annotations. These functions can be used by programs written in C, C++, or Fortran, running under any operating system for which an ANSI/ISO C compiler is available, including all versions of Unix, MS-DOS, MS-Windows, the Macintosh OS, and VMS.
The records that the WFDB uses have three components...
Signals: integer values that are at equal intervals at a certain sampling frequency
Header attributes: recording information such as sample number, gain, sampling frequency
Annotations: information about the record such as a beat labels or alarm triggers
Original software: George Moody, Tom Pollard, Benjamin Moody
R implementation: Anish S. Shah
Last updated: 2026-09-05
Provides the standard label definitions used by WFDB
annotation files. These helper functions make it easier to
interpret the contents of the annotation_table() object by
exposing the symbol, mnemonic, and description that correspond to
each label store value defined in the WFDB Applications Guide
(Moody and collaborators).
wfdb_annotation_labels(symbol = NULL, label_store = NULL) wfdb_annotation_decode(annotation, column = "type")wfdb_annotation_labels(symbol = NULL, label_store = NULL) wfdb_annotation_decode(annotation, column = "type")
symbol |
Optional character vector of WFDB annotation symbols to filter the results. |
label_store |
Optional integer vector of WFDB label store values to filter the results. |
annotation |
An |
column |
Name of the column within |
The returned table is derived from the WFDB Application Guide and matches the canonical label store values used by the WFDB software distribution. Entries that are not currently defined by the specification are omitted.
wfdb_annotation_labels() returns a data frame with columns
label_store, symbol, mnemonic, and description.
wfdb_annotation_decode() returns the input annotation
table with the WFDB nomenclature columns appended.
Moody GB. WFDB Applications Guide. PhysioNet. Available at https://www.physionet.org/physiotools/wag/.
wfdb_annotation_labels() wfdb_annotation_labels(symbol = c("N", "V")) ann <- annotation_table( annotator = "example", sample = c(100L, 200L), type = c("N", "V") ) wfdb_annotation_decode(ann)wfdb_annotation_labels() wfdb_annotation_labels(symbol = c("N", "V")) ann <- annotation_table( annotator = "example", sample = c(100L, 200L), type = c("N", "V") ) wfdb_annotation_decode(ann)
Individual annotation types are described as both a command-line
tool for annotating WFDB-files, as well as the extension that is appended to
the record name to notate the type. Generally, the types of annotations
that are supported are described below:
atr = manually reviewed and corrected reference annotation files
ann = general annotator file
ecgpuwave = files contain surface ECG demarcation (P, QRS, and T waves)
sqrs/wqrs/gqrs = standard WFDB peak detection for R waves
A more thorough explanation is given in the details. Additionally, files when
being read in are converted from a binary format to a textual format. The raw
data however may be inadequate, as the original annotation may be erroneous.
In these cases, an empty annotation_table object will be returned.
read_annotation( record, annotator, record_dir = ".", begin = NULL, end = NULL, header = NULL, interval = NULL, channel_zero = c("auto", "global", "signal") ) write_annotation( data, annotator, record, record_dir = ".", channel_zero = NULL )read_annotation( record, annotator, record_dir = ".", begin = NULL, end = NULL, header = NULL, interval = NULL, channel_zero = c("auto", "global", "signal") ) write_annotation( data, annotator, record, record_dir = ".", channel_zero = NULL )
record |
String that will be used to name the WFDB record. Cannot include extensions, and is not a filepath. alphanumeric characters are acceptable, as well as hyphens (-) and underscores (_) |
annotator |
Character vector of WFDB-compatible annotation types.
When reading, multiple annotators can be specified to read multiple
annotation files simultaneously (e.g., |
record_dir |
File path of directory that should be used read and write files. Defaults to current directory. |
begin, end
|
Times delimiting a half-open range. Time-only character
values such as |
header |
A header file is an optional named list of parameters that will be used to organize and describe the signal input from the data argument. If the type is given, specific additional elements will be searched for, such as the low or high pass filters, colors, or other signal attributes. At minimum, the following elements are required (as cannot be calculated):
|
interval |
A duration after |
channel_zero |
How the annotation file counts its signals. In memory a
|
data |
An |
read_annotation() returns either a single annotation_table object
(when one annotator is specified) or a named list of annotation_table
objects (when multiple annotators are specified). write_annotation()
writes an annotation_table to a WFDB-compatible annotation file.
IMPORTANT: as annotation files are created by annotators that were
developed independently, there is a higher chance of an erroneous file
being created on disk. As such, this function will note an error and return an
empty annotation_table at times.
The following annotation file types are described below.
ecgpuwaveecgpuwave analyzes an ECG signal from the specified record, detecting the
QRS complexes and locating the beginning, peak, and end of the P, QRS, and
ST-T waveforms. The output of ecgpuwave is written as a standard WFDB-format
annotation file (the extension is "*.ecgpuwave", as would be expected). This
file can be converted into text format using rdann. Further details are
given at the ECGPUWAVE
page.
The type column can be p, t, or N for the peak of the P wave, T
wave, and QRS (R peak) directly. The output notation also includes waveform
onset ( and waveform offset ). The number column gives further
information about each of these type labels.
The number column gives modifier information. If the type classifier is a T wave annotation, the number column can be 0 (normal), 1 (inverted), 2 (positive), 3 (negative), 4 (biphasic negative-positive), 5 (biphasic positive-negative). If the type is an waveform onset or offset, then number can be 0 (P wave), 1 (QRS complex), 2 (T wave).
This function allows for WFDB files to be read from any WFDB-compatible system, and also allows writing out WFDB-compatible files from specific EP recording systems, as indicated in the details section. Writing WFDB leads to creation of both a dat (signal) and hea (header) file. These are both required for reading in files as well.
write_wfdb( data, record, record_dir = ".", header = NULL, info_strings = list(), units = c("digital", "physical"), ... ) read_wfdb( record, record_dir = ".", annotator = NULL, begin = NULL, end = NULL, interval = NULL, units = c("digital", "physical"), channels = character(), channel_zero = c("auto", "global", "signal"), ... ) read_signal( record, record_dir = ".", header = NULL, begin = NULL, end = NULL, interval = NULL, units = c("digital", "physical"), channels = character(), ... ) read_header(record, record_dir = ".", ...)write_wfdb( data, record, record_dir = ".", header = NULL, info_strings = list(), units = c("digital", "physical"), ... ) read_wfdb( record, record_dir = ".", annotator = NULL, begin = NULL, end = NULL, interval = NULL, units = c("digital", "physical"), channels = character(), channel_zero = c("auto", "global", "signal"), ... ) read_signal( record, record_dir = ".", header = NULL, begin = NULL, end = NULL, interval = NULL, units = c("digital", "physical"), channels = character(), ... ) read_header(record, record_dir = ".", ...)
data |
Can either be an
|
record |
String that will be used to name the WFDB record. Cannot include extensions, and is not a filepath. alphanumeric characters are acceptable, as well as hyphens (-) and underscores (_) |
record_dir |
File path of directory that should be used read and write files. Defaults to current directory. |
header |
A header file is an optional named list of parameters that will be used to organize and describe the signal input from the data argument. If the type is given, specific additional elements will be searched for, such as the low or high pass filters, colors, or other signal attributes. At minimum, the following elements are required (as cannot be calculated):
|
info_strings |
A |
units |
A
The two differ by the ADC gain, which is commonly 200, so reading digital
counts as though they were millivolts is a 200-fold error - and one that no
downstream check can catch on its own, since the same number is plausible in
either. The signal table returned by |
... |
Additional arguments to be passed to the function |
annotator |
Character vector of WFDB-compatible annotation types.
When reading, multiple annotators can be specified to read multiple
annotation files simultaneously (e.g., |
begin, end
|
Times delimiting a half-open range. Time-only character
values such as |
interval |
A duration after |
channels |
Either the signal/channel in a |
channel_zero |
How the annotation file counts its signals. In memory a
|
The begin, end, and interval arguments are validated as time values and
converted into sample positions using the sampling frequency declared in the
WFDB header. interval takes precedence over end. The resulting range
includes begin and excludes end; requests beyond the recorded duration
are clamped to the final timestamp.
Depends on if it is a reading or writing function. For writing, will
output an WFDB-based object reflecting the function. For reading, will
output an extension of a data.table object reflecting the underlying
function (e.g. signal_table() will return an object of class).
write_wfdb(): Writes out signal and header data into a WFDB-compatible
format from R. The units parameter indicates whether the input signal data
is in digital (raw ADC counts) or physical units. When units="physical",
the function automatically converts to digital units using the inverse formula:
digital = (physical * gain) + baseline before writing to disk.
read_wfdb(): Reads a multicomponent WFDB-formatted set of files
directly into an EGM object. This serves to pull together
read_signal(), read_header(), and read_annotation() for simplicity.
read_signal(): Specifically reads the signal data from the WFDB binary
format, returning a signal_table object for evaluation in the R
environment
read_header(): Specifically reads the header data from the WFDB header
text format, returning a header_table object for evaluation in the R
environment
Type of signal data, as specified by the recording system, that are currently supported.
bard = Bard (LabSystem Pro), e.g. read_bard()
muse = MUSE (GE), e.g. read_muse()
prucka = Prucka (CardioLab), e.g. read_prucka()
How many candidate beats an analysis found but did not use, and why. The counts are recorded on the result rather than reported to the console, because a message is invisible on a background worker and the drop rate across a study is exactly what an audit needs.
window_dropped(x)window_dropped(x)
x |
The reasons are specific to whatever did the dropping. by_beat()
reports incomplete_span, beats lying too near either end of the record for
the full span to be cut. by_rhythm() reports no_offset (an onset with no
matching offset before the record ends), no_reference (no reference
fiducial inside the window), and overlapping (a second onset inside the
window, which for sinus means the beat was not clean).
vcg() reports incomplete_span from its own windowing and
no_delineation for beats the annotator did not mark the wave in.
Counts are of candidate onsets, so length(x) + sum(window_dropped(x)) is
the number of candidates the strategy considered.
A named integer vector of counts, one per reason; empty for an
object that carries no drop accounting.
## Not run: beats <- get_windows(ecg, by = by_beat(channel = 2)) window_dropped(beats) window_dropped(vcg(ecg, channel = 2)) ## End(Not run)## Not run: beats <- get_windows(ecg, by = by_beat(channel = 2)) window_dropped(beats) window_dropped(vcg(ecg, channel = 2)) ## End(Not run)
Describes how windows should be found within a record, without performing
the search. Strategy objects are values: build one once and reuse it across
every record in a study. Pass one to get_windows().
Strategies are created by the by_*() constructors, which validate their own
arguments; this class only carries the resolved result. by_rhythm() follows
the signal and returns windows of differing length; by_beat() cuts a fixed
span around a fiducial and returns windows that are all the same length.
window_strategy(method = character(0), params = list())window_strategy(method = character(0), params = list())
method |
The registry key naming the extraction engine, e.g. |
params |
A named list of resolved, strategy-specific parameters. |
A window_strategy S7 object.
by_rhythm(), by_beat(), get_windows()