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

Add capability to write signal with unique samps_per_frame to wfdb.io.wrsamp #510

Merged
merged 9 commits into from
Nov 25, 2024

Conversation

briangow
Copy link
Contributor

This PR adds the capability for writing signals with unique samples per frame (samps_per_frame) to wfdb.io.wrsamp. This is typically the function that is used to write WFDB files. This was previously only possible to do by creating a Record first and using its wrsamp method to do the write.

@bemoody , I do feel like checking that the frames were uniform took a bit of extra code. I'm happy to leave it like this but also open to discussing other approaches.

I've added a couple of tests to check that this continues to work as expected.

@briangow briangow requested a review from bemoody October 16, 2024 22:00
if p_signal is not None and d_signal is not None:
signal_list = [p_signal, d_signal, e_p_signal, e_d_signal]
signals_set = sum(1 for var in signal_list if var is not None)
if signals_set != 1:
Copy link
Member

@tompollard tompollard Oct 16, 2024

Choose a reason for hiding this comment

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

Is this intentionally changing behavior for calls where p_signal=None and d_signal=None? In the past, this would evaluate to False when e_p_signal=None and e_d_signal=None, but it now evaluates to True.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@tompollard , if all signals are passed as None a failure will occur further down the line. @bemoody , do we want to allow all signals to be None when calling wfdb.io.wrsamp? If we don't want to allow that then I will leave the code as I have it which requires that one of the signals is set.

Copy link
Member

Choose a reason for hiding this comment

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

If passing None for both signals (p_signal and d_signal) previously resulted in an error, then the new behavior is an improvement. Unless people were intentioanlly calling the function with both signals set to None previously, this change isn't a problem.

# required features.

# If samps_per_frame is a list, check that it aligns as expected with the channels in the signal
if len(samps_per_frame) > 1:
Copy link
Member

Choose a reason for hiding this comment

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

