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

[ENH] Add tutorial on time-frequency source estimation with STC viewer GUI #10920 #11352

Closed
wants to merge 129 commits into from
Closed
Show file tree
Hide file tree
Changes from 63 commits
Commits
Show all changes
129 commits
Select commit Hold shift + click to select a range
c6c34c0
try again
alexrockhill Oct 6, 2022
9d5cee0
add tutorial
alexrockhill Oct 6, 2022
9b39a21
fix add init
alexrockhill Oct 6, 2022
4b5da95
fix doc entry
alexrockhill Oct 7, 2022
75369e1
fix ref
alexrockhill Oct 7, 2022
23e6346
fix dependencies
alexrockhill Oct 7, 2022
ebf6200
fix doc
alexrockhill Oct 7, 2022
38c768c
fix dependencies
alexrockhill Oct 7, 2022
55060ef
fix colormap
alexrockhill Oct 11, 2022
78badbf
fix flattening issue
alexrockhill Oct 13, 2022
d8eff54
fix test
alexrockhill Oct 13, 2022
6cd7ad0
SAVED VERSION: use somato dataset, add vectors
alexrockhill Oct 15, 2022
47d32c3
move to example
alexrockhill Oct 15, 2022
35868e5
fix latest
alexrockhill Oct 15, 2022
0c0989f
Alex G comment fixes
alexrockhill Oct 17, 2022
afd06ed
SourceEstimateViewer -> VolSourceEstimateViewer
alexrockhill Oct 20, 2022
88e2679
wip
alexrockhill Nov 9, 2022
9318146
wip
alexrockhill Nov 9, 2022
ebcd626
Britta and Alex comments
alexrockhill Nov 9, 2022
70af173
fix flake
alexrockhill Nov 9, 2022
f46ed03
Dan and Alex suggestion to accept list of lists
alexrockhill Nov 9, 2022
acd6cf6
fix docstring test failure
drammock Nov 9, 2022
ee79a2d
wip
alexrockhill Nov 10, 2022
d28d593
wip
alexrockhill Nov 10, 2022
583e0ad
use custom integer complex number data type for speed
alexrockhill Nov 10, 2022
0d54edc
add setter for baseline
alexrockhill Nov 10, 2022
8c647d8
fix type issue
alexrockhill Nov 10, 2022
67d80cb
review comments
alexrockhill Nov 14, 2022
43e7df8
didn't save
alexrockhill Nov 14, 2022
6e845e6
easier way
alexrockhill Nov 15, 2022
7bd5337
bug
alexrockhill Nov 15, 2022
e4cb526
one more fix
alexrockhill Nov 15, 2022
178baa0
saved version
alexrockhill Nov 15, 2022
62a5331
fix integer version, scales to more data better, replicates qualitati…
alexrockhill Nov 15, 2022
7bdf36c
fix flake
alexrockhill Nov 15, 2022
3f471a9
very small efficiency fix
alexrockhill Nov 16, 2022
adcb02d
Alex review
alexrockhill Dec 1, 2022
9bb8ad0
fix tests
alexrockhill Dec 1, 2022
3cf7634
fix test
alexrockhill Dec 2, 2022
7b57d89
another try to fix test
alexrockhill Dec 2, 2022
8ac9b3b
fix latest numbering
alexrockhill Dec 2, 2022
aac757f
fix comments
alexrockhill Jan 10, 2023
5200cae
implement vmid
alexrockhill Jan 10, 2023
b28bf70
small fixes
alexrockhill Jan 10, 2023
602aece
fix latest
alexrockhill Jan 10, 2023
dc78c56
fix rebase issue
alexrockhill Jan 10, 2023
865f001
fix tests, give up on vmid for now
alexrockhill Jan 10, 2023
1f0f8fc
fix ctable
alexrockhill Jan 10, 2023
50d2522
copy close event from coreg to fix tests
alexrockhill Jan 10, 2023
be7ec61
try close event exactly as coreg, use vmid
alexrockhill Jan 10, 2023
fc60a86
fully implement vmid
alexrockhill Jan 10, 2023
04d39ea
cmap bug
alexrockhill Jan 10, 2023
c48db06
another try at closing tests failed
alexrockhill Jan 10, 2023
3e09bd1
allow unclosed for now
alexrockhill Jan 10, 2023
7fc0e98
try adding a process events
alexrockhill Jan 10, 2023
fc57850
WIP: Revert close changes and allow_unclosed additions
larsoner Jan 11, 2023
a66c6b5
FIX: Port over same change
larsoner Jan 11, 2023
97abfa0
WIP: Closer
larsoner Jan 11, 2023
0c6b226
fix time selection
alexrockhill Jan 19, 2023
f710eaf
Merge branch 'stc2' of https://github.com/alexrockhill/mne-python int…
alexrockhill Jan 19, 2023
32c6931
Merge branch 'main' into stc2
alexrockhill Jan 19, 2023
6284330
remove print
alexrockhill Jan 19, 2023
5c67a70
Merge branch 'stc2' of https://github.com/alexrockhill/mne-python int…
alexrockhill Jan 19, 2023
a222c6a
Merge branch 'main' into stc2
alexrockhill Jan 20, 2023
1d0b5fd
fix slider
alexrockhill Jan 23, 2023
2e31eda
Merge branch 'stc2' of https://github.com/alexrockhill/mne-python int…
alexrockhill Jan 23, 2023
c02eda5
didn't quite work, this is better
alexrockhill Jan 23, 2023
cd8b2ea
Merge branch 'main' into stc2
alexrockhill Jan 23, 2023
b96ceb1
Merge branch 'main' into stc2
alexrockhill Jan 25, 2023
38eaea0
Eric fix
alexrockhill Jan 26, 2023
c0ee101
use mne qtapp init
alexrockhill Jan 26, 2023
0090724
read more closely, another fix
alexrockhill Jan 26, 2023
00a5d33
try without pyside2 unclosed
alexrockhill Jan 26, 2023
860b5f9
nope, pyside still fails
alexrockhill Jan 27, 2023
1452734
Merge branch 'main' into stc2
alexrockhill Feb 2, 2023
252cf83
Merge branch 'main' into stc2
alexrockhill Feb 2, 2023
fb2905d
Merge branch 'main' of https://github.com/mne-tools/mne-python into stc2
alexrockhill Feb 6, 2023
127f883
Merge branch 'main' into stc2
alexrockhill Feb 6, 2023
f72b214
Merge branch 'stc2' of https://github.com/alexrockhill/mne-python int…
alexrockhill Feb 6, 2023
5f14e51
wip
alexrockhill Feb 7, 2023
4834d08
wip
alexrockhill Feb 7, 2023
d8eead1
Britta review
alexrockhill Feb 7, 2023
bafbaac
Merge branch 'main' of https://github.com/mne-tools/mne-python into stc2
alexrockhill Feb 7, 2023
5705f78
add toggle interpolation, fix enter-pressing issue, fix logratio colo…
alexrockhill Feb 8, 2023
2a5493d
no vectors for power stcs, fix test, fix applying baseline
alexrockhill Feb 9, 2023
b635511
less computations
alexrockhill Feb 9, 2023
6b1874d
add topomap
alexrockhill Feb 9, 2023
273392d
clean up a bit
alexrockhill Feb 9, 2023
af15f01
Merge branch 'main' into stc2
alexrockhill Feb 9, 2023
0544e9d
fix zoom, slider event bug, fix vectors
alexrockhill Feb 10, 2023
50198f3
a bit less vector scalar
alexrockhill Feb 10, 2023
94f57e4
Merge branch 'main' into stc2
alexrockhill Feb 10, 2023
2cd7738
fix test
alexrockhill Feb 10, 2023
9e4bc5a
fix sliders
alexrockhill Feb 10, 2023
aaca27a
vector tfr version
alexrockhill Feb 10, 2023
e296dde
Merge branch 'main' into stc2
alexrockhill Feb 13, 2023
aa6354a
Merge branch 'stc2' of https://github.com/alexrockhill/mne-python int…
alexrockhill Feb 13, 2023
c1f7c56
vectors a bit smaller
alexrockhill Feb 13, 2023
cda6c0a
Merge branch 'main' into stc2
alexrockhill Feb 13, 2023
416bfb7
fix tests
alexrockhill Feb 13, 2023
6bb3bc2
fix test
alexrockhill Feb 13, 2023
921b74f
Merge branch 'main' into stc2
alexrockhill Feb 16, 2023
428feb0
Merge branch 'main' of https://github.com/mne-tools/mne-python into stc2
alexrockhill Feb 21, 2023
8fb0893
Revert "vector tfr version"
alexrockhill Feb 21, 2023
c68c789
fix test
alexrockhill Feb 22, 2023
bf5ad21
Britta review
alexrockhill Feb 23, 2023
73d9e3d
Merge branch 'main' into stc2
alexrockhill Feb 23, 2023
59afa5e
didn't actually fix time label, now fixed
alexrockhill Feb 23, 2023
d380f7e
add separate colorbar for topomap
alexrockhill Feb 23, 2023
f7baa08
Merge branch 'main' into stc2
alexrockhill Feb 23, 2023
3e3e613
fix separator bar
alexrockhill Feb 24, 2023
8b479be
Merge branch 'main' of https://github.com/mne-tools/mne-python into stc2
alexrockhill Mar 2, 2023
99e6c0c
small indexing fix
alexrockhill Mar 2, 2023
0449ef2
tweak scaling
alexrockhill Mar 2, 2023
5ea2015
fix colorbar, add option not to cast to integers, include 0 in spectr…
alexrockhill Mar 8, 2023
e49c948
Merge branch 'main' into stc2
alexrockhill Mar 8, 2023
49f3539
fix slightly inaccurate colorbar
alexrockhill Mar 8, 2023
8b350e3
fix rms grad combine, extra white plot
alexrockhill Mar 8, 2023
42b2014
revert rms changes
alexrockhill Mar 8, 2023
1548cfe
Merge branch 'main' into stc2
alexrockhill Mar 9, 2023
2afa8e7
symmetric colorbar
alexrockhill Mar 10, 2023
dfeee13
fix missing values
alexrockhill Mar 10, 2023
83bee35
Merge branch 'main' into stc2
alexrockhill Mar 14, 2023
d84a8ce
Merge branch 'main' into stc2
alexrockhill Mar 17, 2023
01de538
Merge branch 'main' into stc2
alexrockhill Mar 21, 2023
dff4ae3
fix tests
alexrockhill Mar 21, 2023
2302226
fix flake
alexrockhill Mar 21, 2023
9c18b47
Merge branch 'main' into stc2
alexrockhill Mar 22, 2023
33b65ca
Merge branch 'main' into stc2
alexrockhill Mar 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/latest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Enhancements
- Add :meth:`mne.Info.save` to save an :class:`mne.Info` object to a fif file (:gh:`11401` by `Alex Rockhill`_)
- Improved error message when downloads are corrupted for :func:`mne.datasets.sample.data_path` and related functions (:gh:`11407` by `Eric Larson`_)
- Add support for ``skip_by_annotation`` in :func:`mne.io.Raw.notch_filter` (:gh:`11388` by `Mainak Jas`_)
- Add to :ref:`ex-source-loc-methods` how how to apply inverse methods to time-frequency resolved epochs and use :func:`mne.gui.view_vol_stc` to view the output (:gh:`10920` by `Alex Rockhill`_)
alexrockhill marked this conversation as resolved.
Show resolved Hide resolved

Bugs
~~~~
Expand Down
1 change: 1 addition & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@
# unlinkable
'CoregistrationUI',
'IntracranialElectrodeLocator',
'VolSourceEstimateViewer',
'mne_qt_browser.figure.MNEQtBrowser',
}
numpydoc_validate = True
Expand Down
1 change: 1 addition & 0 deletions doc/mri.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ Step by step instructions for using :func:`gui.coregistration`:
transforms.apply_volume_registration
transforms.compute_volume_registration
vertex_to_mni
gui.view_vol_stc
warp_montage_volume
coreg.Coregistration
127 changes: 95 additions & 32 deletions examples/inverse/evoked_ers_source_power.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"""
# Authors: Luke Bloy <luke.bloy@gmail.com>
# Eric Larson <larson.eric.d@gmail.com>
# Alex Rockhill <aprockhill@mailbox.org>
#
# License: BSD-3-Clause

Expand All @@ -22,7 +23,7 @@
import mne
from mne.cov import compute_covariance
from mne.datasets import somato
from mne.time_frequency import csd_morlet
from mne.time_frequency import csd_tfr
from mne.beamformer import (make_dics, apply_dics_csd, make_lcmv,
apply_lcmv_cov)
from mne.minimum_norm import (make_inverse_operator, apply_inverse_cov)
Expand All @@ -31,8 +32,10 @@

# %%
# Reading the raw data and creating epochs:

data_path = somato.data_path()
subject = '01'
subjects_dir = data_path / 'derivatives' / 'freesurfer' / 'subjects'
task = 'somato'
raw_fname = (data_path / 'sub-{}'.format(subject) / 'meg' /
'sub-{}_task-{}_meg.fif'.format(subject, task))
Expand All @@ -53,15 +56,13 @@
preload=True, decim=3)

# Read forward operator and point to freesurfer subject directory
fname_fwd = (data_path / 'derivatives' / 'sub-{}'.format(subject) /
fwd_fname = (data_path / 'derivatives' / 'sub-{}'.format(subject) /
'sub-{}_task-{}-fwd.fif'.format(subject, task))
subjects_dir = data_path / 'derivatives' / 'freesurfer' / 'subjects'

fwd = mne.read_forward_solution(fname_fwd)
fwd = mne.read_forward_solution(fwd_fname)

# %%
# Compute covariances
# -------------------
# Compute covariances and cross-spectral density
# ----------------------------------------------
# ERS activity starts at 0.5 seconds after stimulus onset. Because these
# data have been processed by MaxFilter directly (rather than MNE-Python's
# version), we have to be careful to compute the rank with a more conservative
Expand All @@ -70,17 +71,32 @@
# will be correctly preserved.

rank = mne.compute_rank(epochs, tol=1e-6, tol_kind='relative')
active_win = (0.5, 1.5)
baseline_win = (-1, 0)
baseline_cov = compute_covariance(epochs, tmin=baseline_win[0],
tmax=baseline_win[1], method='shrunk',
win_active = (0.5, 1.5)
win_baseline = (-1, 0)
cov_baseline = compute_covariance(epochs, tmin=win_baseline[0],
tmax=win_baseline[1], method='shrunk',
rank=rank, verbose=True)
active_cov = compute_covariance(epochs, tmin=active_win[0], tmax=active_win[1],
cov_active = compute_covariance(epochs, tmin=win_active[0], tmax=win_active[1],
method='shrunk', rank=rank, verbose=True)

# Weighted averaging is already in the addition of covariance objects.
common_cov = baseline_cov + active_cov
mne.viz.plot_cov(baseline_cov, epochs.info)
# Weighted averaging is already in the addition of covariance objects
Copy link
Member

Choose a reason for hiding this comment

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

Not sure I fully understand this comment. Do you mean that weighted averaging is taken care of?

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 line just got moved, I didn't change it in any way. I didn't write it so I'm not sure the answer, I would infer from the authors that Marijn wrote it

Copy link
Member

Choose a reason for hiding this comment

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

Well given that we both don't understand it, it should probably be replace it with something that is easier to understand.

cov_common = cov_baseline + cov_active
cov_baseline.plot(epochs.info)

# compute cross-spectral density matrices
alexrockhill marked this conversation as resolved.
Show resolved Hide resolved
freqs = np.logspace(np.log10(12), np.log10(30), 9)

# time-frequency decomposition
epochs_tfr = mne.time_frequency.tfr_morlet(
epochs, freqs=freqs, n_cycles=freqs / 2, return_itc=False,
average=False, output='complex')
epochs_tfr.decimate(20) # decimate for speed

csd = csd_tfr(epochs_tfr, tmin=-1, tmax=1.5)
csd_baseline = csd_tfr(epochs_tfr, tmin=win_baseline[0], tmax=win_baseline[1])
csd_ers = csd_tfr(epochs_tfr, tmin=win_active[0], tmax=win_active[1])

csd_baseline.plot()

# %%
# Compute some source estimates
Expand All @@ -90,13 +106,7 @@
# See :ref:`ex-inverse-source-power` for more information about DICS.


def _gen_dics(active_win, baseline_win, epochs):
freqs = np.logspace(np.log10(12), np.log10(30), 9)
csd = csd_morlet(epochs, freqs, tmin=-1, tmax=1.5, decim=20)
csd_baseline = csd_morlet(epochs, freqs, tmin=baseline_win[0],
tmax=baseline_win[1], decim=20)
csd_ers = csd_morlet(epochs, freqs, tmin=active_win[0], tmax=active_win[1],
decim=20)
def _gen_dics(csd, ers_csd, csd_baseline, fwd):
filters = make_dics(epochs.info, fwd, csd.mean(), pick_ori='max-power',
reduce_rank=True, real_filter=True, rank=rank)
stc_base, freqs = apply_dics_csd(csd_baseline.mean(), filters)
Expand All @@ -106,30 +116,30 @@ def _gen_dics(active_win, baseline_win, epochs):


# generate lcmv source estimate
def _gen_lcmv(active_cov, baseline_cov, common_cov):
def _gen_lcmv(active_cov, cov_baseline, common_cov, fwd):
filters = make_lcmv(epochs.info, fwd, common_cov, reg=0.05,
noise_cov=None, pick_ori='max-power')
stc_base = apply_lcmv_cov(baseline_cov, filters)
stc_act = apply_lcmv_cov(active_cov, filters)
stc_base = apply_lcmv_cov(cov_baseline, filters)
stc_act = apply_lcmv_cov(cov_active, filters)
stc_act /= stc_base
return stc_act


# generate mne/dSPM source estimate
def _gen_mne(active_cov, baseline_cov, common_cov, fwd, info, method='dSPM'):
inverse_operator = make_inverse_operator(info, fwd, common_cov)
stc_act = apply_inverse_cov(active_cov, info, inverse_operator,
def _gen_mne(cov_active, cov_baseline, cov_common, fwd, info, method='dSPM'):
inverse_operator = make_inverse_operator(info, fwd, cov_common)
stc_act = apply_inverse_cov(cov_active, info, inverse_operator,
method=method, verbose=True)
stc_base = apply_inverse_cov(baseline_cov, info, inverse_operator,
stc_base = apply_inverse_cov(cov_baseline, info, inverse_operator,
method=method, verbose=True)
stc_act /= stc_base
return stc_act


# Compute source estimates
stc_dics = _gen_dics(active_win, baseline_win, epochs)
stc_lcmv = _gen_lcmv(active_cov, baseline_cov, common_cov)
stc_dspm = _gen_mne(active_cov, baseline_cov, common_cov, fwd, epochs.info)
stc_dics = _gen_dics(csd, csd_ers, csd_baseline, fwd)
stc_lcmv = _gen_lcmv(cov_active, cov_baseline, cov_common, fwd)
stc_dspm = _gen_mne(cov_active, cov_baseline, cov_common, fwd, epochs.info)

# %%
# Plot source estimates
Expand All @@ -153,3 +163,56 @@ def _gen_mne(active_cov, baseline_cov, common_cov, fwd, info, method='dSPM'):
brain_dspm = stc_dspm.plot(
hemi='rh', subjects_dir=subjects_dir, subject=subject,
time_label='dSPM source power in the 12-30 Hz frequency band')

# %%
# Use volume source estimate with time-frequency resolution
# ---------------------------------------------------------

# make a volume source space
surface = subjects_dir / subject / 'bem' / 'inner_skull.surf'
vol_src = mne.setup_volume_source_space(
Copy link
Member

Choose a reason for hiding this comment

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

I am no MNE solution expert and I also see why this is necessary for the plotting here - but is this something we should comment on in the example @larsoner ? It is maybe not the preferred thing to use a volume source space for a distributed model like MNE, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The performance seems to be pretty good in this example--somatosensory stimulation is localized to S1 so I'm not sure that it needs qualification. Also, the level of that qualification shouldn't be in a tutorial, it should be when you go to make the forward model with a volume source space if that's not recommended. I'd open a separate issue about it if you want to discuss recommendations for source modeling.

Copy link
Member

Choose a reason for hiding this comment

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

The point is that if we do something unusual here (which I think we do), we should probably add a note for the user that this is not necessarily recommended to do in other contexts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is done in another example here: https://mne.tools/dev/auto_examples/inverse/compute_mne_inverse_volume.html#sphx-glr-auto-examples-inverse-compute-mne-inverse-volume-py and it's not noted as usual. Again, I'm not saying your wrong, I'm just saying let's start a separate issue to talk about this so we stay on track in this PR.

Copy link
Member

Choose a reason for hiding this comment

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

Fair point, although this is an example on doing this specifically, while here we do it in passing. I've talked to a bunch of people about using a volumetric model with MNE, and they all said it is not advisable in most cases. What speaks against adding a note or comment that says "Please not that using a volumetric source space with a minimum norm estimation algorithm is not necessarily advised in all contexts." or something like this?
@larsoner - thoughts?

subject=subject, subjects_dir=subjects_dir, surface=surface,
pos=10, add_interpolator=False) # just for speed!

conductivity = (0.3,) # one layer for MEG
model = mne.make_bem_model(subject=subject, ico=3, # just for speed
conductivity=conductivity,
subjects_dir=subjects_dir)
bem = mne.make_bem_solution(model)

trans = fwd['info']['mri_head_t']
vol_fwd = mne.make_forward_solution(
raw.info, trans=trans, src=vol_src, bem=bem, meg=True, eeg=True,
mindist=5.0, n_jobs=1, verbose=True)

# Compute source estimate using MNE solver
snr = 3.0
lambda2 = 1.0 / snr ** 2
method = 'MNE' # use MNE method (could also be dSPM or sLORETA)

# make a different inverse operator for each frequency so as to properly
# whiten the sensor data
inverse_operator = list()
for freq_idx in range(epochs_tfr.freqs.size):
# for each frequency, compute a separate covariance matrix
cov_baseline = csd_baseline.get_data(index=freq_idx, as_cov=True)
cov_baseline['data'] = cov_baseline['data'].real # only normalize by real
# then use that covariance matrix as normalization for the inverse
# operator
inverse_operator.append(mne.minimum_norm.make_inverse_operator(
epochs.info, vol_fwd, cov_baseline))

# finally, compute the stcs for each epoch and frequency
stcs = mne.minimum_norm.apply_inverse_tfr_epochs(
epochs_tfr, inverse_operator, lambda2, method=method,
pick_ori='vector')

# %%
# Plot volume source estimates
# ----------------------------

viewer = mne.gui.view_vol_stc(stcs, subject=subject, subjects_dir=subjects_dir,
src=vol_src, inst=epochs_tfr)
viewer.go_to_max() # show the maximum intensity source vertex
viewer.set_cmap(vmin=0.25, vmid=0.8)
viewer.set_3d_view(azimuth=40, elevation=35, distance=350)
17 changes: 9 additions & 8 deletions mne/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@
def pytest_configure(config):
"""Configure pytest options."""
# Markers
for marker in ('slowtest', 'ultraslowtest', 'pgtest'):
for marker in ('slowtest', 'ultraslowtest', 'pgtest', 'allow_unclosed',
'allow_unclosed_pyside2'):
config.addinivalue_line('markers', marker)

# Fixtures
Expand Down Expand Up @@ -969,18 +970,18 @@ def qt_windows_closed(request):
"""Ensure that no new Qt windows are open after a test."""
_check_skip_backend('pyvistaqt')
app = _init_mne_qtapp()
from qtpy import API_NAME
app.processEvents()
gc.collect()
n_before = len(app.topLevelWidgets())
marks = set(mark.name for mark in request.node.iter_markers())
yield
app.processEvents()
gc.collect()
if 'allow_unclosed' in request.fixturenames:
if 'allow_unclosed' in marks:
return
if 'allow_unclosed_pyside2' in marks and API_NAME.lower() == 'pyside2':
return
widgets = app.topLevelWidgets()
n_after = len(widgets)
assert n_before == n_after, widgets[-4:]


@pytest.fixture
def allow_unclosed():
"""Allow unclosed Qt Windows."""
pass
97 changes: 96 additions & 1 deletion mne/gui/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""Convenience functions for opening GUIs."""

