-
Notifications
You must be signed in to change notification settings - Fork 34
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
[MRG] [BUG] [ENH] [WIP] Bug fixes and enhancements for time-resolved spectral connectivity estimation #104
Conversation
…, then average FIX: add working support for computation of multiple connectivity metrics at once, as indicated by existing docstring FIX: correct calculation of PLV and coherence connectivity metrics FIX: block_size parameter now actually corresponds to the size of blocks instead of number of blocks ENH: add PLI and wPLI connectivity metrics ENH: improve docstring and typechecks in code ENH: streamline the public API with mne_connectivity.spectral_connectivity_epochs ENH: enable averaging connectivity results over frequencies and epochs
…requency.tfr_array_multitaper
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR @ruuskas! Excuse me for asking possibly dumb questions, but I'm not as familiar with the time-resolved connectivity as epochs.
My understanding is that spectral_connectivity_epochs
is that each Epoch is seen as a "sample" of connectivity, and then the difference is that the spectral connectivity either preserves the time-dimension, or not by estimating connectivity over time.
How does this differ from your proposal for spectral_connectivity_time
? Is there a good reference that explains the differences? I suspect it would be nice to document this for users and our own sake to keep things straight in our head.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your PR! I did not make it all the way through right now, but left a few comments and questions. My biggest question is: is this function trying to do too much? Can we split it in two functions, e.g. based on the different underlying methods used (Morlet wavelets, multitapers etc)? If I get it correctly, some of the parameters rule each other out right now?
I wonder if the amount of parameters will be overwhelming for the user. What do you think, @adam2392 ?
mne_connectivity/spectral/time.py
Outdated
cwt_freqs : array | ||
Array of frequencies of interest for time-frequency decomposition. | ||
Only used in 'cwt_morlet' mode. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens to fmin
and fmax
if this is supplied?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_compute_freq_mask
from mne_connectivity.spectral.epochs.py
is used, and hence the frequencies outside the range specified by fmin
and fmax
are not used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fine, but this behavior is not clear from the documentation.Hence why I wondered if this should be two different functions to avoid such confusion,.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it might be a good idea to break this up into two wrapper functions and make the existing function private. How would you go about naming such new functions? If this is done, then I believe spectral_connectivity_epochs
should also be broken up, which comes with the cost of breaking the code of everyone currently using it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(in other words, I'm somewhat in favor of keeping this as one function, if the only reason for splitting it is that you want to expose a few non-overlapping params from each of the two methods)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is definitely one option. I don't have a strong opinion here, what about @adam2392, @britta-wstnr ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay for changing then to method_kwargs to keep in line with mne-Python!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked at going for a **method_kw
argument like in the case of psd above. However, I think this only adds confusion, as some of the method parameters are required, not optional as you would expect from additional keyword arguments. Some of the method parameters are also shared, and both shared and non-shared parameters are also used in the spectral_connectivity_time
function itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah in that case 2 separate functions is probably the right choice then
Co-authored-by: Adam Li <[email protected]>
Co-authored-by: Adam Li <[email protected]>
Co-authored-by: Adam Li <[email protected]>
Reply to @adam2392
I apologize if this answer is too basic, but here's how I understand the difference between time-resolved connectivity and connectivity evaluated across trials. My understanding is that when connectivity is computed over epochs (or trials), each epoch is seen as an independent realization of the same event, and the connectivity scores are computed as phase differences between the different trials at each time point. That is, the difference in phase is evaluated across trials. Therefore, the connectivity score measures how consistent the phase is across trials, and this type of connectivity estimation is suitable for evoked responses. When computing time-resolved connectivity, the difference in phase is evaluated over time within each epoch. Hence, the connectivity score measures the consistency of the phase difference over time within an epoch. The scores for each epoch can then be averaged, or we could compute the standard deviation to assess dynamic connectivity. This type of connectivity estimation is useful for resting state measurements. Equations 1 and 2 in Bruna et al., 2018 show the difference between connectivity over trials and time-resolved connectivity for the phase locking value. Mike X Cohen also covers the topic in this short YouTube video. |
…putation may be None Co-authored-by: Britta Westner <[email protected]>
Co-authored-by: Britta Westner <[email protected]>
Makes sense to break it up. Imo I ALWAYS get confused if I don't look at the function for awhile, so I think for a user, it must be worse. I think tho refactoring the code could be a downstream PR, where we possibly even introduce a deprecation cycle? For now, based on #104 (comment), we can move forward by just having sensible error checks/messages? How does that sound? @ruuskas and @britta-wstnr |
Quote from @adam2392
I will add some error checks to the main |
Both this and the previous implementation are incorrect as far as my
understanding goes. One could argue that the new implementation is more
incorrect based on the tests I posted in #84.
|
Ahh... I see. So perhaps, let's figure out what's going on via #84 before this PR moves forward? You can always keep this PR open and open up a larger one whenever we converge in the GH discussion. WDYT? |
Sure. We shall continue the discussion there. |
Compute a weighted average of the tapered cross spectra when using the multitaper mode. Weighting is derived from the concentration ratios between the DPSS windows.
@@ -410,11 +411,25 @@ def _spectral_connectivity(data, method, kernel, foi_idx, | |||
data, sfreq, freqs, n_cycles=n_cycles, output='complex', | |||
decim=decim, n_jobs=n_jobs, **kw_cwt) | |||
out = np.expand_dims(out, axis=2) # same dims with multitaper | |||
weights = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add some comments perhaps in the docstring for future developers of what the weights are intended to doing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did this and tried to make the docstring better in general. For some reason, Sphinx now gives this error
mne-connectivity/mne_connectivity/spectral/time.py:docstring of mne_connectivity.spectral.time.spectral_connectivity_time:59: WARNING: Inline literal start-string without end-string.
I don't see any rogue inline literal start-strings and the HTML looks good. A quick Google search suggests there might be an issue with the configuration (or it's just me).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add the changes into the PR and I can take a look.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added them already after posting the comment above.
mne_connectivity/spectral/time.py
Outdated
if isinstance(n_cycles, (int, float)): | ||
n_cycles = [n_cycles] * len(freqs) | ||
mt_bandwidth = mt_bandwidth if mt_bandwidth else 4 | ||
n_tapers = int(np.floor(mt_bandwidth - 1)) | ||
weights = np.zeros((n_tapers, len(freqs), out.shape[-1])) | ||
for i, (f, n_c) in enumerate(zip(freqs, n_cycles)): | ||
window_length = np.arange(0., n_c / float(f), 1.0 / sfreq).shape[0] | ||
half_nbw = mt_bandwidth / 2. | ||
n_tapers = int(np.floor(mt_bandwidth - 1)) | ||
_, eigvals = dpss_windows(window_length, half_nbw, n_tapers, | ||
sym=False) | ||
weights[:, i, :] = np.sqrt(eigvals[:, np.newaxis]) | ||
# weights have shape (n_tapers, n_freqs, n_times) | ||
else: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@larsoner, @drammock, @britta-wstnr how does this look to you?
Made the docstring more stylish, removed unnecessary things and added better compliance with MNE-Python style guidelines.
The _spectral_connectivity function doesn't need defaults as these are already spelled out in the main spectral_connectivity_time function signature.
What do you think of this, do you want any of this to be merged into this module? The first three points are done and have been merged into the main branch in my fork. The 'hilbert' branch contains the fourth one although its otherwise outdated as I started to think that maybe Hilbert is not necessary for me. I think it would be good to at least follow the suggestion from @britta-wstnr and @drammock and split the function into If any of this is included, would it be best to put everything in separate PRs, or make another big one? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work @ruuskas !
We just want to fix the CI stuff and add an entry to whats_new.rst
now
Thanks @adam2392 ! I added the Notably, averaging over time when computing the metrics is strictly speaking not a bugfix, rather it is the behavior one would expect given the co-existing The time-resolved implementation found in |
CIs all better |
Thanks @larsoner. Is it a bug that |
They are not strictly required, it's just more explicit. Without them the
to
the non-alphanumeric made it clear that the inline code markup had ended |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This LGTM now! Thanks a lot @ruuskas.
As a next step, we can tackle the issues you listed in a single PR, or multiple PRs.
Thanks @adam2392. It will probably be easiest to combine the further updates into one PR again as there have been breaking changes here after they were initially implemented. |
Okay I am merging this for now. Any issues, feel free to make a GH issue! @ruuskas feel free to start the next PR! Will review whenever yiu need me to. Thanks for all the great contribution! |
…spectral connectivity estimation (mne-tools#104)
PR Description
This PR closes #90, closes #84, and closes #17. Issues discussed in #70 and #73 are addressed. Please see the commit messages for description of the changes. Unfortunately, many fixes and enhancements are bundled together, as it's impossible to separate them without breaking functionality.
In summary,
mne_connectivity.spectral_connectivity_epochs
.block_size
parameter now corresponds to actual block size instead of number of blocks.spectral_connectivity_epochs
.The following ZIP archive contains a comparison between the current release version, the version of this PR, and the HyPyP implementation of spectral connectivity (
mne_connectivity_testing_random_data.html
). This shows that the issue with too high connectivity scores with random data is now fixed. The archive also contains a comparison with HyPyP using MNE-Python example data.examples.zip
I marked this as WIP as I still have some concerns. Namely,
numpy.einsum
as is done in HyPyP. With the current implementation, computing source space connectivity scores for the example dataset takes about 20 minutes per connectivity metric when the aparc_sub parcellation (~400 parcels) is used, whereas HyPyP takes less than two minutes for the same task on my local hardware (please see the attached example for details).Merge checklist
Maintainer, please confirm the following before merging: