-
Notifications
You must be signed in to change notification settings - Fork 3
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 TFR vector visualization #6
base: main
Are you sure you want to change the base?
Conversation
@larsoner looks like the examples don't quite build properly for 3D rendering. We could just remove them since they are redundant with Also, I thought Black was supposed to fix your style for you, am I supposed to do something about the failed style test, sorry I'm not as familiar with that. |
For now you need to For the black rendering can you try adding this line |
Hmmm |
I'm guessing you're using a version older than |
Ah I think it was |
Also I don't have write access FYI |
@larsoner , have you solved this CI issue before? |
Yes PySide6 v6.5 broke some stuff, pushed a commit to hopefully fix it |
Hey all green, nice! I'm not quite sure what |
if data.dtype == COMPLEX_DTYPE: | ||
data = data["re"] + 1j * data["im"] | ||
assert np.iscomplexobj(data) | ||
data = np.abs(data) * np.cos(np.angle(data)) |
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 think this is equivalent to data.real
>>> import numpy as np
>>> rng = np.random.default_rng(0)
>>> data = rng.normal(size=(1000,)) + 1j * rng.normal(size=(1000,))
>>> np.allclose(data.real, np.abs(data) * np.cos(np.angle(data)))
True
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.
Interesting, so that was just plotting the real component, that's even simpler to understand potentially. What do you think about interpreting this? The real component is just the amplitude of the cosine for that frequency compared to what I was originally thinking was tapering the magnitude of the vector by it's alignment with 0 or 180 degree phase. Since those are equivalent, I'm thinking there might be a simpler interpretation, I guess that the cosine is already zero at 90 and 270 degrees so the real part already includes that tapering.
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 have a hard time interpreting the real or imaginary parts of these filters. In spectral analysis it means some cosine or sine part of a complex exponential (it's just a convenient way of bookkeeping). And in cross-spectrum / coherence analysis it means some phase shift between signals of interest. So plotting just the real (or imag) part of the signal only gives you part of the information. I don't know the right thing to do here but plotting just the real part (or just the imaginary part) seems to miss a lot
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.
Hmm those are good examples of where the real and imaginary parts of the filters are used directly but I think via the Hilbert transform, the imaginary part of the signal is just the real signal shifted in phase by a quarter cycle so I think that way the information is actually redundant and you might not be missing anything after all. In this gist (https://gist.github.com/alexrockhill/58c4316415c54fcc24f85a617862e2bf) I simulate a pure sine wave and then plot it via this method and similarly in the code below, you what I would expect to recover; a vector oscillating in the direction that was simulated. If there is a reason why visualizing it this way and this interpretation is wrong, I'm happy to abandon the idea or table until we do it some other way that is better, but this is what I was thinking would be helpful to visualize--when you get a pure oscillation, the vector changes direction forward and backward on the brain in the direction of that oscillation, I think the math on how to get there changes the interpretation but I don't quite understand why this isn't helpful or what is wrong about this visualization.
import numpy as np
import mne
import matplotlib.pyplot as plt
info = mne.create_info(['1'], 1000, 'eeg')
epochs = mne.EpochsArray(np.sin(np.linspace(0, 10, 10001) * 2 * np.pi * 20)[None, None] * 1e-6, info)
epochs_tfr = mne.time_frequency.tfr_morlet(epochs, [20], [10], return_itc=False, average=False, output='complex')
plt.plot(epochs_tfr.data.real.flatten())
plt.plot(epochs_tfr.data.imag.flatten())
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.
the imaginary part of the signal is just the real signal shifted in phase by a quarter cycle
Agreed. Same can be said of the real and imaginary parts of the Fourier basis (cos +j sin).
I think that way the information is actually redundant
No, I don't think this is how the Hilbert transform is meant to be interpreted/thought of in practice in signal processing any more than you can think of the real and imaginary coefficients of a Fourier decomposition as being redundant (the bases are actually orthogonal). And it becomes more complicated once you're talking about it in the context of a cross special density estimate, which underlies DICS, which is where I assume this would actually be useful. I think a deeper understanding of the underlying signal processing principles at play here (ideally derived from math) should guide any public implementation here rather than simulations.
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.
The imaginary part isn't totally redundant, but it is similar to the phase-shifted real component in actual data. The cosine of the phase scaling the magnitude is identical to the real value in the experimental data also.
import numpy as np
import mne
import matplotlib.pyplot as plt
evoked = mne.read_evokeds(mne.datasets.sample.data_path() / 'MEG' / 'sample' / 'sample_audvis-ave.fif')[0]
evoked_tfr = mne.time_frequency.tfr_morlet(evoked, [20], [2], return_itc=False, average=False, output='complex')
n = evoked.info['sfreq'] / 20 / 4
plt.plot(evoked_tfr.data[0, 0, 0].real)
plt.plot(evoked_tfr.data[0, 0, 0, int(round(n)):].imag)
plt.show()
np.allclose(evoked_tfr.data.real, np.abs(evoked_tfr.data) * np.cos(np.angle(evoked_tfr.data)))
As I understand it, the cross-spectral density is just for whitening and the complex data itself is what gets input to the minimum norm or beamformer solutions.
I don't want to post hoc justify that the real component is what is the right thing to visualize when the phase-scaled magnitude was what I was going for and was theoretically motivated but it would also be nice to visualize the direction. I know a first-component-of-the-SVD method has also been proposed which would be interesting to see implemented but it would also be nice to plot something that isn't just the direction of maximum variance if possible. Since this gets a bit complicated to understand and interpret, it would be nice if there was something with minimal processing of the data. The real component is as minimal as it gets but obviously might be too simple and not the right solution.
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 a good discussion, how about we table it for now and come back to it later?
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 we can discuss it later
Porting over mne-tools/mne-python#11524
MWE: