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

Integrate physutils - Physio object usage #54

Merged
merged 40 commits into from
Aug 25, 2024

Conversation

maestroque
Copy link
Contributor

@maestroque maestroque commented Jun 12, 2024

Proposed Changes

  • Integrate physutils (currently only testing locally as the package is pending publishing)
  • Integrate the use of the Physio object for the metric functions
  • Retain the ability for the functions to be used without the need for the Physio object

Change Type

  • bugfix (+0.0.1)
  • minor (+0.1.0)
  • major (+1.0.0)
  • refactoring (no version update)
  • test (no version update)
  • infrastructure (no version update)
  • documentation (no version update)
  • other

Checklist before review

  • I added everything I wanted to add to this PR.
  • [Code or tests only] I wrote/updated the necessary docstrings.
  • [Code or tests only] I ran and passed tests locally.
  • [Documentation only] I built the docs locally.
  • My contribution is harmonious with the rest of the code: I'm not introducing repetitions.
  • My code respects the adopted style, especially linting conventions.
  • The title of this PR is explanatory on its own, enough to be understood as part of a changelog.
  • I added or indicated the right labels.
  • I added information regarding the timeline of completion for this PR.
  • Please, comment on my PR while it's a draft and give me feedback on the development!

@maestroque maestroque requested review from smoia, m-miedema and me-pic June 12, 2024 23:31
@maestroque maestroque self-assigned this Jun 12, 2024
@maestroque
Copy link
Contributor Author

Currently tests are failing, because the physutils module cannot be installed

@maestroque maestroque changed the title Integrate physutils - Physio object usage [WIP] Integrate physutils - Physio object usage Jun 12, 2024
Copy link
Member

@m-miedema m-miedema left a comment

Choose a reason for hiding this comment

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

The main thing that I think we need to discuss is checking for peaks/troughs where appropriate in the Physio object. I'm not sure the best way to do this (and we've talked about maintaining functionality for non-Physio object time-series, so we'll need to think about that in this context too). Many parameters e.g. TR could equally be associated with a Physio object, so we need a clear plan that checks for parameters not supplied explicitly but allows flexibility for a non-Physio implementation.

sample_rate : float
Physio sample rate, in Hertz.
data : Physio_like
Object containing the timeseries of the recorded respiratory or cardiac signal
Copy link
Member

Choose a reason for hiding this comment

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

I would suggest including a bit more information here, especially that the object should also contain associated meta-data.

Copy link
Member

Choose a reason for hiding this comment

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

(I think it's worth a discussion where we talk about advantages of forcing some parameters to be in the Physio object vs. explicitly specified before we go too far down this road!)

@maestroque
Copy link
Contributor Author

I believe the support for Physio and non-Physio inputs has been completed. @m-miedema @me-pic let me know your thoughts

The unit tests now need updating.

@maestroque
Copy link
Contributor Author

In CRF, iCRF and RRF there is this line:

time_stamps = np.arange(0, time_length, 1 / samplerate)

Since samplerate is in seconds, shouldn't it be

time_stamps = np.arange(0, time_length, samplerate)

Do you prefer changing the doc to Hz or is it more convinient in seconds?
@m-miedema @me-pic

@maestroque
Copy link
Contributor Author

Also in general, shouldn't we opt in a single way to define sample rate? Is there any reason to not have everything in Hz?

assert rrf_arr.ndim == 1
assert rrf_arr.size == pred_len


@mark.xfail
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure why this was marked as xfail, but now it's fixed and it passes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's also mentioned in #41

@maestroque
Copy link
Contributor Author

@m-miedema metrics calculations are already integrated with the history. This new attribute is to store all the metric values apart from only the history. This attribute can be also used in physioqc for example

@maestroque
Copy link
Contributor Author

maestroque commented Aug 10, 2024

Currently blocked by physiopy/physutils#4, due to important physio object fix (release needed): 0c324c5 and 2b0deb9

@maestroque
Copy link
Contributor Author

@smoia @me-pic @m-miedema I added a wrapper to sort out the returns (only metric or physio with metric), feel free to review.
The failing tests currently are due to:

Also, the following changes were made to deps:

  • numpy: >=1.9.3, <2
  • matplotlib: non-constrained

@maestroque
Copy link
Contributor Author

I believe the Metric class will make it much easier for the pydra tasks to handle computing and exporting metrics

install_requires =
numpy >=1.9.3
matplotlib >=3.1.1
numpy >=1.9.3, <2
Copy link
Member

@smoia smoia Aug 12, 2024

Choose a reason for hiding this comment

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

Can you open an issue to state we need to refactor code to handle numpy >=2.0, please?

(It's good to leave it like this for the moment, though, thanks!)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Opened here: #62

@@ -48,7 +50,7 @@ test =
%(style)s
pytest >=5.3
pytest-cov
peakdet
peakdet>=0.5.0
Copy link
Member

Choose a reason for hiding this comment

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

With the physutils integration, do we still need peakdet?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, there is a unit test with peak finding

"""

def determine_return_type(func):
metrics_with_lags = ["respiratory_variance_time"]
Copy link
Member

Choose a reason for hiding this comment

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

We did not implemented it yet, but technically all metrics could have lags - and all metrics could be convolved. Is this a way to check the type of output you get?

Copy link
Member

Choose a reason for hiding this comment

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

Could you instead make a check on the provided arguments to the function, to check whether convolution and/or lag happened?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll change to detect lagged metrics, since it's dependent on the argument. However convolution is now only done in some metrics without being specified in the arguments and can't be detected easily if you don't read the code. Also you can't deduce that the metric is convolved from the dimensions of the metric output.

I can open an issue about it if you want

Copy link
Member

@m-miedema m-miedema Aug 21, 2024

Choose a reason for hiding this comment

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

An issue sounds good to me, you're right that we can improve transparency and flexibility for those operations as we build up the toolbox. If you don't have time I can open one, let me know your preference @maestroque :)

@@ -155,30 +193,67 @@ def env(resp, samplerate, window=10):
young adults scanned at rest, including systematic changes and 'missed'
deep breaths," Neuroimage, vol. 204, 2020.
"""

def _respiratory_pattern_variability(data, window):
Copy link
Member

Choose a reason for hiding this comment

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

Now that you can either input a physio object or a numpy array, do we still need to duplicate code for this application?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's true the relevant line in env() could be changed as such:

env_arr = (
        pd.Series(data.data)
        .rolling(window=window, center=True)
        .apply(respiratory_pattern_variability, args=(window,), kwargs=dict(return_physio=False))
    )

However we should also include support for pandas.core.series.Series types in the load_physio function for that to work, which also impacts the other module. Still, I think it'd be better to open an issue about it, because it complicates the PRs
Wdyty @smoia

@return_physio_or_metric()
@physio.make_operation()
def respiratory_variance_time(
data, fs=None, peaks=None, troughs=None, lags=(0, 4, 8, 12), **kwargs
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
data, fs=None, peaks=None, troughs=None, lags=(0, 4, 8, 12), **kwargs
data, peaks=None, troughs=None, fs=None, lags=(0, 4, 8, 12), **kwargs

Copy link
Member

Choose a reason for hiding this comment

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

It would be good to have peaks and troughs first when possible (and, in general, avoid changing the order of inputs as much as possible), but if it makes things too complicated with the wrapper, then no problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Member

@smoia smoia left a comment

Choose a reason for hiding this comment

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

Couple of notes for the moment!

@return_physio_or_metric()
@physio.make_operation()
def respiratory_variance_time(
data, fs=None, peaks=None, troughs=None, lags=(0, 4, 8, 12), **kwargs
Copy link
Member

Choose a reason for hiding this comment

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

It would be good to have peaks and troughs first when possible (and, in general, avoid changing the order of inputs as much as possible), but if it makes things too complicated with the wrapper, then no problem.

"""

def determine_return_type(func):
metrics_with_lags = ["respiratory_variance_time"]
Copy link
Member

Choose a reason for hiding this comment

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

Could you instead make a check on the provided arguments to the function, to check whether convolution and/or lag happened?

troughs found by peakdet algorithm
samplerate: float
sample rate in hz of respiratory belt
data : Physio_like
Copy link
Member

Choose a reason for hiding this comment

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

Could we change this to physutils.Physio, np.ndarray, or array-like object instead?
That's also possible now, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeap, done

@@ -42,13 +47,38 @@ def respiratory_variance_time(resp, peaks, troughs, samplerate, lags=(0, 4, 8, 1
respiratory-variation-related fluctuations from neuronal-activity-related
fluctuations in fMRI”, NeuroImage, vol.31, pp. 1536-1548, 2006.
"""
timestep = 1 / samplerate
if isinstance(data, physio.Physio):
Copy link
Member

Choose a reason for hiding this comment

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

I was that you repeat this few following lines often in different metrics - if that is the case, I would suggest to make it a function on its own or even better to add it to the decorator you created. If you make it a function, the return can be done within the metric function itself (without the wrapper), if you move this to the decorator (better), you can revert to the original metric functions, and deal with the input and returns of the metrics within the wrapper!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried to think of a cleaner way, but tbh the error handling cannot be abstracted efficiently, because there are some differences between functions using fs, peaks, troughs and combinations of them. Effectively, if an abstraction would be made it would almost have to be a separate handle case for each function. It could be done by parsing the argument name list in the wrapper but I don't think it can be much cleaner than what exists

@maestroque
Copy link
Contributor Author

@smoia please recheck when you find some time

Copy link
Member

@m-miedema m-miedema left a comment

Choose a reason for hiding this comment

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

With the updated physutils this looks good to me! I don't see a reason not to fix the one test I mentioned on the way out but (feedback from @smoia pending) I would be happy to see this move on and into the workflow.

@@ -20,7 +20,7 @@ def test_mirrorpad_exception(short_arr):
"""When passing array that is too short to perform mirrorpadding, the
function should give an error."""
arr = np.array(short_arr)
with pytest.raises(Exception) as e_info:
with pytest.raises(IndexError):
Copy link
Member

Choose a reason for hiding this comment

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

I get the expected logger warning here, so the test fails. I'm guessing this one is what you were referring to as the failed catch here? If so, I'm wondering what your suggestion is for handling it to catch the warning instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes that's the one. It's not detected because it's being handled. I'll just add a check that the subsequent warning is raised

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a bit convoluted though and it's pending a full integration of loguru in this package as well. I'd say we merge and document in the issue/PR #53

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did a quick fix and it detects the expected warning text (as in the peakdet warning assertion tests I did), however the full integration of loguru needs to be adressed: Relevant note

@m-miedema

Copy link
Member

Choose a reason for hiding this comment

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

Thanks @maestroque !

@maestroque maestroque removed the Internal Changes affect the internal API. It doesn't increase the version, but produces a changelog label Aug 25, 2024
@github-actions github-actions bot added the Internal Changes affect the internal API. It doesn't increase the version, but produces a changelog label Aug 25, 2024
@maestroque maestroque removed the Internal Changes affect the internal API. It doesn't increase the version, but produces a changelog label Aug 25, 2024
Copy link

codecov bot commented Aug 25, 2024

Codecov Report

Attention: Patch coverage is 73.26733% with 54 lines in your changes missing coverage. Please review.

Project coverage is 53.85%. Comparing base (24515ca) to head (6266b6f).
Report is 149 commits behind head on master.

Files Patch % Lines
phys2denoise/metrics/cardiac.py 53.33% 21 Missing ⚠️
phys2denoise/metrics/multimodal.py 17.39% 19 Missing ⚠️
phys2denoise/metrics/chest_belt.py 87.87% 8 Missing ⚠️
phys2denoise/metrics/utils.py 90.47% 6 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master      #54      +/-   ##
==========================================
+ Coverage   47.04%   53.85%   +6.81%     
==========================================
  Files           7        8       +1     
  Lines         321      596     +275     
==========================================
+ Hits          151      321     +170     
- Misses        170      275     +105     
Files Coverage Δ
phys2denoise/metrics/responses.py 94.44% <100.00%> (ø)
phys2denoise/metrics/utils.py 67.23% <90.47%> (-2.34%) ⬇️
phys2denoise/metrics/chest_belt.py 92.38% <87.87%> (+6.83%) ⬆️
phys2denoise/metrics/multimodal.py 26.82% <17.39%> (-6.51%) ⬇️
phys2denoise/metrics/cardiac.py 54.34% <53.33%> (-40.39%) ⬇️

... and 2 files with indirect coverage changes

@maestroque
Copy link
Contributor Author

Now, CI passes online as well, because of the physutils release. I'm merging @m-miedema @smoia

@maestroque maestroque merged commit 087bd4e into physiopy:master Aug 25, 2024
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Minormod-breaking For development only, this PR increments the minor version (0.+1.0) but breaks compatibility Testing This is for testing features, writing tests or producing testing code.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants