Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plotting multi-frequency data #348

Merged
merged 13 commits into from Apr 20, 2022
Merged

Plotting multi-frequency data #348

merged 13 commits into from Apr 20, 2022

Conversation

bemoody
Copy link
Collaborator

@bemoody bemoody commented Mar 18, 2022

We want to be able to plot multi-frequency records, with each signal at its native frequency (issue #337).

To see the individual samples, try:

import wfdb
r = wfdb.rdrecord('sample-data/03700181', smooth_frames=False, sampfrom=5000, sampto=5100)
wfdb.plot_wfdb(r, time_units='seconds', sig_style=['|-'])

Compare to the smoothed version:

r = wfdb.rdrecord('sample-data/03700181', sampfrom=5000, sampto=5100)
wfdb.plot_wfdb(r, time_units='seconds', sig_style=['|-'])

A more dramatic example:

r = wfdb.rdrecord('sample-data/wave_4', smooth_frames=False, sampto=10)
wfdb.plot_wfdb(r)

contrasted with:

r = wfdb.rdrecord('sample-data/wave_4', sampto=10)
wfdb.plot_wfdb(r)

To see the signals synchronized with annotations stored at different frequencies, try:

r = wfdb.rdrecord('sample-data/03700181', smooth_frames=False, sampfrom=5000, sampto=5625)
for ann, f in (('gqrsh', 500), ('gqrsl', 125), ('sqrs', 250)):
    a = wfdb.rdann('sample-data/03700181', ann, shift_samps=True, sampfrom=(5000*f//125), sampto=(5625*f//125))
    wfdb.plot_wfdb(r, a, time_units='seconds', title='annotator %s' % ann)

(Yeah, the sampfrom/sampto thing is a bit annoying.)

Things not appearing in this pull request: I think the default should be sharex=True, or at least, all of the subplots should have identical x-limits by default. plot_wfdb should probably grow a sharex parameter. However, sharex=True is incompatible with multi-frequency and time_units='samples', though I think this could possibly be worked-around using: https://matplotlib.org/stable/api/_as_gen/matplotlib.axis.Axis.set_major_formatter.html

Benjamin Moody added 12 commits March 17, 2022 14:12
The annotation files given here may be useful as examples and for
testing.  They were generated by the following commands (WFDB 10.6.2):

    gqrs -r 03700181 -o gqrsl

(an annotation file with no explicit time resolution - meaning the
resolution is assumed to be one tick per WFDB frame, i.e. 1/125 s.)

    gqrs -H -r 03700181 -o gqrsh

(an annotation file with a time resolution of 1/500 s, which, in this
case, matches the sampling interval of the signal in question.)

    sqrs -r 03700181 && mv 03700181.qrs 03700181.sqrs

(an annotation file with a time resolution of 1/250 s, which doesn't
match either the frame interval or the sampling interval.)
An annotation file (represented by a wfdb.Annotation object) may have
a "sampling frequency" that differs from the sampling frequency or
frequencies of the signals themselves.  This will be the case, for
example, for annotations generated by programs like sqrs that operate
on an upsampled or downsampled copy of the input signals.

In any event, when plotting annotations, we need to translate the
annotation time into a sample number in order to display it in the
correct location.

Furthermore, in the future, we want to permit plotting multiple
synchronized signals that are sampled at different frequencies.

Therefore, to disambiguate between the many possible "sampling
frequencies" invvolved, add parameters 'sampling_freq' and 'ann_freq'.
Both parameters are optional and default to the value of 'fs'.  Either
may be a list (one entry per channel), allowing the API to accomodate
multi-frequency data.  Currently, if 'sampling_freq' is a list, all of
its elements must be equal.
An annotation file (represented by a wfdb.Annotation object) may have
a "sampling frequency" that differs from the sampling frequency or
frequencies of the signals themselves.  This will be the case, for
example, for annotations generated by programs like sqrs that operate
on an upsampled or downsampled copy of the input signals.

To handle this case, since record.fs does not equal annotation.fs, we
must explicitly specify both sampling_freq and ann_freq when calling
plot_items.
There is no sig_len parameter to plot_annotation.
If we are plotting digital (d_signal) values, then the values on the Y
axis are ADC units, not physical units.  Don't label the axes as
physical units, and don't try to calculate grid lines as if ADC units
were physical units.
This function accepts either None, a one-dimensional array, a
two-dimensional array, or a list (or other non-numpy sequence) of
one-dimensional arrays, and converts the result to list of
one-dimensional arrays (where each element represents one channel.)

This will be used by various plotting functions to accept "non-smooth"
signal data as the 'signal' argument while keeping backward
compatibility.
When plotting signals, in addition to allowing the signal argument to
be a one-dimensional or two-dimensional array, allow it to be a list
of one-dimensional arrays (so that each channel can have a different
length.)

If signal is a 1D or 2D array, convert it to a list of arrays using
_expand_channels.  This allows the later logic to be simplified.
When plotting signals, in addition to allowing the signal argument to
be a one-dimensional or two-dimensional array, allow it to be a list
of one-dimensional arrays (so that each channel can have a different
length.)

This means that the array of X values (t) must be calculated
separately for each channel, as both the sampling frequency and the
number of samples may vary.  (Here, we don't require that the lengths
of the signals, in seconds, must all be the same, although they always
will be when plotting a WFDB record.)

The sig_len parameter makes no sense if the channels have different
lengths; this parameter is now unused, and marked as deprecated.
In most cases, the same X-coordinate array will be used for more than
one channel; the contents of this array only depend on ch_len and
ch_freq (and time_units), so we can cache these arrays in a
dictionary, saving some (likely small) amount of time and memory when
plotting a huge record.
When plotting signals, in addition to allowing the signal argument to
be a one-dimensional or two-dimensional array, allow it to be a list
of one-dimensional arrays (so that each channel can have a different
length.)

Using _expand_channels here, as in plot_annotation, allows the logic
to be simplified.

The sig_len return value makes no sense if the channels have different
lengths; this return value is now marked as deprecated, and will be
set to None if the channel lengths differ.
When plotting signals, in addition to allowing the signal argument to
be a one-dimensional or two-dimensional array, allow it to be a list
of one-dimensional arrays (so that each channel can have a different
length.)
If a record is loaded in "non-smooth" ("expanded") mode, it can
contain signals of different lengths sampled at different frequencies.
In such a case, we want to plot each signal at its original
frequency in order to see the effect of the sampling and the temporal
relationships between the signals.

plot_items now allows the signal argument to be either a numpy array
(as in p_signal or d_signal, loaded when 'smooth_frames=True') or a
list of arrays (as in e_p_signal or e_d_signal, loaded when
'smooth_frames=False'); in the latter case, plot_wfdb has to calculate
and provide the correct per-channel sampling frequencies.

(The annotation file, if any, may have its own sampling frequency,
which may differ from the signal sampling frequencies.  The *default*,
if the annotation file doesn't specify a frequency, is always the
record frame frequency (fs), not the sampling frequency of any
particular signal.)
Copy link
Member

@cx1111 cx1111 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plot looks good. Very useful.

assumed to be a one channel signal. If it is 2, axes 0 and 1, must
represent time and channel number respectively.
signal : 1d or 2d numpy array or list, optional
The uniformly sampled signal or signals to be plotted. If signal
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The uniformly sampled signal or signals to be plotted. If signal
The signal or signals to be plotted. If signal

assumed to be a one channel signal. If it is 2, axes 0 and 1, must
represent time and channel number respectively.
signal : 1d or 2d numpy array or list, optional
The uniformly sampled signal or signals to be plotted. If signal
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you added signal = _expand_channels(signal) in plot_items right before get_plot_dims is (solely) called. Perhaps get_plot_dims should only accept signal as a list then?

signal : ndarray
Tranformed expanded signal into uniform signal.
signal : 1d or 2d numpy array or list
The uniformly sampled signal or signals to be plotted. If signal
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The uniformly sampled signal or signals to be plotted. If signal
The signal or signals to be plotted. If signal

sig_len : int
The signal length (per channel) of the dat file.
signal : 1d or 2d numpy array or list
The uniformly sampled signal or signals to be plotted. If signal
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The uniformly sampled signal or signals to be plotted. If signal
The signal or signals to be plotted. If signal

t /= downsample_factor[time_units]
tarrays[ch_len, ch_freq] = t

axes[ch].plot(t, signal[ch], sig_style[ch], zorder=3)


def plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forget, is this an internal helper function? If so, add leading underscore.

@@ -577,8 +768,9 @@ def plot_wfdb(record=None, annotation=None, plot_sym=False,
function, while allowing direct input of WFDB objects.

If the record object is input, the function will extract from it:
- signal values, from the `p_signal` (priority) or `d_signal` attribute
- sampling frequency, from the `fs` attribute
- signal values, from the `e_p_signal`, `e_d_signal`, `p_signal`, or
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a future major version, there should definitely be an enum param for this.

@bemoody
Copy link
Collaborator Author

bemoody commented Mar 29, 2022

I forget, is this an internal helper function? If so, add leading underscore.

Yeah, I'm never quite sure what should be treated as "public" or "private".

I would suggest that we plan to do a major version bump in the near future, and rename all of the internal modules/functions/methods at that time. See also issue #339.

@bemoody
Copy link
Collaborator Author

bemoody commented Mar 29, 2022

In a future major version, there should definitely be an enum param for this.

Agreed but I actually like the DWIM-by-default behavior in this case. I have a Record object, just show me what's there and don't bother me with the details.

@cx1111
Copy link
Member

cx1111 commented Mar 29, 2022

In a future major version, there should definitely be an enum param for this.

Agreed but I actually like the DWIM-by-default behavior in this case. I have a Record object, just show me what's there and don't bother me with the details.

Same. The default enum value/behavior should be that.

@bemoody bemoody merged commit 1e69501 into master Apr 20, 2022
@bemoody bemoody deleted the plot-multi-freq branch April 20, 2022 21:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants