diff --git a/.flake8 b/.flake8 deleted file mode 100644 index e7b1a2d0..00000000 --- a/.flake8 +++ /dev/null @@ -1,3 +0,0 @@ -[flake8] -max-line-length = 80 -extend-ignore = E203, E501 \ No newline at end of file diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 26b078b9..0836d910 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -6,22 +6,6 @@ on: - main jobs: - black: - name: Check code style with black - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.11 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install black - - name: Check code style - run: black --check . - pytest: strategy: fail-fast: false @@ -56,9 +40,15 @@ jobs: python -m pip install --upgrade pip python -m pip install -ve . python -m pip install -r requirements_dev.txt + - name: Get testing version + run: | + mkdir -p mne/datasets + curl --output-dir mne/datasets/ --remote-name https://raw.githubusercontent.com/mne-tools/mne-python/main/mne/datasets/config.py + curl --remote-name https://raw.githubusercontent.com/mne-tools/mne-python/main/tools/get_testing_version.sh + bash ./get_testing_version.sh - uses: actions/cache@v3 with: - key: ${{ runner.os }}-sample-data + key: ${{ env.TESTING_VERSION }} path: ~/mne_data name: 'Cache testing data' - run: python -c 'import mne; print(mne.datasets.testing.data_path(verbose=True))' @@ -66,4 +56,4 @@ jobs: - name: Show system information run: mne sys_info - run: pytest - name: Run Tests \ No newline at end of file + name: Run Tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..6c98d597 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +repos: + # Pre-commit hooks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-json + - id: check-toml + - id: end-of-file-fixer + - id: fix-encoding-pragma + - id: requirements-txt-fixer + - id: trailing-whitespace + + # Black + - repo: https://github.com/psf/black + rev: 23.7.0 + hooks: + - id: black + args: [ --quiet ] + + # Ruff + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.286 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] diff --git a/LICENSE.txt b/LICENSE.txt index 6141f4a6..64658a43 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -26,4 +26,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/doc/Makefile b/doc/Makefile index 5f0694ec..eac31a79 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -22,4 +22,4 @@ help: view: @python -c "import webbrowser; webbrowser.open_new_tab('file://$(PWD)/build/html/index.html')" -show: view \ No newline at end of file +show: view diff --git a/doc/source/conf.py b/doc/source/conf.py index 1b8e5a7f..27002ea6 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full diff --git a/doc/source/parameter_widgets.rst b/doc/source/parameter_widgets.rst index 81e20472..9066a0bc 100644 --- a/doc/source/parameter_widgets.rst +++ b/doc/source/parameter_widgets.rst @@ -20,5 +20,3 @@ Parameter Widgets SliderGui StringGui TupleGui - - diff --git a/mne_pipeline_hd/_version.py b/mne_pipeline_hd/_version.py index a4e39948..743a072f 100644 --- a/mne_pipeline_hd/_version.py +++ b/mne_pipeline_hd/_version.py @@ -1 +1,2 @@ +# -*- coding: utf-8 -*- __version__ = "0.3.3a0.dev0" diff --git a/mne_pipeline_hd/development/dev_utils.py b/mne_pipeline_hd/development/dev_utils.py index 716e2445..d8dee927 100644 --- a/mne_pipeline_hd/development/dev_utils.py +++ b/mne_pipeline_hd/development/dev_utils.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Authors: Martin Schulz License: BSD 3-Clause diff --git a/mne_pipeline_hd/development/development_considerations.md b/mne_pipeline_hd/development/development_considerations.md index e96839cb..8fdc81c4 100644 --- a/mne_pipeline_hd/development/development_considerations.md +++ b/mne_pipeline_hd/development/development_considerations.md @@ -47,4 +47,4 @@ setting should be device/OS-dependent: e.g. `img_format` or `show_plots`). 2. QSettings(), which is stored by Qt on an OS-depending location and which may differ between devices/OS. Settings which dependent on the device/OS should - go here (e.g. `n_jobs` or `use_cuda`) \ No newline at end of file + go here (e.g. `n_jobs` or `use_cuda`) diff --git a/mne_pipeline_hd/extra/default_settings.json b/mne_pipeline_hd/extra/default_settings.json index cb39ed3a..0e05fb5e 100644 --- a/mne_pipeline_hd/extra/default_settings.json +++ b/mne_pipeline_hd/extra/default_settings.json @@ -31,4 +31,4 @@ "app_font_size": 10, "app_style": "auto" } -} \ No newline at end of file +} diff --git a/mne_pipeline_hd/extra/functions.csv b/mne_pipeline_hd/extra/functions.csv index aa696478..68bd5dda 100644 --- a/mne_pipeline_hd/extra/functions.csv +++ b/mne_pipeline_hd/extra/functions.csv @@ -5,12 +5,14 @@ add_erm_ssp;Empty-Room SSP;MEEG;Compute;Preprocessing;True;False;;operations;bas eeg_reference_raw;Set EEG Reference;MEEG;Compute;Preprocessing;False;False;;operations;basic;meeg,ref_channels find_events;Find events;MEEG;Compute;events;False;False;;operations;basic;meeg,stim_channels,min_duration,shortest_event,adjust_timeline_by_msec find_6ch_binary_events;Find events HD;MEEG;Compute;events;False;False;;operations;basic;meeg,min_duration,shortest_event,adjust_timeline_by_msec -run_ica;Run ICA;MEEG;Compute;Preprocessing;False;False;;operations;basic;meeg,ica_method,ica_fitto,n_components,ica_noise_cov,ica_remove_proj,ica_reject,ica_autoreject,ch_types,ch_names,reject_by_annotation,ica_eog,eog_channel,ica_ecg,ecg_channel -apply_ica;Apply ICA;MEEG;Compute;Preprocessing;False;False;;operations;basic;meeg,ica_apply_target,n_pca_components epoch_raw;Get Epochs;MEEG;Compute;events;False;False;;operations;basic;meeg,ch_types,ch_names,t_epoch,baseline,apply_proj,reject,flat,reject_by_annotation,bad_interpolation,use_autoreject,consensus_percs,n_interpolates,overwrite_ar,decim,n_jobs estimate_noise_covariance;Noise-Covariance;MEEG;Compute;Preprocessing;False;False;;operations;basic;meeg,baseline,n_jobs,noise_cov_mode,noise_cov_method +run_ica;Run ICA;MEEG;Compute;Preprocessing;False;False;;operations;basic;meeg,ica_method,ica_fitto,n_components,ica_noise_cov,ica_remove_proj,ica_reject,ica_autoreject,overwrite_ar,ch_types,ch_names,reject_by_annotation,ica_eog,eog_channel,ica_ecg,ecg_channel +apply_ica;Apply ICA;MEEG;Compute;Preprocessing;False;False;;operations;basic;meeg,ica_apply_target,n_pca_components get_evokeds;Get Evokeds;MEEG;Compute;events;False;False;;operations;basic;meeg interpolate_bads;Interpolate Bads;MEEG;Compute;Preprocessing;False;False;;operations;basic;meeg,bad_interpolation +compute_psd_raw;Compute PSD (Raw);MEEG;Compute;Time-Frequency;False;False;;operations;basic;meeg,psd_method,n_jobs +compute_psd_epochs;Compute PSD (Epochs);MEEG;Compute;Time-Frequency;False;False;;operations;basic;meeg,psd_method,n_jobs tfr;Time-Frequency;MEEG;Compute;Time-Frequency;False;False;;operations;basic;meeg,tfr_freqs,tfr_n_cycles,tfr_average,tfr_use_fft,tfr_baseline,tfr_baseline_mode,tfr_method,multitaper_bandwidth,stockwell_width,n_jobs apply_watershed;;FSMRI;Compute;MRI-Preprocessing;False;False;;operations;basic;fsmri prepare_bem;;FSMRI;Compute;MRI-Preprocessing;False;False;;operations;basic;fsmri,bem_spacing,bem_conductivity @@ -41,10 +43,10 @@ plot_sensors;;MEEG;Plot;Forward;True;False;;plot;basic;meeg,plot_sensors_kind,ch plot_raw;;MEEG;Plot;Raw;True;False;;plot;basic;meeg,show_plots plot_filtered;;MEEG;Plot;Raw;True;False;;plot;basic;meeg,show_plots plot_events;;MEEG;Plot;events;True;False;;plot;basic;meeg,show_plots -plot_power_spectra;;MEEG;Plot;Time-Frequency;True;False;;plot;basic;meeg,show_plots,n_jobs -plot_power_spectra_topo;;MEEG;Plot;Time-Frequency;True;False;;plot;basic;meeg,show_plots,n_jobs -plot_power_spectra_epochs;;MEEG;Plot;Time-Frequency;True;False;;plot;basic;meeg,show_plots,n_jobs -plot_power_spectra_epochs_topo;;MEEG;Plot;Time-Frequency;True;False;;plot;basic;meeg,show_plots,n_jobs +plot_power_spectra;;MEEG;Plot;Time-Frequency;True;False;;plot;basic;meeg,show_plots +plot_power_spectra_topomap;;MEEG;Plot;Time-Frequency;True;False;;plot;basic;meeg,psd_topomap_bands,show_plots +plot_power_spectra_epochs;;MEEG;Plot;Time-Frequency;True;False;;plot;basic;meeg,show_plots +plot_power_spectra_epochs_topomap;;MEEG;Plot;Time-Frequency;True;False;;plot;basic;meeg,psd_topomap_bands,show_plots plot_tfr;;MEEG;Plot;Time-Frequency;True;False;;plot;basic;meeg,show_plots plot_epochs;;MEEG;Plot;Epochs;True;False;;plot;basic;meeg,show_plots plot_epochs_image;;MEEG;Plot;Epochs;True;False;;plot;basic;meeg,show_plots @@ -77,6 +79,6 @@ plot_grand_avg_connect;;Group;Plot;Grand-Average;True;False;;plot;basic;group,co plot_ica_components;Plot ICA-Components;MEEG;Plot;ICA;True;False;;plot;basic;meeg,show_plots plot_ica_sources;Plot ICA-Sources;MEEG;Plot;ICA;True;False;;plot;basic;meeg,ica_source_data,show_plots plot_ica_overlay;Plot ICA-Overlay;MEEG;Plot;ICA;True;False;;plot;basic;meeg,ica_overlay_data,show_plots -plot_ica_properties;Plot ICA-Properties;MEEG;Plot;ICA;True;False;;plot;basic;meeg,show_plots +plot_ica_properties;Plot ICA-Properties;MEEG;Plot;ICA;True;False;;plot;basic;meeg,ica_fitto,show_plots plot_ica_scores;Plot ICA-Scores;MEEG;Plot;ICA;True;False;;plot;basic;meeg,show_plots print_info;Print Info;MEEG;Plot;Raw;False;False;;operations;basic;meeg diff --git a/mne_pipeline_hd/extra/license.txt b/mne_pipeline_hd/extra/license.txt index 6141f4a6..64658a43 100644 --- a/mne_pipeline_hd/extra/license.txt +++ b/mne_pipeline_hd/extra/license.txt @@ -26,4 +26,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/mne_pipeline_hd/extra/parameters.csv b/mne_pipeline_hd/extra/parameters.csv index 7aa67ae1..e9a976d5 100644 --- a/mne_pipeline_hd/extra/parameters.csv +++ b/mne_pipeline_hd/extra/parameters.csv @@ -33,7 +33,7 @@ reject;;epochs;{'mag':3000e-15, 'grad':3000e-13, 'eeg':100e-6, 'eog':200e-6};;Ch flat;;epochs;{'mag': 1e-15, 'grad':1e-13, 'eeg': 1e-6};;Chose flat-thresholds;DictGui;{'none_select': True} decim;;epochs;1;;Downsampling-Factor for epochs;IntGui; ica_method;ICA-Method;ICA;fastica;;The method for calculating ICA;ComboGui;{'options': ['fastica', 'infomax', 'picard']} -ica_fitto;Fit ICA to:;ICA;'raw_filtered';;The data to fit the ICA to;ComboGui;{'options': ['raw', 'raw_filtered', 'epochs']} +ica_fitto;Fit ICA to:;ICA;'raw_filtered';;The data to fit the ICA to;ComboGui;{'options': {'raw': 'Raw (unfiltered)', 'raw_filtered': 'Raw (filtered)', 'epochs': 'Epochs'}} ica_apply_target;Apply ICA to:;ICA;raw_filtered;;The target object (Raw, Epochs or Evoked) to apply ICA and remove the selected components from;ComboGui;{'options': {'raw_filtered': 'Raw (filtered)', 'epochs': 'Epochs', 'evoked': 'Evoked'}} n_components;;ICA;25;;The number of components for ICA;IntGui; max_pca_components;;ICA;None;;;IntGui;{'none_select': True} @@ -88,7 +88,7 @@ ecd_orientations;;Inverse;{};;;DictGui; morph_to;;Grand-Average;fsaverage;;name of the freesurfer subject to be morphed to;StringGui; ica_source_data;;ICA;raw_filtered;;Which data to plot in sources-plot from ICA;ComboGui;{'options': {'raw': 'Raw (unfiltered)', 'raw_filtered': 'Raw (filtered)', 'epochs': 'Epochs', 'epochs_eog': 'Epochs (EOG)', 'epochs_ecg': 'Epochs (ECG)', 'evoked': 'Evoked', 'evoked_eog': 'Evoked (EOG)', 'evoked_ecg': 'Evoked (ECG)'}} ica_overlay_data;;ICA;evoked;;Which data to plot in overlay-plot from ICA;ComboGui;{'options': {'raw': 'Raw (unfiltered)', 'raw_filtered': 'Raw (filtered)', 'evoked': 'Evoked', 'evoked_eog': 'Evoked (EOG)', 'evoked_ecg': 'Evoked (ECG)'}} -plot_sensors_kind;;Plot;'topomap';;The kind of plot for plot_sensors;ComboGui;{'options':['topomap', '3d', 'select']} +plot_sensors_kind;;Plot;'topomap';;The kind of plot for plot_sensors;ComboGui;{'options': ['topomap', '3d', 'select']} erm_ssp_duration;;Preprocessing;1;s;The time-chunk to use for ssp;IntGui; erm_n_grad;;Preprocessing;2;;The number of projections for Gradiometer;IntGui; erm_n_mag;;Preprocessing;2;;The number of projections for Magnetometer;IntGui; @@ -96,4 +96,6 @@ erm_n_eeg;;Preprocessing;0;;The number of projections for EEG;IntGui; ga_interpolate_bads;;Grand-Average;True;;If to interpolate bad channels for the Grand-Average;BoolGui; ga_drop_bads;;Grand-Average;True;;If to drop bad channels for the Grand-Average;BoolGui; connectivity_vmin;;Connectivity;None;;Minimum value for colormap;FloatGui;{'step': 0.01, 'none_select':True} -connectivity_vmax;;Connectivity;None;;Maximum value for colormap;FloatGui;{'step': 0.01, 'none_select':True} \ No newline at end of file +connectivity_vmax;;Connectivity;None;;Maximum value for colormap;FloatGui;{'step': 0.01, 'none_select':True} +psd_method;;Time-Frequency;welch;;The method for spectral estimation;ComboGui;{'options': ['welch', 'multitaper']} +psd_topomap_bands;;Time-Frequency;None;;The frequency bands for the topomap-plot;DictGui;{'none_select': True} diff --git a/mne_pipeline_hd/functions/operations.py b/mne_pipeline_hd/functions/operations.py index 577ad197..358a43e6 100644 --- a/mne_pipeline_hd/functions/operations.py +++ b/mne_pipeline_hd/functions/operations.py @@ -18,7 +18,6 @@ from itertools import combinations from os import environ from os.path import isdir, isfile, join -from pathlib import Path import autoreject as ar import mne @@ -557,7 +556,7 @@ def epoch_raw( if use_autoreject == "Threshold": reject = meeg.load_json("autoreject_threshold") if reject is None or overwrite_ar: - reject = ar.get_rejection_threshold(epochs) + reject = ar.get_rejection_threshold(epochs, random_state=8) meeg.save_json("autoreject_threshold", reject) print( f"Dropping bad epochs with autoreject" @@ -595,6 +594,7 @@ def run_ica( ica_remove_proj, ica_reject, ica_autoreject, + overwrite_ar, ch_types, ch_names, reject_by_annotation, @@ -604,16 +604,9 @@ def run_ica( ecg_channel, **kwargs, ): - if ica_fitto == "epochs": - data = meeg.load_epochs() - # Bad-Channels and Channel-Types are already picked in epoch_raw - else: - if ica_fitto == "raw": - data = meeg.load_raw() - - else: - data = meeg.load_filtered() - + data = meeg.load(ica_fitto) + # Bad-Channels and Channel-Types are already picked in epochs + if ica_fitto != "epochs": data.pick(ch_types, exclude="bads") if len(ch_names) > 0 and ch_names != "all": data.pick_channels(ch_names) @@ -645,12 +638,12 @@ def run_ica( simulated_epochs = mne.Epochs( data, simulated_events, baseline=None, tmin=0, tmax=1, proj=False ) - reject = ar.get_rejection_threshold(simulated_epochs) + reject = ar.get_rejection_threshold(simulated_epochs, random_state=8) print(f"Autoreject Rejection-Threshold: {reject}") elif ica_autoreject and ica_fitto == "epochs": reject = meeg.load_json("autoreject_threshold") - if not reject: - reject = ar.get_rejection_threshold(data) + if reject is None or overwrite_ar: + reject = ar.get_rejection_threshold(data, random_state=8) meeg.save_json("autoreject_threshold", reject) else: reject = ica_reject @@ -711,6 +704,11 @@ def run_ica( meeg.save_eog_epochs(eog_epochs) meeg.save_json("eog_indices", eog_indices) meeg.save_json("eog_scores", eog_scores) + else: + # Remove old eog_epochs, eog_indices and eog_scores if new ICA is calculated + meeg.remove_path("eog_epochs") + meeg.remove_json("eog_indices") + meeg.remove_json("eog_scores") if ica_ecg: create_ecg_kwargs = check_kwargs(kwargs, mne.preprocessing.create_ecg_epochs) @@ -748,6 +746,11 @@ def run_ica( meeg.save_ecg_epochs(ecg_epochs) meeg.save_json("ecg_indices", ecg_indices) meeg.save_json("ecg_scores", ecg_scores) + else: + # Remove old ecg_epochs, ecg_indices and ecg_scores if new ICA is calculated + meeg.remove_path("ecg_epochs") + meeg.remove_json("ecg_indices") + meeg.remove_json("ecg_scores") meeg.save_ica(ica) # Add components to ica_exclude-dictionary @@ -837,6 +840,22 @@ def grand_avg_evokeds(group, ga_interpolate_bads, ga_drop_bads): group.save_ga_evokeds(ga_evokeds) +def compute_psd_raw(meeg, psd_method, n_jobs, **kwargs): + raw = meeg.load_filtered() + psd_raw = raw.compute_psd( + method=psd_method, fmax=raw.info["lowpass"], n_jobs=n_jobs, **kwargs + ) + meeg.save_psd_raw(psd_raw) + + +def compute_psd_epochs(meeg, psd_method, n_jobs, **kwargs): + epochs = meeg.load_epochs() + psd_epochs = epochs.compute_psd( + method=psd_method, fmax=epochs.info["lowpass"], n_jobs=n_jobs, **kwargs + ) + meeg.save_psd_epochs(psd_epochs) + + def tfr( meeg, tfr_freqs, diff --git a/mne_pipeline_hd/functions/plot.py b/mne_pipeline_hd/functions/plot.py index 5e967637..bda9a307 100644 --- a/mne_pipeline_hd/functions/plot.py +++ b/mne_pipeline_hd/functions/plot.py @@ -8,7 +8,6 @@ from __future__ import print_function import gc -import multiprocessing from functools import partial from os.path import join @@ -114,51 +113,37 @@ def plot_events(meeg, show_plots): return fig -def plot_power_spectra(meeg, show_plots, n_jobs): - raw = meeg.load_filtered() - - # Does not accept -1 for n_jobs - if n_jobs == -1: - n_jobs = multiprocessing.cpu_count() - - fig = raw.plot_psd(fmax=raw.info["lowpass"], show=show_plots, n_jobs=n_jobs) - fig.suptitle(meeg.name) +def plot_power_spectra(meeg, show_plots): + psd = meeg.load_psd_raw() + fig = psd.plot(show=show_plots) + fig.suptitle(f"Raw: {meeg.name}", x=0.3) meeg.plot_save("power_spectra", subfolder="raw", matplotlib_figure=fig) -def plot_power_spectra_topo(meeg, show_plots, n_jobs): - raw = meeg.load_filtered() - - # Does not accept -1 for n_jobs - if n_jobs == -1: - n_jobs = multiprocessing.cpu_count() - - fig = raw.plot_psd_topo(show=show_plots, n_jobs=n_jobs) +def plot_power_spectra_topomap(meeg, psd_topomap_bands, show_plots): + psd = meeg.load_psd_raw() + fig = psd.plot_topomap(badns=psd_topomap_bands, show=show_plots) + fig.suptitle(f"Raw: {meeg.name}", x=0.3) meeg.plot_save("power_spectra", subfolder="raw_topo", matplotlib_figure=fig) -def plot_power_spectra_epochs(meeg, show_plots, n_jobs): - epochs = meeg.load_epochs() - - # Does not accept -1 for n_jobs - if n_jobs == -1: - n_jobs = multiprocessing.cpu_count() - +def plot_power_spectra_epochs(meeg, show_plots): + psd = meeg.load_psd_epochs() for trial in meeg.sel_trials: - fig = epochs[trial].plot_psd(show=show_plots, n_jobs=n_jobs) - fig.suptitle(meeg.name + "-" + trial) + fig = psd[trial].plot(show=show_plots) + fig.suptitle(f"Epochs: {meeg.name}-{trial}", x=0.3) meeg.plot_save( "power_spectra", subfolder="epochs", trial=trial, matplotlib_figure=fig ) -def plot_power_spectra_epochs_topo(meeg, show_plots, n_jobs): - epochs = meeg.load_epochs() +def plot_power_spectra_epochs_topomap(meeg, psd_topomap_bands, show_plots): + psd = meeg.load_psd_epochs() for trial in meeg.sel_trials: - fig = epochs[trial].plot_psd_topomap(show=show_plots, n_jobs=n_jobs) - fig.suptitle(meeg.name + "-" + trial) + fig = psd[trial].plot_topomap(bands=psd_topomap_bands, show=show_plots) + fig.suptitle(f"Epochs: {meeg.name}-{trial}") meeg.plot_save( "power_spectra", subfolder="epochs_topo", trial=trial, matplotlib_figure=fig ) @@ -455,9 +440,8 @@ def plot_ica_overlay(meeg, ica_overlay_data, show_plots): return overlay_figs -def plot_ica_properties(meeg, show_plots): +def plot_ica_properties(meeg, ica_fitto, show_plots): ica = meeg.load_ica() - epochs = meeg.load_epochs() eog_indices = meeg.load_json("eog_indices", default=list()) ecg_indices = meeg.load_json("ecg_indices", default=list()) @@ -485,8 +469,9 @@ def plot_ica_properties(meeg, show_plots): ix for ix in ica.exclude if ix not in eog_indices + ecg_indices ] if len(remaining_indices) > 0: + data = meeg.load(ica_fitto) prop_figs = ica.plot_properties( - epochs, remaining_indices, psd_args=psd_args, show=show_plots + data, remaining_indices, psd_args=psd_args, show=show_plots ) meeg.plot_save( "ica", subfolder="properties", trial="manually", matplotlib_figure=prop_figs diff --git a/mne_pipeline_hd/gui/base_widgets.py b/mne_pipeline_hd/gui/base_widgets.py index cb6a61da..3bb405fc 100644 --- a/mne_pipeline_hd/gui/base_widgets.py +++ b/mne_pipeline_hd/gui/base_widgets.py @@ -6,6 +6,7 @@ """ import itertools +import re import sys import numpy as np @@ -29,6 +30,7 @@ QVBoxLayout, QWidget, QComboBox, + QMessageBox, ) from mne_pipeline_hd import _object_refs @@ -1474,6 +1476,122 @@ def show_assignments(self): SimpleDialog(EditDict(self.assignments), parent=self, modal=False) +class TimedMessageBox(QMessageBox): + def __init__(self, timeout=10, title=None, text=None, *args, **kwargs): + super().__init__(*args, **kwargs) + + if title is not None: + self.setWindowTitle(title) + if text is not None: + self.setText(text) + + self._got_clicked = False + self.buttonClicked.connect(lambda: setattr(self, "_got_clicked", True)) + + self.timeout = timeout + self._update_timeout_text() + + # Start timer + self.timer = QTimer() + self.timer.timeout.connect(self.countdown) + self.timer.start(1000) + + def _update_timeout_text(self): + text = self.text() + match = re.match(r"(.*)\nTimeout: \d+", text) + if match: + text = match.group(1) + self.setText(f"{text}\nTimeout: {self.timeout}") + + def countdown(self): + self.timeout -= 1 + self._update_timeout_text() + if self.timeout <= 0: + self.timer.stop() + if self.defaultButton() is not None: + self.defaultButton().click() + else: + self.close() + + def _static_setup(icon, timeout, parent, title, text, buttons, defaultButton): + cls = TimedMessageBox( + timeout=timeout, + title=title, + text=text, + icon=icon, + parent=parent, + ) + + cls._update_timeout_text() + cls.setStandardButtons(buttons) + cls.setDefaultButton(defaultButton) + ans = cls.exec() + + # Make sure ans is the default button if timeout is reached + if not cls._got_clicked: + ans = cls.defaultButton() + + return ans + + @staticmethod + def critical( + timeout=10, + parent=None, + title=None, + text=None, + buttons=QMessageBox.Ok, + defaultButton=QMessageBox.NoButton, + ): + return TimedMessageBox._static_setup( + QMessageBox.Critical, timeout, parent, title, text, buttons, defaultButton + ) + + @staticmethod + def information( + timeout=10, + parent=None, + title=None, + text=None, + buttons=QMessageBox.Ok, + defaultButton=QMessageBox.NoButton, + ): + return TimedMessageBox._static_setup( + QMessageBox.Information, + timeout, + parent, + title, + text, + buttons, + defaultButton, + ) + + @staticmethod + def question( + timeout=10, + parent=None, + title=None, + text=None, + buttons=QMessageBox.Yes | QMessageBox.No, + defaultButton=QMessageBox.No, + ): + return TimedMessageBox._static_setup( + QMessageBox.Question, timeout, parent, title, text, buttons, defaultButton + ) + + @staticmethod + def warning( + timeout=10, + parent=None, + title=None, + text=None, + buttons=QMessageBox.Ok, + defaultButton=QMessageBox.NoButton, + ): + return TimedMessageBox._static_setup( + QMessageBox.Warning, timeout, parent, title, text, buttons, defaultButton + ) + + class AllBaseWidgets(QWidget): def __init__(self): super().__init__() diff --git a/mne_pipeline_hd/gui/dialogs.py b/mne_pipeline_hd/gui/dialogs.py index 5db8c4ca..a1f940d1 100644 --- a/mne_pipeline_hd/gui/dialogs.py +++ b/mne_pipeline_hd/gui/dialogs.py @@ -175,6 +175,7 @@ def init_ui(self): self.setLayout(layout) + # ToDo: Just parse repr(info) instead of rewriting all keys def meeg_selected(self, meeg_name): # Get size in Mebibytes of all files associated to this meeg = MEEG(meeg_name, self.mw.ct) @@ -207,7 +208,7 @@ def meeg_selected(self, meeg_name): ) key_list = [ - ("no_files", "Size of all associated files"), + ("no_files", "Number associated files"), ("size", "Size of all associated files", size_unit), ("proj_name", "Project-Name"), ("experimenter", "Experimenter"), diff --git a/mne_pipeline_hd/gui/function_widgets.py b/mne_pipeline_hd/gui/function_widgets.py index ae63a6da..567dc966 100644 --- a/mne_pipeline_hd/gui/function_widgets.py +++ b/mne_pipeline_hd/gui/function_widgets.py @@ -35,7 +35,6 @@ QMessageBox, QPushButton, QSizePolicy, - QStyle, QTabWidget, QVBoxLayout, QGridLayout, diff --git a/mne_pipeline_hd/gui/loading_widgets.py b/mne_pipeline_hd/gui/loading_widgets.py index fe4503c3..a05c1f3e 100644 --- a/mne_pipeline_hd/gui/loading_widgets.py +++ b/mne_pipeline_hd/gui/loading_widgets.py @@ -4,8 +4,6 @@ License: BSD 3-Clause Github: https://github.com/marsipu/mne-pipeline-hd """ -import gc -import logging import os import re import shutil @@ -48,7 +46,6 @@ QWizardPage, ) from matplotlib import pyplot as plt -from mne.preprocessing import find_bad_channels_maxwell from mne_pipeline_hd.functions.operations import find_bads from mne_pipeline_hd.functions.plot import ( diff --git a/mne_pipeline_hd/pipeline/function_utils.py b/mne_pipeline_hd/pipeline/function_utils.py index e9489cfa..53c6e8ce 100644 --- a/mne_pipeline_hd/pipeline/function_utils.py +++ b/mne_pipeline_hd/pipeline/function_utils.py @@ -16,6 +16,7 @@ from PyQt5.QtCore import QThreadPool, QRunnable, pyqtSlot, QObject, pyqtSignal from PyQt5.QtWidgets import QAbstractItemView +from mne_pipeline_hd.gui.base_widgets import TimedMessageBox from mne_pipeline_hd.gui.gui_utils import get_exception_tuple, ExceptionTuple, Worker from mne_pipeline_hd.pipeline.loading import BaseLoading, FSMRI, Group, MEEG from mne_pipeline_hd.pipeline.pipeline_utils import shutdown, ismac, QS @@ -398,7 +399,15 @@ def finished(self): if self.ct.get_setting("shutdown"): self.ct.save() - shutdown() + ans = TimedMessageBox.information( + 60, + parent=self.rd, + title="Shutdown", + text="The PC is about to shutdown. Cancel?", + buttons=TimedMessageBox.Cancel, + ) + if not ans == TimedMessageBox.Cancel: + shutdown() def start(self): kwds = self.prepare_start() diff --git a/mne_pipeline_hd/pipeline/loading.py b/mne_pipeline_hd/pipeline/loading.py index a3c1db96..1148e223 100644 --- a/mne_pipeline_hd/pipeline/loading.py +++ b/mne_pipeline_hd/pipeline/loading.py @@ -16,7 +16,7 @@ import pickle import shutil from datetime import datetime -from os import listdir, makedirs, remove +from os import listdir, makedirs from os.path import exists, getsize, isdir, isfile, join from pathlib import Path @@ -489,6 +489,17 @@ def save_json(self, file_name, data): self.save_file_params(file_path) + def remove_json(self, file_name): + file_path = join(self.save_dir, f"{self.name}_{self.p_preset}_{file_name}.json") + try: + os.remove(file_path) + except FileNotFoundError: + print(f"{file_path} was not found") + except OSError as err: + print(f"{file_path} could not be removed due to {err}") + else: + print(f"{file_path} was removed") + def get_existing_paths(self): """Get existing paths and add the mapped File-Type to existing_paths (set)""" @@ -527,7 +538,7 @@ def remove_path(self, data_type): except KeyError: print(f"{Path(p).name} not in file-parameters") try: - remove(p) + os.remove(p) except FileNotFoundError: # Accounting for Source-Estimate naming-conventions try: @@ -544,6 +555,8 @@ def remove_path(self, data_type): print(f"{p} could not be removed due to {err}") except OSError as err: print(f"{p} could not be removed due to {err}") + else: + print(f"{p} was removed") class MEEG(BaseLoading): @@ -671,6 +684,12 @@ def init_paths(self): self.save_dir, f"{self.name}_{self.p_preset}-ecg-epo.fif" ) self.evokeds_path = join(self.save_dir, f"{self.name}_{self.p_preset}-ave.fif") + self.psd_raw_path = join( + self.save_dir, f"{self.name}_{self.p_preset}-raw-psd.h5" + ) + self.psd_epochs_path = join( + self.save_dir, f"{self.name}_{self.p_preset}-epo-psd.h5" + ) self.power_tfr_epochs_path = join( self.save_dir, f"{self.name}_{self.p_preset}_" f'#{self.pa["tfr_method"]}-epo-pw-tfr.h5', @@ -801,6 +820,16 @@ def init_paths(self): }, "evoked_eog": {"path": None, "load": self.load_eog_evokeds, "save": None}, "evoked_ecg": {"path": None, "load": self.load_ecg_evokeds, "save": None}, + "psd_raw": { + "path": self.psd_raw_path, + "load": self.load_psd_raw, + "save": self.save_psd_raw, + }, + "psd_epochs": { + "path": self.psd_epochs_path, + "load": self.load_psd_epochs, + "save": self.save_psd_epochs, + }, "tf_power_epochs": { "path": self.power_tfr_epochs_path, "load": self.load_power_tfr_epochs, @@ -1068,6 +1097,22 @@ def load_eog_evokeds(self): def load_ecg_evokeds(self): return self.load_ecg_epochs().average() + @load_decorator + def load_psd_raw(self): + return mne.time_frequency.read_spectrum(self.psd_raw_path) + + @save_decorator + def save_psd_raw(self, psd_raw): + psd_raw.save(self.psd_raw_path, overwrite=True) + + @load_decorator + def load_psd_epochs(self): + return mne.time_frequency.read_spectrum(self.psd_epochs_path) + + @save_decorator + def save_psd_epochs(self, psd_epochs): + psd_epochs.save(self.psd_epochs_path, overwrite=True) + @load_decorator def load_power_tfr_epochs(self): return mne.time_frequency.read_tfrs(self.power_tfr_epochs_path) @@ -1198,7 +1243,7 @@ def save_mixn_dipoles(self, mixn_dips): makedirs(join(self.save_dir, "mixn_dipoles")) old_dipoles = listdir(join(self.save_dir, "mixn_dipoles")) for file in old_dipoles: - remove(join(self.save_dir, "mixn_dipoles", file)) + os.remove(join(self.save_dir, "mixn_dipoles", file)) for trial in mixn_dips: for idx, dip in enumerate(mixn_dips[trial]): diff --git a/mne_pipeline_hd/pytest.ini b/mne_pipeline_hd/pytest.ini index d7b805be..0cdb9d8e 100644 --- a/mne_pipeline_hd/pytest.ini +++ b/mne_pipeline_hd/pytest.ini @@ -1,4 +1,4 @@ # pytest.ini [pytest] log_cli = True -log_cli_level = DEBUG \ No newline at end of file +log_cli_level = DEBUG diff --git a/mne_pipeline_hd/tests/_test_utils.py b/mne_pipeline_hd/tests/_test_utils.py index b2b674ad..b7a9408d 100644 --- a/mne_pipeline_hd/tests/_test_utils.py +++ b/mne_pipeline_hd/tests/_test_utils.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import pytest import pytestqt diff --git a/mne_pipeline_hd/tests/test_base_widgets.py b/mne_pipeline_hd/tests/test_base_widgets.py new file mode 100644 index 00000000..7f2fa8a9 --- /dev/null +++ b/mne_pipeline_hd/tests/test_base_widgets.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +""" +Authors: Martin Schulz +License: BSD 3-Clause +Github: https://github.com/marsipu/mne-pipeline-hd +""" + + +def test_timed_messagebox(qtbot): + """Test TimedMessageBox.""" + from mne_pipeline_hd.gui.base_widgets import TimedMessageBox + from mne_pipeline_hd.pipeline.pipeline_utils import iswin + + # Test text and countdown + timed_messagebox = TimedMessageBox(2, text="Test") + qtbot.addWidget(timed_messagebox) + timed_messagebox.show() + + qtbot.wait(1100) + # For some reason Windows-CI seems to fail here, + # maybe timed_messagebox.show() is blocking there + if not iswin: + assert timed_messagebox.text() == "Test\nTimeout: 1" + + # Test messagebox properly closes + qtbot.wait(2100) + assert timed_messagebox.isHidden() + + # Test static methods + # Test setting default button + ans = TimedMessageBox.question(1, defaultButton=TimedMessageBox.Yes) + qtbot.wait(1100) + assert ans == TimedMessageBox.Yes + + # Test setting buttons + ans = TimedMessageBox.critical( + 1, + buttons=TimedMessageBox.Save | TimedMessageBox.Cancel, + defaultButton=TimedMessageBox.Cancel, + ) + qtbot.wait(1100) + assert ans == TimedMessageBox.Cancel + + # Test setting no default button + ans = TimedMessageBox.information( + 1, buttons=TimedMessageBox.Cancel, defaultButton=TimedMessageBox.NoButton + ) + qtbot.wait(1100) + assert ans is None diff --git a/mne_pipeline_hd/tests/test_welcome_window.py b/mne_pipeline_hd/tests/test_welcome_window.py index 2e66ab4f..b42da72b 100644 --- a/mne_pipeline_hd/tests/test_welcome_window.py +++ b/mne_pipeline_hd/tests/test_welcome_window.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Authors: Martin Schulz License: BSD 3-Clause diff --git a/pyproject.toml b/pyproject.toml index 09f88894..ff1745c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,3 +35,7 @@ dependencies = { file = "requirements.txt" } [tool.black] line-lenght = 88 target-version = ["py39", "py310", "py311"] + +[tool.ruff] +line-length = 88 +target-version = "py39" diff --git a/requirements.txt b/requirements.txt index c13abdc8..c22b39db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,19 @@ -# PyQt -PyQt5 -qtpy -pyobjc-framework-Cocoa; sys_platform == "darwin" -pyqtdarktheme + +# Pipeline-related +autoreject +h5io # MNE-related mne mne-connectivity mne-qt-browser -autoreject -h5io -vtk nibabel - -# Pipeline-related -psutil pandas +psutil +pyobjc-framework-Cocoa; sys_platform == "darwin" + +# PyQt +PyQt5 +pyqtdarktheme +qtpy +vtk diff --git a/requirements_dev.txt b/requirements_dev.txt index c7eeb95f..90f76ea8 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,7 +1,7 @@ +black +pre-commit +pydata-sphinx-theme pytest pytest-qt +ruff sphinx -pydata-sphinx-theme -black -flake8 -flake8-bugbear \ No newline at end of file