# Authors: Christian Brodbeck <christianbrodbeck@nyu.edu>
# Alex Rockhill <aprockhill@mailbox.org>
#
# License: BSD-3-Clause

import numpy as np
from ..utils import verbose, get_config, warn


Expand Down Expand Up @@ -247,6 +249,97 @@ def locate_ieeg(info, trans, aligned_ct, subject=None, subjects_dir=None,
return gui


@verbose
def view_vol_stc(stcs, freq_first=True, subject=None, subjects_dir=None,
src=None, inst=None, show=True, block=False, verbose=None):
"""View a volume time and/or frequency source time course estimate.

Parameters
----------
stcs : list of list | generator
The source estimates. List of lists or generators for epochs
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 source estimates. List of lists or generators for epochs
The source estimates. List of list(s) or generators for epochs

Possibly?

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'm not sure this makes sense, all the options are described below, including a list of source estimates. It's a bit complicated so I think it's best to enumerate. For clarity, I will number them though.

and frequencies (i.e. using
:func:`mne.minimum_norm.apply_inverse_tfr_epochs` or
:func:`mne.beamformer.apply_dics_tfr_epochs`-- in this case
use ``freq_first=False``). Lists of source estimates across
frequencies (e.g. :func::func:`mne.beamformer.apply_dics_csd`)
and lists of source estimates across epochs
(e.g. :func:`mne.minimum_norm.apply_inverse_epochs` and
:func:`mne.beamformer.apply_dics_epochs`--in these
alexrockhill marked this conversation as resolved.
Show resolved Hide resolved
case use ``freq_first=False``) are also allowed. Single
source estimates (e.g. :func:`mne.minimum_norm.apply_inverse`
and :func:`mne.beamformer.apply_dics`) are also allowed
(``freq_first`` will not be used in this case).
freq_first : bool
If frequencies are the outer list of ``stcs`` use ``True``.
%(subject)s
%(subjects_dir)s
src : instance of SourceSpaces
The volume source space for the ``stc``.
inst : EpochsTFR | AverageTFR | None
The time-frequency or data object to use to plot topography.
show : bool
Show the GUI if True.
block : bool
Whether to halt program execution until the figure is closed.
%(verbose)s

Returns
-------
gui : instance of VolSourceEstimateViewer
The graphical user interface (GUI) window.
"""
from ..viz.backends._utils import _qt_app_exec
from ._vol_stc import VolSourceEstimateViewer, COMPLEX_DTYPE, RANGE_VALUE
from qtpy.QtWidgets import QApplication
# get application
app = QApplication.instance()
if app is None:
app = QApplication(['Source Estimate Viewer'])

# cast to integers to lower memory usage, use custom complex data
# type if necessary
data = list()
# can be generator, compute using first stc object, just a general
# rescaling of data, does not need to be precise
scalar = None
for inner_stcs in (stcs if np.iterable(stcs) else [stcs]):
inner_data = list()
for stc in (inner_stcs if np.iterable(inner_stcs) else [inner_stcs]):
if np.iscomplexobj(stc.data):
if scalar is None:
# this is an order of magnitude approximation,
# larger stcs will have some clipping
scalar = (RANGE_VALUE - 1) / stc.data.real.max()
stc_data = np.zeros(stc.data.shape, COMPLEX_DTYPE)
stc_data['re'] = np.clip(stc.data.real * scalar,
-RANGE_VALUE, RANGE_VALUE - 1)
stc_data['im'] = np.clip(stc.data.imag * scalar,
-RANGE_VALUE, RANGE_VALUE - 1)
inner_data.append(stc_data)
else:
if scalar is None:
scalar = (RANGE_VALUE - 1) / stc.data.max() / 10
inner_data.append(np.clip(stc.data * scalar,
-RANGE_VALUE, RANGE_VALUE - 1
).astype(np.int16))
data.append(inner_data)
data = np.array(data)
if data.ndim == 4: # scalar solution, add dimension at the end
data = data[:, :, :, None]

# move frequencies to penultimate
data = data.transpose((1, 2, 3, 0, 4) if freq_first else (0, 2, 3, 1, 4))

gui = VolSourceEstimateViewer(
data, subject=subject, subjects_dir=subjects_dir,
src=src, inst=inst, show=show,
verbose=verbose)
if block:
_qt_app_exec(app)
return gui


class _GUIScraper(object):
"""Scrape GUI outputs."""

Expand All @@ -256,11 +349,13 @@ def __repr__(self):
def __call__(self, block, block_vars, gallery_conf):
from ._ieeg_locate import IntracranialElectrodeLocator
from ._coreg import CoregistrationUI
from ._vol_stc import VolSourceEstimateViewer
from sphinx_gallery.scrapers import figure_rst
from qtpy import QtGui
for gui in block_vars['example_globals'].values():
if (isinstance(gui, (IntracranialElectrodeLocator,
CoregistrationUI)) and
CoregistrationUI,
VolSourceEstimateViewer)) and
not getattr(gui, '_scraped', False) and
gallery_conf['builder_name'] == 'html'):
gui._scraped = True # monkey-patch but it's easy enough
Expand Down
Loading