If samps_per_frame is not a list (e.g. it's an int), this will error out.

len(4)
TypeError: object of type 'int' has no len()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

raise TypeError("Unsupported signal format. Must be ndarray or list of lists.")

# Check that the number of channels matches the number of samps_per_frame entries
if num_sig_channels != len(samps_per_frame):
Copy link
Member

Choose a reason for hiding this comment

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

as above, len(samps_per_frame) will error if samps_per_frame is an int.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@briangow briangow force-pushed the bg_wrsamp_multiple_fs branch from 866fe10 to def62e9 Compare October 18, 2024 18:11
@briangow
Copy link
Contributor Author

@tompollard , if this failing test is of concern, could you guide me on what to do to resolve it:

would reformat /home/runner/work/wfdb-python/wfdb-python/wfdb/io/record.py
2 files would be reformatted, 36 files would be left unchanged.

@tompollard
Copy link
Member

tompollard commented Oct 18, 2024

@tompollard , if this failing test is of concern, could you guide me on what to do to resolve it:

It looks like the style checks are failing (https://github.com/MIT-LCP/wfdb-python/actions/runs/11408864264/job/31747939807?pr=510). To fix, you'll need to make sure that the code conforms to black (install black, run black over the code to reformat).

@bemoody
Copy link
Collaborator

bemoody commented Oct 21, 2024

The following raises an error:

>>> wfdb.wrsamp("xxx", fs=500, units=["mV"], sig_name=["I"], fmt=["16"],
                e_p_signal=[numpy.array([0.0, 1.0, 2.0])],
                adc_gain=[1.0], baseline=[0])
TypeError: 'NoneType' object is not subscriptable

In this case, wrsamp should raise an exception to say that if e_p_signal is provided, then samps_per_frame must be too.

@bemoody
Copy link
Collaborator

bemoody commented Oct 21, 2024

The following raises an error:

>>> wfdb.wrsamp("xxx", fs=500, units=["mV"], sig_name=["I"], e_p_signal=[numpy.array([0.0, 1.0, 2.0])], fmt=["16"], samps_per_frame=[3])
TypeError: ufunc 'isinf' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

The problem here is that calc_adc_params doesn't handle the expanded case. calc_adc_params should use the same method to generate adc_gain and baseline as it does in the non-expanded case.

I think it should be straightforward to restructure calc_adc_params to take an expanded argument and to use e_p_signal instead of p_signal where appropriate.

@briangow
Copy link
Contributor Author

The following raises an error:

>>> wfdb.wrsamp("xxx", fs=500, units=["mV"], sig_name=["I"], fmt=["16"],
                e_p_signal=[numpy.array([0.0, 1.0, 2.0])],
                adc_gain=[1.0], baseline=[0])
TypeError: 'NoneType' object is not subscriptable

In this case, wrsamp should raise an exception to say that if e_p_signal is provided, then samps_per_frame must be too.

@bemoody , thanks for catching this. I think this also applies for e_d_signal. I've added a check to make sure samps_per_frame is supplied when writing these expanded signals.

I've also updated the check to make sure samps_per_frame aligns with the channels in the signal, to consider the case when samps_per_frame is an int instead of a list.

Finally, I think we should allow passing samps_per_frame with a 1 for each channel to wrsamp when a mixed frequency signal is read in with smooth_frames=True:

rec = wfdb.io.rdrecord('/wfdb-python/sample-data/mixedsignals', smooth_frames=True)

print(rec.samps_per_frame)
[4, 4, 4, 2, 2, 1]

wfdb.io.wrsamp('written_mixedsignals', fs = rec.fs, units=rec.units, sig_name=rec.sig_name, base_date=rec.base_date, base_time=rec.base_time, comments=rec.comments, p_signal=rec.p_signal, samps_per_frame=[1,1,1,1,1,1], baseline=rec.baseline, adc_gain=rec.adc_gain, fmt=rec.fmt)

Please let me know if you disagree, or have other thoughts with respect to this case.

@briangow
Copy link
Contributor Author

The following raises an error:

>>> wfdb.wrsamp("xxx", fs=500, units=["mV"], sig_name=["I"], e_p_signal=[numpy.array([0.0, 1.0, 2.0])], fmt=["16"], samps_per_frame=[3])
TypeError: ufunc 'isinf' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

The problem here is that calc_adc_params doesn't handle the expanded case. calc_adc_params should use the same method to generate adc_gain and baseline as it does in the non-expanded case.

I think it should be straightforward to restructure calc_adc_params to take an expanded argument and to use e_p_signal instead of p_signal where appropriate.

@bemoody , this is being addressed in a separate PR: #512 .

Copy link
Collaborator

@bemoody bemoody left a comment

Choose a reason for hiding this comment

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

Looks good

@bemoody bemoody merged commit 1d192c5 into main Nov 25, 2024
14 checks passed
@bemoody bemoody deleted the bg_wrsamp_multiple_fs branch November 25, 2024 20:24
tompollard added a commit that referenced this pull request Jan 21, 2025
This pull request adds a changelog for `v4.2.0`. The changelog is based
on the following auto-generated summary of merge commits generated by
GitHub:

```
## What's Changed

* bug-fix: Numpy ValueError when cheking empty list equality by @ajadczaksunriselabs in #459
* bug-fix: Pandas set indexing error by @ajadczaksunriselabs in #460
* fix for /issues/452 by @tecamenz in #465
* Use numpydoc to render documentation by @SnoopJ in #472
* build(deps): bump readthedocs-sphinx-search from 0.1.1 to 0.3.2 in /docs by @dependabot in #477
* Update style by @bemoody in #482
* Fix NaN handling in Record.adc, and other fixes by @bemoody in #481
* Set upper bound on Numpy version (numpy = ">=1.10.1,<2.0.0"). Ref #493. by @tompollard in #494
* Update actions to use actions/checkout@v3 and actions/setup-python@v4. by @tompollard in #495
* Fix: Indent code to ensure 'j' is within for-loop in GQRS algorithm by @tompollard in #499
* Add write_dir argument to csv_to_wfdb. Fixes #67. by @tompollard in #492
* Fix warnings by @cbrnr in #502
* README improvements by @bemoody in #503
* Change in type promotion. Fixes to annotation.py by @tompollard in #506
* Use uv by @cbrnr in #504
* Change in type promotion. Fixes to _signal.py by @tompollard in #507
* Test round-trip write/read of supported binary formats by @bemoody in #509
* Corrected typo and extended allowed types for MultiSegmentRecord by @agent3gatech in #514
* Allow expanded physical signal in `calc_adc_params` by @briangow in #512
* Add capability to write signal with unique `samps_per_frame` to `wfdb.io.wrsamp` by @briangow in #510
* Fix selection of channels when converting to EDF by @SamJelfs in #519
* Change in type promotion introduced in Numpy 2.0. Fixes to edf.py. by @tompollard in #527
* Bump dependencies for NumPy 2 compatibility by @cbrnr in #511
* Bump version to v4.2.0 and update notes on creating new releases by @tompollard in #497

## New Contributors

* @ajadczaksunriselabs made their first contribution in #459
* @tecamenz made their first contribution in #465
* @SnoopJ made their first contribution in #472
* @dependabot made their first contribution in #477
* @agent3gatech made their first contribution in #514
* @SamJelfs made their first contribution in #519

**Full Changelog**: v4.1.2...v4.2.0
```
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.

3 participants